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) } blockRsp := &goproto.GetUnreleasedBlockInfoScRsp{} if err := proto.Unmarshal(body, blockRsp); err != nil { t.Fatalf("unmarshal GetUnreleasedBlockInfo: %v", err) } if blockRsp.Retcode != 0 { t.Fatalf("unexpected UnreleasedBlockInfo retcode: %d", blockRsp.Retcode) } 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) }