refactor(server): generate runtime configs and use .build/config for development
This commit is contained in:
+1
-1
@@ -127,7 +127,7 @@ master key 必须长期稳定:
|
||||
|
||||
## 5. 编辑服务器配置
|
||||
|
||||
编辑与 `bd2server` 可执行文件同目录的 `authentication.json`。Discord 和 Google 都启用时:
|
||||
首次运行时会在 `bd2server` 可执行文件旁生成 `local` 模式的默认配置。开发入口使用 `.build/config/authentication.json`;Docker 镜像默认使用 `/app/data/config/authentication.json`。启用 OAuth 前编辑生成的文件并配置环境变量,然后重启。Discord 和 Google 都启用时:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ RUN apt-get update \
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/bd2server /app/bd2server
|
||||
COPY versions.json authentication.json resources.json game.json /app/
|
||||
COPY versions.json /app/
|
||||
COPY go/seed /app/go/seed
|
||||
COPY schedules /app/schedules
|
||||
RUN mkdir -p /app/data && chown 10001:10001 /app/data
|
||||
@@ -25,4 +25,4 @@ RUN mkdir -p /app/data && chown 10001:10001 /app/data
|
||||
USER 10001:10001
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/bd2server"]
|
||||
CMD ["serve", "--listen", "0.0.0.0:8080", "--data-dir", "/app/data"]
|
||||
CMD ["serve", "--listen", "0.0.0.0:8080", "--data-dir", "/app/data", "--authentication-config", "/app/data/config/authentication.json", "--resource-config", "/app/data/config/resources.json", "--game-config", "/app/data/config/game.json"]
|
||||
|
||||
@@ -2,8 +2,5 @@
|
||||
!go/
|
||||
!go/**
|
||||
!versions.json
|
||||
!authentication.json
|
||||
!resources.json
|
||||
!game.json
|
||||
!schedules/
|
||||
!schedules/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
`game.json` 是服务端统一的游戏规则配置入口。后续可配置的游戏规则在此按功能分组扩展;认证、资源源、版本清单仍分别使用现有配置文件。
|
||||
|
||||
发布部署时,将文件放在 `bd2server.exe`(Linux/macOS 为 `bd2server`)旁。开发入口读取仓库根目录的同名文件。可修改的规则在重启后生效,起始章节只在首次初始化时选择。客户端无需配置此文件。发布包只在服务端包中提供 `game.json` 和本指南。
|
||||
发布包不包含 `game.json`,首次运行时在 `bd2server.exe`(Linux/macOS 为 `bd2server`)旁生成默认文件。开发入口在 `.build/config/game.json` 生成并读取配置。已有文件不会覆盖,可修改的规则在重启后生效,起始章节只在首次初始化时选择。客户端无需配置此文件。
|
||||
|
||||
默认内容:
|
||||
|
||||
@@ -88,4 +88,4 @@
|
||||
|
||||
`schema_version` 当前必须为 `1`。字段拼写错误、未知字段、错误类型(例如字符串 `"true"`)、`null`、格式错误或文件末尾多出的 JSON 都会使服务端拒绝启动,并在错误中给出文件路径。省略 `gacha` 或此布尔开关时使用 `false`。
|
||||
|
||||
旧安装缺少整个文件时使用默认规则,服务端不会自动写入或覆盖文件。升级服务端时保留服主已经修改的 `game.json`;新发布包中的文件可用于比对新增字段。Docker 部署可将自定义文件只读挂载到 `/app/game.json`,重启容器后生效。
|
||||
缺少文件时服务端会生成默认配置;已有文件保留,格式错误会明确报错。升级时保留服主修改过的配置,新增默认字段可参考本指南。Docker 镜像的默认命令在可写数据目录 `/app/data/config/` 生成和读取三个配置;自定义命令或只读挂载时通过 `--game-config` 指定路径,重启后生效。
|
||||
|
||||
@@ -76,7 +76,7 @@ BD2 Client Studio 是使用 Wails 的独立桌面客户端设置工具。Windows
|
||||
|
||||
认证策略由 `bd2server.exe` 同目录的 `authentication.json` 权威决定。`mode=local`(默认)不依赖第三方登录,单人本地服保持不动,联机服可设为 `oauth` 来让玩家通过 Discord/Google 登录。
|
||||
|
||||
游戏规则统一使用服务端同目录的 `game.json`,修改后重启生效;开发入口读取仓库根目录的文件。联动 UR 专武是否加入 UR 必得装备券池及升级保留配置的方法见 [服务端游戏规则配置](GAME_CONFIGURATION.md)。
|
||||
`authentication.json`、`resources.json` 和 `game.json` 不随发布包分发,首次运行时在服务端可执行文件旁生成默认文件,已有文件不会覆盖。开发入口使用 `.build/config/` 中的三个配置。游戏规则修改后重启生效,详细见 [服务端游戏规则配置](GAME_CONFIGURATION.md)。
|
||||
|
||||
服务端日志默认输出 INFO 及以上级别,仅终端启用颜色:TRACE 灰、DEBUG 青、INFO 绿、WARN 黄、ERROR 红。可用 `serve --log-level debug --log-color auto` 调整,或设置 `BD2_LOG_LEVEL` 和 `BD2_LOG_COLOR` 环境变量;显式命令行参数优先。级别支持 `trace/debug/info/warn/error`,颜色支持 `auto/always/never`。`auto` 下文件和重定向保持纯文本,`NO_COLOR` 或 `TERM=dumb` 也会关闭颜色;如需在 Docker 日志流中显示颜色,可显式选择 `always`。
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mode": "local",
|
||||
"providers": {}
|
||||
}
|
||||
@@ -16,9 +16,6 @@ $serverGoDir = Join-Path $serverPackage 'go'
|
||||
$serverStateDir = Join-Path $serverPackage 'data\state'
|
||||
$clientPluginDir = Join-Path $clientPackage 'plugins'
|
||||
$versionConfig = Join-Path $root 'versions.json'
|
||||
$authenticationConfig = Join-Path $root 'authentication.json'
|
||||
$resourceConfig = Join-Path $root 'resources.json'
|
||||
$gameConfig = Join-Path $root 'game.json'
|
||||
$scheduleDirectory = Join-Path $root 'schedules'
|
||||
|
||||
try {
|
||||
@@ -143,9 +140,6 @@ foreach ($scheduleFile in $scheduleFiles) {
|
||||
Copy-Item -LiteralPath $scheduleFile.FullName -Destination $serverScheduleDirectory -Force
|
||||
}
|
||||
Copy-Item -LiteralPath $versionConfig -Destination (Join-Path $serverPackage 'versions.json') -Force
|
||||
Copy-Item -LiteralPath $authenticationConfig -Destination (Join-Path $serverPackage 'authentication.json') -Force
|
||||
Copy-Item -LiteralPath $resourceConfig -Destination (Join-Path $serverPackage 'resources.json') -Force
|
||||
Copy-Item -LiteralPath $gameConfig -Destination (Join-Path $serverPackage 'game.json') -Force
|
||||
Copy-Item -LiteralPath $versionConfig -Destination (Join-Path $clientPackage 'versions.json') -Force
|
||||
Copy-Item -LiteralPath (Join-Path $root 'RELEASE.md') -Destination (Join-Path $serverPackage 'README.md') -Force
|
||||
Copy-Item -LiteralPath (Join-Path $root 'AUTHENTICATION.md') -Destination (Join-Path $serverPackage 'AUTHENTICATION.md') -Force
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"story": {
|
||||
"start_pack_id": 21
|
||||
},
|
||||
"gacha": {
|
||||
"include_collaboration_ur_weapons": false
|
||||
},
|
||||
"purchases": {
|
||||
"diamond_recharge": {
|
||||
"currency": "free",
|
||||
"gold_per_paid_diamond": 1000,
|
||||
"diamonds_per_paid_diamond": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ func runDevelopmentCommand(args []string) (bool, error) {
|
||||
}
|
||||
serveArgs := append([]string(nil), args[2:]...)
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--version-config", filepath.Join(root, "versions.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--authentication-config", filepath.Join(root, "authentication.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--resource-config", filepath.Join(root, "resources.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--game-config", filepath.Join(root, "game.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--authentication-config", filepath.Join(root, ".build", "config", "authentication.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--resource-config", filepath.Join(root, ".build", "config", "resources.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--game-config", filepath.Join(root, ".build", "config", "game.json"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--data-dir", filepath.Join(root, "data"))
|
||||
serveArgs = appendDefaultFlag(serveArgs, "--state", filepath.Join(root, "data", "state", "state.db"))
|
||||
return true, serve(serveArgs)
|
||||
@@ -38,9 +38,7 @@ func findDevelopmentRoot() (string, error) {
|
||||
}
|
||||
for directory := filepath.Clean(working); ; directory = filepath.Dir(directory) {
|
||||
if regularDevelopmentFile(filepath.Join(directory, "versions.json")) &&
|
||||
regularDevelopmentFile(filepath.Join(directory, "go", "go.mod")) &&
|
||||
regularDevelopmentFile(filepath.Join(directory, "authentication.json")) &&
|
||||
regularDevelopmentFile(filepath.Join(directory, "resources.json")) {
|
||||
regularDevelopmentFile(filepath.Join(directory, "go", "go.mod")) {
|
||||
return directory, nil
|
||||
}
|
||||
parent := filepath.Dir(directory)
|
||||
|
||||
@@ -2,7 +2,29 @@
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindDevelopmentRootWithoutRuntimeConfigurations(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
child := filepath.Join(root, "go", "cmd")
|
||||
if err := os.MkdirAll(child, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{filepath.Join(root, "versions.json"), filepath.Join(root, "go", "go.mod")} {
|
||||
if err := os.WriteFile(path, nil, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Chdir(child)
|
||||
got, err := findDevelopmentRoot()
|
||||
if err != nil || got != root {
|
||||
t.Fatalf("development root = %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendDefaultFlagPreservesExplicitOverride(t *testing.T) {
|
||||
for _, args := range [][]string{{"--data-dir", "custom"}, {"--data-dir=custom"}} {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bd2server/internal/server/configfile"
|
||||
)
|
||||
|
||||
const FileName = "authentication.json"
|
||||
@@ -52,11 +54,16 @@ type Public struct {
|
||||
Providers []string `json:"providers"`
|
||||
}
|
||||
|
||||
func Default() Config { return Config{Mode: "local"} }
|
||||
|
||||
func Load(path string) (Config, error) {
|
||||
path, err := filepath.Abs(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("authconfig: resolve path: %w", err)
|
||||
}
|
||||
if err := configfile.Ensure(path, Default()); err != nil {
|
||||
return Config{}, fmt.Errorf("authconfig: initialize %s: %w", path, err)
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("authconfig: open %s: %w", path, err)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Package configfile publishes default configuration without replacing user files.
|
||||
package configfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Ensure publishes a complete JSON file only when path does not exist.
|
||||
// Linking a same-directory temporary file makes publication atomic and refuses
|
||||
// to replace a configuration created by another process in the meantime.
|
||||
func Ensure(path string, defaults any) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(defaults, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("create configuration directory: %w", err)
|
||||
}
|
||||
file, err := os.CreateTemp(filepath.Dir(path), ".config-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
if _, err = file.Write(append(data, '\n')); err == nil {
|
||||
err = file.Sync()
|
||||
}
|
||||
closeErr := file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
if err = os.Link(file.Name(), path); errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("create default configuration %s: %w", path, err)
|
||||
}
|
||||
slog.Info("default configuration created", "path", path)
|
||||
return nil
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"bd2server/internal/server/configfile"
|
||||
)
|
||||
|
||||
const FileName = "game.json"
|
||||
@@ -48,13 +50,12 @@ func Default() Config {
|
||||
return Config{SchemaVersion: 1, Story: StoryConfig{StartPackID: 21}, Purchases: PurchasesConfig{DiamondRecharge: DiamondRechargeConfig{Currency: "free", GoldPerPaidDiamond: 1000, DiamondsPerPaidDiamond: 1}}}
|
||||
}
|
||||
|
||||
// Load reads one explicit path. Older installations without the file retain
|
||||
// the default rules; an existing invalid file must never silently fall back.
|
||||
// Load generates defaults when missing and strictly validates existing files.
|
||||
func Load(path string) (Config, error) {
|
||||
data, err := os.ReadFile(filepath.Clean(path))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return Default(), nil
|
||||
if err := configfile.Ensure(path, Default()); err != nil {
|
||||
return Config{}, fmt.Errorf("game configuration: initialize %s: %w", path, err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("game configuration: read %s: %w", path, err)
|
||||
}
|
||||
|
||||
@@ -74,14 +74,14 @@ func TestStartingChapterConfiguration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingFileDefaultsWithoutCreating(t *testing.T) {
|
||||
func TestMissingFileCreatesDefaults(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), FileName)
|
||||
cfg, err := Load(path)
|
||||
if err != nil || cfg != Default() {
|
||||
t.Fatalf("config = %+v, error = %v", cfg, err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("Load created missing config: %v", err)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("Load did not create missing config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Opt-in performance verification, not a source for game design assertions.
|
||||
func BenchmarkInstalledWaypointPackWarm(b *testing.B) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
b.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
if err := CloseDatabaseCache(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() {
|
||||
if err := CloseDatabaseCache(); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
})
|
||||
if _, err := LoadWaypointPack(root, "20260923193640", 21); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := LoadWaypointPack(root, "20260923193640", 21); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInstalledWaypointPackCold(b *testing.B) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
b.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
b.Cleanup(func() { _ = CloseDatabaseCache() })
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
if err := CloseDatabaseCache(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.StartTimer()
|
||||
if _, err := LoadWaypointPack(root, "20260923193640", 21); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInstalledPackDetailWarm(b *testing.B) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
b.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
if err := CloseDatabaseCache(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() { _ = CloseDatabaseCache() })
|
||||
if _, err := LoadPackDetailDesign(root, "20260923193640", 21); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := LoadPackDetailDesign(root, "20260923193640", 21); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"bd2server/internal/server/configfile"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -51,6 +53,9 @@ func Default() Config {
|
||||
// Load reads one explicit resources.json. Unknown fields and trailing JSON are
|
||||
// rejected so a misspelled URL key cannot silently fall back to another CDN.
|
||||
func Load(path string) (Config, error) {
|
||||
if err := configfile.Ensure(path, Default()); err != nil {
|
||||
return Config{}, fmt.Errorf("resource policy: initialize %s: %w", path, err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("resource policy: read %s: %w", path, err)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"mode": "official"
|
||||
}
|
||||
Reference in New Issue
Block a user