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

212 lines
6.8 KiB
Go

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