refactor(server): generate runtime configs and use .build/config for development

This commit is contained in:
2026-10-05 12:40:07 +08:00
parent d8771caef2
commit 3a9ebfdded
17 changed files with 105 additions and 122 deletions
+7
View File
@@ -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)
+51
View File
@@ -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
}
+6 -5
View File
@@ -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)
}
+3 -3
View File
@@ -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)