514 lines
13 KiB
Go
514 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"eq2emu/internal/common/opcodes"
|
|
"eq2emu/internal/udp"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
// LoginRequest represents parsed login request data
|
|
type LoginRequest struct {
|
|
Username string
|
|
Password string
|
|
Version uint16
|
|
}
|
|
|
|
// parseLoginRequest parses the login request packet data
|
|
func (lc *LoginClient) parseLoginRequest(data []byte) (*LoginRequest, error) {
|
|
if len(data) < 10 {
|
|
return nil, fmt.Errorf("packet too small")
|
|
}
|
|
|
|
offset := 0
|
|
|
|
// Skip access code (16-bit string)
|
|
accessCodeLen := binary.LittleEndian.Uint16(data[offset:])
|
|
offset += 2 + int(accessCodeLen)
|
|
|
|
// Skip unknown1 (16-bit string)
|
|
if offset+2 > len(data) {
|
|
return nil, fmt.Errorf("invalid packet format")
|
|
}
|
|
unknown1Len := binary.LittleEndian.Uint16(data[offset:])
|
|
offset += 2 + int(unknown1Len)
|
|
|
|
// Parse username
|
|
if offset+2 > len(data) {
|
|
return nil, fmt.Errorf("invalid packet format")
|
|
}
|
|
usernameLen := binary.LittleEndian.Uint16(data[offset:])
|
|
offset += 2
|
|
if offset+int(usernameLen) > len(data) {
|
|
return nil, fmt.Errorf("invalid username length")
|
|
}
|
|
username := string(data[offset : offset+int(usernameLen)])
|
|
offset += int(usernameLen)
|
|
|
|
// Parse password
|
|
if offset+2 > len(data) {
|
|
return nil, fmt.Errorf("invalid packet format")
|
|
}
|
|
passwordLen := binary.LittleEndian.Uint16(data[offset:])
|
|
offset += 2
|
|
if offset+int(passwordLen) > len(data) {
|
|
return nil, fmt.Errorf("invalid password length")
|
|
}
|
|
password := string(data[offset : offset+int(passwordLen)])
|
|
offset += int(passwordLen)
|
|
|
|
// Skip unknown fields and get version
|
|
version := uint16(0)
|
|
if offset+18 <= len(data) { // Skip 4 unknown strings + version
|
|
// Simple parsing - in real implementation, parse all unknown fields properly
|
|
version = binary.LittleEndian.Uint16(data[len(data)-10:])
|
|
}
|
|
|
|
return &LoginRequest{
|
|
Username: username,
|
|
Password: password,
|
|
Version: version,
|
|
}, nil
|
|
}
|
|
|
|
// sendLoginDenied sends login failure response
|
|
func (lc *LoginClient) sendLoginDenied() {
|
|
data := make([]byte, 12)
|
|
data[0] = 1 // reply_code: Invalid username or password
|
|
binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF)
|
|
binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF)
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpLoginReplyMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
|
|
// Disconnect after short delay
|
|
time.AfterFunc(1*time.Second, func() {
|
|
lc.Disconnect()
|
|
})
|
|
}
|
|
|
|
// sendLoginDeniedBadVersion sends bad version response
|
|
func (lc *LoginClient) sendLoginDeniedBadVersion() {
|
|
data := make([]byte, 12)
|
|
data[0] = 6 // reply_code: Version mismatch
|
|
binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF)
|
|
binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF)
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpLoginReplyMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
|
|
time.AfterFunc(1*time.Second, func() {
|
|
lc.Disconnect()
|
|
})
|
|
}
|
|
|
|
// sendLoginAccepted sends successful login response
|
|
func (lc *LoginClient) sendLoginAccepted() {
|
|
config := lc.server.GetConfig()
|
|
|
|
// Build login response packet
|
|
data := make([]byte, 64) // Base size, will expand as needed
|
|
offset := 0
|
|
|
|
// Account ID
|
|
binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID))
|
|
offset += 4
|
|
|
|
// Login response code (0 = success)
|
|
data[offset] = 0
|
|
offset++
|
|
|
|
// Do not force SOGA flag
|
|
data[offset] = 1
|
|
offset++
|
|
|
|
// Subscription level
|
|
binary.LittleEndian.PutUint32(data[offset:], config.DefaultSubscriptionLevel)
|
|
offset += 4
|
|
|
|
// Race flags (enabled races)
|
|
binary.LittleEndian.PutUint32(data[offset:], 0x1FFFFF)
|
|
offset += 4
|
|
|
|
// Class flags (enabled classes)
|
|
binary.LittleEndian.PutUint32(data[offset:], 0x7FFFFFE)
|
|
offset += 4
|
|
|
|
// Username (16-bit string)
|
|
username := lc.account.Username
|
|
binary.LittleEndian.PutUint16(data[offset:], uint16(len(username)))
|
|
offset += 2
|
|
copy(data[offset:], username)
|
|
offset += len(username)
|
|
|
|
// Expansion flags
|
|
binary.LittleEndian.PutUint16(data[offset:], config.ExpansionFlag)
|
|
offset += 2
|
|
|
|
// Additional flags
|
|
data[offset] = 0xFF
|
|
data[offset+1] = 0xFF
|
|
data[offset+2] = 0xFF
|
|
offset += 3
|
|
|
|
// Class access flag
|
|
data[offset] = 0xFF
|
|
offset++
|
|
|
|
// Enabled races
|
|
binary.LittleEndian.PutUint32(data[offset:], config.EnabledRaces)
|
|
offset += 4
|
|
|
|
// Cities flag
|
|
data[offset] = config.CitiesFlag
|
|
offset++
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpLoginReplyMsg,
|
|
Data: data[:offset],
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
}
|
|
|
|
// sendWorldList sends available world servers to client
|
|
func (lc *LoginClient) sendWorldList() {
|
|
worlds := lc.server.worldList.GetActiveWorlds()
|
|
|
|
// Build world list packet
|
|
data := make([]byte, 0, 1024)
|
|
|
|
// Number of worlds
|
|
worldCount := uint8(len(worlds))
|
|
data = append(data, worldCount)
|
|
|
|
for _, world := range worlds {
|
|
// World ID
|
|
worldID := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(worldID, uint32(world.ID))
|
|
data = append(data, worldID...)
|
|
|
|
// World name (16-bit string)
|
|
nameLen := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(nameLen, uint16(len(world.Name)))
|
|
data = append(data, nameLen...)
|
|
data = append(data, []byte(world.Name)...)
|
|
|
|
// World status flags
|
|
var flags uint8
|
|
if world.Online {
|
|
flags |= 0x01
|
|
}
|
|
if world.Locked {
|
|
flags |= 0x02
|
|
}
|
|
if world.Hidden {
|
|
flags |= 0x04
|
|
}
|
|
data = append(data, flags)
|
|
|
|
// Population (0-3, where 3 = full)
|
|
data = append(data, world.PopulationLevel)
|
|
}
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpWorldListMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
}
|
|
|
|
// sendCharacterList sends character list to client
|
|
func (lc *LoginClient) sendCharacterList() {
|
|
if lc.account == nil {
|
|
return
|
|
}
|
|
|
|
data := make([]byte, 0, 2048)
|
|
|
|
// Number of characters
|
|
charCount := uint8(len(lc.account.Characters))
|
|
data = append(data, charCount)
|
|
|
|
// Character data
|
|
for _, char := range lc.account.Characters {
|
|
if char.Deleted {
|
|
continue
|
|
}
|
|
|
|
// Character ID
|
|
charID := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(charID, uint32(char.ID))
|
|
data = append(data, charID...)
|
|
|
|
// Server ID
|
|
serverID := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(serverID, uint32(char.ServerID))
|
|
data = append(data, serverID...)
|
|
|
|
// Character name (16-bit string)
|
|
nameLen := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(nameLen, uint16(len(char.Name)))
|
|
data = append(data, nameLen...)
|
|
data = append(data, []byte(char.Name)...)
|
|
|
|
// Character stats
|
|
data = append(data, byte(char.Race))
|
|
data = append(data, byte(char.Gender))
|
|
data = append(data, byte(char.Class))
|
|
data = append(data, byte(char.Level))
|
|
|
|
// Creation timestamp
|
|
timestamp := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(timestamp, uint32(char.CreatedDate.Unix()))
|
|
data = append(data, timestamp...)
|
|
}
|
|
|
|
// Account info
|
|
accountID := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(accountID, uint32(lc.account.ID))
|
|
data = append(data, accountID...)
|
|
|
|
// Max characters
|
|
data = append(data, 0xFF, 0xFF, 0xFF, 0xFF) // unknown1
|
|
data = append(data, 0x00, 0x00) // unknown2
|
|
data = append(data, 0x07, 0x00, 0x00, 0x00) // max chars (7)
|
|
data = append(data, 0x00) // unknown4
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpAllCharactersDescReplyMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
}
|
|
|
|
// handleCharacterCreateRequest processes character creation
|
|
func (lc *LoginClient) handleCharacterCreateRequest(packet *udp.ApplicationPacket) {
|
|
if !lc.authenticated {
|
|
lc.Disconnect()
|
|
return
|
|
}
|
|
|
|
// Parse character creation data (simplified)
|
|
if len(packet.Data) < 20 {
|
|
lc.sendCharacterCreateFailed(1) // Generic error
|
|
return
|
|
}
|
|
|
|
// Extract basic info (this would need full parsing in real implementation)
|
|
serverID := binary.LittleEndian.Uint32(packet.Data[0:4])
|
|
nameLen := binary.LittleEndian.Uint16(packet.Data[4:6])
|
|
name := string(packet.Data[6 : 6+nameLen])
|
|
|
|
// Validate character name
|
|
if len(name) < 3 || len(name) > 20 {
|
|
lc.sendCharacterCreateFailed(9) // Bad name length
|
|
return
|
|
}
|
|
|
|
// Check if name is taken
|
|
exists, err := lc.server.database.CharacterNameExists(name, int32(serverID))
|
|
if err != nil {
|
|
log.Printf("Error checking character name: %v", err)
|
|
lc.sendCharacterCreateFailed(1)
|
|
return
|
|
}
|
|
|
|
if exists {
|
|
lc.sendCharacterCreateFailed(12) // Name taken
|
|
return
|
|
}
|
|
|
|
// Create character in database
|
|
char := &Character{
|
|
AccountID: lc.account.ID,
|
|
ServerID: int32(serverID),
|
|
Name: name,
|
|
Level: 1,
|
|
Race: 1, // Would be parsed from packet
|
|
Gender: 1, // Would be parsed from packet
|
|
Class: 1, // Would be parsed from packet
|
|
CreatedDate: time.Now(),
|
|
}
|
|
|
|
charID, err := lc.server.database.CreateCharacter(char)
|
|
if err != nil {
|
|
log.Printf("Error creating character: %v", err)
|
|
lc.sendCharacterCreateFailed(1)
|
|
return
|
|
}
|
|
|
|
char.ID = charID
|
|
lc.account.Characters = append(lc.account.Characters, char)
|
|
|
|
lc.sendCharacterCreateSuccess(char)
|
|
lc.sendCharacterList() // Refresh character list
|
|
}
|
|
|
|
// sendCharacterCreateSuccess sends successful character creation response
|
|
func (lc *LoginClient) sendCharacterCreateSuccess(char *Character) {
|
|
data := make([]byte, 64)
|
|
offset := 0
|
|
|
|
// Account ID
|
|
binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID))
|
|
offset += 4
|
|
|
|
// Response code (0 = success)
|
|
binary.LittleEndian.PutUint32(data[offset:], 0)
|
|
offset += 4
|
|
|
|
// Character name
|
|
nameLen := uint16(len(char.Name))
|
|
binary.LittleEndian.PutUint16(data[offset:], nameLen)
|
|
offset += 2
|
|
copy(data[offset:], char.Name)
|
|
offset += int(nameLen)
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpCreateCharacterReplyMsg,
|
|
Data: data[:offset],
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
}
|
|
|
|
// sendCharacterCreateFailed sends character creation failure response
|
|
func (lc *LoginClient) sendCharacterCreateFailed(reason uint8) {
|
|
data := make([]byte, 16)
|
|
|
|
binary.LittleEndian.PutUint32(data[0:], uint32(lc.account.ID))
|
|
data[4] = reason
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpCreateCharacterReplyMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
}
|
|
|
|
// handleCharacterDeleteRequest processes character deletion
|
|
func (lc *LoginClient) handleCharacterDeleteRequest(packet *udp.ApplicationPacket) {
|
|
if !lc.authenticated {
|
|
lc.Disconnect()
|
|
return
|
|
}
|
|
|
|
// Parse deletion request (simplified)
|
|
if len(packet.Data) < 12 {
|
|
return
|
|
}
|
|
|
|
charID := binary.LittleEndian.Uint32(packet.Data[0:4])
|
|
serverID := binary.LittleEndian.Uint32(packet.Data[4:8])
|
|
|
|
// Verify character belongs to this account
|
|
var char *Character
|
|
for _, c := range lc.account.Characters {
|
|
if c.ID == int32(charID) && c.ServerID == int32(serverID) {
|
|
char = c
|
|
break
|
|
}
|
|
}
|
|
|
|
if char == nil {
|
|
log.Printf("Account %d attempted to delete character %d that doesn't belong to them", lc.account.ID, charID)
|
|
return
|
|
}
|
|
|
|
// Mark character as deleted
|
|
err := lc.server.database.DeleteCharacter(int32(charID), lc.account.ID)
|
|
if err != nil {
|
|
log.Printf("Error deleting character: %v", err)
|
|
return
|
|
}
|
|
|
|
char.Deleted = true
|
|
|
|
// Send deletion response
|
|
data := make([]byte, 24)
|
|
data[0] = 1 // Success
|
|
binary.LittleEndian.PutUint32(data[4:], serverID)
|
|
binary.LittleEndian.PutUint32(data[8:], charID)
|
|
binary.LittleEndian.PutUint32(data[12:], uint32(lc.account.ID))
|
|
|
|
packet = &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpDeleteCharacterReplyMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
|
|
lc.sendCharacterList() // Refresh character list
|
|
}
|
|
|
|
// handlePlayCharacterRequest processes character selection for gameplay
|
|
func (lc *LoginClient) handlePlayCharacterRequest(packet *udp.ApplicationPacket) {
|
|
if !lc.authenticated {
|
|
lc.Disconnect()
|
|
return
|
|
}
|
|
|
|
// Parse play request
|
|
if len(packet.Data) < 8 {
|
|
lc.sendPlayFailed(1)
|
|
return
|
|
}
|
|
|
|
charID := binary.LittleEndian.Uint32(packet.Data[0:4])
|
|
serverID := binary.LittleEndian.Uint32(packet.Data[4:8])
|
|
|
|
// Find world server
|
|
world := lc.server.worldList.GetWorld(int32(serverID))
|
|
if world == nil || !world.Online {
|
|
lc.sendPlayFailed(2) // Server unavailable
|
|
return
|
|
}
|
|
|
|
// Verify character ownership
|
|
var char *Character
|
|
for _, c := range lc.account.Characters {
|
|
if c.ID == int32(charID) && c.ServerID == int32(serverID) {
|
|
char = c
|
|
break
|
|
}
|
|
}
|
|
|
|
if char == nil {
|
|
lc.sendPlayFailed(1) // Character not found
|
|
return
|
|
}
|
|
|
|
lc.pendingPlayCharID = int32(charID)
|
|
|
|
// Send play request to world server
|
|
err := lc.server.worldList.SendPlayRequest(world, lc.account.ID, int32(charID))
|
|
if err != nil {
|
|
log.Printf("Error sending play request to world server: %v", err)
|
|
lc.sendPlayFailed(1)
|
|
return
|
|
}
|
|
|
|
// World server will respond with connection details
|
|
log.Printf("Account %s requesting to play character %s on server %s",
|
|
lc.account.Username, char.Name, world.Name)
|
|
}
|
|
|
|
// sendPlayFailed sends play character failure response
|
|
func (lc *LoginClient) sendPlayFailed(reason uint8) {
|
|
data := make([]byte, 16)
|
|
data[0] = reason
|
|
binary.LittleEndian.PutUint32(data[4:], uint32(lc.account.ID))
|
|
|
|
packet := &udp.ApplicationPacket{
|
|
Opcode: opcodes.OpPlayCharacterReplyMsg,
|
|
Data: data,
|
|
}
|
|
lc.connection.SendPacket(packet)
|
|
}
|