package gameserver import ( "os" "path/filepath" "testing" "gac-hkrpg-go/internal/common" kcplib "gac-hkrpg-go/pkg/kcp" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) func TestCurrencyWarsXaodevEncounterAdvancesSelectsAndDoesNotDuplicateGold(t *testing.T) { c, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatal(err) } refs, err := common.LoadCurrencyWarsReferenceNodes(c) if err != nil { t.Fatal(err) } var route []common.CurrencyWarsRouteNode var index int for _, r := range c.StageRoutes { rows, err := c.RouteNodes(r.RouteID) if err != nil { t.Fatal(err) } for i, n := range rows { tmpl, err := refs.Template(n.NodeTemplateID) if err == nil && tmpl.NodeType == "EliteBranch" && i > 0 { route, index = rows, i break } } if route != nil { break } } if route == nil { t.Fatal("no Encounter route") } // Build only typed components needed for a detached route transaction. target, previous := route[index], route[index-1] stage := &pb.BEEFLEJAOCN{EMOHLJHPOPC: target.RouteID, DCAHJELNJNP: previous.ChapterID, SectionId: previous.SectionID} policy, err := cwLoadReferenceEnemies(c) if err != nil { t.Fatal(err) } var campID uint32 x, err := common.LoadCurrencyWarsXaodev(c) if err != nil { t.Fatal(err) } for _, camp := range c.MonsterCamps { if camp.SeasonID != c.ActiveSeasonID { continue } for _, id := range camp.Season.MonsterList { if policy.MonsterTiers[id] == x.Policy.MonsterTier { campID = camp.ID break } } if campID != 0 { break } } if campID == 0 { t.Fatal("no eligible camp") } // Obtain camp-list element type from its descriptor instead of copying // an obfuscated generated type name into the fixture. field := stage.ProtoReflect().Descriptor().Fields().ByName("HEKMEJLMGFF") list := stage.ProtoReflect().Mutable(field).List() for i := uint32(0); i < target.ChapterID; i++ { entry := list.NewElement() f := entry.Message().Descriptor().Fields().ByName("LOGHLGKIFEN") entry.Message().Set(f, protoreflect.ValueOfUint32(campID)) list.Append(entry) } for _, n := range route { tmpl, err := refs.Template(n.NodeTemplateID) if err != nil { t.Fatal(err) } var kind uint32 for _, k := range c.NodeTypes { if k.Name == tmpl.NodeType { kind = k.Value } } stage.OOMMIIKPHBC = append(stage.OOMMIIKPHBC, &pb.BCCIJHPLJOJ{DCAHJELNJNP: n.ChapterID, SectionId: n.SectionID, DisplayValue: &pb.OHMDHKGAEMM{StageId: tmpl.StageID, CJCCMAACJDB: kind, PICMDONCKCO: tmpl.BasicGoldRewardNum}}) } player := &pb.NIHNGPKHEPC{Gold: 10} var divisionID uint32 for _, d := range x.Divisions { if d.BinaryNodeDiffAddRule != 0 { divisionID = d.DivisionID break } } run := &pb.MPIMAEMCEFH{UniqueId: 1, DivisionId: c.CapturedStages[0].ID, LJNGPCKPJLF: c.ActiveSeasonID, FMIGOKIDKJL: &pb.CHCHAJIANIM{QueuePosition: 10}, NAGDJGMMCBE: []*pb.GridFightGameInfo{ {LEOIAOFBBFA: &pb.GridFightGameInfo_KLCPAAHDPJF{KLCPAAHDPJF: stage}}, {LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: player}}, }} runtime := &common.CurrencyWarsRuntimeState{RunID: run.UniqueId, Rules: map[string]uint32{}} run.DivisionId = divisionID if _, err := cwAdvanceRouteNode(c, run, runtime); err != nil { t.Fatal(err) } layer := stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ() if len(layer.BPDABHGFBFO) != 2 || !cwXaodevGenerated(run, runtime) { t.Fatal("generated branch/source missing") } _, display := cwStage(run) again, err := cwGenerateXaodevEncounter(c, run, display) if err != nil || !proto.Equal(stage.IMPNAHBCADN, again) { t.Fatalf("Encounter generation unstable: %v", err) } pending, err := cwGeneratedRoundPreparation(c, run) if err != nil || pending.GetBAKAKODKECM() == nil { t.Fatalf("branch pending missing: %v", err) } run.FMIGOKIDKJL = pending if _, err := cwSelectEncounter(c, run, &pb.OHAINOHGKCM{MOADBALHMFD: 3}, runtime); err == nil { t.Fatal("unadvertised branch accepted") } notify, err := cwSelectEncounter(c, run, &pb.OHAINOHGKCM{MOADBALHMFD: 2}, runtime) if err != nil { t.Fatal(err) } if notify.GCPNIHHBGGD[0].JKPJFKCBIEM[1].GetCEDMLOEDPEH() == nil { t.Fatal("difficulty not synchronized with branch") } add, err := x.BranchDifficulty(run.DivisionId, layer.BPDABHGFBFO[1].CJADMFDOBAI) if err != nil { t.Fatal(err) } base, _ := c.CapturedStage(run.DivisionId) if stage.ANJBLBEOJND.PIJBPDPEODG != base.EnemyHardLevel+add { t.Fatal("branch difficulty differs") } if _, err := cwEncounterRewards(c, run, layer.BPDABHGFBFO[1], 100, 0, runtime); err != nil { t.Fatal(err) } if player.Gold != 10 { t.Fatal("preview gold awarded twice") } before := proto.Clone(run) if _, err := cwXaodevDifficulty(c, run, ^uint32(0)); err == nil || !proto.Equal(before, run) { t.Fatal("invalid difficulty changed run") } } func findRepoRoot() string { dir, err := os.Getwd() if err != nil { return "." } for i := 0; i < 6; i++ { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return "." } func TestCurrencyWarsSupplyToEncounterBattleSettlementPipeline(t *testing.T) { repoRoot := findRepoRoot() tempDir := filepath.Join(repoRoot, "test_tmp_cw_integration") if err := os.MkdirAll(tempDir, 0755); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) t.Chdir(tempDir) baseCfg, err := os.ReadFile(filepath.Join(repoRoot, "server.json")) if err != nil { t.Fatal(err) } if err := os.WriteFile("server.json", baseCfg, 0600); err != nil { t.Fatal(err) } if err := os.WriteFile("persistent", []byte(`{}`), 0600); err != nil { t.Fatal(err) } t.Setenv("GAC_CONFIG", "server.json") c, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatal(err) } wd, _ := os.Getwd() t.Logf("pipeline test wd=%s, repoRoot=%s", wd, repoRoot) refs, err := common.LoadCurrencyWarsReferenceNodes(c) if err != nil { t.Fatalf("LoadCurrencyWarsReferenceNodes failed at wd=%s: %v", wd, err) } x, err := common.LoadCurrencyWarsXaodev(c) if err != nil { t.Fatal(err) } routeNodes, err := c.RouteNodes(2801) if err != nil { t.Fatal(err) } policy, err := cwLoadReferenceEnemies(c) if err != nil { t.Fatal(err) } var campID uint32 for _, camp := range c.MonsterCamps { if camp.SeasonID != c.ActiveSeasonID { continue } for _, id := range camp.Season.MonsterList { if policy.MonsterTiers[id] == x.Policy.MonsterTier { campID = camp.ID break } } if campID != 0 { break } } if campID == 0 { t.Fatal("no eligible camp") } var divisionID uint32 for _, d := range x.Divisions { if d.BinaryNodeDiffAddRule != 0 { divisionID = d.DivisionID break } } if divisionID == 0 { t.Fatal("no division with difficulty add rules") } stage := &pb.BEEFLEJAOCN{EMOHLJHPOPC: 2801, DCAHJELNJNP: 2, SectionId: 2} field := stage.ProtoReflect().Descriptor().Fields().ByName("HEKMEJLMGFF") list := stage.ProtoReflect().Mutable(field).List() for i := uint32(0); i < 4; i++ { entry := list.NewElement() f := entry.Message().Descriptor().Fields().ByName("LOGHLGKIFEN") entry.Message().Set(f, protoreflect.ValueOfUint32(campID)) list.Append(entry) } for _, n := range routeNodes { tmpl, err := refs.Template(n.NodeTemplateID) if err != nil { t.Fatal(err) } var kind uint32 for _, k := range c.NodeTypes { if k.Name == tmpl.NodeType { kind = k.Value } } stage.OOMMIIKPHBC = append(stage.OOMMIIKPHBC, &pb.BCCIJHPLJOJ{ DCAHJELNJNP: n.ChapterID, SectionId: n.SectionID, DisplayValue: &pb.OHMDHKGAEMM{ StageId: tmpl.StageID, CJCCMAACJDB: kind, PICMDONCKCO: tmpl.BasicGoldRewardNum, }, }) } firstLevel := c.PlayerLevels[0] maxLevel := c.PlayerLevels[len(c.PlayerLevels)-1].Level player := &pb.NIHNGPKHEPC{ Gold: 50, GridFightLineupHp: 100, PPKKHHNEOHB: 100, AFFCFLFIFPH: firstLevel.AvatarMaxNumber, MICEOJBAKGN: 8, EHNJDIDABHD: firstLevel.Level, NDAINFLOLPJ: maxLevel, } var sampleVariant common.CurrencyWarsRoleVariant for _, v := range c.CapturedVariants { if v.Star == 1 { sampleVariant = v break } } if sampleVariant.Config.ID == 0 { t.Fatal("no star 1 role variant found") } team := &pb.HKFECLCHBFJ{ GridGameRoleList: []*pb.CDEAABODKCP{ { Id: sampleVariant.Config.ID, RoleStar: sampleVariant.Star, Pos: 1, UniqueId: 1, ConvertPropertyToFixpoint: map[uint32]uint32{1: 1000}, }, }, } shop := &pb.POCJDMMANBM{} bag := &pb.CONONEALNOF{} run := &pb.MPIMAEMCEFH{ UniqueId: 101, DivisionId: divisionID, LJNGPCKPJLF: c.ActiveSeasonID, NAGDJGMMCBE: []*pb.GridFightGameInfo{ {LEOIAOFBBFA: &pb.GridFightGameInfo_KLCPAAHDPJF{KLCPAAHDPJF: stage}}, {LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: player}}, {LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{EJIPINIKNCK: team}}, {LEOIAOFBBFA: &pb.GridFightGameInfo_EBHDIOGALIE{EBHDIOGALIE: shop}}, {LEOIAOFBBFA: &pb.GridFightGameInfo_GCKLIONKMJC{GCKLIONKMJC: bag}}, }, } s := &Session{kcp: kcplib.New(1, 1, false, func([]byte) error { return nil })} _ = s.kcp.Update(s.kcp.Now()) s.Token = 1 s.json = &common.FreesrData{ Avatars: map[uint32]*common.AvatarJson{ sampleVariant.Config.AvatarID: {AvatarID: sampleVariant.Config.AvatarID, Level: 80, Data: common.AvatarData{Rank: 6}}, }, } data, err := newCurrencyWarsData(c) if err != nil { t.Fatalf("newCurrencyWarsData: %v", err) } data.DHFOFLFCLAH = run data.LFLCDNGALME.HFPEJIDDDJG.DivisionId = divisionID supplyOffer, err := cwGenerateSupplyOffer(c, run) if err != nil { t.Fatalf("generate supply offer: %v", err) } run.FMIGOKIDKJL = &pb.CHCHAJIANIM{ QueuePosition: 1, FGLIODAJFML: &pb.CHCHAJIANIM_FIKAGBAEOAP{FIKAGBAEOAP: supplyOffer}, } runtime := &common.CurrencyWarsRuntimeState{RunID: run.UniqueId, Rules: map[string]uint32{}} if err := s.saveCurrencyWarsDataWithRuntime(data, c, runtime); err != nil { t.Fatalf("initial save: %v", err) } // 1. Supply Selection -> Advances to Chapter 2 Section 3 (EliteBranch) supplyReq := &pb.HMNMJKCHABE{LOAFKKAFBOK: []uint32{1}} if _, err := cwSelectSupply(c, run, supplyReq, runtime); err != nil { t.Fatalf("select supply: %v", err) } if stage.DCAHJELNJNP != 2 || stage.SectionId != 3 { t.Fatalf("expected advance to Ch2 Sec3, got Ch%d Sec%d", stage.DCAHJELNJNP, stage.SectionId) } if !cwXaodevGenerated(run, runtime) { t.Fatal("expected xaodev generated marker on Encounter node") } if err := s.saveCurrencyWarsDataWithRuntime(data, c, runtime); err != nil { t.Fatalf("save after supply: %v", err) } // 2. Round preparation sets pending to SelectingEncounter prepPending, err := cwGeneratedRoundPreparation(c, run) if err != nil || prepPending.GetBAKAKODKECM() == nil { t.Fatalf("prepare encounter pending: %v", err) } run.FMIGOKIDKJL = prepPending if err := s.saveCurrencyWarsDataWithRuntime(data, c, runtime); err != nil { t.Fatalf("save prep pending: %v", err) } // 3. Player selects Encounter Branch 2 (powerful branch) encounterLayer := stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ() if len(encounterLayer.BPDABHGFBFO) != 2 { t.Fatalf("expected 2 encounter branches, got %d", len(encounterLayer.BPDABHGFBFO)) } selReq := &pb.OHAINOHGKCM{MOADBALHMFD: 2} selNotify, err := cwSelectEncounter(c, run, selReq, runtime) if err != nil { t.Fatalf("select encounter branch: %v", err) } if selNotify == nil || len(selNotify.GCPNIHHBGGD) == 0 { t.Fatal("expected selection notify") } branchAdd, err := x.BranchDifficulty(run.DivisionId, encounterLayer.BPDABHGFBFO[1].CJADMFDOBAI) if err != nil { t.Fatalf("branch difficulty: %v", err) } baseStage, _ := c.CapturedStage(run.DivisionId) expectedHardLevel := baseStage.EnemyHardLevel + branchAdd if stage.ANJBLBEOJND.PIJBPDPEODG != expectedHardLevel { t.Fatalf("difficulty got %d, want %d", stage.ANJBLBEOJND.PIJBPDPEODG, expectedHardLevel) } if run.FMIGOKIDKJL.GetLGNPHHBGKFH() == nil { t.Fatal("expected pending ReturnPreparation after branch selection") } if err := s.saveCurrencyWarsDataWithRuntime(data, c, runtime); err != nil { t.Fatalf("save after encounter select: %v", err) } // 4. ReturnPreparation clears pending action if _, err := cwReturnPreparation(run); err != nil { t.Fatalf("return preparation: %v", err) } if run.FMIGOKIDKJL != nil { t.Fatal("expected pending cleared after ReturnPreparation") } if err := s.saveCurrencyWarsDataWithRuntime(data, c, runtime); err != nil { t.Fatalf("save after ReturnPreparation: %v", err) } // 5. Build Battle with buildCurrencyWarsBattle battleInfo, err := buildCurrencyWarsBattle(s, c, run) if err != nil { t.Fatalf("buildCurrencyWarsBattle: %v", err) } if battleInfo == nil || battleInfo.EIGMKOJOEEI == nil { t.Fatal("battle info or grid missing") } if battleInfo.EIGMKOJOEEI.LODNFLMBIEH != expectedHardLevel { t.Fatalf("battle grid hard level = %d, want %d", battleInfo.EIGMKOJOEEI.LODNFLMBIEH, expectedHardLevel) } if len(battleInfo.BattleAvatarList) == 0 { t.Fatal("battle avatar list empty") } // 6. Settlement with finishCurrencyWarsBattle CURRENT_BATTLE.Store(s.Token, battleInfo) settleReq := &pb.PVEBattleResultCsReq{ BattleId: battleInfo.BattleId, StageId: battleInfo.StageId, EndStatus: pb.BattleEndStatus_BATTLE_END_WIN, Stt: &pb.BattleStatistics{ APAGMJNECCN: &pb.MGMJOPHJCNL{ HFLPDCJDIAC: cwPercentScale, DOGMMGLBJIJ: []*pb.CPNCAPLOIHI{ {RoleId: sampleVariant.Config.ID, Damage: 50000}, }, }, }, } goldBefore := player.Gold if err := finishCurrencyWarsBattle(s, settleReq, battleInfo); err != nil { t.Fatalf("finishCurrencyWarsBattle: %v", err) } // 7. Verify state after settlement & next node transition data, _, err = readCurrencyWarsData(c) if err != nil { t.Fatalf("load reloaded data: %v", err) } run = data.DHFOFLFCLAH stage, _ = cwStage(run) player, _, _ = cwComponents(run) if stage.DCAHJELNJNP != 2 || stage.SectionId != 4 { t.Fatalf("expected advance to Ch2 Sec4, got Ch%d Sec%d", stage.DCAHJELNJNP, stage.SectionId) } if stage.ANJBLBEOJND.PIJBPDPEODG != baseStage.EnemyHardLevel { t.Fatalf("expected hard level reset to base %d, got %d", baseStage.EnemyHardLevel, stage.ANJBLBEOJND.PIJBPDPEODG) } if stage.ANJBLBEOJND.PIFNOKAMBLL[cwDifficultySourceBranch] != 0 { t.Fatalf("expected branch diff add 0, got %d", stage.ANJBLBEOJND.PIFNOKAMBLL[cwDifficultySourceBranch]) } if player.Gold <= goldBefore { t.Fatalf("gold should have increased from %d, got %d", goldBefore, player.Gold) } if player.GridFightCurLevelExp == 0 && player.EHNJDIDABHD <= 1 { t.Fatal("player exp should have increased from settlement") } // 8. Full CAS roundtrip verification from persistent storage reloadedData, _, err := readCurrencyWarsData(c) if err != nil { t.Fatalf("load reloaded data: %v", err) } reloadedStage, _ := cwStage(reloadedData.DHFOFLFCLAH) if reloadedStage.DCAHJELNJNP != 2 || reloadedStage.SectionId != 4 { t.Fatalf("reloaded stage = Ch%d Sec%d, want Ch2 Sec4", reloadedStage.DCAHJELNJNP, reloadedStage.SectionId) } if reloadedStage.ANJBLBEOJND.PIJBPDPEODG != baseStage.EnemyHardLevel { t.Fatalf("reloaded hard level = %d, want %d", reloadedStage.ANJBLBEOJND.PIJBPDPEODG, baseStage.EnemyHardLevel) } if reloadedStage.ANJBLBEOJND.PIFNOKAMBLL[cwDifficultySourceBranch] != 0 { t.Fatalf("reloaded branch diff = %d, want 0", reloadedStage.ANJBLBEOJND.PIFNOKAMBLL[cwDifficultySourceBranch]) } if s.currencyWarsRevision < 5 { t.Fatalf("expected monotonic CAS revisions >= 5, got %d", s.currencyWarsRevision) } }