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
This commit is contained in:
CAtlas0784 committed 2026-10-07 01:19:56 +07:00
1 parent 580717c3d8
commit 91ee027eb7
8 files changed
+1154 -115

No files matched your search

+35 -3
View File
@@ -1,6 +1,8 @@
package gameserver
import (
"strings"
"fmt"
"math/rand"
"os"
@@ -102,6 +104,29 @@ func dummy(name string) {
dummyResponses[uint16(reqID)] = uint16(rspID)
}
// dangerFamilies are client modules that Sync(nil) -> NRE -> black screen when
// they receive an empty ScRsp (lesson recorded in the Rust packet.rs / HANDOFF):
// never auto-answer these, drop them silently instead.
var dangerFamilies = []string{
"GridFight", "ChessRogue", "EvolveBuild", "ChimeraDuel",
"Jukebox", "CycleScore", "MusicAlbum",
}
func isDangerCmd(cmdID uint16) bool {
name := goproto.NameByCmdID[cmdID]
for _, fam := range dangerFamilies {
if strings.Contains(name, fam) {
return true
}
}
// CycleScore cmd ids (4143/4146) and GridFight range carry generic names
switch cmdID {
case 4143, 4146, 8541, 5410, 5449, 7142, 3174, 9252, 8175:
return true
}
return false
}
// DispatchMessage is the Go port of CommandHandler::on_message.
func DispatchMessage(s *Session, cmdID uint16, payload []byte) {
PacketLog(fmt.Sprintf("[PACKET RECV] cmd_id: %d %s (len: %d)", cmdID, goproto.NameByCmdID[cmdID], len(payload)))
@@ -129,9 +154,16 @@ func DispatchMessage(s *Session, cmdID uint16, payload []byte) {
return
}
// 4. unknown — do NOT answer with an empty ScRsp: client modules
// (GridFight/ChessRogue/EvolveBuild/ChimeraDuel/Jukebox) Sync(nil) and NRE
// into a black screen (lesson recorded in the Rust packet.rs)
// 4. unknown: auto-answer with an empty ScRsp when the Cmd enum defines the
// pair, EXCEPT the danger families which must be dropped silently.
// (Answering nothing made the client's UI modules stall - e.g. the Endgame
// entry waits for GetUnreleasedBlockInfo and the pause menu waits for
// SetClientPaused, leaving the game unclickable.)
if rspID, ok := goproto.RspIDByReqID[uint32(cmdID)]; ok && !isDangerCmd(cmdID) {
_ = s.SendRaw(&NetPacket{CmdType: uint16(rspID)})
return
}
if cmdID != 7159 {
PacketLog(fmt.Sprintf("Unknown command ID: %d (%s)", cmdID, goproto.NameByCmdID[cmdID]))
}