package common import ( "bytes" "encoding/json" "os" "strings" "testing" ) func loadCurrencyWarsTestCatalog(t *testing.T) *CurrencyWarsCatalog { t.Helper() raw, err := ReadResourceFile("resources/CurrencyWarsCatalog.json") if err != nil { t.Fatal(err) } var catalog CurrencyWarsCatalog if err := json.Unmarshal(raw, &catalog); err != nil { t.Fatal(err) } if err := catalog.validate(catalog.ClientVersion); err != nil { t.Fatalf("validate captured CW catalog: %v", err) } if err := catalog.resolveActivity(); err != nil { t.Fatalf("resolve captured CW activities: %v", err) } if err := catalog.validateRoutes(); err != nil { t.Fatalf("validate captured CW routes: %v", err) } if err := catalog.validateCapturedGameplay(); err != nil { t.Fatalf("validate captured CW role/stage data: %v", err) } if err := catalog.validateEntrance(); err != nil { t.Fatalf("validate captured CW entrance: %v", err) } return &catalog } func TestCurrencyWarsCapturedCatalogAndConfiguredVersion(t *testing.T) { catalog := loadCurrencyWarsTestCatalog(t) if len(catalog.ActivitySchedules()) == 0 || len(catalog.CapturedRoles) == 0 || len(catalog.CapturedStages) == 0 { t.Fatal("captured CW activity, role, or stage data is empty") } if err := catalog.validate(catalog.ClientVersion + "-mismatch"); err == nil { t.Fatal("CW catalog accepted a different Client version") } } func TestCurrencyWarsCatalogLookupsReturnIsolatedCopies(t *testing.T) { catalog := loadCurrencyWarsTestCatalog(t) roleBefore, err := catalog.CapturedRole(catalog.CapturedRoles[0].Config.ID) if err != nil { t.Fatal(err) } role, err := catalog.CapturedRole(catalog.CapturedRoles[0].Config.ID) if err != nil { t.Fatal(err) } if len(role.Config.TraitList) > 0 { role.Config.TraitList[0]++ } roleAfter, err := catalog.CapturedRole(catalog.CapturedRoles[0].Config.ID) if err != nil { t.Fatal(err) } if len(roleBefore.Config.TraitList) > 0 && roleAfter.Config.TraitList[0] != roleBefore.Config.TraitList[0] { t.Fatal("mutating a returned CW role changed catalog data") } stageBefore, err := catalog.CapturedStage(catalog.CapturedStages[0].ID) if err != nil { t.Fatal(err) } stage, err := catalog.CapturedStage(catalog.CapturedStages[0].ID) if err != nil { t.Fatal(err) } if len(stage.EnvironmentBuffList) > 0 { stage.EnvironmentBuffList[0]++ } stageAfter, err := catalog.CapturedStage(catalog.CapturedStages[0].ID) if err != nil { t.Fatal(err) } if len(stageBefore.EnvironmentBuffList) > 0 && stageAfter.EnvironmentBuffList[0] != stageBefore.EnvironmentBuffList[0] { t.Fatal("mutating a returned CW stage changed catalog data") } routeID := catalog.StageRoutes[0].RouteID routeBefore, err := catalog.RouteNodes(routeID) if err != nil { t.Fatal(err) } route, err := catalog.RouteNodes(routeID) if err != nil { t.Fatal(err) } route[0].NodeTemplateID++ routeAfter, err := catalog.RouteNodes(routeID) if err != nil { t.Fatal(err) } if routeAfter[0].NodeTemplateID != routeBefore[0].NodeTemplateID { t.Fatal("mutating a returned CW route changed catalog data") } } func TestCurrencyWarsCapturedEconomyRules(t *testing.T) { catalog := loadCurrencyWarsTestCatalog(t) role := catalog.CapturedRoles[0] price, err := catalog.BaseSalePrice(role.Config.ID, role.DefaultStar) if err != nil { t.Fatal(err) } if price != role.DefaultSellPrice { t.Fatalf("captured base sale price = %d, want native row value %d", price, role.DefaultSellPrice) } if _, err := catalog.BaseSalePrice(role.Config.ID, role.MaxStar+1); err == nil { t.Fatal("sale price accepted an uncaptured star rank") } deposit, err := catalog.IntegerRule("GridFight_DepositPerInterest") if err != nil || deposit <= 0 { t.Fatalf("invalid captured deposit rule %d: %v", deposit, err) } gold := uint32(deposit*3 + deposit/2) interest, err := catalog.BaseInterest(gold, 2) if err != nil || interest != 2 { t.Fatalf("base interest = %d, %v; want integer division capped at 2", interest, err) } zeroCapped, err := catalog.BaseInterest(gold, 0) if err != nil || zeroCapped != 0 { t.Fatalf("zero-cap interest = %d, %v; want 0", zeroCapped, err) } } func TestCurrencyWarsSnapshotRevisionPreventsStaleWrites(t *testing.T) { t.Chdir(t.TempDir()) const assemblyHash = "abababababababababababababababababababababababababababababababab" if err := os.WriteFile("persistent", []byte(`{"other_service":{"counter":9007199254740993}}`), 0600); err != nil { t.Fatal(err) } first := NewCurrencyWarsSnapshot(42, "4.6.53", assemblyHash, []byte{0x0a, 0x01, 0x01}) revision, err := CommitCurrencyWarsSnapshot(0, first) if err != nil || revision != 1 { t.Fatalf("first CW commit = revision %d, %v; want revision 1", revision, err) } stale := NewCurrencyWarsSnapshot(42, "4.6.53", assemblyHash, []byte{0x0a, 0x01, 0x02}) if _, err := CommitCurrencyWarsSnapshot(0, stale); err != ErrCurrencyWarsSaveConflict { t.Fatalf("stale CW commit error = %v; want save conflict", err) } saved, err := ReadCurrencyWarsSnapshot(42, "4.6.53", assemblyHash) if err != nil || saved.Revision != 1 || !bytes.Equal(saved.WireData, first.WireData) { t.Fatalf("stale write changed saved CW snapshot: %+v, %v", saved, err) } next := NewCurrencyWarsSnapshot(42, "4.6.53", assemblyHash, []byte{0x0a, 0x01, 0x03}) revision, err = CommitCurrencyWarsSnapshot(1, next) if err != nil || revision != 2 { t.Fatalf("next CW commit = revision %d, %v; want revision 2", revision, err) } if _, err := ReadCurrencyWarsSnapshot(42, "6.1", assemblyHash); err == nil { t.Fatal("CW snapshot accepted a different Client version") } persistent, err := os.ReadFile("persistent") if err != nil { t.Fatal(err) } if !bytes.Contains(persistent, []byte(`"other_service"`)) || !bytes.Contains(persistent, []byte("9007199254740993")) { t.Fatalf("CW commit failed to preserve unrelated persistent data: %s", strings.TrimSpace(string(persistent))) } }