package gameserver import ( "bytes" "os" "testing" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) func TestCurrencyWarsTalentsHandlers(t *testing.T) { before, err := os.ReadFile("persistent") if err != nil { t.Fatal(err) } s := &Session{Token: 12345} requests := map[string]proto.Message{ "GridFightPermanentTalentEnableCsReq": &pb.GridFightPermanentTalentEnableCsReq{CKHCLKOALKE: ^uint32(0)}, "GridFightSeasonTalentEnableCsReq": &pb.GridFightSeasonTalentEnableCsReq{CKHCLKOALKE: ^uint32(0)}, "GridFightPermanentTalentResetCsReq": &pb.GridFightPermanentTalentResetCsReq{}, "GridFightSeasonTalentResetCsReq": &pb.GridFightSeasonTalentResetCsReq{}, } for name, req := range requests { raw, _ := proto.Marshal(req) if err := typedHandlers[uint16(pb.CmdIDByName[name])](s, raw); err != nil { t.Fatal(err) } after, err := os.ReadFile("persistent") if err != nil { t.Fatal(err) } if !bytes.Equal(before, after) { t.Fatalf("unsupported talent request changed persistent: %s", name) } } } func TestCurrencyWarsArchiveHandlers(t *testing.T) { c, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatal(err) } data, revision, err := readCurrencyWarsData(c) if err != nil { t.Fatal(err) } if data == nil { data, err = newCurrencyWarsData(c) if err != nil { t.Fatal(err) } } session := &Session{Token: 54321, currencyWarsRevision: revision} record := &pb.OEDHMAHDECC{Reason: pb.PEIJNEFOCHI_PEIJNEFOCHI_JOJPGBCECLN} if err := session.saveCurrencyWarsTerminal(data, c, record); err != nil { t.Fatal(err) } snapshot, err := cwArchiveSnapshot(c) if err != nil { t.Fatal(err) } entries, err := cwReadArchives(snapshot) if err != nil || len(entries) == 0 { t.Fatalf("read archive: %v", err) } entry := entries[len(entries)-1] raw, _ := proto.Marshal(&pb.FavourArchiveCsReq{BAAGDILLLME: entry.BAAGDILLLME, IECKKCHJMLP: true}) if err := typedHandlers[uint16(pb.CmdIDByName["FavourArchiveCsReq"])](session, raw); err != nil { t.Fatal(err) } // A new read/session has no in-memory archive state to rely on. snapshot, err = cwArchiveSnapshot(c) if err != nil { t.Fatal(err) } entries, err = cwReadArchives(snapshot) if err != nil { t.Fatal(err) } if !entries[len(entries)-1].IECKKCHJMLP { t.Fatal("favourite was not persisted") } stale := &Session{currencyWarsRevision: revision} if err := stale.saveCurrencyWarsTerminal(data, c, record); err == nil { t.Fatal("stale settlement appended archive") } again, err := cwArchiveSnapshot(c) if err != nil { t.Fatal(err) } if len(again.Archives) != len(snapshot.Archives) { t.Fatal("failed transaction changed archives") } if err := session.saveCurrencyWarsData(data, c); err != nil { t.Fatal(err) } again, err = cwArchiveSnapshot(c) if err != nil { t.Fatal(err) } if len(again.Archives) != len(snapshot.Archives) { t.Fatal("ordinary save lost archives") } } func TestCurrencyWarsCalculateBoardTraits(t *testing.T) { catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatalf("load catalog: %v", err) } // Role 100402 (Welt) in Season 2 has TraitList: [ 100502, 200302 ] run := &pb.MPIMAEMCEFH{ NAGDJGMMCBE: []*pb.GridFightGameInfo{ { LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{ EJIPINIKNCK: &pb.HKFECLCHBFJ{ GridGameRoleList: []*pb.CDEAABODKCP{ {Id: 100402, RoleStar: 1, Pos: 1, UniqueId: 101}, {Id: 100402, RoleStar: 1, Pos: 2, UniqueId: 102}, }, }, }, }, }, } traits := cwCalculateBoardTraits(catalog, run) if len(traits) == 0 { t.Fatal("expected computed traits for deployed roles") } foundTrait := false for _, tr := range traits { if tr.TraitId == 100502 || tr.TraitId == 200302 { foundTrait = true break } } if !foundTrait { t.Errorf("expected Welt traits to be computed, got %v", traits) } } func TestCurrencyWarsOrbDropsAndClaim(t *testing.T) { origPersistent, _ := os.ReadFile("persistent") if origPersistent != nil { t.Cleanup(func() { _ = os.WriteFile("persistent", origPersistent, 0644) }) } catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatalf("load catalog: %v", err) } // 1. Test cwRollNodeOrbs for various stages in Route 2801 // 1-1: 900 -> 4 rolls orbs1_1, err := cwRollNodeOrbs(catalog, nil, 2801, 1, 1, 100) if err != nil { t.Fatalf("cwRollNodeOrbs 1-1: %v", err) } if len(orbs1_1) != 4 { t.Errorf("expected 4 orbs from 1-1, got %d", len(orbs1_1)) } // Progress 50% on 1-1: (4*50 + 99)/100 = 2 orbs orbs1_1_half, err := cwRollNodeOrbs(catalog, nil, 2801, 1, 1, 50) if err != nil { t.Fatalf("cwRollNodeOrbs 1-1 half: %v", err) } if len(orbs1_1_half) != 2 { t.Errorf("expected 2 orbs from 1-1 at 50%% progress, got %d", len(orbs1_1_half)) } // 1-5 (monster 902 in route 2801): 8 rolls orbs1_5, err := cwRollNodeOrbs(catalog, nil, 2801, 1, 5, 100) if err != nil { t.Fatalf("cwRollNodeOrbs 1-5: %v", err) } if len(orbs1_5) != 8 { t.Errorf("expected 8 orbs from 1-5, got %d", len(orbs1_5)) } // 2-4 (monster 903): 10 rolls orbs2_4, err := cwRollNodeOrbs(catalog, nil, 2801, 2, 4, 100) if err != nil { t.Fatalf("cwRollNodeOrbs 2-4: %v", err) } if len(orbs2_4) != 10 { t.Errorf("expected 10 orbs from 2-4, got %d", len(orbs2_4)) } // 3-4 (monster 904): 6 rolls orbs3_4, err := cwRollNodeOrbs(catalog, nil, 2801, 3, 4, 100) if err != nil { t.Fatalf("cwRollNodeOrbs 3-4: %v", err) } if len(orbs3_4) != 6 { t.Errorf("expected 6 orbs from 3-4, got %d", len(orbs3_4)) } // Non-combat / Supply node (1-3): 0 orbs orbs1_3, err := cwRollNodeOrbs(catalog, nil, 2801, 1, 3, 100) if err != nil { t.Fatalf("cwRollNodeOrbs 1-3: %v", err) } if len(orbs1_3) != 0 { t.Errorf("expected 0 orbs from supply node 1-3, got %d", len(orbs1_3)) } // 2. Test cwUseOrbs with Orb 305 and multiple orbs session := &Session{Token: 99887} orbBag := &pb.DBONCGFHHDN{ MDKGCIKOMLI: []*pb.IBPDPCOGDEP{ {UniqueId: 5001, LDBNOMPCOKK: 102}, {UniqueId: 5002, LDBNOMPCOKK: 103}, {UniqueId: 5003, LDBNOMPCOKK: 305}, }, } data, err := newCurrencyWarsData(catalog) if err != nil { t.Fatalf("new data: %v", err) } testRun := &pb.MPIMAEMCEFH{ UniqueId: 99887, LJNGPCKPJLF: catalog.ActiveSeasonID, DivisionId: data.LFLCDNGALME.HFPEJIDDDJG.DivisionId, NAGDJGMMCBE: []*pb.GridFightGameInfo{ { LEOIAOFBBFA: &pb.GridFightGameInfo_KLCPAAHDPJF{ KLCPAAHDPJF: &pb.BEEFLEJAOCN{ DCAHJELNJNP: 1, SectionId: 1, EMOHLJHPOPC: 2801, }, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{ FAOCFBOFCJP: &pb.NIHNGPKHEPC{ Gold: 100, MICEOJBAKGN: 4, }, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{ EJIPINIKNCK: &pb.HKFECLCHBFJ{}, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_GCKLIONKMJC{ GCKLIONKMJC: &pb.CONONEALNOF{}, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_EBHDIOGALIE{ EBHDIOGALIE: &pb.POCJDMMANBM{}, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_DBAGKJNPDOL{ DBAGKJNPDOL: orbBag, }, }, }, } data.DHFOFLFCLAH = testRun _, rev, _ := readCurrencyWarsData(catalog) session.currencyWarsRevision = rev if err := session.saveCurrencyWarsData(data, catalog); err != nil { t.Fatalf("save initial data with orbs: %v", err) } // Claim all orbs useReq := &pb.IKMOCJBMGOJ{IsGetAll: true} if err := cwUseOrbs(session, useReq); err != nil { t.Fatalf("cwUseOrbs with IsGetAll: %v", err) } // Verify bag in saved data is now empty saved, _, err := readCurrencyWarsData(catalog) if err != nil || saved == nil || saved.DHFOFLFCLAH == nil { t.Fatalf("read saved data: %v", err) } var savedBag *pb.DBONCGFHHDN for _, c := range saved.DHFOFLFCLAH.NAGDJGMMCBE { if b := c.GetDBAGKJNPDOL(); b != nil { savedBag = b break } } if savedBag != nil && len(savedBag.MDKGCIKOMLI) != 0 { t.Errorf("expected 0 orbs left in bag, got %d", len(savedBag.MDKGCIKOMLI)) } // 3. Test idempotency: clicking already consumed or ghost orbs must NOT return error (avoids 6900 error toast) ghostReq := &pb.IKMOCJBMGOJ{CBMGDBJHLHO: []uint32{5001, 5002, 5003}} if err := cwUseOrbs(session, ghostReq); err != nil { t.Fatalf("cwUseOrbs on already consumed orbs returned error: %v", err) } // Calling IsGetAll when bag is already empty must also succeed cleanly if err := cwUseOrbs(session, &pb.IKMOCJBMGOJ{IsGetAll: true}); err != nil { t.Fatalf("cwUseOrbs with IsGetAll on empty bag returned error: %v", err) } } func TestCurrencyWarsTerminalSettlementLogic(t *testing.T) { catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatalf("load catalog: %v", err) } run := &pb.MPIMAEMCEFH{ UniqueId: 999, LJNGPCKPJLF: catalog.ActiveSeasonID, DivisionId: 1, NAGDJGMMCBE: []*pb.GridFightGameInfo{ { LEOIAOFBBFA: &pb.GridFightGameInfo_KLCPAAHDPJF{ KLCPAAHDPJF: &pb.BEEFLEJAOCN{ DCAHJELNJNP: 3, SectionId: 5, EMOHLJHPOPC: 2801, }, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{ FAOCFBOFCJP: &pb.NIHNGPKHEPC{ Gold: 500, GridFightLineupHp: 100, PPKKHHNEOHB: 100, GDAKCOICIDA: 7, }, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{ EJIPINIKNCK: &pb.HKFECLCHBFJ{ GridGameRoleList: []*pb.CDEAABODKCP{ {Id: 1001, UniqueId: 1, Pos: 1, RoleStar: 3}, }, }, }, }, }, } keepWin, settle, err := currencyWarsTerminalSettlement(catalog, nil, run, pb.PEIJNEFOCHI_PEIJNEFOCHI_JOJPGBCECLN, true, nil) if err != nil { t.Fatalf("currencyWarsTerminalSettlement failed: %v", err) } if keepWin == nil || settle == nil { t.Fatal("expected non-nil keepWin and settle notifications") } if settle.BFLLJJBHLDF == nil || settle.BFLLJJBHLDF.Reason != pb.PEIJNEFOCHI_PEIJNEFOCHI_JOJPGBCECLN { t.Errorf("expected victory reason in settlement, got %v", settle.BFLLJJBHLDF) } if settle.BFLLJJBHLDF.NDLIKNEKDLL.DCAHJELNJNP != 3 || settle.BFLLJJBHLDF.NDLIKNEKDLL.SectionId != 5 { t.Errorf("expected 3-5 stage in settlement, got %d-%d", settle.BFLLJJBHLDF.NDLIKNEKDLL.DCAHJELNJNP, settle.BFLLJJBHLDF.NDLIKNEKDLL.SectionId) } } func TestCurrencyWarsBonfireSelectHeal(t *testing.T) { origPersistent, _ := os.ReadFile("persistent") if origPersistent != nil { t.Cleanup(func() { _ = os.WriteFile("persistent", origPersistent, 0644) }) } catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatalf("load catalog: %v", err) } offer := &pb.NCEPOLNLNKH{ Phase: cwBonfireChoosing, EIFICNFGGHI: 10, AHJJJNBOMPG: 2, DANCNIMJOLE: 3, KFNMIEMBJGD: []*pb.MHCOIBONODA{ {EventId: 10200601, HAFOKMHCGFM: 1}, }, } run := &pb.MPIMAEMCEFH{ UniqueId: 101, LJNGPCKPJLF: catalog.ActiveSeasonID, DivisionId: 1, FMIGOKIDKJL: &pb.CHCHAJIANIM{ QueuePosition: 795, FGLIODAJFML: &pb.CHCHAJIANIM_IIDBLCFAAPC{ IIDBLCFAAPC: offer, }, }, NAGDJGMMCBE: []*pb.GridFightGameInfo{ { LEOIAOFBBFA: &pb.GridFightGameInfo_KLCPAAHDPJF{ KLCPAAHDPJF: &pb.BEEFLEJAOCN{ DCAHJELNJNP: 1, SectionId: 5, EMOHLJHPOPC: 2801, BossInfo: &pb.KGMCLFHAJNJ{NCGHKPEMPDP: []*pb.AHKDPCIIELA{{MonsterId: 1004016, RoleStar: 1, LOGHLGKIFEN: 1}}}, HEKMEJLMGFF: []*pb.LHNCMINFONJ{{LOGHLGKIFEN: 1}}, OOMMIIKPHBC: []*pb.BCCIJHPLJOJ{ {DCAHJELNJNP: 1, SectionId: 6, DisplayValue: &pb.OHMDHKGAEMM{StageId: 70000009}}, }, }, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{ FAOCFBOFCJP: &pb.NIHNGPKHEPC{ Gold: 50, GridFightLineupHp: 60, PPKKHHNEOHB: 100, EHNJDIDABHD: 1, NDAINFLOLPJ: 10, }, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{ EJIPINIKNCK: &pb.HKFECLCHBFJ{}, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_EBHDIOGALIE{ EBHDIOGALIE: &pb.POCJDMMANBM{}, }, }, }, } // 1. Test Select Heal HP healReq := &pb.CAMIICJDOBO{ FFBBOFAGGDI: &pb.CAMIICJDOBO_NCGOOEMOBIC{ NCGOOEMOBIC: &pb.MLAENDOFBCK{ FFBBOFAGGDI: &pb.MLAENDOFBCK_LGAJNMNOAEM{ LGAJNMNOAEM: true, }, }, }, } notify, _, err := cwSelectBonfire(catalog, run, healReq) if err != nil { t.Fatalf("cwSelectBonfire heal failed: %v", err) } if notify == nil { t.Fatal("expected notify from heal") } if offer.Phase != cwBonfireSelected { t.Errorf("expected phase %v, got %v", cwBonfireSelected, offer.Phase) } player, _, _ := cwComponents(run) if player.GridFightLineupHp != 80 { t.Errorf("expected 80 HP after heal, got %d", player.GridFightLineupHp) } // 2. Test Take Bonfire rewards and advance node takeReq := &pb.CAMIICJDOBO{ FFBBOFAGGDI: &pb.CAMIICJDOBO_ECDACDNDHGO{ ECDACDNDHGO: &pb.EFIEAIJBHIB{}, }, } takeNotify, _, err := cwSelectBonfire(catalog, run, takeReq) if err != nil { t.Fatalf("cwSelectBonfire take failed: %v", err) } if takeNotify == nil { t.Fatal("expected notify from take") } stage, _ := cwStage(run) if stage.SectionId != 6 { t.Errorf("expected node advance to section 6, got %d", stage.SectionId) } } func TestPrintSaveRoles(t *testing.T) { catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatalf("catalog: %v", err) } data, _, err := readCurrencyWarsData(catalog) if err != nil || data == nil || data.DHFOFLFCLAH == nil { t.Skip("no active run in persistent snapshot") } run := data.DHFOFLFCLAH _, team, _ := cwComponents(run) if team == nil { t.Fatal("team nil") } for _, r := range team.GridGameRoleList { v, _ := catalog.CapturedRoleVariant(r.Id, r.RoleStar) t.Logf("Role ID=%d Star=%d Pos=%d AvatarID=%d BABNHPDGNGB=%v Props=%v ValInit=%v", r.Id, r.RoleStar, r.Pos, v.Config.AvatarID, r.BABNHPDGNGB, r.ConvertPropertyToFixpoint, r.GridFightValueInitComponent) } runtime, _ := cwBonfireRuntime(catalog, run) if runtime != nil { t.Logf("Runtime rules: %v", runtime.Rules) } } func TestCurrencyWarsGrantRandomOrb(t *testing.T) { catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatalf("load catalog: %v", err) } run := &pb.MPIMAEMCEFH{ UniqueId: 999, NAGDJGMMCBE: []*pb.GridFightGameInfo{ { LEOIAOFBBFA: &pb.GridFightGameInfo_DBAGKJNPDOL{ DBAGKJNPDOL: &pb.DBONCGFHHDN{}, }, }, { LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{ FAOCFBOFCJP: &pb.NIHNGPKHEPC{ Gold: 100, }, }, }, }, } cmd := common.CurrencyWarsCommand{ Field: "orb", Value: 0, Quantity: 3, } notify, err := cwApplyCommand(catalog, run, cmd) if err != nil { t.Fatalf("cwApplyCommand random orb: %v", err) } if notify == nil || len(notify.GCPNIHHBGGD) == 0 { t.Fatal("expected notify with update bundle") } if len(notify.GCPNIHHBGGD[0].JKPJFKCBIEM) != 3 { t.Fatalf("expected 3 notify items, got %d", len(notify.GCPNIHHBGGD[0].JKPJFKCBIEM)) } var bag *pb.DBONCGFHHDN for _, c := range run.NAGDJGMMCBE { if b := c.GetDBAGKJNPDOL(); b != nil { bag = b break } } if bag == nil || len(bag.MDKGCIKOMLI) != 3 { t.Fatalf("expected 3 orbs in bag, got %v", bag) } // Verify that all granted orbs have valid, implemented IDs recipes, err := common.LoadCurrencyWarsOrbRecipes() if err != nil { t.Fatalf("load recipes: %v", err) } implemented := make(map[uint32]bool) for _, r := range recipes.Recipes { if r.Status == "Implemented" && r.Engine == "Orb" { implemented[r.ID] = true } } for i, orb := range bag.MDKGCIKOMLI { if !implemented[orb.LDBNOMPCOKK] { t.Errorf("orb[%d] id %d is not an implemented orb", i, orb.LDBNOMPCOKK) } } }