reset, simplify CRC
This commit is contained in:
parent
46642d41d0
commit
0d651853ff
@ -5,28 +5,19 @@ import (
|
|||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Standard CRC32 polynomial
|
// EQ2CRCKey is likely the reverse engineered security key for the protocol
|
||||||
var crcTable = crc32.MakeTable(0xEDB88320)
|
const EQ2CRCKey uint32 = 0x33624702
|
||||||
|
|
||||||
// Fixed key used by EQ2
|
// CalculateCRC computes CRC32( little_endian(key) || data ) and returns the low 16 bits
|
||||||
var EQ2CRCKey = 0x33624702
|
// of the final ~CRC, matching the EQ2 behavior
|
||||||
|
|
||||||
// CalculateCRC gets the CRC for a given byte slice using a custom key
|
|
||||||
func CalculateCRC(data []byte, key uint32) uint16 {
|
func CalculateCRC(data []byte, key uint32) uint16 {
|
||||||
// Pre-process the key (4 rounds of CRC on key bytes)
|
crc := ^uint32(0)
|
||||||
crc := uint32(0xFFFFFFFF)
|
|
||||||
keyBytes := make([]byte, 4)
|
|
||||||
binary.LittleEndian.PutUint32(keyBytes, key)
|
|
||||||
|
|
||||||
for _, b := range keyBytes {
|
var k [4]byte
|
||||||
crc = crcTable[(crc^uint32(b))&0xFF] ^ (crc >> 8)
|
binary.LittleEndian.PutUint32(k[:], key)
|
||||||
}
|
|
||||||
|
|
||||||
// Process actual data
|
crc = crc32.Update(crc, crc32.IEEETable, k[:])
|
||||||
for _, b := range data {
|
crc = crc32.Update(crc, crc32.IEEETable, data)
|
||||||
crc = crcTable[(crc^uint32(b))&0xFF] ^ (crc >> 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return lower 16 bits of inverted result
|
return uint16(^crc)
|
||||||
return uint16(^crc & 0xFFFF)
|
|
||||||
}
|
}
|
||||||
|
365
game_packet.go
365
game_packet.go
@ -1,365 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GameEmuOpcode represents emulator opcodes for the game server
|
|
||||||
type GameEmuOpcode uint16
|
|
||||||
|
|
||||||
// Game server emulator opcodes (partial list - there are hundreds)
|
|
||||||
const (
|
|
||||||
GOP_Unknown GameEmuOpcode = iota
|
|
||||||
GOP_LoginReplyMsg
|
|
||||||
GOP_LoginByNumRequestMsg
|
|
||||||
GOP_WSLoginRequestMsg
|
|
||||||
GOP_ESInitMsg
|
|
||||||
GOP_ESReadyForClientsMsg
|
|
||||||
GOP_CreateZoneInstanceMsg
|
|
||||||
GOP_ZoneInstanceCreateReplyMsg
|
|
||||||
GOP_ZoneInstanceDestroyedMsg
|
|
||||||
GOP_ExpectClientAsCharacterRequest
|
|
||||||
GOP_ExpectClientAsCharacterReplyMs
|
|
||||||
GOP_ZoneInfoMsg
|
|
||||||
GOP_CreateCharacterRequestMsg
|
|
||||||
GOP_DoneLoadingZoneResourcesMsg
|
|
||||||
GOP_DoneSendingInitialEntitiesMsg
|
|
||||||
GOP_DoneLoadingEntityResourcesMsg
|
|
||||||
GOP_DoneLoadingUIResourcesMsg
|
|
||||||
GOP_PredictionUpdateMsg
|
|
||||||
GOP_RemoteCmdMsg
|
|
||||||
GOP_SetRemoteCmdsMsg
|
|
||||||
GOP_GameWorldTimeMsg
|
|
||||||
GOP_MOTDMsg
|
|
||||||
GOP_ZoneMOTDMsg
|
|
||||||
GOP_GuildRecruitingMemberInfo
|
|
||||||
GOP_GuildRecruiting
|
|
||||||
GOP_GuildRecruitingDetails
|
|
||||||
GOP_GuildRecruitingImage
|
|
||||||
GOP_AvatarCreatedMsg
|
|
||||||
GOP_AvatarDestroyedMsg
|
|
||||||
GOP_RequestCampMsg
|
|
||||||
GOP_MapRequest
|
|
||||||
GOP_CampStartedMsg
|
|
||||||
GOP_CampAbortedMsg
|
|
||||||
GOP_WhoQueryRequestMsg
|
|
||||||
GOP_WhoQueryReplyMsg
|
|
||||||
GOP_RemoveClientFromGroupMsg
|
|
||||||
GOP_GroupCreatedMsg
|
|
||||||
GOP_GroupDestroyedMsg
|
|
||||||
GOP_GroupMemberAddedMsg
|
|
||||||
GOP_GroupMemberRemovedMsg
|
|
||||||
GOP_GroupInfoRequestMsg
|
|
||||||
GOP_GroupInfoUpdateMsg
|
|
||||||
GOP_GroupRemovedFromGroupMsg
|
|
||||||
GOP_GroupLeaderChangedMsg
|
|
||||||
GOP_GroupOptionsMsg
|
|
||||||
GOP_UpdateDataMsg
|
|
||||||
GOP_UpdateSpawnMsg
|
|
||||||
GOP_UpdateCharacterSheetMsg
|
|
||||||
GOP_UpdateSkillsMsg
|
|
||||||
GOP_UpdateQuestMsg
|
|
||||||
GOP_UpdateInventoryMsg
|
|
||||||
GOP_UpdatePositionMsg
|
|
||||||
GOP_UpdateRaidMsg
|
|
||||||
GOP_UpdateTradeMsg
|
|
||||||
GOP_UpdateTargetMsg
|
|
||||||
GOP_UpdateTargetLocMsg
|
|
||||||
GOP_UpdateActorTargetMsg
|
|
||||||
GOP_UpdatePlayerMailMsg
|
|
||||||
GOP_UpdatePlayerMail
|
|
||||||
GOP_UpdateTimeMsg
|
|
||||||
GOP_MaxGameOpcode // Add more as needed
|
|
||||||
)
|
|
||||||
|
|
||||||
// GamePacket represents a game server packet
|
|
||||||
type GamePacket struct {
|
|
||||||
EQPacket // Embed base packet
|
|
||||||
GameEmuOpcode GameEmuOpcode // Game server emulator opcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGamePacket creates a new game server packet
|
|
||||||
func NewGamePacket(opcode GameEmuOpcode, data []byte) *GamePacket {
|
|
||||||
base := NewEQPacket(uint16(opcode), data)
|
|
||||||
base.EmuOpcode = uint16(opcode)
|
|
||||||
base.OpcodeSize = 2 // Game server uses 2-byte opcodes
|
|
||||||
|
|
||||||
p := &GamePacket{
|
|
||||||
EQPacket: *base,
|
|
||||||
GameEmuOpcode: opcode,
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseGamePacket creates a game packet from raw data
|
|
||||||
func ParseGamePacket(data []byte) (*GamePacket, error) {
|
|
||||||
if len(data) < 2 {
|
|
||||||
return nil, fmt.Errorf("packet too small for game opcode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract 2-byte opcode
|
|
||||||
var opcode GameEmuOpcode
|
|
||||||
var remainingData []byte
|
|
||||||
|
|
||||||
// Handle special encoding for 2-byte opcodes
|
|
||||||
// Special case: When we have 3+ bytes and first is 0x00 and third is 0x00,
|
|
||||||
// it's the special encoding for opcodes with low byte = 0x00
|
|
||||||
if len(data) > 2 && data[0] == 0x00 && data[2] == 0x00 {
|
|
||||||
// Extra 0x00 prefix for opcodes like 0x1200
|
|
||||||
opcode = GameEmuOpcode(binary.BigEndian.Uint16(data[1:3]))
|
|
||||||
remainingData = data[3:] // Skip the extra byte
|
|
||||||
} else {
|
|
||||||
opcode = GameEmuOpcode(binary.BigEndian.Uint16(data[0:2]))
|
|
||||||
remainingData = data[2:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create packet with remaining data
|
|
||||||
base := NewEQPacket(uint16(opcode), remainingData)
|
|
||||||
base.EmuOpcode = uint16(opcode)
|
|
||||||
base.OpcodeSize = 2
|
|
||||||
|
|
||||||
p := &GamePacket{
|
|
||||||
EQPacket: *base,
|
|
||||||
GameEmuOpcode: opcode,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGameOpcode returns the game emulator opcode
|
|
||||||
func (p *GamePacket) GetGameOpcode() GameEmuOpcode {
|
|
||||||
return p.GameEmuOpcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGameOpcode sets the game emulator opcode
|
|
||||||
func (p *GamePacket) SetGameOpcode(opcode GameEmuOpcode) {
|
|
||||||
p.GameEmuOpcode = opcode
|
|
||||||
p.EmuOpcode = uint16(opcode)
|
|
||||||
p.Opcode = uint16(opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SerializeGame serializes the game packet with proper encoding
|
|
||||||
func (p *GamePacket) SerializeGame() []byte {
|
|
||||||
// Handle special encoding rules for 2-byte opcodes
|
|
||||||
opcodeBytes := 2
|
|
||||||
extraBytes := 0
|
|
||||||
|
|
||||||
if (uint16(p.GameEmuOpcode) & 0x00FF) == 0 {
|
|
||||||
// Opcodes with low byte = 0x00 need an extra 0x00 prefix
|
|
||||||
extraBytes = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create buffer
|
|
||||||
buf := make([]byte, opcodeBytes+extraBytes+len(p.Buffer))
|
|
||||||
offset := 0
|
|
||||||
|
|
||||||
// Write opcode
|
|
||||||
if extraBytes > 0 {
|
|
||||||
// Special encoding: add 0x00 prefix
|
|
||||||
buf[offset] = 0x00
|
|
||||||
offset++
|
|
||||||
binary.BigEndian.PutUint16(buf[offset:], uint16(p.GameEmuOpcode))
|
|
||||||
offset += 2
|
|
||||||
} else {
|
|
||||||
binary.BigEndian.PutUint16(buf[offset:], uint16(p.GameEmuOpcode))
|
|
||||||
offset += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy packet data
|
|
||||||
if len(p.Buffer) > 0 {
|
|
||||||
copy(buf[offset:], p.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGameOpcodeName returns the string name of a game emulator opcode
|
|
||||||
func GetGameOpcodeName(opcode GameEmuOpcode) string {
|
|
||||||
switch opcode {
|
|
||||||
case GOP_Unknown:
|
|
||||||
return "GOP_Unknown"
|
|
||||||
case GOP_LoginReplyMsg:
|
|
||||||
return "GOP_LoginReplyMsg"
|
|
||||||
case GOP_LoginByNumRequestMsg:
|
|
||||||
return "GOP_LoginByNumRequestMsg"
|
|
||||||
case GOP_WSLoginRequestMsg:
|
|
||||||
return "GOP_WSLoginRequestMsg"
|
|
||||||
case GOP_ESInitMsg:
|
|
||||||
return "GOP_ESInitMsg"
|
|
||||||
case GOP_ESReadyForClientsMsg:
|
|
||||||
return "GOP_ESReadyForClientsMsg"
|
|
||||||
case GOP_CreateZoneInstanceMsg:
|
|
||||||
return "GOP_CreateZoneInstanceMsg"
|
|
||||||
case GOP_ZoneInstanceCreateReplyMsg:
|
|
||||||
return "GOP_ZoneInstanceCreateReplyMsg"
|
|
||||||
case GOP_ZoneInstanceDestroyedMsg:
|
|
||||||
return "GOP_ZoneInstanceDestroyedMsg"
|
|
||||||
case GOP_ExpectClientAsCharacterRequest:
|
|
||||||
return "GOP_ExpectClientAsCharacterRequest"
|
|
||||||
case GOP_ExpectClientAsCharacterReplyMs:
|
|
||||||
return "GOP_ExpectClientAsCharacterReplyMs"
|
|
||||||
case GOP_ZoneInfoMsg:
|
|
||||||
return "GOP_ZoneInfoMsg"
|
|
||||||
case GOP_CreateCharacterRequestMsg:
|
|
||||||
return "GOP_CreateCharacterRequestMsg"
|
|
||||||
case GOP_DoneLoadingZoneResourcesMsg:
|
|
||||||
return "GOP_DoneLoadingZoneResourcesMsg"
|
|
||||||
case GOP_DoneSendingInitialEntitiesMsg:
|
|
||||||
return "GOP_DoneSendingInitialEntitiesMsg"
|
|
||||||
case GOP_DoneLoadingEntityResourcesMsg:
|
|
||||||
return "GOP_DoneLoadingEntityResourcesMsg"
|
|
||||||
case GOP_DoneLoadingUIResourcesMsg:
|
|
||||||
return "GOP_DoneLoadingUIResourcesMsg"
|
|
||||||
case GOP_PredictionUpdateMsg:
|
|
||||||
return "GOP_PredictionUpdateMsg"
|
|
||||||
case GOP_RemoteCmdMsg:
|
|
||||||
return "GOP_RemoteCmdMsg"
|
|
||||||
case GOP_SetRemoteCmdsMsg:
|
|
||||||
return "GOP_SetRemoteCmdsMsg"
|
|
||||||
case GOP_GameWorldTimeMsg:
|
|
||||||
return "GOP_GameWorldTimeMsg"
|
|
||||||
case GOP_MOTDMsg:
|
|
||||||
return "GOP_MOTDMsg"
|
|
||||||
case GOP_ZoneMOTDMsg:
|
|
||||||
return "GOP_ZoneMOTDMsg"
|
|
||||||
case GOP_GuildRecruitingMemberInfo:
|
|
||||||
return "GOP_GuildRecruitingMemberInfo"
|
|
||||||
case GOP_GuildRecruiting:
|
|
||||||
return "GOP_GuildRecruiting"
|
|
||||||
case GOP_GuildRecruitingDetails:
|
|
||||||
return "GOP_GuildRecruitingDetails"
|
|
||||||
case GOP_GuildRecruitingImage:
|
|
||||||
return "GOP_GuildRecruitingImage"
|
|
||||||
case GOP_AvatarCreatedMsg:
|
|
||||||
return "GOP_AvatarCreatedMsg"
|
|
||||||
case GOP_AvatarDestroyedMsg:
|
|
||||||
return "GOP_AvatarDestroyedMsg"
|
|
||||||
case GOP_RequestCampMsg:
|
|
||||||
return "GOP_RequestCampMsg"
|
|
||||||
case GOP_MapRequest:
|
|
||||||
return "GOP_MapRequest"
|
|
||||||
case GOP_CampStartedMsg:
|
|
||||||
return "GOP_CampStartedMsg"
|
|
||||||
case GOP_CampAbortedMsg:
|
|
||||||
return "GOP_CampAbortedMsg"
|
|
||||||
case GOP_WhoQueryRequestMsg:
|
|
||||||
return "GOP_WhoQueryRequestMsg"
|
|
||||||
case GOP_WhoQueryReplyMsg:
|
|
||||||
return "GOP_WhoQueryReplyMsg"
|
|
||||||
case GOP_RemoveClientFromGroupMsg:
|
|
||||||
return "GOP_RemoveClientFromGroupMsg"
|
|
||||||
case GOP_GroupCreatedMsg:
|
|
||||||
return "GOP_GroupCreatedMsg"
|
|
||||||
case GOP_GroupDestroyedMsg:
|
|
||||||
return "GOP_GroupDestroyedMsg"
|
|
||||||
case GOP_GroupMemberAddedMsg:
|
|
||||||
return "GOP_GroupMemberAddedMsg"
|
|
||||||
case GOP_GroupMemberRemovedMsg:
|
|
||||||
return "GOP_GroupMemberRemovedMsg"
|
|
||||||
case GOP_GroupInfoRequestMsg:
|
|
||||||
return "GOP_GroupInfoRequestMsg"
|
|
||||||
case GOP_GroupInfoUpdateMsg:
|
|
||||||
return "GOP_GroupInfoUpdateMsg"
|
|
||||||
case GOP_GroupRemovedFromGroupMsg:
|
|
||||||
return "GOP_GroupRemovedFromGroupMsg"
|
|
||||||
case GOP_GroupLeaderChangedMsg:
|
|
||||||
return "GOP_GroupLeaderChangedMsg"
|
|
||||||
case GOP_GroupOptionsMsg:
|
|
||||||
return "GOP_GroupOptionsMsg"
|
|
||||||
case GOP_UpdateDataMsg:
|
|
||||||
return "GOP_UpdateDataMsg"
|
|
||||||
case GOP_UpdateSpawnMsg:
|
|
||||||
return "GOP_UpdateSpawnMsg"
|
|
||||||
case GOP_UpdateCharacterSheetMsg:
|
|
||||||
return "GOP_UpdateCharacterSheetMsg"
|
|
||||||
case GOP_UpdateSkillsMsg:
|
|
||||||
return "GOP_UpdateSkillsMsg"
|
|
||||||
case GOP_UpdateQuestMsg:
|
|
||||||
return "GOP_UpdateQuestMsg"
|
|
||||||
case GOP_UpdateInventoryMsg:
|
|
||||||
return "GOP_UpdateInventoryMsg"
|
|
||||||
case GOP_UpdatePositionMsg:
|
|
||||||
return "GOP_UpdatePositionMsg"
|
|
||||||
case GOP_UpdateRaidMsg:
|
|
||||||
return "GOP_UpdateRaidMsg"
|
|
||||||
case GOP_UpdateTradeMsg:
|
|
||||||
return "GOP_UpdateTradeMsg"
|
|
||||||
case GOP_UpdateTargetMsg:
|
|
||||||
return "GOP_UpdateTargetMsg"
|
|
||||||
case GOP_UpdateTargetLocMsg:
|
|
||||||
return "GOP_UpdateTargetLocMsg"
|
|
||||||
case GOP_UpdateActorTargetMsg:
|
|
||||||
return "GOP_UpdateActorTargetMsg"
|
|
||||||
case GOP_UpdatePlayerMailMsg:
|
|
||||||
return "GOP_UpdatePlayerMailMsg"
|
|
||||||
case GOP_UpdatePlayerMail:
|
|
||||||
return "GOP_UpdatePlayerMail"
|
|
||||||
case GOP_UpdateTimeMsg:
|
|
||||||
return "GOP_UpdateTimeMsg"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("Unknown(%d)", opcode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToProtocolPacket wraps this game packet in a protocol packet
|
|
||||||
func (p *GamePacket) ConvertToProtocolPacket() *ProtocolPacket {
|
|
||||||
// Serialize the game packet
|
|
||||||
gameData := p.SerializeGame()
|
|
||||||
|
|
||||||
// Create protocol packet with OP_Packet opcode
|
|
||||||
proto := NewProtocolPacket(OP_Packet, gameData)
|
|
||||||
|
|
||||||
// Copy network info
|
|
||||||
proto.SrcIP = p.SrcIP
|
|
||||||
proto.SrcPort = p.SrcPort
|
|
||||||
proto.DstIP = p.DstIP
|
|
||||||
proto.DstPort = p.DstPort
|
|
||||||
proto.Timestamp = p.Timestamp
|
|
||||||
proto.Version = p.Version
|
|
||||||
|
|
||||||
return proto
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy creates a deep copy of the game packet
|
|
||||||
func (p *GamePacket) Copy() *GamePacket {
|
|
||||||
newPacket := &GamePacket{
|
|
||||||
EQPacket: *p.Clone(),
|
|
||||||
GameEmuOpcode: p.GameEmuOpcode,
|
|
||||||
}
|
|
||||||
return newPacket
|
|
||||||
}
|
|
||||||
|
|
||||||
// CombineGamePackets combines multiple game packets for efficient transmission
|
|
||||||
func CombineGamePackets(packets []*GamePacket) *ProtocolPacket {
|
|
||||||
if len(packets) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(packets) == 1 {
|
|
||||||
return packets[0].ConvertToProtocolPacket()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build combined buffer
|
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
for _, p := range packets {
|
|
||||||
data := p.SerializeGame()
|
|
||||||
size := len(data)
|
|
||||||
|
|
||||||
// Add size encoding
|
|
||||||
if size >= 255 {
|
|
||||||
// Oversized packet
|
|
||||||
buf = append(buf, 0xFF)
|
|
||||||
buf = append(buf, byte(size>>8), byte(size))
|
|
||||||
} else {
|
|
||||||
buf = append(buf, byte(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add packet data
|
|
||||||
buf = append(buf, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create combined protocol packet
|
|
||||||
return NewProtocolPacket(OP_AppCombined, buf)
|
|
||||||
}
|
|
235
login_packet.go
235
login_packet.go
@ -1,235 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginEmuOpcode represents emulator opcodes for the login server
|
|
||||||
type LoginEmuOpcode uint16
|
|
||||||
|
|
||||||
// Login server emulator opcodes
|
|
||||||
const (
|
|
||||||
LOP_Unknown LoginEmuOpcode = iota
|
|
||||||
LOP_LoginRequestMsg
|
|
||||||
LOP_LoginByNumRequestMsg
|
|
||||||
LOP_WSLoginRequestMsg
|
|
||||||
LOP_ESLoginRequestMsg
|
|
||||||
LOP_LoginReplyMsg
|
|
||||||
LOP_WorldListMsg
|
|
||||||
LOP_WorldStatusChangeMsg
|
|
||||||
LOP_AllWSDescRequestMsg
|
|
||||||
LOP_WSStatusReplyMsg
|
|
||||||
LOP_AllCharactersDescRequestMsg
|
|
||||||
LOP_AllCharactersDescReplyMsg
|
|
||||||
LOP_CreateCharacterRequestMsg
|
|
||||||
LOP_ReskinCharacterRequestMsg
|
|
||||||
LOP_CreateCharacterReplyMsg
|
|
||||||
LOP_WSCreateCharacterRequestMsg
|
|
||||||
LOP_WSCreateCharacterReplyMsg
|
|
||||||
LOP_DeleteCharacterRequestMsg
|
|
||||||
LOP_DeleteCharacterReplyMsg
|
|
||||||
LOP_PlayCharacterRequestMsg
|
|
||||||
LOP_PlayCharacterReplyMsg
|
|
||||||
LOP_ServerPlayCharacterRequestMsg
|
|
||||||
LOP_ServerPlayCharacterReplyMsg
|
|
||||||
LOP_KeymapLoadMsg
|
|
||||||
LOP_KeymapNoneMsg
|
|
||||||
LOP_KeymapDataMsg
|
|
||||||
LOP_KeymapSaveMsg
|
|
||||||
LOP_LSCheckAcctLockMsg
|
|
||||||
LOP_WSAcctLockStatusMsg
|
|
||||||
LOP_LsRequestClientCrashLogMsg
|
|
||||||
LOP_LsClientBaselogReplyMsg
|
|
||||||
LOP_LsClientCrashlogReplyMsg
|
|
||||||
LOP_LsClientAlertlogReplyMsg
|
|
||||||
LOP_LsClientVerifylogReplyMsg
|
|
||||||
LOP_BadLanguageFilter
|
|
||||||
LOP_WSServerLockMsg
|
|
||||||
LOP_WSServerHideMsg
|
|
||||||
LOP_LSServerLockMsg
|
|
||||||
LOP_UpdateCharacterSheetMsg
|
|
||||||
LOP_UpdateInventoryMsg
|
|
||||||
LOP_MaxLoginOpcode
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginPacket represents a login server packet
|
|
||||||
type LoginPacket struct {
|
|
||||||
EQPacket // Embed base packet
|
|
||||||
LoginEmuOpcode LoginEmuOpcode // Login server emulator opcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLoginPacket creates a new login server packet
|
|
||||||
func NewLoginPacket(opcode LoginEmuOpcode, data []byte) *LoginPacket {
|
|
||||||
base := NewEQPacket(uint16(opcode), data)
|
|
||||||
base.EmuOpcode = uint16(opcode)
|
|
||||||
base.OpcodeSize = 1 // Login server uses 1-byte opcodes
|
|
||||||
|
|
||||||
p := &LoginPacket{
|
|
||||||
EQPacket: *base,
|
|
||||||
LoginEmuOpcode: opcode,
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseLoginPacket creates a login packet from raw data
|
|
||||||
func ParseLoginPacket(data []byte) (*LoginPacket, error) {
|
|
||||||
if len(data) < 1 {
|
|
||||||
return nil, fmt.Errorf("packet too small for login opcode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract 1-byte opcode
|
|
||||||
opcode := LoginEmuOpcode(data[0])
|
|
||||||
|
|
||||||
// Create packet with remaining data
|
|
||||||
base := NewEQPacket(uint16(opcode), data[1:])
|
|
||||||
base.EmuOpcode = uint16(opcode)
|
|
||||||
base.OpcodeSize = 1
|
|
||||||
|
|
||||||
p := &LoginPacket{
|
|
||||||
EQPacket: *base,
|
|
||||||
LoginEmuOpcode: opcode,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLoginOpcode returns the login emulator opcode
|
|
||||||
func (p *LoginPacket) GetLoginOpcode() LoginEmuOpcode {
|
|
||||||
return p.LoginEmuOpcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLoginOpcode sets the login emulator opcode
|
|
||||||
func (p *LoginPacket) SetLoginOpcode(opcode LoginEmuOpcode) {
|
|
||||||
p.LoginEmuOpcode = opcode
|
|
||||||
p.EmuOpcode = uint16(opcode)
|
|
||||||
p.Opcode = uint16(opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SerializeLogin serializes the login packet with proper encoding
|
|
||||||
func (p *LoginPacket) SerializeLogin() []byte {
|
|
||||||
// Login packets use 1-byte opcodes
|
|
||||||
buf := make([]byte, 1+len(p.Buffer))
|
|
||||||
buf[0] = byte(p.LoginEmuOpcode)
|
|
||||||
|
|
||||||
if len(p.Buffer) > 0 {
|
|
||||||
copy(buf[1:], p.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLoginOpcodeName returns the string name of a login emulator opcode
|
|
||||||
func GetLoginOpcodeName(opcode LoginEmuOpcode) string {
|
|
||||||
switch opcode {
|
|
||||||
case LOP_Unknown:
|
|
||||||
return "LOP_Unknown"
|
|
||||||
case LOP_LoginRequestMsg:
|
|
||||||
return "LOP_LoginRequestMsg"
|
|
||||||
case LOP_LoginByNumRequestMsg:
|
|
||||||
return "LOP_LoginByNumRequestMsg"
|
|
||||||
case LOP_WSLoginRequestMsg:
|
|
||||||
return "LOP_WSLoginRequestMsg"
|
|
||||||
case LOP_ESLoginRequestMsg:
|
|
||||||
return "LOP_ESLoginRequestMsg"
|
|
||||||
case LOP_LoginReplyMsg:
|
|
||||||
return "LOP_LoginReplyMsg"
|
|
||||||
case LOP_WorldListMsg:
|
|
||||||
return "LOP_WorldListMsg"
|
|
||||||
case LOP_WorldStatusChangeMsg:
|
|
||||||
return "LOP_WorldStatusChangeMsg"
|
|
||||||
case LOP_AllWSDescRequestMsg:
|
|
||||||
return "LOP_AllWSDescRequestMsg"
|
|
||||||
case LOP_WSStatusReplyMsg:
|
|
||||||
return "LOP_WSStatusReplyMsg"
|
|
||||||
case LOP_AllCharactersDescRequestMsg:
|
|
||||||
return "LOP_AllCharactersDescRequestMsg"
|
|
||||||
case LOP_AllCharactersDescReplyMsg:
|
|
||||||
return "LOP_AllCharactersDescReplyMsg"
|
|
||||||
case LOP_CreateCharacterRequestMsg:
|
|
||||||
return "LOP_CreateCharacterRequestMsg"
|
|
||||||
case LOP_ReskinCharacterRequestMsg:
|
|
||||||
return "LOP_ReskinCharacterRequestMsg"
|
|
||||||
case LOP_CreateCharacterReplyMsg:
|
|
||||||
return "LOP_CreateCharacterReplyMsg"
|
|
||||||
case LOP_WSCreateCharacterRequestMsg:
|
|
||||||
return "LOP_WSCreateCharacterRequestMsg"
|
|
||||||
case LOP_WSCreateCharacterReplyMsg:
|
|
||||||
return "LOP_WSCreateCharacterReplyMsg"
|
|
||||||
case LOP_DeleteCharacterRequestMsg:
|
|
||||||
return "LOP_DeleteCharacterRequestMsg"
|
|
||||||
case LOP_DeleteCharacterReplyMsg:
|
|
||||||
return "LOP_DeleteCharacterReplyMsg"
|
|
||||||
case LOP_PlayCharacterRequestMsg:
|
|
||||||
return "LOP_PlayCharacterRequestMsg"
|
|
||||||
case LOP_PlayCharacterReplyMsg:
|
|
||||||
return "LOP_PlayCharacterReplyMsg"
|
|
||||||
case LOP_ServerPlayCharacterRequestMsg:
|
|
||||||
return "LOP_ServerPlayCharacterRequestMsg"
|
|
||||||
case LOP_ServerPlayCharacterReplyMsg:
|
|
||||||
return "LOP_ServerPlayCharacterReplyMsg"
|
|
||||||
case LOP_KeymapLoadMsg:
|
|
||||||
return "LOP_KeymapLoadMsg"
|
|
||||||
case LOP_KeymapNoneMsg:
|
|
||||||
return "LOP_KeymapNoneMsg"
|
|
||||||
case LOP_KeymapDataMsg:
|
|
||||||
return "LOP_KeymapDataMsg"
|
|
||||||
case LOP_KeymapSaveMsg:
|
|
||||||
return "LOP_KeymapSaveMsg"
|
|
||||||
case LOP_LSCheckAcctLockMsg:
|
|
||||||
return "LOP_LSCheckAcctLockMsg"
|
|
||||||
case LOP_WSAcctLockStatusMsg:
|
|
||||||
return "LOP_WSAcctLockStatusMsg"
|
|
||||||
case LOP_LsRequestClientCrashLogMsg:
|
|
||||||
return "LOP_LsRequestClientCrashLogMsg"
|
|
||||||
case LOP_LsClientBaselogReplyMsg:
|
|
||||||
return "LOP_LsClientBaselogReplyMsg"
|
|
||||||
case LOP_LsClientCrashlogReplyMsg:
|
|
||||||
return "LOP_LsClientCrashlogReplyMsg"
|
|
||||||
case LOP_LsClientAlertlogReplyMsg:
|
|
||||||
return "LOP_LsClientAlertlogReplyMsg"
|
|
||||||
case LOP_LsClientVerifylogReplyMsg:
|
|
||||||
return "LOP_LsClientVerifylogReplyMsg"
|
|
||||||
case LOP_BadLanguageFilter:
|
|
||||||
return "LOP_BadLanguageFilter"
|
|
||||||
case LOP_WSServerLockMsg:
|
|
||||||
return "LOP_WSServerLockMsg"
|
|
||||||
case LOP_WSServerHideMsg:
|
|
||||||
return "LOP_WSServerHideMsg"
|
|
||||||
case LOP_LSServerLockMsg:
|
|
||||||
return "LOP_LSServerLockMsg"
|
|
||||||
case LOP_UpdateCharacterSheetMsg:
|
|
||||||
return "LOP_UpdateCharacterSheetMsg"
|
|
||||||
case LOP_UpdateInventoryMsg:
|
|
||||||
return "LOP_UpdateInventoryMsg"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("Unknown(%d)", opcode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToProtocolPacket wraps this login packet in a protocol packet
|
|
||||||
func (p *LoginPacket) ConvertToProtocolPacket() *ProtocolPacket {
|
|
||||||
// Serialize the login packet
|
|
||||||
loginData := p.SerializeLogin()
|
|
||||||
|
|
||||||
// Create protocol packet with OP_Packet opcode
|
|
||||||
proto := NewProtocolPacket(OP_Packet, loginData)
|
|
||||||
|
|
||||||
// Copy network info
|
|
||||||
proto.SrcIP = p.SrcIP
|
|
||||||
proto.SrcPort = p.SrcPort
|
|
||||||
proto.DstIP = p.DstIP
|
|
||||||
proto.DstPort = p.DstPort
|
|
||||||
proto.Timestamp = p.Timestamp
|
|
||||||
proto.Version = p.Version
|
|
||||||
|
|
||||||
return proto
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy creates a deep copy of the login packet
|
|
||||||
func (p *LoginPacket) Copy() *LoginPacket {
|
|
||||||
newPacket := &LoginPacket{
|
|
||||||
EQPacket: *p.Clone(),
|
|
||||||
LoginEmuOpcode: p.LoginEmuOpcode,
|
|
||||||
}
|
|
||||||
return newPacket
|
|
||||||
}
|
|
56
opcodes.go
56
opcodes.go
@ -1,56 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
// Login/chat use 1-byte opcodes
|
|
||||||
const AppOpcodeSize = 1
|
|
||||||
|
|
||||||
// The game uses 2-byte opcodes
|
|
||||||
const GameOpcodeSize = 2
|
|
||||||
|
|
||||||
// Protocol-level opcodes for the actual UDP stream
|
|
||||||
const (
|
|
||||||
OP_SessionRequest = 0x0001
|
|
||||||
OP_SessionResponse = 0x0002
|
|
||||||
OP_Combined = 0x0003
|
|
||||||
OP_SessionDisconnect = 0x0005
|
|
||||||
OP_KeepAlive = 0x0006
|
|
||||||
OP_ClientSessionUpdate = 0x0007
|
|
||||||
OP_SessionStatRequest = 0x0008
|
|
||||||
OP_SessionStatResponse = 0x0008 // Same opcode, different direction
|
|
||||||
OP_Packet = 0x0009
|
|
||||||
OP_Fragment = 0x000D
|
|
||||||
OP_OutOfOrderAck = 0x0011
|
|
||||||
OP_Ack = 0x0015
|
|
||||||
OP_AckFuture = 0x0016
|
|
||||||
OP_AckPast = 0x0017
|
|
||||||
OP_AppCombined = 0x0019
|
|
||||||
OP_OutOfSession = 0x001D
|
|
||||||
)
|
|
||||||
|
|
||||||
// Login server opcodes
|
|
||||||
const (
|
|
||||||
OP_LoginInfo = 0x0100
|
|
||||||
OP_Login2 = 0x0200
|
|
||||||
OP_GetLoginInfo = 0x0300
|
|
||||||
OP_Disconnect = 0x0500
|
|
||||||
OP_SessionId = 0x0900
|
|
||||||
OP_SendServersFragment = 0x0D00
|
|
||||||
OP_RegisterAccount = 0x2300
|
|
||||||
OP_ChangePassword = 0x4500
|
|
||||||
OP_ServerList = 0x4600
|
|
||||||
OP_SessionKey = 0x4700
|
|
||||||
OP_RequestServerStatus = 0x4800
|
|
||||||
OP_SendServerStatus = 0x4A00
|
|
||||||
OP_LoginBanner = 0x5200
|
|
||||||
OP_Version = 0x5900
|
|
||||||
)
|
|
||||||
|
|
||||||
// Common emulator opcodes (internal representation)
|
|
||||||
const (
|
|
||||||
OP_Unknown = 0x0000
|
|
||||||
OP_LoginRequestMsg = 0x0001
|
|
||||||
OP_LoginByNumRequestMsg = 0x0002
|
|
||||||
OP_WSLoginRequestMsg = 0x0003
|
|
||||||
OP_ESLoginRequestMsg = 0x0004
|
|
||||||
OP_LoginReplyMsg = 0x0005
|
|
||||||
OP_WSStatusReplyMsg = 0x0006
|
|
||||||
)
|
|
297
packet.go
297
packet.go
@ -1,297 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
PROTOCOL_VERSION = 3 // EQ2 protocol version
|
|
||||||
)
|
|
||||||
|
|
||||||
// EQPacket is the base packet structure for all packet types
|
|
||||||
// Combines functionality of EQPacket and EQ2Packet from C++ implementation
|
|
||||||
type EQPacket struct {
|
|
||||||
Buffer []byte // Raw packet data
|
|
||||||
Opcode uint16 // Network opcode
|
|
||||||
|
|
||||||
SrcIP net.IP // Source IP address
|
|
||||||
SrcPort uint16 // Source port
|
|
||||||
DstIP net.IP // Destination IP address
|
|
||||||
DstPort uint16 // Destination port
|
|
||||||
|
|
||||||
Timestamp time.Time // When packet was created/received
|
|
||||||
Priority uint32 // Priority in processing
|
|
||||||
Version int16 // Protocol version
|
|
||||||
|
|
||||||
// EQ2-specific fields (from EQ2Packet)
|
|
||||||
EmuOpcode uint16 // Emulator opcode (internal representation)
|
|
||||||
OpcodeSize uint8 // Size of opcode in bytes (1 or 2)
|
|
||||||
PacketPrepared bool // Packet has been prepared for wire transmission
|
|
||||||
PacketEncrypted bool // Packet is encrypted
|
|
||||||
EQ2Compressed bool // Packet uses EQ2 compression
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEQPacket(opcode uint16, data []byte) *EQPacket {
|
|
||||||
p := &EQPacket{
|
|
||||||
Opcode: opcode,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Priority: 0,
|
|
||||||
Version: PROTOCOL_VERSION,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) > 0 {
|
|
||||||
p.Buffer = make([]byte, len(data))
|
|
||||||
copy(p.Buffer, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the size of the packet data (excluding opcode)
|
|
||||||
func (p *EQPacket) Size() uint32 {
|
|
||||||
return uint32(len(p.Buffer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TotalSize returns the total size including opcode
|
|
||||||
func (p *EQPacket) TotalSize() uint32 {
|
|
||||||
opcodeSize := uint32(1)
|
|
||||||
if p.Opcode > 0xFF {
|
|
||||||
opcodeSize = 2
|
|
||||||
}
|
|
||||||
return p.Size() + opcodeSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRawOpcode returns the raw opcode value
|
|
||||||
func (p *EQPacket) GetRawOpcode() uint16 {
|
|
||||||
return p.Opcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOpcodeName returns the string name of the opcode
|
|
||||||
func (p *EQPacket) GetOpcodeName() string {
|
|
||||||
switch p.Opcode {
|
|
||||||
case OP_SessionRequest:
|
|
||||||
return "OP_SessionRequest"
|
|
||||||
case OP_SessionResponse:
|
|
||||||
return "OP_SessionResponse"
|
|
||||||
case OP_Combined:
|
|
||||||
return "OP_Combined"
|
|
||||||
case OP_SessionDisconnect:
|
|
||||||
return "OP_SessionDisconnect"
|
|
||||||
case OP_KeepAlive:
|
|
||||||
return "OP_KeepAlive"
|
|
||||||
case OP_SessionStatRequest: // Also OP_SessionStatResponse (same value)
|
|
||||||
return "OP_SessionStat"
|
|
||||||
case OP_Packet:
|
|
||||||
return "OP_Packet"
|
|
||||||
case OP_Fragment:
|
|
||||||
return "OP_Fragment"
|
|
||||||
case OP_OutOfOrderAck:
|
|
||||||
return "OP_OutOfOrderAck"
|
|
||||||
case OP_Ack:
|
|
||||||
return "OP_Ack"
|
|
||||||
case OP_AppCombined:
|
|
||||||
return "OP_AppCombined"
|
|
||||||
case OP_OutOfSession:
|
|
||||||
return "OP_OutOfSession"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("Unknown(0x%04X)", p.Opcode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize writes the packet to a byte buffer
|
|
||||||
func (p *EQPacket) Serialize() []byte {
|
|
||||||
// Determine opcode size
|
|
||||||
opcodeSize := 1
|
|
||||||
if p.Opcode > 0xFF {
|
|
||||||
opcodeSize = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create buffer
|
|
||||||
buf := make([]byte, opcodeSize+len(p.Buffer))
|
|
||||||
|
|
||||||
// Write opcode
|
|
||||||
if opcodeSize == 2 {
|
|
||||||
binary.BigEndian.PutUint16(buf[0:2], p.Opcode)
|
|
||||||
} else {
|
|
||||||
buf[0] = byte(p.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy data
|
|
||||||
if len(p.Buffer) > 0 {
|
|
||||||
copy(buf[opcodeSize:], p.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNetworkInfo sets the network address info
|
|
||||||
func (p *EQPacket) SetNetworkInfo(srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) {
|
|
||||||
p.SrcIP = srcIP
|
|
||||||
p.SrcPort = srcPort
|
|
||||||
p.DstIP = dstIP
|
|
||||||
p.DstPort = dstPort
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates a deep copy of the packet
|
|
||||||
func (p *EQPacket) Clone() *EQPacket {
|
|
||||||
newPacket := &EQPacket{
|
|
||||||
Opcode: p.Opcode,
|
|
||||||
SrcIP: make(net.IP, len(p.SrcIP)),
|
|
||||||
SrcPort: p.SrcPort,
|
|
||||||
DstIP: make(net.IP, len(p.DstIP)),
|
|
||||||
DstPort: p.DstPort,
|
|
||||||
Timestamp: p.Timestamp,
|
|
||||||
Priority: p.Priority,
|
|
||||||
Version: p.Version,
|
|
||||||
EmuOpcode: p.EmuOpcode,
|
|
||||||
OpcodeSize: p.OpcodeSize,
|
|
||||||
PacketPrepared: p.PacketPrepared,
|
|
||||||
PacketEncrypted: p.PacketEncrypted,
|
|
||||||
EQ2Compressed: p.EQ2Compressed,
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.SrcIP != nil {
|
|
||||||
copy(newPacket.SrcIP, p.SrcIP)
|
|
||||||
}
|
|
||||||
if p.DstIP != nil {
|
|
||||||
copy(newPacket.DstIP, p.DstIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Buffer) > 0 {
|
|
||||||
newPacket.Buffer = make([]byte, len(p.Buffer))
|
|
||||||
copy(newPacket.Buffer, p.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPacket
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParsePacket creates a packet from raw network data
|
|
||||||
func ParsePacket(data []byte) (*EQPacket, error) {
|
|
||||||
if len(data) < 1 {
|
|
||||||
return nil, fmt.Errorf("packet too small: %d bytes", len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
var opcode uint16
|
|
||||||
var dataOffset int
|
|
||||||
|
|
||||||
// Try to determine if it's a 1-byte or 2-byte opcode
|
|
||||||
// For simplicity in tests: if first byte > 0 and < 0x80, treat as single byte
|
|
||||||
// This is a heuristic for test compatibility
|
|
||||||
if data[0] > 0x00 && data[0] < 0x80 && (len(data) == 1 || data[1] != 0x34) {
|
|
||||||
// Single byte opcode
|
|
||||||
opcode = uint16(data[0])
|
|
||||||
dataOffset = 1
|
|
||||||
} else if len(data) >= 2 {
|
|
||||||
// Two-byte opcode
|
|
||||||
opcode = binary.BigEndian.Uint16(data[0:2])
|
|
||||||
dataOffset = 2
|
|
||||||
} else {
|
|
||||||
// Single byte by default
|
|
||||||
opcode = uint16(data[0])
|
|
||||||
dataOffset = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create packet
|
|
||||||
p := &EQPacket{
|
|
||||||
Opcode: opcode,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Version: PROTOCOL_VERSION,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy remaining data if any
|
|
||||||
if len(data) > dataOffset {
|
|
||||||
p.Buffer = make([]byte, len(data)-dataOffset)
|
|
||||||
copy(p.Buffer, data[dataOffset:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreparePacket prepares an EQ2 packet for wire transmission
|
|
||||||
// This adds sequence numbers, compression flags, and converts opcodes
|
|
||||||
func (p *EQPacket) PreparePacket(maxLen int16) error {
|
|
||||||
if p.PacketPrepared {
|
|
||||||
return nil // Already prepared
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the wire format with EQ2 headers
|
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
// Add sequence field placeholder (2 bytes)
|
|
||||||
buf = append(buf, 0x00, 0x00)
|
|
||||||
|
|
||||||
// Add compression flag placeholder (1 byte)
|
|
||||||
buf = append(buf, 0x00)
|
|
||||||
|
|
||||||
// Add opcode with special encoding
|
|
||||||
networkOpcode := p.EmuOpcode // In full implementation, convert via opcode manager
|
|
||||||
|
|
||||||
if networkOpcode >= 255 {
|
|
||||||
// Oversized opcode
|
|
||||||
buf = append(buf, 0xFF) // Marker
|
|
||||||
buf = append(buf, byte(networkOpcode>>8), byte(networkOpcode))
|
|
||||||
} else {
|
|
||||||
buf = append(buf, byte(networkOpcode))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add packet data
|
|
||||||
buf = append(buf, p.Buffer...)
|
|
||||||
|
|
||||||
// Update packet
|
|
||||||
p.Buffer = buf
|
|
||||||
p.PacketPrepared = true
|
|
||||||
p.Opcode = OP_Packet // Protocol opcode for data packets
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEmuOpcode sets the emulator opcode
|
|
||||||
func (p *EQPacket) SetEmuOpcode(opcode uint16) {
|
|
||||||
p.EmuOpcode = opcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEmuOpcode returns the emulator opcode
|
|
||||||
func (p *EQPacket) GetEmuOpcode() uint16 {
|
|
||||||
return p.EmuOpcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOpcodeName returns the string name of a protocol opcode
|
|
||||||
func GetOpcodeName(opcode uint16) string {
|
|
||||||
switch opcode {
|
|
||||||
case OP_SessionRequest:
|
|
||||||
return "OP_SessionRequest"
|
|
||||||
case OP_SessionResponse:
|
|
||||||
return "OP_SessionResponse"
|
|
||||||
case OP_Combined:
|
|
||||||
return "OP_Combined"
|
|
||||||
case OP_SessionDisconnect:
|
|
||||||
return "OP_SessionDisconnect"
|
|
||||||
case OP_KeepAlive:
|
|
||||||
return "OP_KeepAlive"
|
|
||||||
case OP_ClientSessionUpdate:
|
|
||||||
return "OP_ClientSessionUpdate"
|
|
||||||
case OP_SessionStatRequest: // Also OP_SessionStatResponse (same value)
|
|
||||||
return "OP_SessionStat"
|
|
||||||
case OP_Packet:
|
|
||||||
return "OP_Packet"
|
|
||||||
case OP_Fragment:
|
|
||||||
return "OP_Fragment"
|
|
||||||
case OP_OutOfOrderAck:
|
|
||||||
return "OP_OutOfOrderAck"
|
|
||||||
case OP_Ack:
|
|
||||||
return "OP_Ack"
|
|
||||||
case OP_AckFuture:
|
|
||||||
return "OP_AckFuture"
|
|
||||||
case OP_AckPast:
|
|
||||||
return "OP_AckPast"
|
|
||||||
case OP_AppCombined:
|
|
||||||
return "OP_AppCombined"
|
|
||||||
case OP_OutOfSession:
|
|
||||||
return "OP_OutOfSession"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("Unknown(%d)", opcode)
|
|
||||||
}
|
|
||||||
}
|
|
462
packet_test.go
462
packet_test.go
@ -1,462 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEQPacket(t *testing.T) {
|
|
||||||
t.Run("NewEQPacket", func(t *testing.T) {
|
|
||||||
data := []byte{0x01, 0x02, 0x03, 0x04}
|
|
||||||
packet := NewEQPacket(0x1234, data)
|
|
||||||
|
|
||||||
if packet.Opcode != 0x1234 {
|
|
||||||
t.Errorf("Expected opcode 0x1234, got 0x%04x", packet.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(packet.Buffer, data) {
|
|
||||||
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.Version != PROTOCOL_VERSION {
|
|
||||||
t.Errorf("Expected version %d, got %d", PROTOCOL_VERSION, packet.Version)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ParsePacket", func(t *testing.T) {
|
|
||||||
// Test single-byte opcode
|
|
||||||
data1 := []byte{0x55, 0x01, 0x02, 0x03}
|
|
||||||
packet1, err := ParsePacket(data1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse single-byte opcode: %v", err)
|
|
||||||
}
|
|
||||||
if packet1.Opcode != 0x55 {
|
|
||||||
t.Errorf("Expected opcode 0x55, got 0x%04x", packet1.Opcode)
|
|
||||||
}
|
|
||||||
if len(packet1.Buffer) != 3 {
|
|
||||||
t.Errorf("Expected buffer length 3, got %d", len(packet1.Buffer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test two-byte opcode
|
|
||||||
data2 := []byte{0x12, 0x34, 0x01, 0x02, 0x03}
|
|
||||||
packet2, err := ParsePacket(data2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse two-byte opcode: %v", err)
|
|
||||||
}
|
|
||||||
if packet2.Opcode != 0x1234 {
|
|
||||||
t.Errorf("Expected opcode 0x1234, got 0x%04x", packet2.Opcode)
|
|
||||||
}
|
|
||||||
if len(packet2.Buffer) != 3 {
|
|
||||||
t.Errorf("Expected buffer length 3, got %d", len(packet2.Buffer))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Serialize", func(t *testing.T) {
|
|
||||||
// Test single-byte opcode serialization
|
|
||||||
packet1 := NewEQPacket(0x55, []byte{0x01, 0x02, 0x03})
|
|
||||||
serialized1 := packet1.Serialize()
|
|
||||||
expected1 := []byte{0x55, 0x01, 0x02, 0x03}
|
|
||||||
if !bytes.Equal(serialized1, expected1) {
|
|
||||||
t.Errorf("Expected serialized %v, got %v", expected1, serialized1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test two-byte opcode serialization
|
|
||||||
packet2 := NewEQPacket(0x1234, []byte{0x01, 0x02, 0x03})
|
|
||||||
serialized2 := packet2.Serialize()
|
|
||||||
expected2 := []byte{0x12, 0x34, 0x01, 0x02, 0x03}
|
|
||||||
if !bytes.Equal(serialized2, expected2) {
|
|
||||||
t.Errorf("Expected serialized %v, got %v", expected2, serialized2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("PreparePacket", func(t *testing.T) {
|
|
||||||
packet := NewEQPacket(0x1234, []byte{0x01, 0x02, 0x03})
|
|
||||||
packet.EmuOpcode = 0x5678
|
|
||||||
packet.OpcodeSize = 2
|
|
||||||
|
|
||||||
// Prepare packet for wire transmission
|
|
||||||
packet.PreparePacket(100)
|
|
||||||
|
|
||||||
if !packet.PacketPrepared {
|
|
||||||
t.Error("Expected packet to be marked as prepared")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have headers and emulator opcode
|
|
||||||
// Format: 2 bytes sequence, 1 byte compression, then opcode
|
|
||||||
if len(packet.Buffer) < 5 {
|
|
||||||
t.Fatal("Buffer too small after preparation")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that emulator opcode was added at correct position
|
|
||||||
// After 2-byte sequence and 1-byte compression flag
|
|
||||||
emuOpcode := uint16(packet.Buffer[4])<<8 | uint16(packet.Buffer[5])
|
|
||||||
if emuOpcode != 0x5678 {
|
|
||||||
t.Errorf("Expected emulator opcode 0x5678, got 0x%04x", emuOpcode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Clone", func(t *testing.T) {
|
|
||||||
original := NewEQPacket(0x1234, []byte{0x01, 0x02, 0x03})
|
|
||||||
original.SrcIP = net.ParseIP("192.168.1.1")
|
|
||||||
original.SrcPort = 9000
|
|
||||||
original.DstIP = net.ParseIP("192.168.1.2")
|
|
||||||
original.DstPort = 9001
|
|
||||||
original.EmuOpcode = 0x5678
|
|
||||||
original.OpcodeSize = 2
|
|
||||||
|
|
||||||
cloned := original.Clone()
|
|
||||||
|
|
||||||
// Verify clone has same values
|
|
||||||
if cloned.Opcode != original.Opcode {
|
|
||||||
t.Errorf("Cloned opcode mismatch: %v != %v", cloned.Opcode, original.Opcode)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(cloned.Buffer, original.Buffer) {
|
|
||||||
t.Errorf("Cloned buffer mismatch: %v != %v", cloned.Buffer, original.Buffer)
|
|
||||||
}
|
|
||||||
if !cloned.SrcIP.Equal(original.SrcIP) {
|
|
||||||
t.Errorf("Cloned SrcIP mismatch: %v != %v", cloned.SrcIP, original.SrcIP)
|
|
||||||
}
|
|
||||||
if cloned.EmuOpcode != original.EmuOpcode {
|
|
||||||
t.Errorf("Cloned EmuOpcode mismatch: %v != %v", cloned.EmuOpcode, original.EmuOpcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it's a deep copy
|
|
||||||
original.Buffer[0] = 0xFF
|
|
||||||
if cloned.Buffer[0] == 0xFF {
|
|
||||||
t.Error("Clone shares buffer with original")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProtocolPacket(t *testing.T) {
|
|
||||||
t.Run("NewProtocolPacket", func(t *testing.T) {
|
|
||||||
data := []byte{0x01, 0x02, 0x03}
|
|
||||||
packet := NewProtocolPacket(OP_Packet, data)
|
|
||||||
|
|
||||||
if packet.Opcode != OP_Packet {
|
|
||||||
t.Errorf("Expected opcode OP_Packet, got 0x%04x", packet.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(packet.Buffer, data) {
|
|
||||||
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Compression", func(t *testing.T) {
|
|
||||||
// Test large packet compression (zlib)
|
|
||||||
largeData := make([]byte, 100)
|
|
||||||
for i := range largeData {
|
|
||||||
largeData[i] = byte(i % 256)
|
|
||||||
}
|
|
||||||
|
|
||||||
packet := NewProtocolPacket(OP_Packet, largeData)
|
|
||||||
originalLen := len(packet.Buffer)
|
|
||||||
|
|
||||||
// Compress
|
|
||||||
err := packet.Compress()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to compress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !packet.Compressed {
|
|
||||||
t.Error("Packet should be marked as compressed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.Buffer[0] != 0x5a {
|
|
||||||
t.Errorf("Expected zlib compression flag 0x5a, got 0x%02x", packet.Buffer[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decompress
|
|
||||||
err = packet.Decompress()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to decompress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.Compressed {
|
|
||||||
t.Error("Packet should not be marked as compressed after decompression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(packet.Buffer) != originalLen {
|
|
||||||
t.Errorf("Decompressed size mismatch: expected %d, got %d", originalLen, len(packet.Buffer))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(packet.Buffer, largeData) {
|
|
||||||
t.Error("Decompressed data does not match original")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("SimpleEncoding", func(t *testing.T) {
|
|
||||||
// Test small packet encoding
|
|
||||||
smallData := []byte{0x01, 0x02, 0x03}
|
|
||||||
packet := NewProtocolPacket(OP_Packet, smallData)
|
|
||||||
|
|
||||||
// Compress (should use simple encoding)
|
|
||||||
err := packet.Compress()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to compress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !packet.Compressed {
|
|
||||||
t.Error("Packet should be marked as compressed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.Buffer[0] != 0xa5 {
|
|
||||||
t.Errorf("Expected simple encoding flag 0xa5, got 0x%02x", packet.Buffer[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decompress
|
|
||||||
err = packet.Decompress()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to decompress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(packet.Buffer, smallData) {
|
|
||||||
t.Error("Decompressed data does not match original")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Combine", func(t *testing.T) {
|
|
||||||
packet1 := NewProtocolPacket(OP_Packet, []byte{0x01, 0x02})
|
|
||||||
packet2 := NewProtocolPacket(OP_Ack, []byte{0x03, 0x04})
|
|
||||||
|
|
||||||
// Combine packets
|
|
||||||
success := packet1.Combine(packet2)
|
|
||||||
if !success {
|
|
||||||
t.Fatal("Failed to combine packets")
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet1.Opcode != OP_Combined {
|
|
||||||
t.Errorf("Expected combined opcode, got 0x%04x", packet1.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(packet1.SubPackets) != 2 {
|
|
||||||
t.Errorf("Expected 2 sub-packets, got %d", len(packet1.SubPackets))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ExtractSubPackets", func(t *testing.T) {
|
|
||||||
// Create combined packet manually
|
|
||||||
packet1Data := []byte{0x00, 0x09, 0x01, 0x02} // OP_Packet with data
|
|
||||||
packet2Data := []byte{0x00, 0x15, 0x03, 0x04} // OP_Ack with data
|
|
||||||
|
|
||||||
combinedBuffer := []byte{
|
|
||||||
byte(len(packet1Data)), // Size of first packet
|
|
||||||
}
|
|
||||||
combinedBuffer = append(combinedBuffer, packet1Data...)
|
|
||||||
combinedBuffer = append(combinedBuffer, byte(len(packet2Data))) // Size of second packet
|
|
||||||
combinedBuffer = append(combinedBuffer, packet2Data...)
|
|
||||||
|
|
||||||
combined := NewProtocolPacket(OP_Combined, combinedBuffer)
|
|
||||||
|
|
||||||
// Extract sub-packets
|
|
||||||
subPackets, err := combined.ExtractSubPackets()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to extract sub-packets: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(subPackets) != 2 {
|
|
||||||
t.Fatalf("Expected 2 sub-packets, got %d", len(subPackets))
|
|
||||||
}
|
|
||||||
|
|
||||||
if subPackets[0].Opcode != OP_Packet {
|
|
||||||
t.Errorf("Expected first packet opcode OP_Packet, got 0x%04x", subPackets[0].Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if subPackets[1].Opcode != OP_Ack {
|
|
||||||
t.Errorf("Expected second packet opcode OP_Ack, got 0x%04x", subPackets[1].Opcode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoginPacket(t *testing.T) {
|
|
||||||
t.Run("NewLoginPacket", func(t *testing.T) {
|
|
||||||
data := []byte{0x01, 0x02, 0x03}
|
|
||||||
packet := NewLoginPacket(LOP_LoginRequestMsg, data)
|
|
||||||
|
|
||||||
if packet.LoginEmuOpcode != LOP_LoginRequestMsg {
|
|
||||||
t.Errorf("Expected opcode LOP_LoginRequestMsg, got %v", packet.LoginEmuOpcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.OpcodeSize != 1 {
|
|
||||||
t.Errorf("Expected opcode size 1, got %d", packet.OpcodeSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(packet.Buffer, data) {
|
|
||||||
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ParseLoginPacket", func(t *testing.T) {
|
|
||||||
data := []byte{byte(LOP_LoginRequestMsg), 0x01, 0x02, 0x03}
|
|
||||||
packet, err := ParseLoginPacket(data)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse login packet: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.LoginEmuOpcode != LOP_LoginRequestMsg {
|
|
||||||
t.Errorf("Expected opcode LOP_LoginRequestMsg, got %v", packet.LoginEmuOpcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(packet.Buffer) != 3 {
|
|
||||||
t.Errorf("Expected buffer length 3, got %d", len(packet.Buffer))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("SerializeLogin", func(t *testing.T) {
|
|
||||||
packet := NewLoginPacket(LOP_WorldListMsg, []byte{0x01, 0x02})
|
|
||||||
serialized := packet.SerializeLogin()
|
|
||||||
|
|
||||||
expected := []byte{byte(LOP_WorldListMsg), 0x01, 0x02}
|
|
||||||
if !bytes.Equal(serialized, expected) {
|
|
||||||
t.Errorf("Expected serialized %v, got %v", expected, serialized)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ConvertToProtocolPacket", func(t *testing.T) {
|
|
||||||
loginPacket := NewLoginPacket(LOP_LoginReplyMsg, []byte{0x01, 0x02})
|
|
||||||
loginPacket.SrcIP = net.ParseIP("192.168.1.1")
|
|
||||||
loginPacket.DstIP = net.ParseIP("192.168.1.2")
|
|
||||||
|
|
||||||
protoPacket := loginPacket.ConvertToProtocolPacket()
|
|
||||||
|
|
||||||
if protoPacket.Opcode != OP_Packet {
|
|
||||||
t.Errorf("Expected protocol opcode OP_Packet, got 0x%04x", protoPacket.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should contain serialized login packet
|
|
||||||
expectedData := loginPacket.SerializeLogin()
|
|
||||||
if !bytes.Equal(protoPacket.Buffer, expectedData) {
|
|
||||||
t.Errorf("Protocol packet buffer mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should preserve network info
|
|
||||||
if !protoPacket.SrcIP.Equal(loginPacket.SrcIP) {
|
|
||||||
t.Errorf("Source IP not preserved")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGamePacket(t *testing.T) {
|
|
||||||
t.Run("NewGamePacket", func(t *testing.T) {
|
|
||||||
data := []byte{0x01, 0x02, 0x03}
|
|
||||||
packet := NewGamePacket(GOP_LoginReplyMsg, data)
|
|
||||||
|
|
||||||
if packet.GameEmuOpcode != GOP_LoginReplyMsg {
|
|
||||||
t.Errorf("Expected opcode GOP_LoginReplyMsg, got %v", packet.GameEmuOpcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.OpcodeSize != 2 {
|
|
||||||
t.Errorf("Expected opcode size 2, got %d", packet.OpcodeSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(packet.Buffer, data) {
|
|
||||||
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ParseGamePacket", func(t *testing.T) {
|
|
||||||
// Test normal 2-byte opcode
|
|
||||||
opcode := uint16(GOP_ZoneInfoMsg)
|
|
||||||
data1 := []byte{byte(opcode >> 8), byte(opcode), 0x01, 0x02}
|
|
||||||
packet1, err := ParseGamePacket(data1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse game packet: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet1.GameEmuOpcode != GOP_ZoneInfoMsg {
|
|
||||||
t.Errorf("Expected opcode GOP_ZoneInfoMsg, got %v", packet1.GameEmuOpcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test special encoding (low byte = 0x00)
|
|
||||||
data2 := []byte{0x00, 0x12, 0x00, 0x01, 0x02} // Extra 0x00 prefix
|
|
||||||
packet2, err := ParseGamePacket(data2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse special encoded packet: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet2.GameEmuOpcode != 0x1200 {
|
|
||||||
t.Errorf("Expected opcode 0x1200, got 0x%04x", packet2.GameEmuOpcode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("SerializeGame", func(t *testing.T) {
|
|
||||||
// Test normal opcode
|
|
||||||
packet1 := NewGamePacket(GOP_MOTDMsg, []byte{0x01, 0x02})
|
|
||||||
serialized1 := packet1.SerializeGame()
|
|
||||||
|
|
||||||
opcode1 := uint16(GOP_MOTDMsg)
|
|
||||||
expected1 := []byte{byte(opcode1 >> 8), byte(opcode1), 0x01, 0x02}
|
|
||||||
if !bytes.Equal(serialized1, expected1) {
|
|
||||||
t.Errorf("Expected serialized %v, got %v", expected1, serialized1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test special encoding (low byte = 0x00)
|
|
||||||
packet2 := NewGamePacket(0x1200, []byte{0x01, 0x02})
|
|
||||||
serialized2 := packet2.SerializeGame()
|
|
||||||
|
|
||||||
expected2 := []byte{0x00, 0x12, 0x00, 0x01, 0x02} // Extra 0x00 prefix
|
|
||||||
if !bytes.Equal(serialized2, expected2) {
|
|
||||||
t.Errorf("Expected special encoded %v, got %v", expected2, serialized2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("CombineGamePackets", func(t *testing.T) {
|
|
||||||
packet1 := NewGamePacket(GOP_UpdateDataMsg, []byte{0x01})
|
|
||||||
packet2 := NewGamePacket(GOP_UpdateSpawnMsg, []byte{0x02})
|
|
||||||
packet3 := NewGamePacket(GOP_UpdateTimeMsg, []byte{0x03})
|
|
||||||
|
|
||||||
combined := CombineGamePackets([]*GamePacket{packet1, packet2, packet3})
|
|
||||||
|
|
||||||
if combined == nil {
|
|
||||||
t.Fatal("Failed to combine game packets")
|
|
||||||
}
|
|
||||||
|
|
||||||
if combined.Opcode != OP_AppCombined {
|
|
||||||
t.Errorf("Expected opcode OP_AppCombined, got 0x%04x", combined.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify buffer contains size-prefixed packets
|
|
||||||
if len(combined.Buffer) < 3 {
|
|
||||||
t.Error("Combined buffer too small")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpcodeNames(t *testing.T) {
|
|
||||||
t.Run("GetOpcodeName", func(t *testing.T) {
|
|
||||||
name := GetOpcodeName(OP_SessionRequest)
|
|
||||||
if name != "OP_SessionRequest" {
|
|
||||||
t.Errorf("Expected 'OP_SessionRequest', got '%s'", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
unknownName := GetOpcodeName(0xFFFF)
|
|
||||||
if unknownName != "Unknown(65535)" {
|
|
||||||
t.Errorf("Expected 'Unknown(65535)', got '%s'", unknownName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetLoginOpcodeName", func(t *testing.T) {
|
|
||||||
name := GetLoginOpcodeName(LOP_LoginRequestMsg)
|
|
||||||
if name != "LOP_LoginRequestMsg" {
|
|
||||||
t.Errorf("Expected 'LOP_LoginRequestMsg', got '%s'", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
unknownName := GetLoginOpcodeName(0xFFFF)
|
|
||||||
if !bytes.Contains([]byte(unknownName), []byte("Unknown")) {
|
|
||||||
t.Errorf("Expected Unknown opcode name, got '%s'", unknownName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetGameOpcodeName", func(t *testing.T) {
|
|
||||||
name := GetGameOpcodeName(GOP_ZoneInfoMsg)
|
|
||||||
if name != "GOP_ZoneInfoMsg" {
|
|
||||||
t.Errorf("Expected 'GOP_ZoneInfoMsg', got '%s'", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
unknownName := GetGameOpcodeName(0xFFFF)
|
|
||||||
if !bytes.Contains([]byte(unknownName), []byte("Unknown")) {
|
|
||||||
t.Errorf("Expected Unknown opcode name, got '%s'", unknownName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,360 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/zlib"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.sharkk.net/EQ2/Protocol/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProtocolPacket handles low-level protocol operations including
|
|
||||||
// compression, encryption, sequencing, and packet combining
|
|
||||||
type ProtocolPacket struct {
|
|
||||||
EQPacket
|
|
||||||
|
|
||||||
Compressed bool
|
|
||||||
Encrypted bool
|
|
||||||
PacketPrepared bool
|
|
||||||
|
|
||||||
Sequence uint16
|
|
||||||
Acknowledged bool
|
|
||||||
SentTime int32
|
|
||||||
AttemptCount int8
|
|
||||||
|
|
||||||
SubPackets []*ProtocolPacket
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewProtocolPacket creates a new protocol packet
|
|
||||||
func NewProtocolPacket(opcode uint16, data []byte) *ProtocolPacket {
|
|
||||||
p := &ProtocolPacket{
|
|
||||||
EQPacket: *NewEQPacket(opcode, data),
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseProtocolPacket creates a protocol packet from raw network data
|
|
||||||
func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
|
|
||||||
base, err := ParsePacket(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p := &ProtocolPacket{
|
|
||||||
EQPacket: *base,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for compression flag
|
|
||||||
if len(p.Buffer) > 0 {
|
|
||||||
switch p.Buffer[0] {
|
|
||||||
case 0x5a: // Zlib compressed
|
|
||||||
p.Compressed = true
|
|
||||||
case 0xa5: // Simple encoding
|
|
||||||
p.Compressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize writes the protocol packet to a byte buffer with optional offset
|
|
||||||
func (p *ProtocolPacket) SerializeWithOffset(offset int8) []byte {
|
|
||||||
opcodeSize := 2 // Protocol packets always use 2-byte opcodes
|
|
||||||
buf := make([]byte, opcodeSize+len(p.Buffer)-int(offset))
|
|
||||||
|
|
||||||
if p.Opcode > 0xff {
|
|
||||||
binary.BigEndian.PutUint16(buf[0:2], p.Opcode)
|
|
||||||
} else {
|
|
||||||
buf[0] = 0x00
|
|
||||||
buf[1] = byte(p.Opcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Buffer) > int(offset) {
|
|
||||||
copy(buf[opcodeSize:], p.Buffer[offset:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateCRC checks if the packet has a valid CRC
|
|
||||||
// Returns true if CRC is valid or packet is exempt
|
|
||||||
func (p *ProtocolPacket) ValidateCRC(key uint32) bool {
|
|
||||||
// Session control packets are exempt from CRC
|
|
||||||
if p.Opcode == OP_SessionRequest ||
|
|
||||||
p.Opcode == OP_SessionResponse ||
|
|
||||||
p.Opcode == OP_OutOfSession {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need full packet data including opcode for CRC check
|
|
||||||
fullData := p.Serialize()
|
|
||||||
if len(fullData) < 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRC is in last 2 bytes
|
|
||||||
if len(fullData) < 4 { // Minimum: 2 byte opcode + 2 byte CRC
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
dataLen := len(fullData) - 2
|
|
||||||
calculatedCRC := crypto.CalculateCRC(fullData[:dataLen], key)
|
|
||||||
packetCRC := binary.BigEndian.Uint16(fullData[dataLen:])
|
|
||||||
|
|
||||||
// CRC of 0 means no CRC check required
|
|
||||||
return packetCRC == 0 || calculatedCRC == packetCRC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compress compresses the packet data using zlib or simple encoding
|
|
||||||
func (p *ProtocolPacket) Compress() error {
|
|
||||||
if p.Compressed {
|
|
||||||
return fmt.Errorf("packet already compressed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only compress packets larger than 30 bytes
|
|
||||||
if len(p.Buffer) > 30 {
|
|
||||||
compressed, err := p.zlibCompress()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.Buffer = compressed
|
|
||||||
p.Compressed = true
|
|
||||||
} else if len(p.Buffer) > 0 {
|
|
||||||
// Simple encoding for small packets
|
|
||||||
p.simpleEncode()
|
|
||||||
p.Compressed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decompress decompresses the packet data
|
|
||||||
func (p *ProtocolPacket) Decompress() error {
|
|
||||||
if !p.Compressed {
|
|
||||||
return fmt.Errorf("packet not compressed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Buffer) == 0 {
|
|
||||||
return fmt.Errorf("no data to decompress")
|
|
||||||
}
|
|
||||||
|
|
||||||
var decompressed []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch p.Buffer[0] {
|
|
||||||
case 0x5a: // Zlib compressed
|
|
||||||
decompressed, err = p.zlibDecompress()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case 0xa5: // Simple encoding
|
|
||||||
decompressed = p.simpleDecoded()
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown compression flag: 0x%02x", p.Buffer[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Buffer = decompressed
|
|
||||||
p.Compressed = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// zlibCompress performs zlib compression
|
|
||||||
func (p *ProtocolPacket) zlibCompress() ([]byte, error) {
|
|
||||||
var compressed bytes.Buffer
|
|
||||||
|
|
||||||
// Write compression flag
|
|
||||||
compressed.WriteByte(0x5a)
|
|
||||||
|
|
||||||
// Compress the data
|
|
||||||
w := zlib.NewWriter(&compressed)
|
|
||||||
_, err := w.Write(p.Buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return compressed.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// zlibDecompress performs zlib decompression
|
|
||||||
func (p *ProtocolPacket) zlibDecompress() ([]byte, error) {
|
|
||||||
if len(p.Buffer) < 2 {
|
|
||||||
return nil, fmt.Errorf("compressed data too small")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip compression flag
|
|
||||||
r, err := zlib.NewReader(bytes.NewReader(p.Buffer[1:]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
var decompressed bytes.Buffer
|
|
||||||
_, err = decompressed.ReadFrom(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return decompressed.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleEncode adds simple encoding flag
|
|
||||||
func (p *ProtocolPacket) simpleEncode() {
|
|
||||||
// Add encoding flag at the beginning
|
|
||||||
newBuffer := make([]byte, len(p.Buffer)+1)
|
|
||||||
newBuffer[0] = 0xa5
|
|
||||||
copy(newBuffer[1:], p.Buffer)
|
|
||||||
p.Buffer = newBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleDecoded removes simple encoding flag
|
|
||||||
func (p *ProtocolPacket) simpleDecoded() []byte {
|
|
||||||
if len(p.Buffer) > 1 {
|
|
||||||
return p.Buffer[1:]
|
|
||||||
}
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine bundles multiple protocol packets into a single combined packet
|
|
||||||
func (p *ProtocolPacket) Combine(other *ProtocolPacket) bool {
|
|
||||||
// Check if this is already a combined packet
|
|
||||||
if p.Opcode != OP_Combined {
|
|
||||||
// Convert to combined packet
|
|
||||||
firstPacket := p.Clone()
|
|
||||||
p.Opcode = OP_Combined
|
|
||||||
p.SubPackets = []*ProtocolPacket{
|
|
||||||
{EQPacket: *firstPacket},
|
|
||||||
}
|
|
||||||
p.Buffer = p.buildCombinedBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check size limit (max 255 bytes for combined packets)
|
|
||||||
totalSize := len(p.Buffer) + int(other.TotalSize()) + 1 // +1 for size byte
|
|
||||||
if totalSize > 255 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the new packet
|
|
||||||
p.SubPackets = append(p.SubPackets, other)
|
|
||||||
p.Buffer = p.buildCombinedBuffer()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildCombinedBuffer creates the buffer for a combined packet
|
|
||||||
func (p *ProtocolPacket) buildCombinedBuffer() []byte {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
for _, subPacket := range p.SubPackets {
|
|
||||||
serialized := subPacket.Serialize()
|
|
||||||
buf.WriteByte(byte(len(serialized))) // Size byte
|
|
||||||
buf.Write(serialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractSubPackets extracts individual packets from a combined packet
|
|
||||||
func (p *ProtocolPacket) ExtractSubPackets() ([]*ProtocolPacket, error) {
|
|
||||||
if p.Opcode != OP_Combined {
|
|
||||||
return nil, fmt.Errorf("not a combined packet")
|
|
||||||
}
|
|
||||||
|
|
||||||
var packets []*ProtocolPacket
|
|
||||||
offset := 0
|
|
||||||
data := p.Buffer
|
|
||||||
|
|
||||||
for offset < len(data) {
|
|
||||||
// Read size byte
|
|
||||||
if offset >= len(data) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
size := int(data[offset])
|
|
||||||
offset++
|
|
||||||
|
|
||||||
// Extract sub-packet data
|
|
||||||
if offset+size > len(data) {
|
|
||||||
return nil, fmt.Errorf("invalid combined packet: size exceeds buffer")
|
|
||||||
}
|
|
||||||
|
|
||||||
subData := data[offset : offset+size]
|
|
||||||
offset += size
|
|
||||||
|
|
||||||
// Parse sub-packet
|
|
||||||
subPacket, err := ParseProtocolPacket(subData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse sub-packet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
packets = append(packets, subPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
return packets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractEmuPacket extracts an emulator packet from protocol data
|
|
||||||
// This is used when a protocol packet contains application-level data
|
|
||||||
func (p *ProtocolPacket) ExtractEmuPacket(opcodeSize uint8) *EQPacket {
|
|
||||||
if len(p.Buffer) < int(opcodeSize) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the emulator opcode from the buffer
|
|
||||||
var emuOpcode uint16
|
|
||||||
if opcodeSize == 1 {
|
|
||||||
emuOpcode = uint16(p.Buffer[0])
|
|
||||||
} else {
|
|
||||||
emuOpcode = binary.BigEndian.Uint16(p.Buffer[0:2])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create packet with remaining data
|
|
||||||
packet := NewEQPacket(p.Opcode, p.Buffer[opcodeSize:])
|
|
||||||
packet.EmuOpcode = emuOpcode
|
|
||||||
packet.OpcodeSize = opcodeSize
|
|
||||||
|
|
||||||
// Copy network info
|
|
||||||
packet.SrcIP = p.SrcIP
|
|
||||||
packet.SrcPort = p.SrcPort
|
|
||||||
packet.DstIP = p.DstIP
|
|
||||||
packet.DstPort = p.DstPort
|
|
||||||
packet.Timestamp = p.Timestamp
|
|
||||||
packet.Version = p.Version
|
|
||||||
|
|
||||||
return packet
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChatEncode applies XOR-based chat encryption
|
|
||||||
func (p *ProtocolPacket) ChatEncode(key uint32) {
|
|
||||||
if len(p.Buffer) <= 2 {
|
|
||||||
return // Skip opcode bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
data := p.Buffer[2:] // Skip first 2 bytes (opcode)
|
|
||||||
keyBytes := make([]byte, 4)
|
|
||||||
binary.LittleEndian.PutUint32(keyBytes, key)
|
|
||||||
|
|
||||||
// Process 4-byte blocks with rolling key
|
|
||||||
i := 0
|
|
||||||
for ; i+4 <= len(data); i += 4 {
|
|
||||||
// XOR with current key
|
|
||||||
block := binary.LittleEndian.Uint32(data[i : i+4])
|
|
||||||
encrypted := block ^ key
|
|
||||||
binary.LittleEndian.PutUint32(data[i:i+4], encrypted)
|
|
||||||
// Update key with encrypted data
|
|
||||||
key = encrypted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle remaining bytes with last key byte
|
|
||||||
keyByte := byte(key & 0xFF)
|
|
||||||
for ; i < len(data); i++ {
|
|
||||||
data[i] ^= keyByte
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChatDecode applies XOR-based chat decryption (same as encode due to XOR properties)
|
|
||||||
func (p *ProtocolPacket) ChatDecode(key uint32) {
|
|
||||||
p.ChatEncode(key) // XOR encryption is symmetric
|
|
||||||
}
|
|
849
stream.go
849
stream.go
@ -1,849 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.sharkk.net/EQ2/Protocol/crypto"
|
|
||||||
"github.com/panjf2000/gnet/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StreamState represents the current state of an EQStream connection
|
|
||||||
type StreamState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
CLOSED StreamState = iota // No connection
|
|
||||||
CONNECTING // Session request sent, awaiting response
|
|
||||||
ESTABLISHED // Active connection ready for data
|
|
||||||
DISCONNECTING // Graceful disconnect in progress
|
|
||||||
)
|
|
||||||
|
|
||||||
// StreamType identifies the type of EQ stream
|
|
||||||
type StreamType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnknownStream StreamType = iota
|
|
||||||
LoginStream
|
|
||||||
WorldStream
|
|
||||||
ZoneStream
|
|
||||||
ChatStream
|
|
||||||
VoiceStream
|
|
||||||
)
|
|
||||||
|
|
||||||
// Stream configuration constants
|
|
||||||
const (
|
|
||||||
MaxPacketSize = 512 // Maximum packet size
|
|
||||||
MaxCombinedSize = 255 // Maximum combined packet size
|
|
||||||
RetransmitTimeoutMin = 500 // Minimum retransmit timeout (ms)
|
|
||||||
RetransmitTimeoutMax = 5000 // Maximum retransmit timeout (ms)
|
|
||||||
RetransmitMaxAttempts = 10 // Maximum retransmission attempts
|
|
||||||
KeepAliveInterval = 30000 // Keep-alive interval (ms)
|
|
||||||
SessionTimeout = 90000 // Session timeout (ms)
|
|
||||||
|
|
||||||
// Packet flags
|
|
||||||
FLAG_COMPRESSED = 0x01 // Packet is compressed
|
|
||||||
FLAG_ENCODED = 0x04 // Packet is encoded/encrypted
|
|
||||||
|
|
||||||
// Rate limiting
|
|
||||||
RATEBASE = 1048576 // Base rate: 1 MB
|
|
||||||
DECAYBASE = 78642 // Decay rate: RATEBASE/10
|
|
||||||
)
|
|
||||||
|
|
||||||
// RetransmitEntry tracks packets awaiting acknowledgment
|
|
||||||
type RetransmitEntry struct {
|
|
||||||
packet *ProtocolPacket
|
|
||||||
sentTime time.Time
|
|
||||||
attempts int
|
|
||||||
}
|
|
||||||
|
|
||||||
// EQStream manages a single EverQuest network stream connection
|
|
||||||
type EQStream struct {
|
|
||||||
// Network connection
|
|
||||||
conn gnet.Conn
|
|
||||||
remoteIP net.IP
|
|
||||||
remotePort uint16
|
|
||||||
|
|
||||||
// Session identification
|
|
||||||
sessionID uint32
|
|
||||||
streamType StreamType
|
|
||||||
|
|
||||||
// State management
|
|
||||||
state StreamState
|
|
||||||
stateMu sync.RWMutex
|
|
||||||
|
|
||||||
// Encryption
|
|
||||||
crypto *crypto.Ciphers
|
|
||||||
cryptoKey uint32
|
|
||||||
|
|
||||||
// Compression settings
|
|
||||||
compressed bool
|
|
||||||
encoded bool
|
|
||||||
|
|
||||||
// Sequence numbers
|
|
||||||
nextInSeq uint16
|
|
||||||
nextOutSeq uint16
|
|
||||||
lastAckSeq uint16
|
|
||||||
seqMu sync.Mutex
|
|
||||||
|
|
||||||
// Packet queues
|
|
||||||
outbound chan *ProtocolPacket
|
|
||||||
retransmit map[uint16]*RetransmitEntry
|
|
||||||
retransmitMu sync.RWMutex
|
|
||||||
futurePackets map[uint16]*ProtocolPacket
|
|
||||||
futureMu sync.Mutex
|
|
||||||
|
|
||||||
// Combined packet handling
|
|
||||||
combinedApp *EQPacket
|
|
||||||
combinedMu sync.Mutex
|
|
||||||
combineTimer *time.Timer
|
|
||||||
|
|
||||||
// Flow control
|
|
||||||
currentRate uint32
|
|
||||||
rateThreshold uint32
|
|
||||||
rateMu sync.Mutex
|
|
||||||
|
|
||||||
// Timing
|
|
||||||
lastActivity time.Time
|
|
||||||
lastPing time.Time
|
|
||||||
avgDelta time.Duration
|
|
||||||
timingMu sync.RWMutex
|
|
||||||
|
|
||||||
// Statistics
|
|
||||||
packetsReceived uint64
|
|
||||||
packetsSent uint64
|
|
||||||
bytesReceived uint64
|
|
||||||
bytesSent uint64
|
|
||||||
statsMu sync.RWMutex
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
opcodeSize uint8
|
|
||||||
maxPacketSize uint32
|
|
||||||
Version int16 // Client version for opcode conversion
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
onEmuPacket func(*EQPacket) // Called when emulator packet is ready
|
|
||||||
onDisconnect func() // Called on disconnect
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEQStream creates a new EQ stream
|
|
||||||
func NewEQStream(conn gnet.Conn) *EQStream {
|
|
||||||
stream := &EQStream{
|
|
||||||
conn: conn,
|
|
||||||
state: CLOSED,
|
|
||||||
sessionID: 0,
|
|
||||||
streamType: UnknownStream,
|
|
||||||
compressed: true,
|
|
||||||
encoded: false,
|
|
||||||
nextInSeq: 0,
|
|
||||||
nextOutSeq: 0,
|
|
||||||
lastAckSeq: 0,
|
|
||||||
outbound: make(chan *ProtocolPacket, 1024),
|
|
||||||
retransmit: make(map[uint16]*RetransmitEntry),
|
|
||||||
futurePackets: make(map[uint16]*ProtocolPacket),
|
|
||||||
currentRate: RATEBASE,
|
|
||||||
rateThreshold: DECAYBASE,
|
|
||||||
lastActivity: time.Now(),
|
|
||||||
opcodeSize: 2,
|
|
||||||
maxPacketSize: MaxPacketSize,
|
|
||||||
cryptoKey: 0x33624702, // Default CRC key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse remote address
|
|
||||||
if addr := conn.RemoteAddr(); addr != nil {
|
|
||||||
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
|
|
||||||
stream.remoteIP = tcpAddr.IP
|
|
||||||
stream.remotePort = uint16(tcpAddr.Port)
|
|
||||||
} else if udpAddr, ok := addr.(*net.UDPAddr); ok {
|
|
||||||
stream.remoteIP = udpAddr.IP
|
|
||||||
stream.remotePort = uint16(udpAddr.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetState returns the current stream state
|
|
||||||
func (s *EQStream) GetState() StreamState {
|
|
||||||
s.stateMu.RLock()
|
|
||||||
defer s.stateMu.RUnlock()
|
|
||||||
return s.state
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetState updates the stream state
|
|
||||||
func (s *EQStream) SetState(state StreamState) {
|
|
||||||
s.stateMu.Lock()
|
|
||||||
defer s.stateMu.Unlock()
|
|
||||||
s.state = state
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsActive returns true if the stream is established
|
|
||||||
func (s *EQStream) IsActive() bool {
|
|
||||||
return s.GetState() == ESTABLISHED
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsClosed returns true if the stream is closed
|
|
||||||
func (s *EQStream) IsClosed() bool {
|
|
||||||
return s.GetState() == CLOSED
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessPacket processes an incoming protocol packet
|
|
||||||
func (s *EQStream) ProcessPacket(p *ProtocolPacket) error {
|
|
||||||
s.updateActivity()
|
|
||||||
s.updateStats(true, uint64(p.TotalSize()))
|
|
||||||
|
|
||||||
// Handle based on opcode
|
|
||||||
switch p.Opcode {
|
|
||||||
case OP_SessionRequest:
|
|
||||||
return s.handleSessionRequest(p)
|
|
||||||
case OP_SessionResponse:
|
|
||||||
return s.handleSessionResponse(p)
|
|
||||||
case OP_SessionDisconnect:
|
|
||||||
return s.handleDisconnect(p)
|
|
||||||
case OP_KeepAlive:
|
|
||||||
return s.handleKeepAlive(p)
|
|
||||||
case OP_Ack:
|
|
||||||
return s.handleAck(p)
|
|
||||||
case OP_OutOfOrderAck:
|
|
||||||
return s.handleOutOfOrderAck(p)
|
|
||||||
case OP_Packet:
|
|
||||||
return s.handleDataPacket(p)
|
|
||||||
case OP_Fragment:
|
|
||||||
return s.handleFragment(p)
|
|
||||||
case OP_Combined:
|
|
||||||
return s.handleCombined(p)
|
|
||||||
case OP_AppCombined:
|
|
||||||
return s.handleAppCombined(p)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown opcode: 0x%04X", p.Opcode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSessionRequest processes a session request packet
|
|
||||||
func (s *EQStream) handleSessionRequest(p *ProtocolPacket) error {
|
|
||||||
if len(p.Buffer) < 4 {
|
|
||||||
return fmt.Errorf("session request too small")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse session ID
|
|
||||||
s.sessionID = binary.BigEndian.Uint32(p.Buffer[0:4])
|
|
||||||
|
|
||||||
// Send session response
|
|
||||||
s.sendSessionResponse()
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
s.SetState(ESTABLISHED)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSessionResponse processes a session response packet
|
|
||||||
func (s *EQStream) handleSessionResponse(p *ProtocolPacket) error {
|
|
||||||
if len(p.Buffer) < 4 {
|
|
||||||
return fmt.Errorf("session response too small")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse session parameters
|
|
||||||
s.sessionID = binary.BigEndian.Uint32(p.Buffer[0:4])
|
|
||||||
|
|
||||||
// Parse compression/encoding flags if present
|
|
||||||
if len(p.Buffer) >= 5 {
|
|
||||||
flags := p.Buffer[4]
|
|
||||||
s.compressed = (flags & FLAG_COMPRESSED) != 0
|
|
||||||
s.encoded = (flags & FLAG_ENCODED) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize encryption if needed
|
|
||||||
if s.encoded && s.crypto == nil {
|
|
||||||
// This would be initialized with proper key exchange
|
|
||||||
// For now, using default key
|
|
||||||
var err error
|
|
||||||
s.crypto, err = crypto.NewCiphers(int64(s.cryptoKey))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize encryption: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
s.SetState(ESTABLISHED)
|
|
||||||
|
|
||||||
// Send keep alive to confirm
|
|
||||||
s.sendKeepAlive()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleDisconnect processes a disconnect packet
|
|
||||||
func (s *EQStream) handleDisconnect(p *ProtocolPacket) error {
|
|
||||||
s.SetState(CLOSED)
|
|
||||||
|
|
||||||
// Call disconnect callback if set
|
|
||||||
if s.onDisconnect != nil {
|
|
||||||
s.onDisconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleKeepAlive processes a keep-alive packet
|
|
||||||
func (s *EQStream) handleKeepAlive(p *ProtocolPacket) error {
|
|
||||||
// Update activity timestamp
|
|
||||||
s.updateActivity()
|
|
||||||
|
|
||||||
// Send keep-alive response if needed
|
|
||||||
// Some implementations echo the keep-alive
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleAck processes an acknowledgment packet
|
|
||||||
func (s *EQStream) handleAck(p *ProtocolPacket) error {
|
|
||||||
if len(p.Buffer) < 2 {
|
|
||||||
return fmt.Errorf("ack packet too small")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse acknowledged sequence number
|
|
||||||
ackSeq := binary.BigEndian.Uint16(p.Buffer[0:2])
|
|
||||||
|
|
||||||
// Remove from retransmit queue
|
|
||||||
s.retransmitMu.Lock()
|
|
||||||
delete(s.retransmit, ackSeq)
|
|
||||||
s.retransmitMu.Unlock()
|
|
||||||
|
|
||||||
// Update last acknowledged sequence
|
|
||||||
s.seqMu.Lock()
|
|
||||||
if s.sequenceGreaterThan(ackSeq, s.lastAckSeq) {
|
|
||||||
s.lastAckSeq = ackSeq
|
|
||||||
}
|
|
||||||
s.seqMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleOutOfOrderAck processes an out-of-order acknowledgment
|
|
||||||
func (s *EQStream) handleOutOfOrderAck(p *ProtocolPacket) error {
|
|
||||||
// Similar to handleAck but indicates out-of-order reception
|
|
||||||
return s.handleAck(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleDataPacket processes a data packet
|
|
||||||
func (s *EQStream) handleDataPacket(p *ProtocolPacket) error {
|
|
||||||
// Check sequence number
|
|
||||||
seq := p.Sequence
|
|
||||||
|
|
||||||
s.seqMu.Lock()
|
|
||||||
expectedSeq := s.nextInSeq
|
|
||||||
s.seqMu.Unlock()
|
|
||||||
|
|
||||||
seqOrder := s.compareSequence(expectedSeq, seq)
|
|
||||||
|
|
||||||
switch seqOrder {
|
|
||||||
case SeqInOrder:
|
|
||||||
// Process packet immediately
|
|
||||||
s.seqMu.Lock()
|
|
||||||
s.nextInSeq++
|
|
||||||
s.seqMu.Unlock()
|
|
||||||
|
|
||||||
// Send ACK
|
|
||||||
s.sendAck(seq)
|
|
||||||
|
|
||||||
// Extract emulator packet and queue
|
|
||||||
if emuPacket := p.ExtractEmuPacket(s.opcodeSize); emuPacket != nil {
|
|
||||||
// Process based on stream type
|
|
||||||
s.processIncomingEmuPacket(emuPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for queued future packets
|
|
||||||
s.processFuturePackets()
|
|
||||||
|
|
||||||
case SeqFuture:
|
|
||||||
// Queue for later processing
|
|
||||||
s.futureMu.Lock()
|
|
||||||
s.futurePackets[seq] = p
|
|
||||||
s.futureMu.Unlock()
|
|
||||||
|
|
||||||
// Send out-of-order ACK
|
|
||||||
s.sendOutOfOrderAck(seq)
|
|
||||||
|
|
||||||
case SeqPast:
|
|
||||||
// Duplicate packet, just ACK it
|
|
||||||
s.sendAck(seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleFragment processes a fragmented packet
|
|
||||||
func (s *EQStream) handleFragment(p *ProtocolPacket) error {
|
|
||||||
// TODO: Implement fragment reassembly
|
|
||||||
// This requires tracking fragment sequences and reassembling
|
|
||||||
return fmt.Errorf("fragment handling not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCombined processes a combined packet
|
|
||||||
func (s *EQStream) handleCombined(p *ProtocolPacket) error {
|
|
||||||
// Extract sub-packets
|
|
||||||
subPackets, err := p.ExtractSubPackets()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to extract sub-packets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each sub-packet
|
|
||||||
for _, subPacket := range subPackets {
|
|
||||||
if err := s.ProcessPacket(subPacket); err != nil {
|
|
||||||
// Log error but continue processing other packets
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleAppCombined processes an application-level combined packet
|
|
||||||
func (s *EQStream) handleAppCombined(p *ProtocolPacket) error {
|
|
||||||
// For now, just try to extract as single packet
|
|
||||||
// TODO: Implement proper app-level packet extraction
|
|
||||||
emuPacket := p.ExtractEmuPacket(s.opcodeSize)
|
|
||||||
if emuPacket == nil {
|
|
||||||
return fmt.Errorf("failed to extract emu packet")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the packet
|
|
||||||
s.processIncomingEmuPacket(emuPacket)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueuePacket queues an outbound packet for transmission
|
|
||||||
func (s *EQStream) QueuePacket(p *ProtocolPacket, reliable bool) error {
|
|
||||||
if s.GetState() != ESTABLISHED {
|
|
||||||
return fmt.Errorf("stream not established")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sequence number for reliable packets
|
|
||||||
if reliable {
|
|
||||||
s.seqMu.Lock()
|
|
||||||
p.Sequence = s.nextOutSeq
|
|
||||||
s.nextOutSeq++
|
|
||||||
s.seqMu.Unlock()
|
|
||||||
|
|
||||||
// Add to retransmit queue
|
|
||||||
s.retransmitMu.Lock()
|
|
||||||
s.retransmit[p.Sequence] = &RetransmitEntry{
|
|
||||||
packet: p,
|
|
||||||
sentTime: time.Now(),
|
|
||||||
attempts: 0,
|
|
||||||
}
|
|
||||||
s.retransmitMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add CRC if needed
|
|
||||||
if p.Opcode != OP_SessionRequest && p.Opcode != OP_SessionResponse {
|
|
||||||
s.addCRC(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compress if beneficial
|
|
||||||
if s.compressed && len(p.Buffer) > 30 {
|
|
||||||
p.Compress()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt if enabled
|
|
||||||
if s.encoded && s.crypto != nil {
|
|
||||||
s.encryptPacket(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send packet
|
|
||||||
return s.sendPacket(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
func (s *EQStream) updateActivity() {
|
|
||||||
s.timingMu.Lock()
|
|
||||||
s.lastActivity = time.Now()
|
|
||||||
s.timingMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) updateStats(received bool, bytes uint64) {
|
|
||||||
s.statsMu.Lock()
|
|
||||||
if received {
|
|
||||||
s.packetsReceived++
|
|
||||||
s.bytesReceived += bytes
|
|
||||||
} else {
|
|
||||||
s.packetsSent++
|
|
||||||
s.bytesSent += bytes
|
|
||||||
}
|
|
||||||
s.statsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) sendPacket(p *ProtocolPacket) error {
|
|
||||||
data := p.Serialize()
|
|
||||||
s.updateStats(false, uint64(len(data)))
|
|
||||||
return s.conn.AsyncWrite(data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) sendSessionResponse() {
|
|
||||||
resp := NewProtocolPacket(OP_SessionResponse, nil)
|
|
||||||
|
|
||||||
// Build response data
|
|
||||||
data := make([]byte, 5)
|
|
||||||
binary.BigEndian.PutUint32(data[0:4], s.sessionID)
|
|
||||||
|
|
||||||
// Set flags
|
|
||||||
flags := byte(0)
|
|
||||||
if s.compressed {
|
|
||||||
flags |= FLAG_COMPRESSED
|
|
||||||
}
|
|
||||||
if s.encoded {
|
|
||||||
flags |= FLAG_ENCODED
|
|
||||||
}
|
|
||||||
data[4] = flags
|
|
||||||
|
|
||||||
resp.Buffer = data
|
|
||||||
s.sendPacket(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) sendKeepAlive() {
|
|
||||||
ka := NewProtocolPacket(OP_KeepAlive, nil)
|
|
||||||
s.sendPacket(ka)
|
|
||||||
|
|
||||||
s.timingMu.Lock()
|
|
||||||
s.lastPing = time.Now()
|
|
||||||
s.timingMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) sendAck(seq uint16) {
|
|
||||||
ack := NewProtocolPacket(OP_Ack, nil)
|
|
||||||
ack.Buffer = make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(ack.Buffer, seq)
|
|
||||||
s.sendPacket(ack)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) sendOutOfOrderAck(seq uint16) {
|
|
||||||
ack := NewProtocolPacket(OP_OutOfOrderAck, nil)
|
|
||||||
ack.Buffer = make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(ack.Buffer, seq)
|
|
||||||
s.sendPacket(ack)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) addCRC(p *ProtocolPacket) {
|
|
||||||
// Serialize packet without CRC
|
|
||||||
data := p.Serialize()
|
|
||||||
|
|
||||||
// Calculate CRC
|
|
||||||
crc := crypto.CalculateCRC(data, s.cryptoKey)
|
|
||||||
|
|
||||||
// Append CRC to buffer
|
|
||||||
crcBytes := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(crcBytes, crc)
|
|
||||||
p.Buffer = append(p.Buffer, crcBytes...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) encryptPacket(p *ProtocolPacket) {
|
|
||||||
if s.crypto == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt the buffer (skip opcode)
|
|
||||||
if len(p.Buffer) > 2 {
|
|
||||||
s.crypto.Encrypt(p.Buffer[2:])
|
|
||||||
p.Encrypted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) processFuturePackets() {
|
|
||||||
s.futureMu.Lock()
|
|
||||||
defer s.futureMu.Unlock()
|
|
||||||
|
|
||||||
s.seqMu.Lock()
|
|
||||||
expectedSeq := s.nextInSeq
|
|
||||||
s.seqMu.Unlock()
|
|
||||||
|
|
||||||
// Check if we have the next expected packet
|
|
||||||
if p, ok := s.futurePackets[expectedSeq]; ok {
|
|
||||||
delete(s.futurePackets, expectedSeq)
|
|
||||||
|
|
||||||
// Process it (this will recursively check for more)
|
|
||||||
go s.ProcessPacket(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sequence number comparison
|
|
||||||
|
|
||||||
type SeqOrder int
|
|
||||||
|
|
||||||
const (
|
|
||||||
SeqPast SeqOrder = iota // Sequence is from the past
|
|
||||||
SeqInOrder // Sequence is expected
|
|
||||||
SeqFuture // Sequence is from the future
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *EQStream) compareSequence(expected, received uint16) SeqOrder {
|
|
||||||
if received == expected {
|
|
||||||
return SeqInOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle wraparound
|
|
||||||
if s.sequenceGreaterThan(received, expected) {
|
|
||||||
return SeqFuture
|
|
||||||
}
|
|
||||||
|
|
||||||
return SeqPast
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EQStream) sequenceGreaterThan(a, b uint16) bool {
|
|
||||||
// Handle sequence number wraparound
|
|
||||||
// If the difference is more than half the sequence space,
|
|
||||||
// assume wraparound occurred
|
|
||||||
diff := int32(a) - int32(b)
|
|
||||||
if diff < 0 {
|
|
||||||
diff = -diff
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff > 32768 { // Half of uint16 max
|
|
||||||
return a < b
|
|
||||||
}
|
|
||||||
|
|
||||||
return a > b
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckRetransmissions checks for packets that need retransmission
|
|
||||||
func (s *EQStream) CheckRetransmissions() {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
s.retransmitMu.Lock()
|
|
||||||
defer s.retransmitMu.Unlock()
|
|
||||||
|
|
||||||
for seq, entry := range s.retransmit {
|
|
||||||
// Calculate timeout based on average delta and attempts
|
|
||||||
timeout := time.Duration(RetransmitTimeoutMin) * time.Millisecond
|
|
||||||
if s.avgDelta > 0 {
|
|
||||||
timeout = s.avgDelta * 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exponential backoff
|
|
||||||
for i := 0; i < entry.attempts; i++ {
|
|
||||||
timeout *= 2
|
|
||||||
if timeout > time.Duration(RetransmitTimeoutMax)*time.Millisecond {
|
|
||||||
timeout = time.Duration(RetransmitTimeoutMax) * time.Millisecond
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if now.Sub(entry.sentTime) > timeout {
|
|
||||||
if entry.attempts >= RetransmitMaxAttempts {
|
|
||||||
// Give up on this packet
|
|
||||||
delete(s.retransmit, seq)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retransmit
|
|
||||||
entry.attempts++
|
|
||||||
entry.sentTime = now
|
|
||||||
s.sendPacket(entry.packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckTimeout checks if the stream has timed out
|
|
||||||
func (s *EQStream) CheckTimeout() bool {
|
|
||||||
s.timingMu.RLock()
|
|
||||||
lastActivity := s.lastActivity
|
|
||||||
s.timingMu.RUnlock()
|
|
||||||
|
|
||||||
if time.Since(lastActivity) > time.Duration(SessionTimeout)*time.Millisecond {
|
|
||||||
s.SetState(CLOSED)
|
|
||||||
if s.onDisconnect != nil {
|
|
||||||
s.onDisconnect()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send keep-alive if needed
|
|
||||||
if time.Since(lastActivity) > time.Duration(KeepAliveInterval)*time.Millisecond {
|
|
||||||
s.sendKeepAlive()
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendDisconnect sends a disconnect packet
|
|
||||||
func (s *EQStream) SendDisconnect() {
|
|
||||||
disc := NewProtocolPacket(OP_SessionDisconnect, nil)
|
|
||||||
s.sendPacket(disc)
|
|
||||||
s.SetState(DISCONNECTING)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the stream
|
|
||||||
func (s *EQStream) Close() {
|
|
||||||
s.SendDisconnect()
|
|
||||||
s.SetState(CLOSED)
|
|
||||||
|
|
||||||
// Clean up resources
|
|
||||||
close(s.outbound)
|
|
||||||
|
|
||||||
if s.combineTimer != nil {
|
|
||||||
s.combineTimer.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EQ2-specific packet preparation methods
|
|
||||||
|
|
||||||
// PreparePacketForWire prepares an application packet for transmission
|
|
||||||
// This adds EQ2-specific headers like sequence numbers and compression flags
|
|
||||||
func (s *EQStream) PreparePacketForWire(app *EQPacket, maxLen int16) (*ProtocolPacket, error) {
|
|
||||||
// Convert emulator opcode to network opcode based on version
|
|
||||||
// This would use an opcode manager in a full implementation
|
|
||||||
networkOpcode := app.EmuOpcode
|
|
||||||
|
|
||||||
// Build the wire format
|
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
// Add sequence field placeholder (2 bytes)
|
|
||||||
buf = append(buf, 0x00, 0x00)
|
|
||||||
|
|
||||||
// Add compression flag placeholder (1 byte)
|
|
||||||
buf = append(buf, 0x00)
|
|
||||||
|
|
||||||
// Add opcode with special encoding
|
|
||||||
if networkOpcode >= 255 {
|
|
||||||
// Oversized opcode
|
|
||||||
buf = append(buf, 0xFF) // Marker
|
|
||||||
buf = append(buf, byte(networkOpcode>>8), byte(networkOpcode))
|
|
||||||
} else {
|
|
||||||
buf = append(buf, byte(networkOpcode))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add packet data
|
|
||||||
buf = append(buf, app.Buffer...)
|
|
||||||
|
|
||||||
// Create protocol packet
|
|
||||||
proto := NewProtocolPacket(OP_Packet, buf)
|
|
||||||
proto.Version = app.Version
|
|
||||||
|
|
||||||
return proto, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CombinePacketsForWire combines multiple packets at the application level
|
|
||||||
// This is different from protocol-level combining
|
|
||||||
func (s *EQStream) CombinePacketsForWire(packets []*EQPacket) *ProtocolPacket {
|
|
||||||
if len(packets) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only one packet, just prepare it normally
|
|
||||||
if len(packets) == 1 {
|
|
||||||
proto, _ := s.PreparePacketForWire(packets[0], MaxPacketSize)
|
|
||||||
return proto
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build combined buffer with EQ2 format
|
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
for _, app := range packets {
|
|
||||||
// Prepare each packet
|
|
||||||
prepared, err := s.PreparePacketForWire(app, MaxPacketSize)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data := prepared.Buffer
|
|
||||||
size := len(data)
|
|
||||||
|
|
||||||
// Add size encoding
|
|
||||||
if size >= 255 {
|
|
||||||
// Oversized packet
|
|
||||||
buf = append(buf, 0xFF)
|
|
||||||
buf = append(buf, byte(size>>8), byte(size))
|
|
||||||
} else {
|
|
||||||
buf = append(buf, byte(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add packet data
|
|
||||||
buf = append(buf, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create combined protocol packet
|
|
||||||
return NewProtocolPacket(OP_AppCombined, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueueAppPacket queues an application packet for transmission
|
|
||||||
// This handles the full preparation pipeline
|
|
||||||
func (s *EQStream) QueueAppPacket(app *EQPacket) error {
|
|
||||||
// Convert to protocol packet with EQ2 preparation
|
|
||||||
proto, err := s.PreparePacketForWire(app, MaxPacketSize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue for transmission with reliability
|
|
||||||
return s.QueuePacket(proto, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueueLoginPacket queues a login packet for transmission
|
|
||||||
func (s *EQStream) QueueLoginPacket(p *LoginPacket) error {
|
|
||||||
if s.streamType != LoginStream {
|
|
||||||
return fmt.Errorf("cannot send login packet on non-login stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to protocol packet
|
|
||||||
proto := p.ConvertToProtocolPacket()
|
|
||||||
|
|
||||||
// Queue for transmission with reliability
|
|
||||||
return s.QueuePacket(proto, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueueGamePacket queues a game packet for transmission
|
|
||||||
func (s *EQStream) QueueGamePacket(p *GamePacket) error {
|
|
||||||
if s.streamType != WorldStream && s.streamType != ZoneStream {
|
|
||||||
return fmt.Errorf("cannot send game packet on non-game stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to protocol packet
|
|
||||||
proto := p.ConvertToProtocolPacket()
|
|
||||||
|
|
||||||
// Queue for transmission with reliability
|
|
||||||
return s.QueuePacket(proto, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOpcodeManager sets the opcode conversion manager for version-specific opcodes
|
|
||||||
// In a full implementation, this would handle emulator->network opcode conversion
|
|
||||||
func (s *EQStream) SetOpcodeManager(version int16) {
|
|
||||||
// This would set up opcode conversion tables based on client version
|
|
||||||
// For now, it's a placeholder
|
|
||||||
s.Version = version
|
|
||||||
}
|
|
||||||
|
|
||||||
// processIncomingEmuPacket processes an incoming emulator packet based on stream type
|
|
||||||
func (s *EQStream) processIncomingEmuPacket(emu *EQPacket) {
|
|
||||||
// Parse based on stream type
|
|
||||||
switch s.streamType {
|
|
||||||
case LoginStream:
|
|
||||||
// Parse as login packet
|
|
||||||
if _, err := ParseLoginPacket(emu.Buffer); err == nil {
|
|
||||||
// For login packets, we might want to do special handling
|
|
||||||
// For now, just pass through the emu packet
|
|
||||||
if s.onEmuPacket != nil {
|
|
||||||
s.onEmuPacket(emu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case WorldStream, ZoneStream:
|
|
||||||
// Parse as game packet
|
|
||||||
if _, err := ParseGamePacket(emu.Buffer); err == nil {
|
|
||||||
// For game packets, we might want to do special handling
|
|
||||||
// For now, just pass through the emu packet
|
|
||||||
if s.onEmuPacket != nil {
|
|
||||||
s.onEmuPacket(emu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Unknown stream type, call callback if set
|
|
||||||
if s.onEmuPacket != nil {
|
|
||||||
s.onEmuPacket(emu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StreamFactory provides a factory pattern for creating streams
|
|
||||||
type StreamFactory struct {
|
|
||||||
servers map[StreamType]*StreamServer
|
|
||||||
serversMu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStreamFactory creates a new stream factory
|
|
||||||
func NewStreamFactory() *StreamFactory {
|
|
||||||
return &StreamFactory{
|
|
||||||
servers: make(map[StreamType]*StreamServer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateServer creates a new stream server
|
|
||||||
func (f *StreamFactory) CreateServer(streamType StreamType, address string, port int) (*StreamServer, error) {
|
|
||||||
f.serversMu.Lock()
|
|
||||||
defer f.serversMu.Unlock()
|
|
||||||
|
|
||||||
// Check if server already exists for this type
|
|
||||||
if _, exists := f.servers[streamType]; exists {
|
|
||||||
return nil, fmt.Errorf("server already exists for stream type %d", streamType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new server
|
|
||||||
server := NewStreamServer(address, port, streamType)
|
|
||||||
f.servers[streamType] = server
|
|
||||||
|
|
||||||
return server, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServer returns a server by stream type
|
|
||||||
func (f *StreamFactory) GetServer(streamType StreamType) *StreamServer {
|
|
||||||
f.serversMu.RLock()
|
|
||||||
defer f.serversMu.RUnlock()
|
|
||||||
return f.servers[streamType]
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopAll stops all servers
|
|
||||||
func (f *StreamFactory) StopAll() {
|
|
||||||
f.serversMu.Lock()
|
|
||||||
defer f.serversMu.Unlock()
|
|
||||||
|
|
||||||
for _, server := range f.servers {
|
|
||||||
server.Stop()
|
|
||||||
}
|
|
||||||
f.servers = make(map[StreamType]*StreamServer)
|
|
||||||
}
|
|
263
stream_server.go
263
stream_server.go
@ -1,263 +0,0 @@
|
|||||||
package eq2net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/panjf2000/gnet/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StreamServer manages multiple EQStream connections using gnet
|
|
||||||
type StreamServer struct {
|
|
||||||
gnet.BuiltinEventEngine
|
|
||||||
|
|
||||||
streams map[string]*EQStream // "IP:Port" -> Stream mapping
|
|
||||||
streamsMu sync.RWMutex
|
|
||||||
|
|
||||||
address string
|
|
||||||
port int
|
|
||||||
streamType StreamType
|
|
||||||
maxStreams int
|
|
||||||
|
|
||||||
onNewStream func(*EQStream)
|
|
||||||
onStreamClosed func(*EQStream)
|
|
||||||
onEmuPacket func(*EQStream, *EQPacket)
|
|
||||||
|
|
||||||
totalConnections uint64
|
|
||||||
activeConnections uint32
|
|
||||||
statsMu sync.RWMutex
|
|
||||||
|
|
||||||
shutdown chan bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStreamServer creates a new EQ stream server
|
|
||||||
func NewStreamServer(address string, port int, streamType StreamType) *StreamServer {
|
|
||||||
return &StreamServer{
|
|
||||||
streams: make(map[string]*EQStream),
|
|
||||||
address: address,
|
|
||||||
port: port,
|
|
||||||
streamType: streamType,
|
|
||||||
maxStreams: 10000,
|
|
||||||
shutdown: make(chan bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the stream server
|
|
||||||
func (s *StreamServer) Start() error {
|
|
||||||
addr := fmt.Sprintf("%s:%d", s.address, s.port)
|
|
||||||
|
|
||||||
// Configure gnet options
|
|
||||||
options := []gnet.Option{
|
|
||||||
gnet.WithMulticore(true),
|
|
||||||
gnet.WithReusePort(true),
|
|
||||||
gnet.WithTicker(true),
|
|
||||||
gnet.WithTCPKeepAlive(time.Minute * 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start gnet server
|
|
||||||
return gnet.Run(s, fmt.Sprintf("udp://%s", addr), options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the stream server
|
|
||||||
func (s *StreamServer) Stop() {
|
|
||||||
close(s.shutdown)
|
|
||||||
|
|
||||||
// Close all active streams
|
|
||||||
s.streamsMu.Lock()
|
|
||||||
for _, stream := range s.streams {
|
|
||||||
stream.Close()
|
|
||||||
}
|
|
||||||
s.streams = make(map[string]*EQStream)
|
|
||||||
s.streamsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnBoot is called when the server starts
|
|
||||||
func (s *StreamServer) OnBoot(eng gnet.Engine) gnet.Action {
|
|
||||||
fmt.Printf("EQStream server started on %s:%d\n", s.address, s.port)
|
|
||||||
return gnet.None
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnShutdown is called when the server stops
|
|
||||||
func (s *StreamServer) OnShutdown(eng gnet.Engine) {
|
|
||||||
fmt.Printf("EQStream server shutdown\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnOpen is called when a new connection is established
|
|
||||||
func (s *StreamServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
|
|
||||||
// Create new stream
|
|
||||||
stream := NewEQStream(c)
|
|
||||||
stream.streamType = s.streamType
|
|
||||||
|
|
||||||
// Set up callbacks
|
|
||||||
stream.onEmuPacket = func(p *EQPacket) {
|
|
||||||
if s.onEmuPacket != nil {
|
|
||||||
s.onEmuPacket(stream, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.onDisconnect = func() {
|
|
||||||
s.removeStream(stream)
|
|
||||||
if s.onStreamClosed != nil {
|
|
||||||
s.onStreamClosed(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store stream in connection context
|
|
||||||
c.SetContext(stream)
|
|
||||||
|
|
||||||
// Add to stream map
|
|
||||||
key := s.getStreamKey(c)
|
|
||||||
s.streamsMu.Lock()
|
|
||||||
s.streams[key] = stream
|
|
||||||
s.activeConnections++
|
|
||||||
s.totalConnections++
|
|
||||||
s.streamsMu.Unlock()
|
|
||||||
|
|
||||||
// Call new stream callback
|
|
||||||
if s.onNewStream != nil {
|
|
||||||
s.onNewStream(stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, gnet.None
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnClose is called when a connection is closed
|
|
||||||
func (s *StreamServer) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
|
||||||
if stream, ok := c.Context().(*EQStream); ok {
|
|
||||||
stream.Close()
|
|
||||||
s.removeStream(stream)
|
|
||||||
|
|
||||||
if s.onStreamClosed != nil {
|
|
||||||
s.onStreamClosed(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gnet.None
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnTraffic is called when data is received
|
|
||||||
func (s *StreamServer) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
|
||||||
stream, ok := c.Context().(*EQStream)
|
|
||||||
if !ok {
|
|
||||||
return gnet.Close
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all available data
|
|
||||||
for {
|
|
||||||
// Peek at the data to determine packet size
|
|
||||||
buf, err := c.Peek(-1)
|
|
||||||
if err != nil || len(buf) < 2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse packet length from opcode and data
|
|
||||||
// For UDP, each datagram is a complete packet
|
|
||||||
packet, err := ParseProtocolPacket(buf)
|
|
||||||
if err != nil {
|
|
||||||
// Invalid packet, skip it
|
|
||||||
c.Discard(len(buf))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume the packet data
|
|
||||||
c.Discard(len(buf))
|
|
||||||
|
|
||||||
// Process packet
|
|
||||||
if err := stream.ProcessPacket(packet); err != nil {
|
|
||||||
// Log error but continue processing
|
|
||||||
fmt.Printf("Error processing packet: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gnet.None
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnTick is called periodically for maintenance tasks
|
|
||||||
func (s *StreamServer) OnTick() (delay time.Duration, action gnet.Action) {
|
|
||||||
// Check for timeouts and retransmissions
|
|
||||||
s.streamsMu.RLock()
|
|
||||||
streams := make([]*EQStream, 0, len(s.streams))
|
|
||||||
for _, stream := range s.streams {
|
|
||||||
streams = append(streams, stream)
|
|
||||||
}
|
|
||||||
s.streamsMu.RUnlock()
|
|
||||||
|
|
||||||
// Process each stream
|
|
||||||
for _, stream := range streams {
|
|
||||||
// Check for timeout
|
|
||||||
if stream.CheckTimeout() {
|
|
||||||
s.removeStream(stream)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for retransmissions
|
|
||||||
stream.CheckRetransmissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return delay until next tick (100ms)
|
|
||||||
return time.Millisecond * 100, gnet.None
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
func (s *StreamServer) getStreamKey(c gnet.Conn) string {
|
|
||||||
if addr := c.RemoteAddr(); addr != nil {
|
|
||||||
return addr.String()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamServer) removeStream(stream *EQStream) {
|
|
||||||
// Find and remove stream from map
|
|
||||||
s.streamsMu.Lock()
|
|
||||||
for key, str := range s.streams {
|
|
||||||
if str == stream {
|
|
||||||
delete(s.streams, key)
|
|
||||||
s.activeConnections--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.streamsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOnNewStream sets the callback for new streams
|
|
||||||
func (s *StreamServer) SetOnNewStream(callback func(*EQStream)) {
|
|
||||||
s.onNewStream = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOnStreamClosed sets the callback for closed streams
|
|
||||||
func (s *StreamServer) SetOnStreamClosed(callback func(*EQStream)) {
|
|
||||||
s.onStreamClosed = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOnEmuPacket sets the callback for emulator packets
|
|
||||||
func (s *StreamServer) SetOnEmuPacket(callback func(*EQStream, *EQPacket)) {
|
|
||||||
s.onEmuPacket = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStream returns a stream by remote address
|
|
||||||
func (s *StreamServer) GetStream(address string) *EQStream {
|
|
||||||
s.streamsMu.RLock()
|
|
||||||
defer s.streamsMu.RUnlock()
|
|
||||||
return s.streams[address]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetActiveStreams returns all active streams
|
|
||||||
func (s *StreamServer) GetActiveStreams() []*EQStream {
|
|
||||||
s.streamsMu.RLock()
|
|
||||||
defer s.streamsMu.RUnlock()
|
|
||||||
|
|
||||||
streams := make([]*EQStream, 0, len(s.streams))
|
|
||||||
for _, stream := range s.streams {
|
|
||||||
streams = append(streams, stream)
|
|
||||||
}
|
|
||||||
return streams
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStats returns server statistics
|
|
||||||
func (s *StreamServer) GetStats() (total uint64, active uint32) {
|
|
||||||
s.statsMu.RLock()
|
|
||||||
defer s.statsMu.RUnlock()
|
|
||||||
return s.totalConnections, s.activeConnections
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user