Files
Ananta-ps/internal/gameserver/server.go
T

653 lines
19 KiB
Go

package gameserver
import (
"context"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"ananta-ps/internal/config"
"ananta-ps/pkg/crypto/chacha8"
"ananta-ps/pkg/protocol"
)
type Server struct {
cfg *config.Config
templatePath string
ln net.Listener
cancelFunc context.CancelFunc
wg sync.WaitGroup
connections map[net.Conn]struct{}
sessions map[string]*Session
mu sync.RWMutex
trafficEngine *CityTrafficEngine
crowdEngine *UrbanCrowdEngine
adminBridge *AdminBridge
}
type Session struct {
replyKind protocol.RpcPacketKind
stateMu sync.Mutex
ready atomic.Bool
hangup atomic.Bool
initialStateSent bool
writeTimeout time.Duration
id string
conn net.Conn
readCipher *chacha8.UxChaCha8
sendCipher *chacha8.UxChaCha8
sendMu sync.Mutex
pid uint64
unitId uint64
templateId uint32
posX float32
posY float32
posZ float32
facing float32
activeScene uint64
activeRaid uint32
universeId uint32
lastShopExitMs int64
pendingTeleport *PreTeleportOption
activeVehicleId uint64
isDriving bool
wantedLevel uint32
balloonScore uint32
racing *RacingSession
}
func New(cfg *config.Config, templatePath string) *Server {
if templatePath == "" {
candidates := []string{
"data/player_info_template.bin",
"../data/player_info_template.bin",
"../../data/player_info_template.bin",
}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
templatePath, _ = filepath.Abs(c)
break
}
}
}
srv := &Server{
cfg: cfg,
templatePath: templatePath,
sessions: make(map[string]*Session),
connections: make(map[net.Conn]struct{}),
}
srv.trafficEngine = NewCityTrafficEngine(srv)
srv.crowdEngine = NewUrbanCrowdEngine(srv)
srv.adminBridge = NewAdminBridge(srv)
return srv
}
func (s *Server) GetAdminBridge() *AdminBridge {
return s.adminBridge
}
func (s *Server) Start() error {
ctx, cancel := context.WithCancel(context.Background())
s.cancelFunc = cancel
addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, s.cfg.Network.GamePort)
ln, err := net.Listen("tcp", addr)
if err != nil {
cancel()
return fmt.Errorf("failed to listen on game server %s: %w", addr, err)
}
s.ln = ln
_ = s.trafficEngine.LoadRoadNetwork("data/client_data/World/RoadNetwork.json")
s.trafficEngine.Start(ctx)
log.Printf("[GAME-SERVER] Listening on %s:%d", s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort)
s.wg.Add(1)
go s.acceptLoop(ctx)
return nil
}
func (s *Server) Stop() error {
if s.cancelFunc != nil {
s.cancelFunc()
}
if s.ln != nil {
_ = s.ln.Close()
}
s.mu.Lock()
for conn := range s.connections {
_ = conn.Close()
}
s.mu.Unlock()
if s.trafficEngine != nil {
s.trafficEngine.Stop()
}
s.wg.Wait()
return nil
}
func (s *Server) GetActiveSessions() []*Session {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]*Session, 0, len(s.sessions))
for _, sess := range s.sessions {
list = append(list, sess)
}
return list
}
func (s *Server) acceptLoop(ctx context.Context) {
defer s.wg.Done()
for {
conn, err := s.ln.Accept()
if err != nil {
select {
case <-ctx.Done():
return
default:
log.Printf("[GAME-SERVER] Accept error: %v", err)
return
}
}
s.mu.Lock()
if ctx.Err() != nil {
s.mu.Unlock()
conn.Close()
return
}
s.connections[conn] = struct{}{}
s.wg.Add(1)
s.mu.Unlock()
go func() {
defer s.wg.Done()
defer func() { s.mu.Lock(); delete(s.connections, conn); s.mu.Unlock() }()
s.handleConnection(ctx, conn)
}()
}
}
func (sess *Session) SendPacket(packet *protocol.RpcPacket) error {
sess.sendMu.Lock()
defer sess.sendMu.Unlock()
var payload []byte
switch packet.Kind {
case protocol.PacketKindInvoke:
payload = protocol.BuildInvoke(packet.MethodID, packet.InvokeID, packet.Body)
case protocol.PacketKindReturn:
payload = protocol.BuildReturn(packet.MethodID, packet.InvokeID, packet.Error, packet.Body)
case protocol.PacketKindNotify:
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
}
return sess.writeFrameLocked(0x09, payload)
}
func (sess *Session) writeFrameLocked(mode byte, payload []byte) error {
timeout := sess.writeTimeout
if timeout <= 0 {
timeout = 15 * time.Second
}
if err := sess.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
return err
}
if err := protocol.WriteFrame(sess.conn, mode, payload, sess.sendCipher); err != nil {
// The cipher has advanced; this stream cannot be reused after a failed write.
sess.conn.Close()
return err
}
return nil
}
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
if sess.replyKind != protocol.PacketKindInvoke {
return nil
}
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: 0,
Body: nil,
})
}
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
if sess.replyKind != protocol.PacketKindInvoke {
return nil
}
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: 0,
Body: body,
})
}
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
if methodID == 0 {
return fmt.Errorf("unverified notification method")
}
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindNotify,
MethodID: methodID,
Body: body,
})
}
func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
defer conn.Close()
log.Printf("[GAME-SERVER] Client connected from %s", conn.RemoteAddr())
readCipher, writeCipher, err := protocol.HandleHandshake(conn)
if err != nil {
log.Printf("[GAME-SERVER] Handshake error from %s: %v", conn.RemoteAddr(), err)
return
}
sess := &Session{
id: conn.RemoteAddr().String(),
writeTimeout: time.Duration(s.cfg.Network.WriteTimeoutMs) * time.Millisecond,
conn: conn,
readCipher: readCipher,
sendCipher: writeCipher,
pid: s.cfg.Player.Pid,
unitId: s.cfg.Player.InitialUnitId,
templateId: s.cfg.Player.InitialSpiritTemplateId,
posX: float32(s.cfg.World.Spawn.X),
posY: float32(s.cfg.World.Spawn.Y),
posZ: float32(s.cfg.World.Spawn.Z),
facing: s.cfg.World.Facing,
activeRaid: s.cfg.World.RaidId,
activeScene: s.cfg.World.SceneInstanceId,
universeId: s.cfg.World.UniverseId,
}
s.mu.Lock()
s.sessions[sess.id] = sess
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.sessions, sess.id)
s.mu.Unlock()
}()
for {
select {
case <-ctx.Done():
return
default:
}
frame, err := protocol.ReadFrame(conn, sess.readCipher)
if err != nil {
if err != io.EOF {
log.Printf("[GAME-SERVER] Connection %s read error: %v", conn.RemoteAddr(), err)
}
return
}
if frame.Mode == 0x03 {
sess.sendMu.Lock()
err = sess.writeFrameLocked(0x03, frame.Payload)
sess.sendMu.Unlock()
if err != nil {
return
}
continue
}
if frame.Mode != 0x09 {
continue
}
packet := protocol.ParseRpcPacket(frame.Payload)
if err := s.dispatch(sess, &packet); err != nil {
log.Printf("[GAME-SERVER] Dispatch error for method %d (%s): %v",
packet.MethodID, protocol.GetMethodName(packet.MethodID), err)
}
}
}
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
if pkt.Kind != protocol.PacketKindInvoke && pkt.Kind != protocol.PacketKindNotify {
return nil
}
sess.stateMu.Lock()
defer sess.stateMu.Unlock()
sess.replyKind = pkt.Kind
defer func() { sess.replyKind = protocol.PacketKindUnknown }()
switch pkt.MethodID {
// ---------------- Login / World Entry ----------------
case protocol.MethodLoginGame:
if sess.initialStateSent {
return nil
}
sess.initialStateSent = true
log.Printf("[WORLD] LoginGame received, preparing world state for player %d", sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0)); err != nil {
return err
}
playerInfoBytes, err := protocol.EncodePlayerInfo(s.templatePath, s.cfg.Client.Aid, sess.pid, s.cfg.Player.AccountId, s.cfg.Player.DisplayName)
if err == nil {
if err := sess.NotifyData(protocol.MethodSyncPlayerInfo, playerInfoBytes); err != nil {
return err
}
} else {
log.Printf("[WORLD] Warning: player_info_template.bin load error: %v", err)
}
enterScenePayload := protocol.EncodeEnterScene(
sess.pid,
sess.activeRaid,
sess.activeScene,
sess.universeId,
sess.unitId,
sess.templateId,
sess.posX,
sess.posY,
sess.posZ,
sess.facing,
)
log.Printf("[WORLD-V7] Sending SyncEnterScene raid=%d instance=%d universe=%d unit=%d template=%d pos=(%.2f,%.2f,%.2f)",
sess.activeRaid, sess.activeScene, sess.universeId, sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ)
return sess.NotifyData(protocol.MethodSyncEnterScene, enterScenePayload)
case protocol.MethodAskLoadSceneCompleted:
log.Printf("[WORLD-V7] AskLoadSceneCompleted ack received, committing V7 4-step handoff")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId)); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0)); err != nil {
return err
}
unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
if err := sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection); err != nil {
return err
}
return sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, sess.templateId, sess.unitId, false))
case protocol.MethodAskLoadingFinished:
log.Printf("[WORLD-V7] AskLoadingFinished received, scene ready")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene)); err != nil {
return err
}
sess.ready.Store(true)
wPause := protocol.NewUxWriter()
wPause.Bool(false)
if err := sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes()); err != nil {
return err
}
wFog := protocol.NewUxWriter()
wFog.U32(uint32(sess.activeScene)).Bool(true)
if err := sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes()); err != nil {
return err
}
wPower := protocol.NewUxWriter()
wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0)
if err := sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes()); err != nil {
return err
}
wDive := protocol.NewUxWriter()
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
return sess.NotifyData(protocol.MethodSyncRunGmUtils, wDive.Bytes())
case protocol.MethodAskLoadedInSameScene:
w := protocol.NewUxWriter()
w.U64(sess.pid).F64(1.0)
return sess.NotifyData(protocol.MethodSyncPlayerLoadRate, w.Bytes())
case protocol.MethodRequestPlayerStartHangup:
sess.hangup.Store(true)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodRequestPlayerStopHangup:
sess.hangup.Store(false)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodAskReportLogicAgentSyncData:
return s.handleMovementReport(sess, pkt, false)
case protocol.MethodAskUnitMoveActionSimple:
return s.handleMovementReport(sess, pkt, true)
// ---------------- Movement & Position ----------------
// ---------------- Character Switching ----------------
case protocol.MethodAskSwitchSpirit:
r := protocol.NewUxReader(pkt.Body)
newTemplateId, err := r.U32()
if err != nil {
return err
}
if newTemplateId == 0 {
return fmt.Errorf("spirit template must be nonzero")
}
newUnitId := sess.unitId + 1
log.Printf("[SWITCH] Switching character %d -> %d", sess.templateId, newTemplateId)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(sess.unitId)); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true)); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId)); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, sess.pid, 0)); err != nil {
return err
}
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newTemplateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
if err := sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj); err != nil {
return err
}
if err := sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true)); err != nil {
return err
}
wInSitu := protocol.NewUxWriter()
wInSitu.U32(newTemplateId).U64(newUnitId)
if err := sess.NotifyData(protocol.MethodSyncSwitchSpiritInSitu, wInSitu.Bytes()); err != nil {
return err
}
sess.templateId = newTemplateId
sess.unitId = newUnitId
return nil
// ---------------- Teleport & Map Entrance ----------------
case protocol.MethodAskTeleport:
return s.handleAskTeleport(sess, pkt)
case protocol.MethodReportPreTeleportFinish:
return s.handleReportPreTeleportFinish(sess, pkt)
case protocol.MethodReportPostTeleportFinish:
return s.handleReportPostTeleportFinish(sess, pkt)
case protocol.MethodAskEnterRaidByMapEntrance:
return s.handleAskEnterRaidByMapEntrance(sess, pkt)
// ---------------- Vehicles ----------------
case protocol.MethodAskSummonVehicle:
return s.handleAskSummonVehicle(sess, pkt)
case protocol.MethodAskVehicleMove:
return s.handleAskVehicleMove(sess, pkt)
case protocol.MethodChangeVehicleDoorState:
return s.handleChangeVehicleDoorState(sess, pkt)
case protocol.MethodAskVehicleSendGamePlaySignal:
return s.handleAskVehicleSendGamePlaySignal(sess, pkt)
// ---------------- Police & Crime ----------------
// ---------------- Dialog & NPC & Crowd ----------------
case protocol.MethodAskStartDialog:
return s.handleAskStartDialog(sess, pkt)
case protocol.MethodAskDoDialogAction:
return s.handleAskDoDialogAction(sess, pkt)
case protocol.MethodAskForceFinishDialog:
return s.handleAskForceFinishDialog(sess, pkt)
case protocol.MethodGetCharacterRandomDialog:
return s.handleGetCharacterRandomDialog(sess, pkt)
case protocol.MethodAskAgentStartNpcSound:
return s.handleAskAgentStartNpcSound(sess, pkt)
case protocol.MethodAskDoNpcAction:
return s.handleAskDoNpcAction(sess, pkt)
case protocol.MethodAskPhoneAddCallRecord:
return s.handleAskPhoneAddCallRecord(sess, pkt)
case protocol.MethodAskPhoneContactOptionAction:
return s.handleAskPhoneContactOptionAction(sess, pkt)
case protocol.MethodAskSummonGangMember:
return s.handleAskSummonGangMember(sess, pkt)
case protocol.MethodAskDestroyAllGangMember:
return s.handleAskDestroyAllGangMember(sess, pkt)
// ---------------- Combat ----------------
case protocol.MethodGmWorldRefreshEnemyGroup:
return s.handleGmWorldRefreshEnemyGroup(sess, pkt)
case protocol.MethodReportUnitHitFly:
return s.handleReportUnitHitFly(sess, pkt)
case protocol.MethodReportUnitHitFlyEnd:
return s.handleReportUnitHitFlyEnd(sess, pkt)
case protocol.MethodReportUnitFallGround:
return s.handleReportUnitFallGround(sess, pkt)
case protocol.MethodReportUnitFallGroundEnd:
return s.handleReportUnitFallGroundEnd(sess, pkt)
case protocol.MethodAskControlPowerHoldEnemyUp:
return s.handleAskControlPowerHoldEnemyUp(sess, pkt)
case protocol.MethodAskControlPowerHoldEnemyFallGrou:
return s.handleAskControlPowerHoldEnemyFallGrou(sess, pkt)
// ---------------- Mini-games ----------------
case protocol.MethodEnterBalloonZone:
return s.handleEnterBalloonZone(sess, pkt)
case protocol.MethodLeaveBalloon:
return s.handleLeaveBalloon(sess, pkt)
case protocol.MethodAskBreakDestructibleObject:
return s.handleAskBreakDestructibleObject(sess, pkt)
case protocol.MethodEnterBowlingZone:
return s.handleEnterBowlingZone(sess, pkt)
case protocol.MethodAskClawBuyTicket:
return s.handleAskClawBuyTicket(sess, pkt)
case protocol.MethodAskClawSettlement:
return s.handleAskClawSettlement(sess, pkt)
case protocol.MethodAskBuyMiniGameTicket:
return s.handleAskBuyMiniGameTicket(sess, pkt)
// ---------------- Racing ----------------
case protocol.MethodAskAcceptAndSetCurrentTask:
return s.handleAskAcceptAndSetCurrentTask(sess, pkt)
case protocol.MethodAskSetRacingCompetitionVehicle:
return s.handleAskSetRacingCompetitionVehicle(sess, pkt)
case protocol.MethodAskSetTaskCounterValue:
return s.handleAskSetTaskCounterValue(sess, pkt)
// ---------------- Fashion & Wardrobe ----------------
case protocol.MethodAskSetSpiritFashionsWithSource:
return s.handleAskSetSpiritFashionsWithSource(sess, pkt)
case protocol.MethodAskApplyFashionColoringSchemeInfos:
return s.handleAskApplyFashionColoringSchemeInfos(sess, pkt)
case protocol.MethodAskSetWeaponSkins:
return s.handleAskSetWeaponSkins(sess, pkt)
case protocol.MethodAskSyncWeaponSkinToSpirits:
return s.handleAskSyncWeaponSkinToSpirits(sess, pkt)
// ---------------- Weather & Fog ----------------
case protocol.MethodGmUnlockAllFogMap:
return s.handleGmUnlockAllFogMap(sess, pkt)
case protocol.MethodGmSetAmbianceEnabled:
return s.handleGmSetAmbianceEnabled(sess, pkt)
default:
// Safe neutral typed fallback for unrouted RPC invokes
if pkt.Kind == protocol.PacketKindInvoke {
if body, ok := protocol.DefaultReturnBody(pkt.MethodID); ok {
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body)
}
return sess.SendPacket(&protocol.RpcPacket{Kind: protocol.PacketKindReturn, InvokeID: pkt.InvokeID, MethodID: pkt.MethodID, Error: -1})
}
}
return nil
}
// Teleport moves an active session to the specified coordinates.
func (sess *Session) Teleport(x, y, z, facing float32) error {
sess.stateMu.Lock()
defer sess.stateMu.Unlock()
return sess.teleport(x, y, z, facing)
}
func (sess *Session) teleport(x, y, z, facing float32) error {
sess.posX = x
sess.posY = y
sess.posZ = z
sess.facing = facing
w := protocol.NewUxWriter()
w.U64(sess.unitId).F32(x).F32(y).F32(z).F32(facing).U8(0).Bool(false).U8(6).U8(0).U8(0) // nullable moveGroundInfo and loadingInfo
return sess.NotifyData(protocol.MethodSyncUnitPositionAndFacing, w.Bytes())
}