301 lines
7.6 KiB
Go
301 lines
7.6 KiB
Go
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)
|
|
}
|