package logingate import ( "context" "fmt" "io" "log" "net" "sync" "ananta-ps/internal/config" "ananta-ps/pkg/crypto/chacha8" "ananta-ps/pkg/protocol" ) type Server struct { cfg *config.Config listeners []net.Listener cancelFunc context.CancelFunc wg sync.WaitGroup } func New(cfg *config.Config) *Server { return &Server{ cfg: cfg, } } func (s *Server) Start() error { ctx, cancel := context.WithCancel(context.Background()) s.cancelFunc = cancel for _, port := range s.cfg.Network.LoginPorts { addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, port) ln, err := net.Listen("tcp", addr) if err != nil { s.Stop() return fmt.Errorf("failed to listen on login gate %s: %w", addr, err) } s.listeners = append(s.listeners, ln) log.Printf("[LOGIN-GATE] Listening on %s:%d", s.cfg.Network.AdvertisedHost, port) s.wg.Add(1) go s.acceptLoop(ctx, ln, port) } return nil } func (s *Server) Stop() error { if s.cancelFunc != nil { s.cancelFunc() } for _, ln := range s.listeners { _ = ln.Close() } s.wg.Wait() return nil } func (s *Server) acceptLoop(ctx context.Context, ln net.Listener, port int) { defer s.wg.Done() for { conn, err := ln.Accept() if err != nil { select { case <-ctx.Done(): return default: log.Printf("[LOGIN-GATE] Accept error on port %d: %v", port, err) return } } go s.handleConnection(ctx, conn, port) } } type Session struct { conn net.Conn readCipher *chacha8.UxChaCha8 sendCipher *chacha8.UxChaCha8 mu sync.Mutex } func (sess *Session) SendPacket(packet *protocol.RpcPacket) error { sess.mu.Lock() defer sess.mu.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) ReturnEmpty(invokeID int32, methodID uint32, errCode int32) error { return sess.SendPacket(&protocol.RpcPacket{ Kind: protocol.PacketKindReturn, InvokeID: invokeID, MethodID: methodID, Error: errCode, 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, port int) { defer conn.Close() log.Printf("[LOGIN-GATE] Client connected from %s on port %d", conn.RemoteAddr(), port) readCipher, writeCipher, err := protocol.HandleHandshake(conn) if err != nil { log.Printf("[LOGIN-GATE] Handshake error from %s: %v", conn.RemoteAddr(), err) return } sess := &Session{ conn: conn, readCipher: readCipher, sendCipher: writeCipher, } for { select { case <-ctx.Done(): return default: } frame, err := protocol.ReadFrame(conn, sess.readCipher) if err != nil { if err != io.EOF { log.Printf("[LOGIN-GATE] Connection %s read error: %v", conn.RemoteAddr(), err) } return } // Frame mode 0x03 is heartbeat if frame.Mode == 0x03 { // Echo heartbeat back _ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher) continue } packet := protocol.ParseRpcPacket(frame.Payload) if err := s.dispatch(sess, &packet); err != nil { log.Printf("[LOGIN-GATE] Dispatch error for method %d: %v", packet.MethodID, err) } } } func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error { switch pkt.MethodID { case protocol.MethodCheckVersion, protocol.MethodHasOnlinePlayer: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) case protocol.MethodAskNewHotFixPatchLogin: return sess.ReturnEmpty(pkt.InvokeID, pkt.MethodID, int32(s.cfg.Client.NoMoreHotfixPatchError)) case protocol.MethodRequestPatchesCheckDataFromLogin: return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodePatchCheck()) case protocol.MethodRequestPatchesFromLogin: return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodeEmptyList()) case protocol.MethodAskUniSdkShareToken_Login, protocol.MethodAskUniSdkShareToken_Gate: w := protocol.NewUxWriter() w.UxString(s.cfg.Player.ShareToken) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodRequestFpPassToken: w := protocol.NewUxWriter() w.UxString(s.cfg.Player.FpPassToken) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodCheckAccount, protocol.MethodCheckAccountPassBy, protocol.MethodCheckAccountOpenId: body := protocol.EncodeCheckAccount( s.cfg.Player.AccountId, s.cfg.Player.UserName, s.cfg.Player.LoginToken, s.cfg.Client.Aid, s.cfg.Client.ServerID, s.cfg.Player.Pid, ) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body) case protocol.MethodTryLogin: if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } // Send SyncRoleList (35167428) w := protocol.NewUxWriter() w.U64(s.cfg.Player.Pid) return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes()) case protocol.MethodRequestCreateRoleEx: w := protocol.NewUxWriter() w.U64(s.cfg.Player.Pid) if err := sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()); err != nil { return err } return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes()) case protocol.MethodRequestEnterGame, protocol.MethodDebugRequestEnterGame: body := protocol.EncodeEnterGame( s.cfg.Client.Aid, s.cfg.Player.Pid, s.cfg.Network.AdvertisedHost, s.cfg.Network.LoginPorts[0], s.cfg.Player.LoginToken, s.cfg.Client.ServerID, s.cfg.Player.AccountId, ) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body) case protocol.MethodAskCloseConnectionToGate: w := protocol.NewUxWriter() w.U32(0) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodGetServerTime: if pkt.Kind == protocol.PacketKindInvoke { if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } } return sess.NotifyData(protocol.MethodSendServerTime, protocol.EncodeServerTime(0)) case protocol.MethodQuerySkey: w := protocol.NewUxWriter() w.UxString(s.cfg.Player.Skey) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodGateLogin: log.Printf("[GATE] Authenticated Gate session registered for IMasterToClient S2C") if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } // 1. SendServerTime _ = sess.NotifyData(protocol.MethodSendServerTime, protocol.EncodeServerTime(0)) // 2. SyncGameSwitchToClient_3 (53940944) - Crucial for vehicles, NPCs, gadgets gameSwitches := protocol.BuildGameSwitchPayload() log.Printf("[GATE] SyncGameSwitchToClient_3 entries=%d body=%dB", len(protocol.GameSwitchCatalog), len(gameSwitches)) _ = sess.NotifyData(protocol.MethodSyncGameSwitchToClient_3, gameSwitches) // 3. SyncPlayerGameServerInfo (52737039) - Handoff to Game Server gameInfo := protocol.EncodeGameServerInfo(s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort, s.cfg.Player.GameToken) return sess.NotifyData(protocol.MethodSyncPlayerGameServerInfo, gameInfo) case protocol.MethodGetFriendApplicationListToMe, protocol.MethodGetAllChatGroupLatestMessage, protocol.MethodGetSimplePlayerInfoByPidList: return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodeEmptyList()) case protocol.MethodReportNetworkQuality, protocol.MethodDavinciCode: // S2C Notify ignore return nil default: if pkt.Kind == protocol.PacketKindInvoke { log.Printf("[LOGIN-GATE] Unhandled Invoke MethodID %d, returning empty OK", pkt.MethodID) return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) } } return nil }