Files
GaCy-GO/internal/gameserver/smoke_test.go
T
CAtlas0784 91ee027eb7 fix(gameserver): answer UI-gating cmds the 4.6.51 client stalls on
First real-client session (2026-10-07 01:00) showed the client walking and
attacking but UI interactions stalling: ~120 CsReq dropped with no reply.

- auto-empty responder: any unknown CsReq with a Cmd-enum ScRsp pair now
  gets an empty ScRsp, except the danger families (GridFight/ChessRogue/
  EvolveBuild/ChimeraDuel/Jukebox/CycleScore) which must stay dropped
  (client Sync(nil) NREs on empty, per the Rust lessons)
- GetUnreleasedBlockInfo: the Endgame entry waits on this - 4.6.51 wire is
  CsReq 4190 -> ScRsp 4112 retcode tag 14 (the Rust build answered 4130,
  an id this client never sends)
- SetClientPaused acked (client pauses itself when opening menus/map)
- explicit handlers: TakeQuestReward, GetFirstTalkNpc, QuitBattle,
  GetMapRotationData (Amphoreus teleport pillars), GetPamSkinData,
  GetAllServerPrefsData/UpdateServerPrefsData, GetHeartDialInfo, GetPetData,
  GetShopList
- SendRaw now logs (dummy/raw replies were invisible in gameserver.log)
- cmdpairs.go: req->rsp id pairs generated from the Cmd enum (865 pairs),
  stable across message renames
- smoke test extended: 4190->4112 and SetClientPaused ack verified
2026-10-07 01:19:56 +07:00

272 lines
7.7 KiB
Go

