From 8785f146b68cd88fdfcff09531d6fd839943d9fc Mon Sep 17 00:00:00 2001 From: Flechazo <2558755403@qq.com> Date: Mon, 5 Oct 2026 12:30:54 +0800 Subject: [PATCH] refactor(server): generate runtime configs and use .build/config for development --- AUTHENTICATION.md | 2 +- Dockerfile.server | 4 +- Dockerfile.server.dockerignore | 3 -- GAME_CONFIGURATION.md | 4 +- README.md | 2 +- authentication.json | 4 -- build-release.ps1 | 6 --- game.json | 16 ------ go/cmd/bd2server/dev_command.go | 10 ++-- go/cmd/bd2server/dev_command_test.go | 24 ++++++++- go/internal/server/authconfig/config.go | 7 +++ go/internal/server/configfile/default.go | 51 ++++++++++++++++++++ go/internal/server/gameconfig/config.go | 11 +++-- go/internal/server/gameconfig/config_test.go | 6 +-- go/internal/server/resourcepolicy/config.go | 5 ++ resources.json | 3 -- 16 files changed, 105 insertions(+), 53 deletions(-) delete mode 100644 authentication.json delete mode 100644 game.json create mode 100644 go/internal/server/configfile/default.go delete mode 100644 resources.json diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md index 7756385..73c08f5 100644 --- a/AUTHENTICATION.md +++ b/AUTHENTICATION.md @@ -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 { diff --git a/Dockerfile.server b/Dockerfile.server index c6896c2..2f41c02 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -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"] diff --git a/Dockerfile.server.dockerignore b/Dockerfile.server.dockerignore index be567cb..7c3e7a8 100644 --- a/Dockerfile.server.dockerignore +++ b/Dockerfile.server.dockerignore @@ -2,8 +2,5 @@ !go/ !go/** !versions.json -!authentication.json -!resources.json -!game.json !schedules/ !schedules/** diff --git a/GAME_CONFIGURATION.md b/GAME_CONFIGURATION.md index 928c234..42a0b83 100644 --- a/GAME_CONFIGURATION.md +++ b/GAME_CONFIGURATION.md @@ -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` 指定路径,重启后生效。 diff --git a/README.md b/README.md index c3b68ff..eb36410 100644 --- a/README.md +++ b/README.md @@ -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`。 diff --git a/authentication.json b/authentication.json deleted file mode 100644 index b971679..0000000 --- a/authentication.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mode": "local", - "providers": {} -} diff --git a/build-release.ps1 b/build-release.ps1 index 42a3911..df593a3 100644 --- a/build-release.ps1 +++ b/build-release.ps1 @@ -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 diff --git a/game.json b/game.json deleted file mode 100644 index ebee8b9..0000000 --- a/game.json +++ /dev/null @@ -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 - } - } -} diff --git a/go/cmd/bd2server/dev_command.go b/go/cmd/bd2server/dev_command.go index 685f0e8..16ee9e6 100644 --- a/go/cmd/bd2server/dev_command.go +++ b/go/cmd/bd2server/dev_command.go @@ -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) diff --git a/go/cmd/bd2server/dev_command_test.go b/go/cmd/bd2server/dev_command_test.go index db3d5ad..68c559a 100644 --- a/go/cmd/bd2server/dev_command_test.go +++ b/go/cmd/bd2server/dev_command_test.go @@ -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"}} { diff --git a/go/internal/server/authconfig/config.go b/go/internal/server/authconfig/config.go index a1fd233..26ac7af 100644 --- a/go/internal/server/authconfig/config.go +++ b/go/internal/server/authconfig/config.go @@ -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) diff --git a/go/internal/server/configfile/default.go b/go/internal/server/configfile/default.go new file mode 100644 index 0000000..06821cd --- /dev/null +++ b/go/internal/server/configfile/default.go @@ -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 +} diff --git a/go/internal/server/gameconfig/config.go b/go/internal/server/gameconfig/config.go index 777f938..b24d5ad 100644 --- a/go/internal/server/gameconfig/config.go +++ b/go/internal/server/gameconfig/config.go @@ -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) } diff --git a/go/internal/server/gameconfig/config_test.go b/go/internal/server/gameconfig/config_test.go index 1400b0e..e42ce8f 100644 --- a/go/internal/server/gameconfig/config_test.go +++ b/go/internal/server/gameconfig/config_test.go @@ -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) } } diff --git a/go/internal/server/resourcepolicy/config.go b/go/internal/server/resourcepolicy/config.go index b43b0d8..28e441d 100644 --- a/go/internal/server/resourcepolicy/config.go +++ b/go/internal/server/resourcepolicy/config.go @@ -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) diff --git a/resources.json b/resources.json deleted file mode 100644 index dd93c9f..0000000 --- a/resources.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "mode": "official" -}