212 lines
6.8 KiB
Go
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")
|
|
}
|
|
}
|