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 } 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 } 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 } } return &Server{ cfg: cfg, templatePath: templatePath, sessions: make(map[string]*Session), } } 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 { 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.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 } // Frame mode 0x03 is heartbeat 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: %v", packet.MethodID, err) } } } func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error { switch pkt.MethodID { 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 } // 1. SendServerTimeGame _ = sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0)) // 2. SyncPlayerInfo (from template binary) 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) } // 3. SyncEnterScene 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 } // V7 4-step Handoff: // Step 1: SyncLogicAgentEnter _ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId)) // Step 2: SyncManagedLogicAgent _ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0)) // Step 3: SyncRaidBattleUnitSpirit unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid) _ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection) // Step 4: SyncPlayerCurrentSpirit 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 } // 1. SyncSceneLoadCompleted _ = sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene)) // 2. SyncGamePause (false) wPause := protocol.NewUxWriter() wPause.Bool(false) _ = sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes()) // 3. Unlock Map Fog wFog := protocol.NewUxWriter() wFog.U32(uint32(sess.activeScene)).Bool(true) _ = sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes()) // 4. Combat power wPower := protocol.NewUxWriter() wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0) _ = sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes()) // 5. Unlock Diving 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.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) case protocol.MethodAskPreSwitchSpirit: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) case protocol.MethodAskSwitchSpirit: // Clean DRMK character swap 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 case protocol.MethodAskSummonVehicle, protocol.MethodAskDriveVehicle, protocol.MethodAskExitVehicle: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) case protocol.MethodAskPlayerAttack, protocol.MethodAskPlayerCastSkill, protocol.MethodAskUnlockFightStyle: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) default: 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()) }