package gameserver import ( "bytes" "gac-hkrpg-go/internal/common" "gac-hkrpg-go/internal/testutil" kcplib "gac-hkrpg-go/pkg/kcp" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" "os" "testing" ) func isolatedCWTest(t *testing.T) *common.CurrencyWarsCatalog { t.Helper() temporary := t.TempDir() if err := testutil.CopyRuntime(testWorkspaceRoot, temporary); err != nil { t.Fatal(err) } t.Chdir(temporary) t.Setenv("GAC_CONFIG", "server.json") if err := os.WriteFile("persistent", []byte(`{}`), 0600); err != nil { t.Fatal(err) } c, err := common.LoadCurrencyWarsCatalog() if err != nil { t.Fatal(err) } return c } // Use the real typed dispatcher and transport framing, so assertions examine // the Client response rather than merely checking handler registration. func replySession(t *testing.T) (*Session, func(proto.Message)) { t.Helper() client := kcplib.New(1, 1, false, func([]byte) error { return nil }) s := NewSession(func(data []byte) error { return client.Input(data) }, 1, 1) s.kcp.SetNodelay(true, 10, 2, true) if err := s.kcp.Update(s.kcp.Now()); err != nil { t.Fatal(err) } return s, func(response proto.Message) { t.Helper() wanted := pb.CmdIDByName[string(response.ProtoReflect().Descriptor().Name())] for { buf := make([]byte, 65536) n, err := client.Recv(buf) if err != nil { t.Fatal(err) } packet, err := DecodeNetPacket(buf[:n]) if err != nil { t.Fatal(err) } if packet.CmdType != wanted { continue } if err := proto.Unmarshal(packet.Body, response); err != nil { t.Fatal(err) } return } } } func callReviewRequest(t *testing.T, s *Session, request proto.Message) { t.Helper() raw, err := proto.Marshal(request) if err != nil { t.Fatal(err) } handler := typedHandlers[uint16(pb.CmdIDByName[string(request.ProtoReflect().Descriptor().Name())])] if handler == nil { t.Fatal("handler missing") } if err := handler(s, raw); err != nil { t.Fatal(err) } } func TestUnsupportedCWMutationsReturnFailureAndPreserveSave(t *testing.T) { isolatedCWTest(t) before, _ := os.ReadFile("persistent") cases := []struct{ req, rsp proto.Message }{ {&pb.GridFightPermanentTalentEnableCsReq{CKHCLKOALKE: ^uint32(0)}, &pb.GridFightPermanentTalentEnableScRsp{}}, {&pb.GridFightSeasonTalentEnableCsReq{CKHCLKOALKE: 1}, &pb.GridFightSeasonTalentEnableScRsp{}}, {&pb.GridFightPermanentTalentResetCsReq{}, &pb.GridFightPermanentTalentResetScRsp{}}, {&pb.GridFightSeasonTalentResetCsReq{}, &pb.GridFightSeasonTalentResetScRsp{}}, {&pb.GridFightTakeWeeklyRewardCsReq{JKCEBHFIENI: []uint32{1, ^uint32(0)}}, &pb.GridFightTakeWeeklyRewardScRsp{}}, {&pb.CycleScoreRewardTakeRewardCsReq{JKCEBHFIENI: []uint32{1}}, &pb.CycleScoreRewardTakeRewardScRsp{}}, } for _, tc := range cases { s, receive := replySession(t) callReviewRequest(t, s, tc.req) receive(tc.rsp) view := tc.rsp.ProtoReflect() fd := view.Descriptor().Fields().ByName("retcode") if view.Get(fd).Uint() == 0 { t.Fatalf("unsupported operation succeeded: %s", view.Descriptor().Name()) } after, _ := os.ReadFile("persistent") if !bytes.Equal(before, after) { t.Fatal("unsupported mutation changed save") } } } func TestCurrencyWarsPositionValidationBothHandlers(t *testing.T) { c := isolatedCWTest(t) data, err := newCurrencyWarsData(c) if err != nil { t.Fatal(err) } var roles []*pb.CDEAABODKCP seen := map[uint32]bool{} for _, v := range c.CapturedVariants { if seen[v.Config.AvatarID] { continue } seen[v.Config.AvatarID] = true roles = append(roles, &pb.CDEAABODKCP{Id: v.Config.ID, RoleStar: v.Star, UniqueId: uint32(len(roles) + 1), Pos: uint32(len(roles) + 1)}) if len(roles) == 2 { break } } if len(roles) != 2 { t.Fatal("fixture needs two avatars") } data.DHFOFLFCLAH = &pb.MPIMAEMCEFH{UniqueId: 1, DivisionId: c.CapturedStages[0].ID, LJNGPCKPJLF: c.ActiveSeasonID, NAGDJGMMCBE: []*pb.GridFightGameInfo{ {LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{EJIPINIKNCK: &pb.HKFECLCHBFJ{GridGameRoleList: roles}}}, {LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: &pb.NIHNGPKHEPC{AFFCFLFIFPH: 2, MICEOJBAKGN: 1}}}, }} writer := &Session{} if err := writer.saveCurrencyWarsData(data, c); err != nil { t.Fatal(err) } for _, batch := range []bool{false, true} { for _, kind := range []string{"duplicate", "outside", "unknown", "battle", "stale", "swap"} { s, receive := replySession(t) s.currencyWarsRevision = writer.currencyWarsRevision before, _ := os.ReadFile("persistent") positions := []*pb.MHEGMHKHAAD{{UniqueId: 1, Pos: 2}, {UniqueId: 2, Pos: 1}} switch kind { case "duplicate": positions[1].Pos = 2 case "outside": positions[0].Pos = ^uint32(0) case "unknown": positions[0].UniqueId = ^uint32(0) case "battle": CURRENT_BATTLE.Store(s.Token, &pb.SceneBattleInfo{}) case "stale": s.currencyWarsRevision = 0 } var code uint32 if batch { req := &pb.GEBBMDCOLCH{} for _, p := range positions { req.CONJCEMAHGN = append(req.CONJCEMAHGN, &pb.EPKFPIDHMOC{MDNAGANGNKK: p.UniqueId, JFCJNHNBMCB: p.Pos}) } callReviewRequest(t, s, req) rsp := &pb.ALNFEBKHCFC{} receive(rsp) code = rsp.Retcode } else { callReviewRequest(t, s, &pb.GridFightUpdatePosCsReq{AAKDHMHCMLP: positions}) rsp := &pb.GridFightUpdatePosScRsp{} receive(rsp) code = rsp.Retcode } CURRENT_BATTLE.Delete(s.Token) after, _ := os.ReadFile("persistent") if kind == "swap" { if code != 0 { t.Fatalf("valid swap rejected batch=%t code=%d", batch, code) } writer.currencyWarsRevision = s.currencyWarsRevision } else if code == 0 || !bytes.Equal(before, after) { t.Fatalf("invalid move changed save or succeeded: batch=%t case=%s code=%d", batch, kind, code) } } } } func TestCurrencyWarsUnplacedAndBenchRolesGiveNoTraits(t *testing.T) { c := isolatedCWTest(t) var sample common.CurrencyWarsRoleVariant for _, v := range c.CapturedVariants { if len(v.Config.TraitList) > 0 { sample = v break } } if sample.Config.ID == 0 { t.Fatal("trait role fixture missing") } front, _ := c.IntegerRule("GridFight_Front_AvatarMaxNum") back, _ := c.IntegerRule("GridFight_Back_AvatarMaxNum") role := &pb.CDEAABODKCP{Id: sample.Config.ID, RoleStar: sample.Star, UniqueId: 1} run := &pb.MPIMAEMCEFH{NAGDJGMMCBE: []*pb.GridFightGameInfo{{LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{EJIPINIKNCK: &pb.HKFECLCHBFJ{GridGameRoleList: []*pb.CDEAABODKCP{role}}}}}} for _, pos := range []uint32{0, uint32(front + back + 1)} { role.Pos = pos if len(cwCalculateBoardTraits(c, run)) != 0 || len(cwCalculateBattleTraits(c, run, nil)) != 0 { t.Fatalf("off-board role contributes traits: pos=%d", pos) } } role.Pos = 1 if len(cwCalculateBoardTraits(c, run)) == 0 || len(cwCalculateBattleTraits(c, run, nil)) == 0 { t.Fatal("active role traits missing") } }