Files
GaCy-GO/internal/gameserver/session.go
T

301 lines
7.9 KiB
Go

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
stateMu sync.Mutex // serializes packet handlers and hot reload mutations
json *common.FreesrData
kcpMu sync.Mutex
kcp *kcplib.KCP
startTime time.Time
nextSceneSave int64
battleProbeMu sync.Mutex
battleProbe []byte
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
// Set by the challenge scene loader, cleared when loading an overworld scene.
ChallengeStageID uint32
pendingExRankSync map[uint32]bool
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 == goproto.CmdIDByName["PlayerLogoutCsReq"] {
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))
if err := s.SendRaw(&NetPacket{CmdType: cmdID, Body: body}); err != nil {
return err
}
s.captureBattleResponse(msg, cmdID, body)
return nil
}
// SendRaw writes an already-framed packet into the KCP stream.
func (s *Session) SendRaw(pkt *NetPacket) error {
if s.closed.Load() {
return nil
}
PacketLog(fmt.Sprintf("[PACKET SEND RAW] cmd_id: %d %s (len: %d)", pkt.CmdType, goproto.NameByCmdID[pkt.CmdType], len(pkt.Body)))
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...))
}