package gameserver
import (
"encoding/binary"
"sync"
"testing"
"time"
"google.golang.org/protobuf/proto"
"gac-hkrpg-go/internal/common"
kcplib "gac-hkrpg-go/pkg/kcp"
goproto "gac-hkrpg-go/protocol/proto"
)
// TestGatewayHandshakeAndLogin simulates a game client: NetOperation
// handshake -> KCP session -> PlayerGetTokenCsReq -> expect ScRsp back.
func TestGatewayHandshakeAndLogin(t *testing.T) {
gw, err := NewGateway("127.0.0.1", 0)
if err != nil {
t.Fatal(err)
}
go func() {
_ = gw.Listen()
}()
client, err := newTestClient(t, gw.conn.LocalAddr().String())
if err != nil {
t.Fatal(err)
}
// 1. handshake
conv, token, err := client.handshake()
if err != nil {
t.Fatalf("handshake failed: %v", err)
}
t.Logf("handshake ok: conv=%d token=%d", conv, token)
// 2. configure the SERVER-side session held by the gateway
player, _ := common.LoadFreesrData()
if player == nil {
player = &common.FreesrData{}
}
serverSession := gw.GetSession(conv)
if serverSession == nil {
t.Fatal("gateway did not register the session")
}
serverSession.SetJSON(player)
// 3. send PlayerGetTokenCsReq (cmd 5)
reqID := uint16(goproto.CmdIDByName["PlayerGetTokenCsReq"])
rspID := uint16(goproto.CmdIDByName["PlayerGetTokenScRsp"])
if err := client.sendCmd(reqID, nil); err != nil {
t.Fatalf("send cmd: %v", err)
}
cmdID, body, err := client.recvCmd(3 * time.Second)
if err != nil {
t.Fatalf("no response: %v", err)
}
if cmdID != rspID {
t.Fatalf("unexpected cmd id: got %d want %d", cmdID, rspID)
}
rsp := &goproto.PlayerGetTokenScRsp{}
if err := proto.Unmarshal(body, rsp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if rsp.Retcode != 0 || rsp.Uid != playerUID {
t.Fatalf("bad token rsp: retcode=%d uid=%d", rsp.Retcode, rsp.Uid)
}
t.Logf("login token ok: retcode=%d uid=%d msg=%q", rsp.Retcode, rsp.Uid, rsp.Msg)
// 4. exercise the SceneCastSkill fix: attack with no real monsters ->
// must NOT enter battle (no battle_info in the response)
skillReqID := uint16(goproto.CmdIDByName["SceneCastSkillCsReq"])
skillRspID := uint16(goproto.CmdIDByName["SceneCastSkillScRsp"])
skillReq := &goproto.SceneCastSkillCsReq{
CastEntityId: 1,
CasterId: 1,
SkillIndex: 0,
HitTargetEntityIdList: []uint32{1005, 20010, 3}, // prop / npc / own avatar: none are monsters
}
reqBody, _ := proto.Marshal(skillReq)
if err := client.sendCmd(skillReqID, reqBody); err != nil {
t.Fatalf("send cast skill: %v", err)
}
cmdID, body, err = client.recvCmd(3 * time.Second)
if err != nil {
t.Fatalf("no cast skill response: %v", err)
}
if cmdID != skillRspID {
t.Fatalf("unexpected cast skill cmd id: got %d want %d", cmdID, skillRspID)
}
skillRsp := &goproto.SceneCastSkillScRsp{}
if err := proto.Unmarshal(body, skillRsp); err != nil {
t.Fatalf("unmarshal cast skill rsp: %v", err)
}
if skillRsp.BattleInfo != nil || len(skillRsp.MonsterBattleInfo) != 0 {
t.Fatalf("camera bug regression: server entered battle on non-monster targets")
}
if skillRsp.CastEntityId != 1 || skillRsp.Retcode != 0 {
t.Fatalf("bad cast skill rsp: %+v", skillRsp)
}
t.Log("SceneCastSkill fix verified: no battle triggered for non-monster targets")
// 5. attacking a real scene monster DOES start a battle
serverSession.MonsterEntities[30001] = 30124011
skillReq.HitTargetEntityIdList = []uint32{30001}
reqBody, _ = proto.Marshal(skillReq)
if err := client.sendCmd(skillReqID, reqBody); err != nil {
t.Fatalf("send cast skill 2: %v", err)
}
cmdID, body, err = client.recvCmd(3 * time.Second)
if err != nil {
t.Fatalf("no response for monster attack: %v", err)
}
if cmdID != skillRspID {
t.Fatalf("unexpected cmd: got %d want %d", cmdID, skillRspID)
}
skillRsp = &goproto.SceneCastSkillScRsp{}
if err := proto.Unmarshal(body, skillRsp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if skillRsp.BattleInfo == nil || len(skillRsp.MonsterBattleInfo) == 0 {
t.Fatalf("expected battle to start for a real monster target")
}
t.Log("SceneCastSkill verified: battle starts for real monster target")
// 6. GetUnreleasedBlockInfo (4190): Endgame entry gates on this reply
if err := client.sendCmd(4190, nil); err != nil {
t.Fatalf("send GetUnreleasedBlockInfo: %v", err)
}
cmdID, body, err = client.recvCmd(3 * time.Second)
if err != nil {
t.Fatalf("no GetUnreleasedBlockInfo response: %v", err)
}
if cmdID != 4112 {
t.Fatalf("unexpected GetUnreleasedBlockInfoScRsp id: got %d want 4112", cmdID)
}
if len(body) != 2 || body[0] != 0x70 || body[1] != 0x00 {
t.Fatalf("unexpected UnreleasedBlockInfo body: %x", body)
}
t.Log("GetUnreleasedBlockInfo verified: 4190 -> 4112 retcode 0")
// 7. SetClientPaused must be acked (pause menu waits for it)
pauseReq, _ := proto.Marshal(&goproto.SetClientPausedCsReq{Paused: true})
if err := client.sendCmd(1466, pauseReq); err != nil {
t.Fatalf("send SetClientPaused: %v", err)
}
cmdID, body, err = client.recvCmd(3 * time.Second)
if err != nil {
t.Fatalf("no SetClientPaused response: %v", err)
}
if cmdID != 1490 { // SetClientPausedScRsp
t.Fatalf("unexpected SetClientPausedScRsp id: got %d want 1490", cmdID)
}
t.Log("SetClientPaused verified: acked")
}
// ---------------------------------------------------------------------------
// minimal KCP test client
// ---------------------------------------------------------------------------
type testClient struct {
conn *testUDPConn
mu sync.Mutex
kcp *kcplib.KCP
now uint32
conv uint32
token uint32
}
func newTestClient(t *testing.T, serverAddr string) (*testClient, error) {
conn, err := dialUDP(serverAddr)
if err != nil {
return nil, err
}
c := &testClient{conn: conn}
return c, nil
}
func (c *testClient) handshake() (uint32, uint32, error) {
// head=0xFF, tail=0xFFFFFFFF, data=client seed
op := make([]byte, 20)
binary.BigEndian.PutUint32(op[0:], 0xFF)
binary.BigEndian.PutUint32(op[12:], 0xABCD1234)
binary.BigEndian.PutUint32(op[16:], 0xFFFFFFFF)
if _, err := c.conn.write(op); err != nil {
return 0, 0, err
}
c.conn.setReadDeadline(time.Now().Add(3 * time.Second))
reply, err := c.conn.read()
if err != nil {
return 0, 0, err
}
rsp, err := DecodeNetOperation(reply)
if err != nil {
return 0, 0, err
}
if rsp.Head != NetOpAcceptHead || rsp.Tail != NetOpAcceptTail || rsp.Data != 0xABCD1234 {
return 0, 0, errBadHandshake
}
c.conv, c.token = rsp.Param1, rsp.Param2
c.kcp = kcplib.New(c.conv, c.token, false, func(data []byte) error {
_, err := c.conn.write(data)
return err
})
return c.conv, c.token, nil
}
func (c *testClient) sendCmd(cmdID uint16, body []byte) error {
pkt := (&NetPacket{CmdType: cmdID, Body: body}).Encode()
c.mu.Lock()
defer c.mu.Unlock()
if _, err := c.kcp.Send(pkt); err != nil {
return err
}
if err := c.kcp.Update(c.kcp.Now()); err != nil {
return err
}
return c.kcp.Flush()
}
func (c *testClient) recvCmd(timeout time.Duration) (uint16, []byte, error) {
deadline := time.Now().Add(timeout)
buf := make([]byte, 65536)
for time.Now().Before(deadline) {
c.mu.Lock()
now := c.now
c.now += 50
if err := c.kcp.Update(now); err != nil {
c.mu.Unlock()
return 0, nil, err
}
for {
n, err := c.kcp.Recv(buf)
if err != nil {
break
}
pkt, err := DecodeNetPacket(buf[:n])
if err != nil {
continue
}
c.mu.Unlock()
return pkt.CmdType, pkt.Body, nil
}
c.mu.Unlock()
c.conn.setReadDeadline(time.Now().Add(10 * time.Millisecond))
data, err := c.conn.read()
if err == nil && len(data) >= 28 {
c.mu.Lock()
_ = c.kcp.Input(data)
_ = c.kcp.Flush()
c.mu.Unlock()
}
}
return 0, nil, errTimeout
}
var (
errBadHandshake = errString("bad handshake reply")
errTimeout = errString("timeout")
)
type errString string
func (e errString) Error() string { return string(e) }