package gameserver import ( "context" "fmt" "io" "log" "net" "path/filepath" "sync" "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 sessions map[string]*Session mu sync.RWMutex trafficEngine *CityTrafficEngine crowdEngine *UrbanCrowdEngine adminBridge *AdminBridge } type Session struct { 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", } for _, c := range candidates { abs, _ := filepath.Abs(c) templatePath = abs break } } srv := &Server{ cfg: cfg, templatePath: templatePath, sessions: make(map[string]*Session), } 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 // Load Road Network for city traffic simulation _ = s.trafficEngine.LoadRoadNetwork("data/client_data/World/RoadNetwork.json") s.trafficEngine.Start(ctx) addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, s.cfg.Network.GamePort) ln, err := net.Listen("tcp", addr) if err != nil { return fmt.Errorf("failed to listen on game server %s: %w", addr, err) } s.ln = ln 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.trafficEngine != nil { s.trafficEngine.Stop() } if s.cancelFunc != nil { s.cancelFunc() } if s.ln != nil { _ = s.ln.Close() } 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 } } go 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 protocol.WriteFrame(sess.conn, 0x09, payload, sess.sendCipher) } func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error { 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 { 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 { 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(), 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 { _ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher) 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 { switch pkt.MethodID { // ---------------- Login / World Entry ---------------- case protocol.MethodLoginGame: 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 } _ = sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0)) playerInfoBytes, err := protocol.EncodePlayerInfo(s.templatePath, s.cfg.Client.Aid, sess.pid, s.cfg.Player.AccountId, s.cfg.Player.DisplayName) if err == nil { _ = sess.NotifyData(protocol.MethodSyncPlayerInfo, playerInfoBytes) } 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 } _ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId)) _ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0)) unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid) _ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection) 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 } _ = sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene)) wPause := protocol.NewUxWriter() wPause.Bool(false) _ = sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes()) wFog := protocol.NewUxWriter() wFog.U32(uint32(sess.activeScene)).Bool(true) _ = sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes()) wPower := protocol.NewUxWriter() wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0) _ = sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes()) 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()) // ---------------- Movement & Position ---------------- case protocol.MethodAskMoveStart, protocol.MethodAskMoveStop: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) case protocol.MethodAskUpdatePlayerPosition: r := protocol.NewUxReader(pkt.Body) if x, err := r.F32(); err == nil { sess.posX = x } if y, err := r.F32(); err == nil { sess.posY = y } if z, err := r.F32(); err == nil { sess.posZ = z } return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) // ---------------- Character Switching ---------------- case protocol.MethodAskPreSwitchSpirit: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) case protocol.MethodAskSwitchSpirit: r := protocol.NewUxReader(pkt.Body) newTemplateId := sess.templateId if tid, err := r.U32(); err == nil { newTemplateId = tid } 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 } _ = sess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(sess.unitId)) _ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId)) _ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, sess.pid, 0)) newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newTemplateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid) _ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj) _ = sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true)) wInSitu := protocol.NewUxWriter() wInSitu.U32(newTemplateId).U64(newUnitId) _ = sess.NotifyData(protocol.MethodSyncSwitchSpiritInSitu, wInSitu.Bytes()) 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.MethodAskDriveVehicle: return s.handleAskDriveVehicle(sess, pkt) case protocol.MethodAskExitVehicle: return s.handleAskExitVehicle(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 ---------------- case protocol.MethodAskSetWantedLevel: return s.handleAskSetWantedLevel(sess, pkt) case protocol.MethodAskPoliceArrest: return s.handleAskPoliceArrest(sess, pkt) case protocol.MethodReportPoliceArrestFinish: return s.handleReportPoliceArrestFinish(sess, pkt) case protocol.MethodAskPayFine: return s.handleAskPayFine(sess, pkt) case protocol.MethodAskReleaseFromPrison: return s.handleAskReleaseFromPrison(sess, pkt) // ---------------- 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.MethodAskSetCrowdDensity: return s.handleAskSetCrowdDensity(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) case protocol.MethodAskPlayerAttack: return s.handleAskPlayerAttack(sess, pkt) case protocol.MethodAskPlayerCastSkill: return s.handleAskPlayerCastSkill(sess, pkt) case protocol.MethodAskUnlockFightStyle: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) // ---------------- Mini-games ---------------- case protocol.MethodAskChineseChessMove: return s.handleAskChineseChessMove(sess, pkt) case protocol.MethodAskMahjongAction: return s.handleAskMahjongAction(sess, pkt) 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 { return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) } } return nil } // Teleport moves an active session to the specified coordinates. 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) // SetPositionType.Teleport = 6 return sess.NotifyData(protocol.MethodSyncUnitPositionAndFacing, w.Bytes()) }