Violet

In Packet

On this page

The in_packet object represents incoming packet data. It comes in two flavors:

  1. Read-only view — passed to on_packet_recv / on_packet_send callbacks. Use it to decode bytes from a real packet the game is processing.
  2. Builder — created with in_packet(opcode). Build a synthetic incoming packet, then call :inject() to have the game process it as if the server had sent it.
Sending vs. Injecting
  • out_packet(...):send() — sends a packet to the server (outgoing).
  • in_packet(...):inject() — the game processes the packet locally as if the server had sent it. The server never sees this packet.
  • Both share the same encoding methods.

Read Methods

These methods are available on packets passed to on_packet_recv / on_packet_send.

packet:get_opcode() -> number

Available on GMS and CW

Returns the packet opcode.

lua
function on_packet_recv(packet)
    local opcode = packet:get_opcode()
    core.log(string.format("Opcode: 0x%04X", opcode))
    return true
end

packet:decode_1() -> number

Available on GMS and CW

Reads 1 byte (0-255).


packet:decode_2() -> number

Available on GMS and CW

Reads 2 bytes (0-65535).


packet:decode_4() -> number

Available on GMS and CW

Reads 4 bytes (0-4294967295).


packet:decode_8() -> integer

Available on GMS and CW

Reads 8 bytes as an unsigned 64-bit value, returned as a Luau integer (int64) to preserve the full bit pattern. This is a distinct numeric type from regular number — see Luau Notes › 64-bit integers for comparison/arithmetic caveats.


packet:decode_string() -> string

Available on GMS and CW

Reads a length-prefixed string.


packet:decode_buffer(size: number) -> buffer | nil

Available on GMS and CW

Reads size bytes and returns them as a native Luau buffer. Returns nil if not enough data remains.

Access bytes through the buffer library rather than table indexing:

lua
local buf = packet:decode_buffer(16)
for i = 0, buffer.len(buf) - 1 do
    local byte = buffer.readu8(buf, i)  -- 0-indexed, unlike table access
    core.log(string.format("byte[%d] = 0x%02X", i, byte))
end

buffer is mutable, byte-addressable, and avoids the allocation cost of a Lua table per byte. Pass it straight to out_packet:encode_buffer to forward the bytes untouched.


packet:get_length() -> number

Available on GMS and CW

Returns the total packet length in bytes.


packet:get_offset() -> number

Available on GMS and CW

Returns the current read offset position.


packet:set_offset(offset: number)

Available on GMS and CW

Sets the read offset to a specific position. Allows rewinding to re-read data.


packet:to_string() -> string

Available on GMS and CW

Returns the whole packet as space-separated hex, opcode included — the same shape the packet monitor shows, so the result can be pasted straight into the packet injector. Reading it does not move the decode offset. packet:get_data() is an alias.

lua
function on_packet_recv(packet)
    if packet:get_opcode() == 0x1D then
        core.log(packet:to_string())  -- "1D 00 05 00 ..."
    end
    return true
end
Sequential Reading

Decode methods maintain an internal offset. Each read advances the position. Use set_offset() to rewind if you need to re-read data.

Read Example

lua
function on_packet_recv(packet)
    local opcode = packet:get_opcode()

    if opcode == 0x1234 then  -- Replace with actual opcode
        local player_id = packet:decode_4()
        local x_pos = packet:decode_2()
        local y_pos = packet:decode_2()

        core.log(string.format("Player %d at (%d, %d)",
            player_id, x_pos, y_pos))
    end

    return true
end

Constructor (Injection)

in_packet(opcode: number) -> InPacketBuilder

Available on GMS and CW

Creates a new packet for injection. The opcode is written automatically; chain encode_* calls to fill the body, then call :inject().

lua
local pkt = in_packet(0x100)
Important

Injecting malformed packets can crash the client or trigger server-side detection. Only inject opcodes whose layout you've verified by reading real captured packets first.

Encoding Methods

All methods return the builder so calls can be chained.

builder:encode_1(value: number) -> InPacketBuilder

Available on GMS and CW

Encodes 1 byte (0-255).


builder:encode_2(value: number) -> InPacketBuilder

Available on GMS and CW

Encodes 2 bytes (0-65535).


builder:encode_4(value: number) -> InPacketBuilder

Available on GMS and CW

Encodes 4 bytes (0-4294967295).


builder:encode_8(value: number) -> InPacketBuilder

Available on GMS and CW

Encodes 8 bytes.


builder:encode_string(text: string) -> InPacketBuilder

Available on GMS and CW

Encodes a length-prefixed string.


builder:encode_buffer(data: string | buffer) -> InPacketBuilder

Available on GMS and CW

Encodes raw binary data. Accepts either a Lua string or a Luau buffer.


builder:encode_timestamp() -> InPacketBuilder

Available on GMS and CW

Encodes the client's current update time as 4 bytes. Equivalent to encode_4(core.get_update_time()). Errors if the game is not loaded yet.


builder:encode_hex(hex: string) -> InPacketBuilder

Available on GMS and CW

Encodes bytes written as hex text, e.g. "05 00 1A FF" (whitespace optional). Errors on a non-hex character or an odd number of digits rather than skipping them, so a typo can't quietly become different bytes.

The opcode comes from in_packet(opcode), so pass the body only — when replaying a packet captured with to_string(), drop its first two bytes.

Injection Methods

builder:inject()

Available on GMS and CW

Queues the packet to be dispatched as if received from the server. Processed on the next main tick, so it's safe to call from inside on_packet_recv without re-entrancy issues.


builder:to_string() -> string

Available on GMS and CW

Returns the hex representation of the packet contents (debug helper).


builder:get_opcode() -> number

Available on GMS and CW

Returns the packet's opcode.


builder:get_data() -> string

Available on GMS and CW

Alias of to_string().

Injection Example

lua
-- Loop back a synthetic packet
in_packet(0x00A2)           -- Replace with actual opcode
    :encode_1(0)            -- channel
    :encode_string("hi")    -- message
    :inject()
lua
-- Method chaining + buffer forwarding from a real incoming packet
function on_packet_recv(packet)
    if packet:get_opcode() == 0x1234 then
        local body = packet:decode_buffer(packet:get_length() - 4)
        in_packet(0x5678):encode_buffer(body):inject()
    end
    return true
end