Files
GaCy-GO/internal/gameserver/dispatch.go
T
CAtlas0784 59ea97b1a6 feat: port GaC-hkrpg-ps to Go with SceneCastSkill camera-detach fix
- mihoyo-variant KCP in pure Go (28-byte conv+token header), UDP gateway
  with 0xFF/0x145/0x194 handshake, ms-precision KCP clock + retransmit ticker
- protobuf for 4.6.51 generated from the AstralOS StarRail.proto (4932
  messages) plus CmdID map from // CmdID: comments (2114 ids)
- sdkserver port: query_dispatch/query_gateway (Gateserver pb + base64),
  hotfix fetch, auth endpoints, SRTools freesr-data.json upload
- gameserver handlers: login flow, scene load/spawn with real monster
  entity tracking, lineup, avatar/skin, missions, tutorials (1249+855),
  bag/equip, challenge incl. StartChallenge 1787 layout + Tierce 8973 +
  peak 8948, all raw tierce replies
- SceneCastSkill camera bug fix: only real scene monsters (tracked per
  session) trigger battle; attacking props/NPCs/air replies retcode 0
  without battle_info so the camera stays locked on the character
- end-to-end smoke test simulating a client over real UDP: handshake,
  PlayerGetToken, and both SceneCastSkill paths
- reuses the Rust PS data files unchanged (freesr-data.json, persistent,
  res.json, teleports.json, challenge_data.json, mission_unlocks.json)
2026-10-07 00:22:23 +07:00

139 lines
4.5 KiB
Go

package gameserver
import (
"fmt"
"math/rand"
"os"
"time"
"google.golang.org/protobuf/proto"
goproto "gac-hkrpg-go/protocol/proto"
)
func statFile(path string) (time.Time, error) {
info, err := os.Stat(path)
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
}
func randomToken() uint32 {
return rand.Uint32()
}
func protoMarshal(msg proto.Message) ([]byte, error) {
return proto.Marshal(msg)
}
// ---------------------------------------------------------------------------
// BASE_AVATAR_IDS — 89 avatars (Rust: handlers/avatar.rs)
// ---------------------------------------------------------------------------
var BaseAvatarIDs = []uint32{
8001, 1001,
1002, 1003, 1004, 1005, 1006, 1008, 1009, 1013, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108,
1109, 1110, 1111, 1112, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212,
1213, 1214, 1215, 1217, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1312, 1315, 1310,
1314, 1218, 1221, 1220, 1222, 1223, 1317, 1313, 1225, 1402, 1401, 1404, 1403, 1405, 1407, 1406,
1409, 1014, 1015, 1408, 1410, 1412, 1413, 1414, 1415, 1321, 1501, 1502, 1503, 1504, 1505, 1506,
1507, 1508, 1509, 1510, 1511, 1512, 1513,
}
// ---------------------------------------------------------------------------
// Dispatch — Go port of the trait_handler! macro + on_message special cases.
// ---------------------------------------------------------------------------
type handlerFunc func(s *Session, payload []byte) error
var (
typedHandlers = map[uint16]handlerFunc{}
dummyResponses = map[uint16]uint16{} // req cmd -> rsp cmd (empty body reply)
rawHandlers = map[uint16]func(s *Session, payload []byte) error{}
)
// handle registers a typed CsReq handler; it mirrors the default Rust macro
// behaviour: run handler with a default ScRsp, then always send the response.
func handle[ReqT any, RspT any](name string, fn func(s *Session, req *ReqT, rsp *RspT) error) {
handleNamed(name, name+"ScRsp", fn)
}
// handleNamed registers a handler whose response message name differs from
// "<name>ScRsp" (e.g. StartQuickCocoonStageRsp).
func handleNamed[ReqT any, RspT any](name, rspName string, fn func(s *Session, req *ReqT, rsp *RspT) error) {
reqID, ok := goproto.CmdIDByName[name+"CsReq"]
if !ok {
Log("[DISPATCH] proto 4.6.51 has no " + name + "CsReq; handler skipped")
return
}
rspID, ok := goproto.CmdIDByName[rspName]
if !ok {
Log("[DISPATCH] proto 4.6.51 has no " + rspName + "; handler skipped")
return
}
typedHandlers[uint16(reqID)] = func(s *Session, payload []byte) error {
req := new(ReqT)
if len(payload) > 0 {
if m, okc := any(req).(proto.Message); okc {
if err := proto.Unmarshal(payload, m); err != nil {
PacketLog(fmt.Sprintf("[ERROR] Failed to decode cmd_id %d (%s): %v", reqID, name, err))
return nil
}
}
}
rsp := new(RspT)
if err := fn(s, req, rsp); err != nil {
return err
}
return s.Send(any(rsp).(proto.Message))
}
_ = rspID
}
// dummy registers a CsReq that only needs an empty ScRsp.
func dummy(name string) {
reqID, ok1 := goproto.CmdIDByName[name+"CsReq"]
rspID, ok2 := goproto.CmdIDByName[name+"ScRsp"]
if !ok1 || !ok2 {
Log("[DISPATCH] proto 4.6.51 has no dummy pair " + name + "; skipped")
return
}
dummyResponses[uint16(reqID)] = uint16(rspID)
}
// 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)))
Log(fmt.Sprintf("[PACKET] Received cmd_id: %d (%s)", cmdID, goproto.NameByCmdID[cmdID]))
// 1. empty-dummy responses
if rspID, ok := dummyResponses[cmdID]; ok {
_ = s.SendRaw(&NetPacket{CmdType: rspID})
return
}
// 2. raw special-case handlers (challenge tierce etc.)
if h, ok := rawHandlers[cmdID]; ok {
if err := h(s, payload); err != nil {
PacketLog("[ERROR] raw handler " + fmt.Sprint(cmdID) + ": " + err.Error())
}
return
}
// 3. typed handlers
if h, ok := typedHandlers[cmdID]; ok {
if err := h(s, payload); err != nil {
PacketLog("[ERROR] handler cmd " + fmt.Sprint(cmdID) + ": " + err.Error())
}
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)
if cmdID != 7159 {
PacketLog(fmt.Sprintf("Unknown command ID: %d (%s)", cmdID, goproto.NameByCmdID[cmdID]))
}
}