package main import ( "encoding/binary" "log" "time" "github.com/goccy/go-json" "github.com/panjf2000/gnet/v2" ) type TCPServer struct { gnet.BuiltinEventEngine server *LoginServer clients *ClientManager worlds *WorldManager } func (s *TCPServer) OnBoot(eng gnet.Engine) gnet.Action { log.Printf("TCP server started on %s", eng.Addr()) return gnet.None } func (s *TCPServer) OnOpen(c gnet.Conn) ([]byte, gnet.Action) { addr := c.RemoteAddr().String() log.Printf("New connection from %s", addr) client := &Client{ conn: c, address: addr, connected: time.Now(), state: StateConnected, version: 0, } s.clients.AddClient(c.Fd(), client) return nil, gnet.None } func (s *TCPServer) OnClose(c gnet.Conn, err error) gnet.Action { s.clients.RemoveClient(c.Fd()) if err != nil { log.Printf("Connection closed with error: %v", err) } return gnet.None } func (s *TCPServer) OnTraffic(c gnet.Conn) gnet.Action { client := s.clients.GetClient(c.Fd()) if client == nil { return gnet.Close } for { packet, err := s.readPacket(c) if err != nil { if err != ErrIncompletePacket { log.Printf("Error reading packet: %v", err) return gnet.Close } break } if err := s.handlePacket(client, packet); err != nil { log.Printf("Error handling packet: %v", err) return gnet.Close } } return gnet.None } func (s *TCPServer) readPacket(c gnet.Conn) (*EQ2Packet, error) { // Read packet header (2 bytes length + 2 bytes opcode) if c.InboundBuffered() < 4 { return nil, ErrIncompletePacket } header := make([]byte, 4) if _, err := c.Peek(header); err != nil { return nil, err } length := binary.LittleEndian.Uint16(header[0:2]) opcode := binary.LittleEndian.Uint16(header[2:4]) totalLength := int(length) + 4 // Add header size if c.InboundBuffered() < totalLength { return nil, ErrIncompletePacket } // Read complete packet data := make([]byte, totalLength) if _, err := c.Read(data); err != nil { return nil, err } return &EQ2Packet{ Opcode: opcode, Size: length, Data: data[4:], // Skip header }, nil } func (s *TCPServer) handlePacket(client *Client, packet *EQ2Packet) error { switch packet.Opcode { case OpLoginRequest: return s.handleLoginRequest(client, packet) case OpWorldListRequest: return s.handleWorldListRequest(client, packet) case OpCharacterListRequest: return s.handleCharacterListRequest(client, packet) case OpCreateCharacter: return s.handleCreateCharacter(client, packet) case OpDeleteCharacter: return s.handleDeleteCharacter(client, packet) case OpPlayCharacter: return s.handlePlayCharacter(client, packet) default: log.Printf("Unknown opcode: 0x%04X", packet.Opcode) } return nil } func (s *TCPServer) handleLoginRequest(client *Client, packet *EQ2Packet) error { var loginReq LoginRequest if err := json.Unmarshal(packet.Data, &loginReq); err != nil { return s.sendLoginDenied(client, "Invalid login data") } account, err := s.server.db.Get(s.server.ctx) if err != nil { return s.sendLoginDenied(client, "Database error") } defer s.server.db.Put(account) // Authenticate user authenticated, accountID, err := authenticateUser(account, loginReq.Username, loginReq.Password, s.server.config.Game.AllowAccountCreation) if err != nil { return s.sendLoginDenied(client, "Authentication failed") } if !authenticated { return s.sendLoginDenied(client, "Invalid credentials") } client.accountID = accountID client.accountName = loginReq.Username client.state = StateAuthenticated client.version = loginReq.Version return s.sendLoginAccepted(client) } func (s *TCPServer) sendLoginAccepted(client *Client) error { response := LoginResponse{ Response: 1, AccountID: client.accountID, SubLevel: s.server.config.Game.DefaultSubscriptionLevel, RaceFlag: s.server.config.Game.EnabledRaces, ClassFlag: 0x7FFFFFE, Username: client.accountName, Unknown5: s.server.config.Game.ExpansionFlag, CitiesFlag: s.server.config.Game.CitiesFlag, } return s.sendPacket(client, OpLoginReply, &response) } func (s *TCPServer) sendLoginDenied(client *Client, reason string) error { response := LoginResponse{ Response: 0, Reason: reason, } return s.sendPacket(client, OpLoginReply, &response) } func (s *TCPServer) handleWorldListRequest(client *Client, packet *EQ2Packet) error { if client.state != StateAuthenticated { return s.sendError(client, "Not authenticated") } worlds := s.worlds.GetWorldList() worldList := WorldListResponse{ NumWorlds: uint32(len(worlds)), Worlds: worlds, } return s.sendPacket(client, OpWorldListReply, &worldList) } func (s *TCPServer) handleCharacterListRequest(client *Client, packet *EQ2Packet) error { if client.state != StateAuthenticated { return s.sendError(client, "Not authenticated") } conn := s.server.db.Get(s.server.ctx) defer s.server.db.Put(conn) characters, err := getCharacterList(conn, client.accountID) if err != nil { return s.sendError(client, "Failed to load characters") } charList := CharacterListResponse{ NumCharacters: uint32(len(characters)), Characters: characters, AccountID: client.accountID, MaxChars: uint32(s.server.config.Game.MaxCharactersPerAccount), } return s.sendPacket(client, OpCharacterListReply, &charList) } func (s *TCPServer) sendPacket(client *Client, opcode uint16, data any) error { jsonData, err := json.Marshal(data) if err != nil { return err } packet := make([]byte, 4+len(jsonData)) binary.LittleEndian.PutUint16(packet[0:2], uint16(len(jsonData))) binary.LittleEndian.PutUint16(packet[2:4], opcode) copy(packet[4:], jsonData) _, err = client.conn.Write(packet) return err } func (s *TCPServer) sendError(client *Client, message string) error { return s.sendPacket(client, OpError, map[string]string{"error": message}) } func (s *TCPServer) handleCreateCharacter(client *Client, packet *EQ2Packet) error { var createReq CreateCharacterRequest if err := json.Unmarshal(packet.Data, &createReq); err != nil { return s.sendError(client, "Invalid character data") } conn := s.server.db.Get(s.server.ctx) defer s.server.db.Put(conn) charID, err := createCharacter(conn, client.accountID, &createReq) if err != nil { return s.sendError(client, "Failed to create character") } response := CreateCharacterResponse{ Success: true, CharacterID: charID, Name: createReq.Name, } return s.sendPacket(client, OpCreateCharacterReply, &response) } func (s *TCPServer) handleDeleteCharacter(client *Client, packet *EQ2Packet) error { var deleteReq DeleteCharacterRequest if err := json.Unmarshal(packet.Data, &deleteReq); err != nil { return s.sendError(client, "Invalid delete request") } conn := s.server.db.Get(s.server.ctx) defer s.server.db.Put(conn) if err := deleteCharacter(conn, client.accountID, deleteReq.CharacterID); err != nil { return s.sendError(client, "Failed to delete character") } response := DeleteCharacterResponse{ Success: true, CharacterID: deleteReq.CharacterID, } return s.sendPacket(client, OpDeleteCharacterReply, &response) } func (s *TCPServer) handlePlayCharacter(client *Client, packet *EQ2Packet) error { var playReq PlayCharacterRequest if err := json.Unmarshal(packet.Data, &playReq); err != nil { return s.sendError(client, "Invalid play request") } world := s.worlds.GetWorld(playReq.ServerID) if world == nil { return s.sendError(client, "World server not available") } // Generate session key and send to world server sessionKey := generateSessionKey() response := PlayCharacterResponse{ Success: true, ServerIP: world.Address, ServerPort: world.Port, SessionKey: sessionKey, } return s.sendPacket(client, OpPlayCharacterReply, &response) }