Files
GaCy-GO/internal/gameserver/currency_wars_xaodev_test.go
T

502 lines
15 KiB
Go

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)
}
}