256 lines
6.5 KiB
Go
256 lines
6.5 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
kcplib "gac-hkrpg-go/pkg/kcp"
|
|
)
|
|
|
|
// Gateway is the Go port of net/gateway.rs: a raw UDP listener speaking the
|
|
// mihoyo KCP handshake (20-byte NetOperation) and dispatching KCP payloads
|
|
// to sessions.
|
|
type Gateway struct {
|
|
conn *net.UDPConn
|
|
idSeq atomic.Uint32
|
|
mu sync.Mutex
|
|
sessions map[uint32]*Session
|
|
publicSDKURL string
|
|
}
|
|
|
|
func NewGateway(host string, port uint16) (*Gateway, error) {
|
|
cfg, err := common.LoadRuntimeConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
playerUID = cfg.PlayerUID
|
|
playerName = cfg.AccountName
|
|
world, err := loadWorldBattleResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
playerWorldLevel = world.WorldLevel
|
|
if _, err := loadPropRules(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := loadNavigationProfiles(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := common.LoadPlayerCosmetics(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := common.LoadCurrencyWarsCatalog(); err != nil {
|
|
return nil, err
|
|
}
|
|
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, itoa(int(port))))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn, err := net.ListenUDP("udp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Gateway{conn: conn, sessions: map[uint32]*Session{}, publicSDKURL: cfg.PublicSDKURL}, nil
|
|
}
|
|
|
|
func itoa(v int) string {
|
|
if v == 0 {
|
|
return "0"
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for v > 0 {
|
|
i--
|
|
buf[i] = byte('0' + v%10)
|
|
v /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|
|
|
|
// Listen blocks, serving UDP traffic until the process exits.
|
|
func (g *Gateway) Listen() error {
|
|
Log("KCP Gateway is listening at " + g.conn.LocalAddr().String())
|
|
|
|
buf := make([]byte, maxPacketSize)
|
|
for {
|
|
n, addr, err := g.conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if n == 20 {
|
|
if err := g.processNetOperation(append([]byte(nil), buf[:n]...), addr); err != nil {
|
|
PacketLog("[ERROR] netop: " + err.Error())
|
|
}
|
|
} else if n >= 28 {
|
|
g.processKCPPayload(append([]byte(nil), buf[:n]...), addr)
|
|
} else {
|
|
PacketLog("unk data len " + itoa(n))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Gateway) processNetOperation(data []byte, addr *net.UDPAddr) error {
|
|
op, err := DecodeNetOperation(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case op.Head == NetOpConnectHead && op.Tail == NetOpConnectTail:
|
|
return g.establishSession(op, addr)
|
|
case op.Head == NetOpDropHead && op.Tail == NetOpDropTail:
|
|
g.dropSession(op.Param1, op.Param2, addr)
|
|
default:
|
|
Log("Unknown magic pair received")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// establishSession mirrors establish_kcp_session: assign conv/token, load
|
|
// freesr-data.json, keep only the newest session, and reply 0x145.
|
|
func (g *Gateway) establishSession(op *NetOperation, addr *net.UDPAddr) error {
|
|
conv := g.idSeq.Add(1)
|
|
token := randomToken()
|
|
|
|
Log("New connection from addr: " + addr.String() + " with conv_id: " + itoa(int(conv)))
|
|
PacketLog("[KCP] New connection from " + addr.String() + " (conv_id: " + itoa(int(conv)) + ", token: " + itoa(int(token)) + ")")
|
|
|
|
session := NewSession(func(payload []byte) error {
|
|
_, err := g.conn.WriteToUDP(payload, addr)
|
|
return err
|
|
}, conv, token)
|
|
session.publicSDKURL = g.publicSDKURL
|
|
|
|
if data, err := common.LoadFreesrData(); err == nil {
|
|
session.SetJSON(data)
|
|
PacketLog("[KCP] FreesrData loaded successfully")
|
|
} else {
|
|
PacketLog("[ERROR] Failed to load initial freesr-data.json: " + err.Error())
|
|
session.Close()
|
|
return fmt.Errorf("refuse session with invalid player data: %w", err)
|
|
}
|
|
|
|
// file watcher: reload freesr-data.json when the SRTools overwrites it
|
|
go watchFreesrData(session)
|
|
|
|
// single-player behaviour from the Rust gateway: drop older sessions
|
|
g.mu.Lock()
|
|
for _, old := range g.sessions {
|
|
old.Close()
|
|
}
|
|
g.sessions = map[uint32]*Session{conv: session}
|
|
g.mu.Unlock()
|
|
|
|
// periodic KCP driver (retransmissions, window probes)
|
|
go func() {
|
|
ticker := time.NewTicker(50 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
session.DriveUpdate()
|
|
case <-session.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
reply := (&NetOperation{
|
|
Head: NetOpAcceptHead,
|
|
Param1: conv,
|
|
Param2: token,
|
|
Data: op.Data,
|
|
Tail: NetOpAcceptTail,
|
|
}).Encode()
|
|
_, err := g.conn.WriteToUDP(reply, addr)
|
|
return err
|
|
}
|
|
|
|
// GetSession returns the live session for a conv id (used by tests/tools).
|
|
func (g *Gateway) GetSession(conv uint32) *Session {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
return g.sessions[conv]
|
|
}
|
|
|
|
func (g *Gateway) dropSession(conv, token uint32, addr *net.UDPAddr) {
|
|
Log("drop_kcp_session " + itoa(int(conv)) + " " + itoa(int(token)))
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
s, ok := g.sessions[conv]
|
|
if !ok {
|
|
Log("drop_kcp_session failed, no session with conv_id " + itoa(int(conv)) + " was found")
|
|
return
|
|
}
|
|
if s.Token == token {
|
|
s.Close()
|
|
delete(g.sessions, conv)
|
|
Log("Client from " + addr.String() + " disconnected")
|
|
}
|
|
}
|
|
|
|
func (g *Gateway) processKCPPayload(data []byte, addr *net.UDPAddr) {
|
|
conv := kcplib.GetConv(data)
|
|
|
|
g.mu.Lock()
|
|
session := g.sessions[conv]
|
|
g.mu.Unlock()
|
|
|
|
if session == nil {
|
|
Log("Session with conv_id " + itoa(int(conv)) + " not found!")
|
|
return
|
|
}
|
|
|
|
// same-conv replays from a stale client get rejected by the token check
|
|
if err := session.Consume(data); err != nil {
|
|
PacketLog("[ERROR] Session consume error (" + addr.String() + "): " + err.Error())
|
|
}
|
|
}
|
|
|
|
// watchFreesrData polls freesr-data.json mtime every second
|
|
// and re-syncs the player in real-time when changed.
|
|
func watchFreesrData(session *Session) {
|
|
var lastModFreesr time.Time
|
|
var lastRejectedFreesr time.Time
|
|
if st, err := statFile("freesr-data.json"); err == nil {
|
|
lastModFreesr = st
|
|
}
|
|
ticker := time.NewTicker(time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-session.Done():
|
|
return
|
|
case <-ticker.C:
|
|
session.stateMu.Lock()
|
|
if err := syncPersistedExRanks(session); err != nil {
|
|
Log("[EX-RANK SYNC] " + err.Error())
|
|
}
|
|
session.stateMu.Unlock()
|
|
stFreesr, _ := statFile("freesr-data.json")
|
|
if stFreesr.IsZero() || !stFreesr.After(lastModFreesr) {
|
|
continue
|
|
}
|
|
session.stateMu.Lock()
|
|
if _, active := CURRENT_BATTLE.Load(session.Token); active {
|
|
session.stateMu.Unlock()
|
|
continue
|
|
}
|
|
if data, err := common.LoadFreesrData(); err == nil {
|
|
if err := reloadPlayerData(session, data); err == nil {
|
|
lastModFreesr = stFreesr
|
|
Log("player data updated & live synced to client")
|
|
}
|
|
} else if !stFreesr.Equal(lastRejectedFreesr) {
|
|
Log("[PLAYER DATA REJECTED] keeping previous session data: " + err.Error())
|
|
lastRejectedFreesr = stFreesr
|
|
}
|
|
session.stateMu.Unlock()
|
|
}
|
|
}
|
|
}
|