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)
This commit is contained in:
commit
59ea97b1a6
45 files changed
+1963868
No files matched your search
@@ -0,0 +1,288 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
kcplib "gac-hkrpg-go/pkg/kcp"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
const maxPacketSize = 1400
|
||||
|
||||
// PropEntry mirrors PlayerSession.prop_entities: entity_id -> (prop_id, inst_id, state).
|
||||
type PropEntry struct {
|
||||
PropID uint32
|
||||
InstID uint32
|
||||
State uint32
|
||||
}
|
||||
|
||||
// Session is the Go port of PlayerSession.
|
||||
type Session struct {
|
||||
Token uint32
|
||||
ConvID uint32
|
||||
Addr string
|
||||
|
||||
jsonMu sync.RWMutex
|
||||
json *common.FreesrData
|
||||
|
||||
kcpMu sync.Mutex
|
||||
kcp *kcplib.KCP
|
||||
|
||||
startTime time.Time
|
||||
nextSceneSave int64
|
||||
PropEntities map[uint32]PropEntry
|
||||
// MonsterEntities tracks real overworld monster entities (entity_id -> event_id)
|
||||
// spawned in the current scene. Used by SceneCastSkill to decide whether an
|
||||
// attack actually hit a monster (the camera-detach bug fix).
|
||||
MonsterEntities map[uint32]uint32
|
||||
|
||||
closed atomic.Bool
|
||||
closeOnce sync.Once
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
// NewSession builds a session bound to one UDP endpoint.
|
||||
func NewSession(sendTo func(data []byte) error, conv, token uint32) *Session {
|
||||
s := &Session{
|
||||
Token: token,
|
||||
ConvID: conv,
|
||||
kcp: kcplib.New(conv, token, false, sendTo),
|
||||
startTime: time.Now(),
|
||||
PropEntities: map[uint32]PropEntry{},
|
||||
MonsterEntities: map[uint32]uint32{},
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Close marks the session as shut down (idempotent).
|
||||
func (s *Session) Close() {
|
||||
s.closeOnce.Do(func() {
|
||||
s.closed.Store(true)
|
||||
close(s.closeCh)
|
||||
})
|
||||
}
|
||||
|
||||
// Done returns a channel closed when the session shuts down.
|
||||
func (s *Session) Done() <-chan struct{} { return s.closeCh }
|
||||
|
||||
// JSON returns the player data snapshot (read-mostly).
|
||||
func (s *Session) JSON() *common.FreesrData {
|
||||
s.jsonMu.RLock()
|
||||
defer s.jsonMu.RUnlock()
|
||||
return s.json
|
||||
}
|
||||
|
||||
// SetJSON installs (or reloads) the player data.
|
||||
func (s *Session) SetJSON(d *common.FreesrData) {
|
||||
s.jsonMu.Lock()
|
||||
s.json = d
|
||||
s.jsonMu.Unlock()
|
||||
}
|
||||
|
||||
// sessionTimeMs is the KCP clock (milliseconds since session start).
|
||||
// The Rust server fed KCP seconds, which broke RTT math; ms matches the
|
||||
// protocol's design and the client's clock domain.
|
||||
func (s *Session) sessionTimeMs() uint32 {
|
||||
return uint32(time.Since(s.startTime).Milliseconds())
|
||||
}
|
||||
|
||||
// Consume feeds a raw UDP payload into KCP and processes completed packets.
|
||||
func (s *Session) Consume(data []byte) error {
|
||||
if s.closed.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.kcpMu.Lock()
|
||||
if err := s.kcp.Input(data); err != nil {
|
||||
s.kcpMu.Unlock()
|
||||
return err
|
||||
}
|
||||
if err := s.kcp.Update(s.sessionTimeMs()); err != nil {
|
||||
s.kcpMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
var packets []*NetPacket
|
||||
buf := make([]byte, 65536)
|
||||
for {
|
||||
n, err := s.kcp.Recv(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
pkt, err := DecodeNetPacket(buf[:n])
|
||||
if err != nil {
|
||||
PacketLog(fmt.Sprintf("[ERROR] bad packet: %v", err))
|
||||
continue
|
||||
}
|
||||
packets = append(packets, pkt)
|
||||
}
|
||||
s.kcpMu.Unlock()
|
||||
|
||||
for _, pkt := range packets {
|
||||
if pkt.CmdType == 31 { // CmdPlayerLogoutCsReq
|
||||
Log("[PACKET] Player logged out")
|
||||
s.Close()
|
||||
return nil
|
||||
}
|
||||
DispatchMessage(s, pkt.CmdType, pkt.Body)
|
||||
}
|
||||
|
||||
s.kcpMu.Lock()
|
||||
err := s.kcp.Update(s.sessionTimeMs())
|
||||
s.kcpMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Send marshals a proto response and pushes it through KCP.
|
||||
func (s *Session) Send(msg proto.Message) error {
|
||||
name := string(msg.ProtoReflect().Descriptor().Name())
|
||||
cmdID, ok := goproto.CmdIDByName[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("no cmd id registered for %s", name)
|
||||
}
|
||||
|
||||
body, err := protoMarshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
PacketLog(fmt.Sprintf("[PACKET SEND] cmd_id: %d %s (len: %d)", cmdID, name, len(body)))
|
||||
Log(fmt.Sprintf("sent packet with CmdID: %d (%s)", cmdID, name))
|
||||
|
||||
return s.SendRaw(&NetPacket{CmdType: cmdID, Body: body})
|
||||
}
|
||||
|
||||
// SendRaw writes an already-framed packet into the KCP stream.
|
||||
func (s *Session) SendRaw(pkt *NetPacket) error {
|
||||
if s.closed.Load() {
|
||||
return nil
|
||||
}
|
||||
payload := pkt.Encode()
|
||||
|
||||
s.kcpMu.Lock()
|
||||
defer s.kcpMu.Unlock()
|
||||
if _, err := s.kcp.Send(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.kcp.Flush()
|
||||
}
|
||||
|
||||
// DriveUpdate is called by the gateway ticker to run retransmissions.
|
||||
func (s *Session) DriveUpdate() {
|
||||
if s.closed.Load() {
|
||||
return
|
||||
}
|
||||
s.kcpMu.Lock()
|
||||
defer s.kcpMu.Unlock()
|
||||
_ = s.kcp.Update(s.sessionTimeMs())
|
||||
}
|
||||
|
||||
// SyncPlayer is the Go port of PlayerSession::sync_player — re-pushes the
|
||||
// whole inventory + avatar set after freesr-data.json changes.
|
||||
func (s *Session) SyncPlayer() error {
|
||||
player := s.JSON()
|
||||
if player == nil {
|
||||
Log("data is not init!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// clear relics & lightcones
|
||||
del := &goproto.PlayerSyncScNotify{
|
||||
DelRelicList: uint32Range(1, 3000),
|
||||
DelEquipmentList: uint32Range(3001, 3500),
|
||||
}
|
||||
if err := s.Send(del); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// clear all avatar equip items
|
||||
avatarSync := &goproto.AvatarSync{}
|
||||
for _, id := range BaseAvatarIDs {
|
||||
if av, ok := player.Avatars[id]; ok {
|
||||
avatarSync.AvatarList = append(avatarSync.AvatarList, av.ToAvatarProto(nil, player.MainCharacter.ID(), player.MarchType.ID()))
|
||||
}
|
||||
}
|
||||
for _, av := range player.Avatars {
|
||||
avatarSync.AvatarPathDataInfoList = append(avatarSync.AvatarPathDataInfoList,
|
||||
av.ToAvatarPathDataProto(nil, nil, player.DressedSkins[av.AvatarID]))
|
||||
}
|
||||
if err := s.Send(&goproto.PlayerSyncScNotify{AvatarSync: avatarSync}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sync new relics & lightcones
|
||||
sync := &goproto.PlayerSyncScNotify{}
|
||||
for i := range player.Relics {
|
||||
sync.RelicList = append(sync.RelicList, player.Relics[i].ToProtoRelic())
|
||||
}
|
||||
for i := range player.Lightcones {
|
||||
sync.EquipmentList = append(sync.EquipmentList, player.Lightcones[i].ToProtoEquipment())
|
||||
}
|
||||
if err := s.Send(sync); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sync avatars with equip items attached
|
||||
avatarSync2 := &goproto.AvatarSync{}
|
||||
for _, id := range BaseAvatarIDs {
|
||||
if av, ok := player.Avatars[id]; ok {
|
||||
avatarSync2.AvatarList = append(avatarSync2.AvatarList,
|
||||
av.ToAvatarProto(player.FindLightconeFor(id), player.MainCharacter.ID(), player.MarchType.ID()))
|
||||
}
|
||||
}
|
||||
for _, av := range player.Avatars {
|
||||
avatarSync2.AvatarPathDataInfoList = append(avatarSync2.AvatarPathDataInfoList,
|
||||
av.ToAvatarPathDataProto(player.FindLightconeFor(av.AvatarID), player.FindRelicsFor(av.AvatarID), player.DressedSkins[av.AvatarID]))
|
||||
}
|
||||
return s.Send(&goproto.PlayerSyncScNotify{AvatarSync: avatarSync2})
|
||||
}
|
||||
|
||||
func uint32Range(from, to uint32) []uint32 {
|
||||
out := make([]uint32, 0, to-from+1)
|
||||
for i := from; i <= to; i++ {
|
||||
out = append(out, i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// logging helpers (Rust: tracing + util::packet_log)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var logMu sync.Mutex
|
||||
|
||||
func Log(msg string) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
common.LogInfo("%s", msg)
|
||||
}
|
||||
|
||||
func PacketLog(msg string) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
f, err := os.OpenFile("gameserver.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(f, "[%d] %s\n", time.Now().UnixMilli(), msg)
|
||||
}
|
||||
|
||||
// SceneDebugLog appends to server_debug.log (Rust: handlers::scene_debug_log).
|
||||
func SceneDebugLog(format string, args ...any) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
f, err := os.OpenFile("server_debug.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(f, "[%d] %s\n", time.Now().UnixMilli(), fmt.Sprintf(format, args...))
|
||||
}
|
||||
Reference in new issue
Block a user