Files
bd2/go/cmd/bd2server/preflight.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00

156 lines
5.0 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"bd2server/internal/server/account"
"bd2server/internal/server/authconfig"
"bd2server/internal/server/calendar"
"bd2server/internal/server/deck"
"bd2server/internal/server/gameconfig"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/mail"
"bd2server/internal/server/player"
"bd2server/internal/server/readonly"
"bd2server/internal/server/resourcepolicy"
"bd2server/internal/server/versionconfig"
)
// preflight validates the candidate binary's immutable configuration without
// opening state.db or claiming writer_epoch. Deployment may run it while the
// old instance is still active, then drain the old writer before activation.
func preflight(args []string) error {
fs := flag.NewFlagSet("preflight", flag.ContinueOnError)
dataDir := fs.String("data-dir", "", "server data directory")
versionPath := fs.String("version-config", "", "repository versions.json override")
authPath := fs.String("authentication-config", "", "authentication.json override")
resourcePath := fs.String("resource-config", "", "resources.json override")
gamePath := fs.String("game-config", "", "game.json server gameplay configuration override")
if err := fs.Parse(args); err != nil {
return err
}
versions, err := versionconfig.Find()
if *versionPath != "" {
versions, err = versionconfig.Load(*versionPath)
}
if err != nil {
return err
}
versionconfig.Use(versions)
if *gamePath == "" {
*gamePath, err = gameconfig.BesideExecutable()
if err != nil {
return err
}
}
gameRules, err := gameconfig.Load(*gamePath)
if err != nil {
return err
}
if *authPath == "" {
*authPath, err = authconfig.BesideExecutable()
if err != nil {
return err
}
}
authentication, err := authconfig.Load(*authPath)
if err != nil {
return err
}
runtime, err := authentication.ResolveEnvironment()
if err != nil {
return err
}
clear(runtime.MasterKey)
if *resourcePath == "" {
*resourcePath, err = resourcepolicy.BesideExecutable()
if err != nil {
return err
}
}
if _, err := resourcepolicy.Load(*resourcePath); err != nil {
return err
}
if *dataDir == "" {
executable, err := os.Executable()
if err != nil {
return err
}
*dataDir = filepath.Join(filepath.Dir(executable), "data")
}
gameData := filepath.Join(filepath.Clean(*dataDir), "resources", "GameData")
if _, _, err := gamedata.Ensure(context.Background(), nil, gameData, versions.GameDataVersion, resourcepolicy.OfficialGameDataURL); err != nil {
return fmt.Errorf("preflight GameData: %w", err)
}
seedRoot := versions.Resolve(versions.SeedDirectory)
login, err := account.Load(filepath.Join(seedRoot, "login_user.json"))
if err != nil {
return err
}
starter, err := player.Load(filepath.Join(seedRoot, "starter_player.json"))
if err != nil {
return err
}
if login.Version != versions.GameVersion || starter.Version != versions.GameVersion {
return errorsVersionMismatch(versions.GameVersion, login.Version, starter.Version)
}
if _, err := mail.Load(filepath.Join(seedRoot, "mail.json")); err != nil {
return err
}
if _, err := deck.LoadSeed(filepath.Join(seedRoot, "decks.json")); err != nil {
return err
}
defaults, err := readonly.Load(filepath.Join(seedRoot, "readonly.json"))
if err != nil {
return err
}
calendars, err := calendar.LoadDirectory(versions.Resolve("schedules"), versions.GameVersion, versions.GameDataVersion)
if err != nil {
return err
}
if calendars.RegularService == nil || calendars.MonsterHunt == nil || len(calendars.MonsterHunt.Seasons) == 0 {
return fmt.Errorf("project calendars require regular content and monster hunt schedules")
}
if _, err := calendars.ApplyReadonly(defaults); err != nil {
return err
}
if err := calendars.ValidateDesign(gameData, versions.GameDataVersion); err != nil {
return fmt.Errorf("preflight project calendar GameData references: %w", err)
}
schedule := calendars.GachaSeed
var groups, steps []uint64
for _, window := range schedule.Schedules {
groups = append(groups, window.GroupID)
}
for _, window := range schedule.StepUps {
steps = append(steps, window.GroupID)
}
_, equipment, err := gamedata.LoadActiveGachaForSchedules(gameData, versions.GameDataVersion, groups, steps)
if err != nil {
return err
}
if gameRules.Gacha.IncludeCollaborationURWeapons {
if err := equipment.IncludeCollaborationURWeapons(gameData, versions.GameDataVersion); err != nil {
return fmt.Errorf("preflight collaboration UR weapon game rule: %w", err)
}
}
if _, err := gamedata.LoadFirstGacha(gameData, versions.GameDataVersion); err != nil {
return err
}
if _, err := gamedata.LoadLimitedCostumes(gameData, versions.GameDataVersion); err != nil {
return err
}
if _, err := gamedata.LoadCostumeBurstDesign(gameData, versions.GameDataVersion); err != nil {
return err
}
return nil
}
func errorsVersionMismatch(want, login, player string) error {
return fmt.Errorf("game version %s requires matching account and player seeds (got %s and %s)", want, login, player)
}