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()) }