feat: migrate state and complete live systems
- replace the Haskell bridge with transactional SQLite state and validation - centralize versions and harden resource, capture, and release tooling - complete active gacha schedules, point exchange, and mail grants - add equipment refinement, presets, and ordinary formation workflows - improve client plugins, request timing, and normalized persistence
This commit is contained in:
+20
-120
@@ -1,151 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
controlv1 "bd2server/gen/state/control/v1"
|
||||
"bd2server/internal/gacha"
|
||||
"bd2server/internal/gamedata"
|
||||
"bd2server/internal/player"
|
||||
"bd2server/internal/statebridge"
|
||||
"bd2server/internal/accountstate"
|
||||
)
|
||||
|
||||
func stateCommand(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("usage: bd2server state <check|migrate-v1-v2|repair> [options]")
|
||||
}
|
||||
switch args[0] {
|
||||
case "check":
|
||||
return stateCheckCommand(args[1:])
|
||||
case "migrate-v1-v2":
|
||||
return stateMigrateV1ToV2Command(args[1:])
|
||||
case "repair":
|
||||
return stateRepairCommand(args[1:])
|
||||
default:
|
||||
return errors.New("usage: bd2server state <check|migrate-v1-v2|repair> [options]")
|
||||
if len(args) == 0 || args[0] != "check" {
|
||||
return errors.New("usage: bd2server state check [options]")
|
||||
}
|
||||
return stateCheckCommand(args[1:])
|
||||
}
|
||||
|
||||
func stateCheckCommand(args []string) error {
|
||||
fs := flag.NewFlagSet("state check", flag.ContinueOnError)
|
||||
stateDir := fs.String("state", filepath.FromSlash("../data/state"), "directory containing the nine JSON state files")
|
||||
tool := fs.String("tool", "", "optional Haskell state tool override")
|
||||
stateDB := fs.String("state", filepath.FromSlash("../data/state/state.db"), "account SQLite database")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
resolved, cleanup, err := statebridge.ResolveTool(*tool)
|
||||
repository, err := accountstate.Open(filepath.Clean(*stateDB))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
result, err := statebridge.Check(context.Background(), filepath.Clean(*stateDir), resolved)
|
||||
defer repository.Close()
|
||||
version, err := repository.SchemaVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("state SHA-256: %s\n", hex.EncodeToString(result.SourceSHA256[:]))
|
||||
var errorsFound int
|
||||
for _, violation := range result.Violations {
|
||||
severity := violation.Severity.String()
|
||||
fmt.Printf("%s %s %s: %s", severity, violation.Code, filepath.Join(violation.Path...), violation.Message)
|
||||
if len(violation.RelatedIds) != 0 {
|
||||
fmt.Printf(" ids=%v", violation.RelatedIds)
|
||||
problems, err := repository.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("schema version: %d\n", version)
|
||||
for _, problem := range problems {
|
||||
fmt.Printf("ERROR %s %s: %s", problem.Code, filepath.Join(problem.Path...), problem.Message)
|
||||
if len(problem.RelatedIDs) != 0 {
|
||||
fmt.Printf(" ids=%v", problem.RelatedIDs)
|
||||
}
|
||||
fmt.Println()
|
||||
if violation.Severity == controlv1.Severity_SEVERITY_ERROR {
|
||||
errorsFound++
|
||||
}
|
||||
}
|
||||
if errorsFound != 0 {
|
||||
return fmt.Errorf("state check rejected with %d error(s)", errorsFound)
|
||||
}
|
||||
fmt.Printf("state check passed (%d warning(s))\n", len(result.Violations))
|
||||
return nil
|
||||
}
|
||||
|
||||
func stateMigrateV1ToV2Command(args []string) error {
|
||||
fs := flag.NewFlagSet("state migrate-v1-v2", flag.ContinueOnError)
|
||||
stateDir := fs.String("state", filepath.FromSlash("../data/state"), "directory containing the nine JSON state files")
|
||||
tool := fs.String("tool", "", "optional Haskell state tool override")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
resolved, cleanup, err := statebridge.ResolveTool(*tool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
result, err := statebridge.MigrateV1ToV2(context.Background(), filepath.Clean(*stateDir), resolved)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("source JSON SHA-256: %s\n", hex.EncodeToString(result.SourceSHA256[:]))
|
||||
for _, violation := range result.Violations {
|
||||
fmt.Printf("%s %s %s: %s\n", violation.Severity.String(), violation.Code, filepath.Join(violation.Path...), violation.Message)
|
||||
}
|
||||
if result.Target == nil {
|
||||
return errors.New("state migration rejected; no files were changed")
|
||||
}
|
||||
fmt.Printf("target protobuf SHA-256: %s\n", hex.EncodeToString(result.TargetSHA256[:]))
|
||||
fmt.Printf("V1 -> V2 migration passed: %d character(s), %d costume(s); no files were changed\n",
|
||||
len(result.Target.Roster.Characters), len(result.Target.Roster.Costumes))
|
||||
return nil
|
||||
}
|
||||
|
||||
func stateRepairCommand(args []string) error {
|
||||
fs := flag.NewFlagSet("state repair", flag.ContinueOnError)
|
||||
stateDir := fs.String("state", filepath.FromSlash("../data/state"), "directory containing the nine JSON state files")
|
||||
tool := fs.String("tool", "", "optional Haskell state tool override")
|
||||
gameData := fs.String("game-data", "", "versioned GameData root (required)")
|
||||
gameDataVersion := fs.String("game-data-version", "", "validated GameData version (required)")
|
||||
playerSeed := fs.String("player-seed", `seed\v2_34_13\starter_player.json`, "versioned starter player seed")
|
||||
apply := fs.Bool("apply", false, "install a changed repair result with rollback protection")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *gameData == "" || *gameDataVersion == "" {
|
||||
return errors.New("state repair requires --game-data and --game-data-version")
|
||||
}
|
||||
resolved, cleanup, err := statebridge.ResolveTool(*tool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
catalog, err := gamedata.LoadRegularCostumeGacha(filepath.Clean(*gameData), *gameDataVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
starter, err := player.Load(filepath.Clean(*playerSeed))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupID, gachaIDs := gacha.StepUpMigrationFact()
|
||||
validation, err := statebridge.BuildValidationContext(*gameDataVersion, catalog, starter, groupID, gachaIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := statebridge.Repair(context.Background(), filepath.Clean(*stateDir), resolved, validation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("source JSON SHA-256: %s\n", hex.EncodeToString(result.SourceSHA256[:]))
|
||||
if result.Target == nil {
|
||||
return violationsError("state repair rejected", result.Violations)
|
||||
}
|
||||
fmt.Printf("target protobuf SHA-256: %s\nchanged: %t\n", hex.EncodeToString(result.TargetSHA256[:]), result.Changed)
|
||||
if result.Changed && *apply {
|
||||
if err := statebridge.InstallRepair(filepath.Clean(*stateDir), result); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("repair installed; rollback protection cleared after complete commit")
|
||||
} else if result.Changed {
|
||||
fmt.Println("dry run only; pass --apply to install")
|
||||
} else {
|
||||
fmt.Println("state is already current; no files changed")
|
||||
if len(problems) != 0 {
|
||||
return fmt.Errorf("state check rejected with %d error(s)", len(problems))
|
||||
}
|
||||
fmt.Println("state check passed")
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user