365 lines
9.6 KiB
Go
365 lines
9.6 KiB
Go
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)
|
|
} |