From a1dd39d45a27b256d00556c08f768ad7eba151a7 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Wed, 2 Jul 2025 13:25:43 -0500 Subject: [PATCH] convert packet management --- internal/packets/app.go | 120 +++++++++++++++ internal/packets/compression.go | 64 ++++++++ internal/packets/protocol.go | 202 ++++++++++++++++++++++++ internal/packets/reader.go | 263 ++++++++++++++++++++++++++++++++ internal/packets/session.go | 75 +++++++++ internal/packets/types.go | 88 +++++++++++ internal/packets/writer.go | 144 +++++++++++++++++ 7 files changed, 956 insertions(+) create mode 100644 internal/packets/app.go create mode 100644 internal/packets/compression.go create mode 100644 internal/packets/protocol.go create mode 100644 internal/packets/reader.go create mode 100644 internal/packets/session.go create mode 100644 internal/packets/types.go create mode 100644 internal/packets/writer.go diff --git a/internal/packets/app.go b/internal/packets/app.go new file mode 100644 index 0000000..6f6ce44 --- /dev/null +++ b/internal/packets/app.go @@ -0,0 +1,120 @@ +package packets + +import ( + "encoding/binary" + + "eq2emu/internal/opcodes" +) + +type AppHandler struct { + pool *PacketPool +} + +func NewAppHandler(pool *PacketPool) *AppHandler { + return &AppHandler{pool: pool} +} + +// Create application packet for sending +func (h *AppHandler) CreateAppPacket(opcode opcodes.AppOpcode, data []byte) *AppPacket { + packet := h.pool.GetAppPacket() + packet.Opcode = opcode + packet.Data = make([]byte, len(data)) + copy(packet.Data, data) + return packet +} + +// Serialize app packet to protocol packet +func (h *AppHandler) SerializeAppPacket(app *AppPacket, sequence uint16) []byte { + size := 3 + len(app.Data) // opcode(1) + seq(2) + opcode(2) + data + if app.Compressed { + size++ // compression flag + } + + data := make([]byte, size) + offset := 0 + + data[offset] = byte(opcodes.OP_Packet) + offset++ + + binary.LittleEndian.PutUint16(data[offset:], sequence) + offset += 2 + + if app.Compressed { + data[offset] = 0xFF + offset++ + } + + binary.LittleEndian.PutUint16(data[offset:], uint16(app.Opcode)) + offset += 2 + + copy(data[offset:], app.Data) + + return data +} + +// Create combined packet +func (h *AppHandler) SerializeCombinedPacket(apps []*AppPacket, sequence uint16) []byte { + // Calculate total size + totalSize := 3 // opcode + sequence + for _, app := range apps { + subSize := 2 + len(app.Data) // opcode + data + if subSize < 0xFF { + totalSize += 1 + subSize + } else { + totalSize += 3 + subSize + } + } + + data := make([]byte, totalSize) + offset := 0 + + data[offset] = byte(opcodes.OP_Combined) + offset++ + + binary.LittleEndian.PutUint16(data[offset:], sequence) + offset += 2 + + for _, app := range apps { + subSize := 2 + len(app.Data) + + // Write length + if subSize < 0xFF { + data[offset] = byte(subSize) + offset++ + } else { + data[offset] = 0xFF + offset++ + binary.LittleEndian.PutUint16(data[offset:], uint16(subSize)) + offset += 2 + } + + // Write opcode + binary.LittleEndian.PutUint16(data[offset:], uint16(app.Opcode)) + offset += 2 + + // Write data + copy(data[offset:], app.Data) + offset += len(app.Data) + } + + return data +} + +// Common EQ2 packet helpers +func (h *AppHandler) CreateChatPacket(message string, channel uint8) *AppPacket { + writer := NewPacketWriter() + writer.WriteUint8(channel) + writer.WriteEQ2String(message) + + return h.CreateAppPacket(opcodes.OP_EqHearChatCmd, writer.Bytes()) +} + +func (h *AppHandler) CreatePositionUpdatePacket(playerID uint32, x, y, z float32) *AppPacket { + writer := NewPacketWriter() + writer.WriteUint32(playerID) + writer.WriteFloat32(x) + writer.WriteFloat32(y) + writer.WriteFloat32(z) + + return h.CreateAppPacket(opcodes.OP_UpdatePositionMsg, writer.Bytes()) +} diff --git a/internal/packets/compression.go b/internal/packets/compression.go new file mode 100644 index 0000000..9f41f4b --- /dev/null +++ b/internal/packets/compression.go @@ -0,0 +1,64 @@ +package packets + +import ( + "bytes" + "compress/zlib" + "io" +) + +type EQ2Compressor struct { + level int +} + +func NewEQ2Compressor() *EQ2Compressor { + return &EQ2Compressor{level: zlib.DefaultCompression} +} + +func (c *EQ2Compressor) Compress(data []byte) ([]byte, error) { + if len(data) == 0 { + return data, nil + } + + var buf bytes.Buffer + writer, err := zlib.NewWriterLevel(&buf, c.level) + if err != nil { + return nil, err + } + + _, err = writer.Write(data) + if err != nil { + writer.Close() + return nil, err + } + + err = writer.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (c *EQ2Compressor) Decompress(data []byte) ([]byte, error) { + if len(data) == 0 { + return data, nil + } + + reader, err := zlib.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer reader.Close() + + var buf bytes.Buffer + _, err = io.Copy(&buf, reader) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (c *EQ2Compressor) ShouldCompress(data []byte) bool { + return len(data) > 100 // EQ2 compression threshold +} diff --git a/internal/packets/protocol.go b/internal/packets/protocol.go new file mode 100644 index 0000000..87c8ad5 --- /dev/null +++ b/internal/packets/protocol.go @@ -0,0 +1,202 @@ +package packets + +import ( + "encoding/binary" + "fmt" + + "eq2emu/internal/opcodes" +) + +type ProtocolHandler struct { + pool *PacketPool +} + +func NewProtocolHandler(pool *PacketPool) *ProtocolHandler { + return &ProtocolHandler{pool: pool} +} + +// Parse EQ2 protocol packet from gnet data +func (h *ProtocolHandler) ParsePacket(data []byte) (*EQ2Packet, error) { + if len(data) < 1 { + return nil, fmt.Errorf("packet too small") + } + + packet := h.pool.GetEQ2Packet() + packet.Opcode = opcodes.ProtocolOpcode(data[0]) + + if len(data) > 1 { + packet.Data = make([]byte, len(data)-1) + copy(packet.Data, data[1:]) + } + + // Extract sequence if present + if packet.Opcode == opcodes.OP_Packet || packet.Opcode == opcodes.OP_Combined { + if len(packet.Data) >= 2 { + packet.Sequence = binary.LittleEndian.Uint16(packet.Data[:2]) + } + } + + return packet, nil +} + +// Extract application packets from protocol packet +func (h *ProtocolHandler) ExtractAppPackets(packet *EQ2Packet, session *Session) ([]*AppPacket, error) { + switch packet.Opcode { + case opcodes.OP_Packet: + return h.extractSingleApp(packet.Data) + case opcodes.OP_Combined: + return h.extractCombinedApps(packet.Data) + case opcodes.OP_Fragment: + return h.handleFragment(packet, session) + default: + return nil, nil + } +} + +func (h *ProtocolHandler) extractSingleApp(data []byte) ([]*AppPacket, error) { + if len(data) < 4 { // seq(2) + opcode(2) + return nil, fmt.Errorf("packet too small") + } + + offset := 2 // Skip sequence + + // Check compression flag + compressed := false + if data[offset] == 0xFF { + compressed = true + offset++ + } + + if len(data) < offset+2 { + return nil, fmt.Errorf("insufficient data") + } + + opcode := binary.LittleEndian.Uint16(data[offset:]) + payload := data[offset+2:] + + app := h.pool.GetAppPacket() + app.Opcode = opcodes.AppOpcode(opcode) + app.Data = make([]byte, len(payload)) + copy(app.Data, payload) + app.Compressed = compressed + + return []*AppPacket{app}, nil +} + +func (h *ProtocolHandler) extractCombinedApps(data []byte) ([]*AppPacket, error) { + if len(data) < 2 { + return nil, fmt.Errorf("combined packet too small") + } + + remaining := data[2:] // Skip sequence + var apps []*AppPacket + + for len(remaining) > 0 { + if len(remaining) < 1 { + break + } + + // Get sub-packet length + subLen := uint16(remaining[0]) + offset := 1 + + if subLen == 0xFF { + if len(remaining) < 3 { + break + } + subLen = binary.LittleEndian.Uint16(remaining[1:3]) + offset = 3 + } + + if len(remaining) < offset+int(subLen) { + break + } + + subData := remaining[offset : offset+int(subLen)] + remaining = remaining[offset+int(subLen):] + + if len(subData) < 2 { + continue + } + + opcode := binary.LittleEndian.Uint16(subData[:2]) + payload := subData[2:] + + app := h.pool.GetAppPacket() + app.Opcode = opcodes.AppOpcode(opcode) + app.Data = make([]byte, len(payload)) + copy(app.Data, payload) + + apps = append(apps, app) + } + + return apps, nil +} + +func (h *ProtocolHandler) handleFragment(packet *EQ2Packet, session *Session) ([]*AppPacket, error) { + if len(packet.Data) < 6 { // seq(2) + fragInfo(2) + data + return nil, fmt.Errorf("fragment too small") + } + + sequence := packet.Sequence + fragInfo := binary.LittleEndian.Uint16(packet.Data[2:4]) + fragData := packet.Data[4:] + + current := fragInfo & 0xFF + total := fragInfo >> 8 + + // Get or create fragment buffer + fragBuffer := session.Fragments[sequence] + if fragBuffer == nil { + fragBuffer = &FragmentBuffer{ + Fragments: make(map[uint16][]byte), + Total: total, + Received: 0, + } + session.Fragments[sequence] = fragBuffer + } + + // Store fragment + fragBuffer.Fragments[current] = make([]byte, len(fragData)) + copy(fragBuffer.Fragments[current], fragData) + fragBuffer.Received++ + + // Check if complete + if fragBuffer.Received == fragBuffer.Total { + // Reassemble + var complete []byte + for i := uint16(0); i < fragBuffer.Total; i++ { + complete = append(complete, fragBuffer.Fragments[i]...) + } + + // Clean up + delete(session.Fragments, sequence) + + // Process as normal packet + return h.extractSingleApp(complete) + } + + return nil, nil +} + +// Create protocol packets +func (h *ProtocolHandler) CreateSessionRequest(sessionID uint32, maxLen uint32) []byte { + data := make([]byte, 13) + data[0] = byte(opcodes.OP_SessionRequest) + binary.LittleEndian.PutUint32(data[1:5], sessionID) + binary.LittleEndian.PutUint32(data[5:9], maxLen) + return data +} + +func (h *ProtocolHandler) CreateSessionResponse(sessionID uint32, key uint32, format uint8) []byte { + data := make([]byte, 10) + data[0] = byte(opcodes.OP_SessionResponse) + binary.LittleEndian.PutUint32(data[1:5], sessionID) + binary.LittleEndian.PutUint32(data[5:9], key) + data[9] = format + return data +} + +func (h *ProtocolHandler) CreateKeepAlive() []byte { + return []byte{byte(opcodes.OP_KeepAlive)} +} diff --git a/internal/packets/reader.go b/internal/packets/reader.go new file mode 100644 index 0000000..27f9b7e --- /dev/null +++ b/internal/packets/reader.go @@ -0,0 +1,263 @@ +package packets + +import ( + "encoding/binary" + "fmt" + "io" + "unsafe" +) + +// PacketReader for efficient packet parsing +type PacketReader struct { + data []byte + offset int +} + +func NewPacketReader(data []byte) *PacketReader { + return &PacketReader{ + data: data, + offset: 0, + } +} + +func (r *PacketReader) Remaining() int { + return len(r.data) - r.offset +} + +func (r *PacketReader) Position() int { + return r.offset +} + +func (r *PacketReader) Seek(pos int) error { + if pos < 0 || pos > len(r.data) { + return fmt.Errorf("seek position out of bounds") + } + r.offset = pos + return nil +} + +func (r *PacketReader) Skip(n int) error { + if r.offset+n > len(r.data) { + return io.ErrUnexpectedEOF + } + r.offset += n + return nil +} + +// Read raw bytes +func (r *PacketReader) ReadBytes(n int) ([]byte, error) { + if r.offset+n > len(r.data) { + return nil, io.ErrUnexpectedEOF + } + result := make([]byte, n) + copy(result, r.data[r.offset:r.offset+n]) + r.offset += n + return result, nil +} + +func (r *PacketReader) ReadBytesRef(n int) ([]byte, error) { + if r.offset+n > len(r.data) { + return nil, io.ErrUnexpectedEOF + } + result := r.data[r.offset : r.offset+n] + r.offset += n + return result, nil +} + +// Read integers +func (r *PacketReader) ReadUint8() (uint8, error) { + if r.offset >= len(r.data) { + return 0, io.ErrUnexpectedEOF + } + result := r.data[r.offset] + r.offset++ + return result, nil +} + +func (r *PacketReader) ReadInt8() (int8, error) { + val, err := r.ReadUint8() + return int8(val), err +} + +func (r *PacketReader) ReadUint16() (uint16, error) { + if r.offset+2 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + result := binary.LittleEndian.Uint16(r.data[r.offset:]) + r.offset += 2 + return result, nil +} + +func (r *PacketReader) ReadInt16() (int16, error) { + val, err := r.ReadUint16() + return int16(val), err +} + +func (r *PacketReader) ReadUint32() (uint32, error) { + if r.offset+4 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + result := binary.LittleEndian.Uint32(r.data[r.offset:]) + r.offset += 4 + return result, nil +} + +func (r *PacketReader) ReadInt32() (int32, error) { + val, err := r.ReadUint32() + return int32(val), err +} + +func (r *PacketReader) ReadUint64() (uint64, error) { + if r.offset+8 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + result := binary.LittleEndian.Uint64(r.data[r.offset:]) + r.offset += 8 + return result, nil +} + +func (r *PacketReader) ReadInt64() (int64, error) { + val, err := r.ReadUint64() + return int64(val), err +} + +func (r *PacketReader) ReadFloat32() (float32, error) { + if r.offset+4 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + bits := binary.LittleEndian.Uint32(r.data[r.offset:]) + result := *(*float32)(unsafe.Pointer(&bits)) + r.offset += 4 + return result, nil +} + +func (r *PacketReader) ReadFloat64() (float64, error) { + if r.offset+8 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + bits := binary.LittleEndian.Uint64(r.data[r.offset:]) + result := *(*float64)(unsafe.Pointer(&bits)) + r.offset += 8 + return result, nil +} + +// Read strings +func (r *PacketReader) ReadString() (string, error) { + length, err := r.ReadUint16() + if err != nil { + return "", err + } + + if r.offset+int(length) > len(r.data) { + return "", io.ErrUnexpectedEOF + } + + result := string(r.data[r.offset : r.offset+int(length)]) + r.offset += int(length) + return result, nil +} + +func (r *PacketReader) ReadStringFixed(length int) (string, error) { + if r.offset+length > len(r.data) { + return "", io.ErrUnexpectedEOF + } + + // Find null terminator + end := r.offset + length + for i := r.offset; i < end; i++ { + if r.data[i] == 0 { + end = i + break + } + } + + result := string(r.data[r.offset:end]) + r.offset += length + return result, nil +} + +func (r *PacketReader) ReadCString() (string, error) { + start := r.offset + for r.offset < len(r.data) && r.data[r.offset] != 0 { + r.offset++ + } + + if r.offset >= len(r.data) { + return "", io.ErrUnexpectedEOF + } + + result := string(r.data[start:r.offset]) + r.offset++ // Skip null terminator + return result, nil +} + +// EQ2-specific reads +func (r *PacketReader) ReadEQ2String() (string, error) { + // EQ2 strings are prefixed with length as uint16 + return r.ReadString() +} + +func (r *PacketReader) ReadPackedInt() (uint32, error) { + // EQ2 packed integer format + val, err := r.ReadUint8() + if err != nil { + return 0, err + } + + if val < 0xFF { + return uint32(val), nil + } + + // Extended format + val2, err := r.ReadUint16() + if err != nil { + return 0, err + } + + if val2 < 0xFFFF { + return uint32(val2), nil + } + + // Full 32-bit + return r.ReadUint32() +} + +func (r *PacketReader) ReadBool() (bool, error) { + val, err := r.ReadUint8() + return val != 0, err +} + +// Peek without advancing +func (r *PacketReader) PeekUint8() (uint8, error) { + if r.offset >= len(r.data) { + return 0, io.ErrUnexpectedEOF + } + return r.data[r.offset], nil +} + +func (r *PacketReader) PeekUint16() (uint16, error) { + if r.offset+2 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + return binary.LittleEndian.Uint16(r.data[r.offset:]), nil +} + +func (r *PacketReader) PeekUint32() (uint32, error) { + if r.offset+4 > len(r.data) { + return 0, io.ErrUnexpectedEOF + } + return binary.LittleEndian.Uint32(r.data[r.offset:]), nil +} + +// Buffer management +func (r *PacketReader) Reset(data []byte) { + r.data = data + r.offset = 0 +} + +func (r *PacketReader) Clone() *PacketReader { + return &PacketReader{ + data: r.data, + offset: r.offset, + } +} diff --git a/internal/packets/session.go b/internal/packets/session.go new file mode 100644 index 0000000..dd9c012 --- /dev/null +++ b/internal/packets/session.go @@ -0,0 +1,75 @@ +package packets + +import ( + "sync" + "time" +) + +type SessionManager struct { + sessions map[uint32]*Session + mu sync.RWMutex +} + +func NewSessionManager() *SessionManager { + return &SessionManager{ + sessions: make(map[uint32]*Session), + } +} + +func (sm *SessionManager) GetSession(id uint32) *Session { + sm.mu.RLock() + defer sm.mu.RUnlock() + return sm.sessions[id] +} + +func (sm *SessionManager) CreateSession(id uint32) *Session { + sm.mu.Lock() + defer sm.mu.Unlock() + + session := &Session{ + ID: id, + Established: false, + NextSeq: 0, + LastKeep: time.Now(), + Compressed: false, + Fragments: make(map[uint16]*FragmentBuffer), + } + + sm.sessions[id] = session + return session +} + +func (sm *SessionManager) RemoveSession(id uint32) { + sm.mu.Lock() + defer sm.mu.Unlock() + delete(sm.sessions, id) +} + +func (sm *SessionManager) UpdateKeepAlive(id uint32) { + sm.mu.Lock() + defer sm.mu.Unlock() + if session := sm.sessions[id]; session != nil { + session.LastKeep = time.Now() + } +} + +func (sm *SessionManager) GetNextSequence(id uint32) uint16 { + sm.mu.Lock() + defer sm.mu.Unlock() + + if session := sm.sessions[id]; session != nil { + seq := session.NextSeq + session.NextSeq++ + return seq + } + return 0 +} + +func (sm *SessionManager) SetEstablished(id uint32, established bool) { + sm.mu.Lock() + defer sm.mu.Unlock() + + if session := sm.sessions[id]; session != nil { + session.Established = established + } +} diff --git a/internal/packets/types.go b/internal/packets/types.go new file mode 100644 index 0000000..fdde7cb --- /dev/null +++ b/internal/packets/types.go @@ -0,0 +1,88 @@ +package packets + +import ( + "time" + + "eq2emu/internal/opcodes" +) + +// EQ2 protocol packet +type EQ2Packet struct { + Opcode opcodes.ProtocolOpcode + Data []byte + Sequence uint16 +} + +// EQ2 application packet +type AppPacket struct { + Opcode opcodes.AppOpcode + Data []byte + Compressed bool +} + +// Session for EQ2 connection state +type Session struct { + ID uint32 + Established bool + NextSeq uint16 + LastKeep time.Time + Compressed bool + Fragments map[uint16]*FragmentBuffer +} + +// Fragment reassembly +type FragmentBuffer struct { + Fragments map[uint16][]byte + Total uint16 + Received uint16 + Timestamp time.Time +} + +// Packet pools +type PacketPool struct { + eq2Packets chan *EQ2Packet + appPackets chan *AppPacket +} + +func NewPacketPool(size int) *PacketPool { + return &PacketPool{ + eq2Packets: make(chan *EQ2Packet, size), + appPackets: make(chan *AppPacket, size), + } +} + +func (p *PacketPool) GetEQ2Packet() *EQ2Packet { + select { + case packet := <-p.eq2Packets: + return packet + default: + return &EQ2Packet{} + } +} + +func (p *PacketPool) PutEQ2Packet(packet *EQ2Packet) { + packet.Data = packet.Data[:0] + packet.Opcode = 0 + select { + case p.eq2Packets <- packet: + default: + } +} + +func (p *PacketPool) GetAppPacket() *AppPacket { + select { + case packet := <-p.appPackets: + return packet + default: + return &AppPacket{} + } +} + +func (p *PacketPool) PutAppPacket(packet *AppPacket) { + packet.Data = packet.Data[:0] + packet.Opcode = 0 + select { + case p.appPackets <- packet: + default: + } +} diff --git a/internal/packets/writer.go b/internal/packets/writer.go new file mode 100644 index 0000000..b8b9515 --- /dev/null +++ b/internal/packets/writer.go @@ -0,0 +1,144 @@ +package packets + +import ( + "encoding/binary" + "unsafe" +) + +type PacketWriter struct { + data []byte +} + +func NewPacketWriter() *PacketWriter { + return &PacketWriter{ + data: make([]byte, 0, 512), + } +} + +func NewPacketWriterSize(size int) *PacketWriter { + return &PacketWriter{ + data: make([]byte, 0, size), + } +} + +func (w *PacketWriter) Bytes() []byte { + return w.data +} + +func (w *PacketWriter) Len() int { + return len(w.data) +} + +func (w *PacketWriter) Reset() { + w.data = w.data[:0] +} + +func (w *PacketWriter) WriteBytes(data []byte) { + w.data = append(w.data, data...) +} + +func (w *PacketWriter) WriteUint8(val uint8) { + w.data = append(w.data, val) +} + +func (w *PacketWriter) WriteInt8(val int8) { + w.WriteUint8(uint8(val)) +} + +func (w *PacketWriter) WriteUint16(val uint16) { + b := make([]byte, 2) + binary.LittleEndian.PutUint16(b, val) + w.data = append(w.data, b...) +} + +func (w *PacketWriter) WriteInt16(val int16) { + w.WriteUint16(uint16(val)) +} + +func (w *PacketWriter) WriteUint32(val uint32) { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, val) + w.data = append(w.data, b...) +} + +func (w *PacketWriter) WriteInt32(val int32) { + w.WriteUint32(uint32(val)) +} + +func (w *PacketWriter) WriteUint64(val uint64) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, val) + w.data = append(w.data, b...) +} + +func (w *PacketWriter) WriteInt64(val int64) { + w.WriteUint64(uint64(val)) +} + +func (w *PacketWriter) WriteFloat32(val float32) { + bits := *(*uint32)(unsafe.Pointer(&val)) + w.WriteUint32(bits) +} + +func (w *PacketWriter) WriteFloat64(val float64) { + bits := *(*uint64)(unsafe.Pointer(&val)) + w.WriteUint64(bits) +} + +func (w *PacketWriter) WriteString(s string) { + w.WriteUint16(uint16(len(s))) + w.data = append(w.data, []byte(s)...) +} + +func (w *PacketWriter) WriteStringFixed(s string, length int) { + data := make([]byte, length) + copy(data, []byte(s)) + w.data = append(w.data, data...) +} + +func (w *PacketWriter) WriteCString(s string) { + w.data = append(w.data, []byte(s)...) + w.data = append(w.data, 0) +} + +func (w *PacketWriter) WriteEQ2String(s string) { + w.WriteString(s) +} + +func (w *PacketWriter) WritePackedInt(val uint32) { + if val < 0xFF { + w.WriteUint8(uint8(val)) + } else if val < 0xFFFF { + w.WriteUint8(0xFF) + w.WriteUint16(uint16(val)) + } else { + w.WriteUint8(0xFF) + w.WriteUint16(0xFFFF) + w.WriteUint32(val) + } +} + +func (w *PacketWriter) WriteBool(val bool) { + if val { + w.WriteUint8(1) + } else { + w.WriteUint8(0) + } +} + +func (w *PacketWriter) WriteZeros(count int) { + zeros := make([]byte, count) + w.data = append(w.data, zeros...) +} + +func (w *PacketWriter) SetUint16At(pos int, val uint16) { + if pos+1 < len(w.data) { + binary.LittleEndian.PutUint16(w.data[pos:], val) + } +} + +func (w *PacketWriter) SetUint32At(pos int, val uint32) { + if pos+3 < len(w.data) { + binary.LittleEndian.PutUint32(w.data[pos:], val) + } +}