297 lines
7.7 KiB
Go
297 lines
7.7 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
|
|
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
|
|
|
|
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...))
|
|
}
|