Skip to main content

Overview

This page showcases real scripts from the GTProxy scripts/ directory, demonstrating practical uses of the Lua API.

Rainbow Skin Effect

A complete command that cycles through rainbow colors on your character’s skin.
local rainbow = {}
local is_active = false
local current_task_id = nil
local current_color_index = 0

local RAINBOW_COLORS = {
    0xFF0000FF, -- Red
    0xFF3300FF, -- Red-Orange
    0xFF6600FF, -- Orange
    0xFF9900FF,
    0xFFCC00FF, -- Yellow-Orange
    0xFFFF00FF, -- Yellow
    0x99FF00FF,
    0x00FF00FF, -- Green
    0x00FF99FF,
    0x00FFFFFF, -- Cyan
    0x0099FFFF,
    0x0000FFFF, -- Blue
    0x000099FF,
    0x6600FFFF, -- Indigo
    0x9900FFFF  -- Purple
}

local DEFAULT_INTERVAL = 200
local MIN_INTERVAL = 50
local MAX_INTERVAL = 5000

local function send_next_color()
    local net_id = world:get_local_net_id()
    if net_id < 0 then
        if is_active then
            stop_rainbow()
            logger.info("Rainbow effect stopped (exited world)")
        end
        return
    end

    local color = RAINBOW_COLORS[current_color_index + 1]

    local pkt = OnChangeSkinPacket.new()
    pkt.net_id = net_id
    pkt.skin = color
    send.to_client(pkt)

    current_color_index = (current_color_index + 1) % #RAINBOW_COLORS
end

local function toggle_on(ctx, interval)
    current_task_id = scheduler.schedule_periodic(interval, function()
        send_next_color()
        return true
    end)

    is_active = true
    current_color_index = 0

    send_next_color()

    ctx:reply("`2Rainbow effect started ``(speed: {}ms)", interval)
end

local function stop_rainbow()
    if current_task_id ~= nil and scheduler.is_pending(current_task_id) then
        scheduler.cancel(current_task_id)
        current_task_id = nil
    end

    is_active = false
end

local function toggle_off(ctx)
    stop_rainbow()
    ctx:reply("`2Rainbow effect stopped")
end

local function parse_interval(args)
    if #args == 0 then
        return DEFAULT_INTERVAL, nil
    end

    local speed_str = args[1]
    local speed_ms = tonumber(speed_str)

    if speed_ms == nil then
        return DEFAULT_INTERVAL, "Invalid speed value '" .. speed_str .. "'"
    end

    if speed_ms < MIN_INTERVAL then
        return DEFAULT_INTERVAL, "Speed too slow (min " .. MIN_INTERVAL .. "ms)"
    end

    if speed_ms > MAX_INTERVAL then
        return DEFAULT_INTERVAL, "Speed too fast (max " .. MAX_INTERVAL .. "ms)"
    end

    return speed_ms, nil
end

command.register("rainbow", "Toggle rainbow skin effect", function(ctx)
    local net_id = world:get_local_net_id()
    if net_id < 0 then
        ctx:reply("`4Error: ``You are not in a world")
        return false
    end

    if is_active then
        toggle_off(ctx)
        return true
    end

    local interval, error_msg = parse_interval(ctx.args)
    if error_msg then
        ctx:reply("`4Error: ``" .. error_msg)
        return false
    end

    toggle_on(ctx, interval)
    return true
end)

logger.info("Rainbow command loaded")

Command Examples

Simple command registration patterns.
command.register("say", "Echo a message to the client", function(ctx)
    if #ctx.args < 1 then
        ctx:reply("Usage: /say <message>")
        return false
    end

    local message = table.concat(ctx.args, " ")

    local log = LogPacket.new()
    log.msg = message
    send.to_client(log)

    return true
end)

command.register("echo", function(ctx)
    if #ctx.args < 1 then
        ctx:reply("Usage: /echo <message>")
        return false
    end

    local message = table.concat(ctx.args, " ")

    local log = LogPacket.new()
    log.msg = message
    send.to_client(log)

    return true
end)

logger.info("Command test script loaded")
Key Features:
  • Command with description (say)
  • Command without description (echo)
  • Argument validation
  • Using ctx:reply() for user feedback
  • Sending log packets to client

World and Player Tracking

Comprehensive player and world state monitoring.
local function test_local_player()
    local local_player = world:get_local_player()
    if local_player then
        logger.info("[Test 1] Local player found!")
        logger.info("[Test 1]   Name: " .. local_player.name)
        logger.info("[Test 1]   Net ID: " .. local_player.net_id)
        logger.info("[Test 1]   User ID: " .. local_player.user_id)
        logger.info("[Test 1]   Country: " .. local_player.country_code)
        logger.info("[Test 1]   Position: (" .. local_player.position.x .. ", " .. local_player.position.y .. ")")
        logger.info("[Test 1]   Is Local: " .. tostring(local_player.is_local))
    else
        logger.warn("[Test 1] No local player found (not spawned yet?)")
    end
end

local function test_local_net_id()
    local local_net_id = world:get_local_net_id()
    logger.info("[Test 2] Local Net ID: " .. local_net_id)
end

local function test_list_players()
    local players = world:get_players()
    logger.info("[Test 3] Players in world: " .. #players)

    for net_id, player in pairs(players) do
        logger.info("[Test 3]   Player " .. player.net_id .. " - " .. player.name .. " (Pos: " .. player.position.x .. ", " .. player.position.y .. ")")
    end
end

local function test_find_player()
    local local_net_id = world:get_local_net_id()
    if local_net_id >= 0 then
        local player = world:get_player(local_net_id)
        if player then
            logger.info("[Test 4] Found player by net_id " .. local_net_id .. ": " .. player.name)
        else
            logger.warn("[Test 4] Could not find player by net_id " .. local_net_id)
        end
    else
        logger.warn("[Test 4] Cannot test - no local net_id")
    end
end

local function test_collision_data()
    local local_player = world:get_local_player()
    if local_player then
        local col = local_player.collision
        logger.info("[Test 5] Collision: x=" .. col.x .. ", y=" .. col.y .. ", z=" .. col.z .. ", w=" .. col.w)
        logger.info("[Test 5] Invisible: " .. local_player.invisible .. ", Mod State: " .. local_player.mod_state)
    end
end
Key Features:
  • Testing all player API functions
  • Event-driven updates on spawn
  • Periodic monitoring with scheduler
  • Comprehensive player property access
  • Collision and mod state checking

Scheduler Patterns

Different ways to use the scheduler API.
local tasks = {}

local function schedule(key, delay, fn)
    tasks[key] = scheduler.schedule(delay, fn)
    return tasks[key]
end

local function schedule_periodic(key, interval, fn, initial_delay)
    if initial_delay then
        tasks[key] = scheduler.schedule_periodic(interval, fn, initial_delay)
    else
        tasks[key] = scheduler.schedule_periodic(interval, fn)
    end
    return tasks[key]
end

logger.info("Scheduler test script loaded")

-- One-shot timer
tasks.oneshot_500 = schedule("oneshot_500", 500, function()
    logger.info("One-shot fired after 500ms")
end)

-- Limited periodic timer
do
    local counter = 0

    tasks.periodic_300 = schedule_periodic("periodic_300", 300, function()
        counter = counter + 1
        logger.info("Periodic tick: " .. counter .. "/5")

        if counter >= 5 then
            logger.info("Stopping periodic timer")
            return false
        end

        return true
    end)
end

-- Periodic with initial delay
tasks.periodic_delayed = schedule_periodic("periodic_delayed", 500, function()
    logger.info("Periodic with initial delay fired")
    return true
end, 2000)

logger.info("Pending task count: " .. scheduler.pending_count())

-- Task cancellation test
schedule("cancel_oneshot", 1500, function()
    if scheduler.is_pending(tasks.oneshot_500) then
        logger.info("One-shot still pending, cancelling...")
        scheduler.cancel(tasks.oneshot_500)
        logger.info("Pending count after cancellation: " .. scheduler.pending_count())
    else
        logger.info("One-shot already completed")
    end
end)

-- Cancel all tasks
schedule("cancel_all", 4000, function()
    logger.info("Cancelling all " .. scheduler.pending_count() .. " pending tasks")
    scheduler.cancel_all()
    logger.info("Pending count after cancel_all: " .. scheduler.pending_count())
end)
Key Features:
  • Task management with named tasks
  • One-shot timers
  • Limited periodic timers
  • Periodic timers with initial delay
  • Task cancellation
  • Pending task checking
  • Batch cancellation with cancel_all

Event Handling Patterns

Complex packet interception and handling.
event.on("ServerBoundPacket", function(ctx)
    local pkt = ctx:get_packet()
    if not pkt then return end

    if pkt:has_raw_data() then
        logger.debug("[Raw] " .. #pkt.raw .. " bytes, modified=" .. tostring(pkt:is_modified()))
    end

    if pkt.text_parse then
        local action = pkt.text_parse:get("action")
        if action ~= "" then
            logger.info("[Text] Action: " .. action)
        end
    elseif pkt.game_packet then
        logger.info("[Game] Type: " .. tostring(pkt.game_packet.type) .. " NetID: " .. pkt.game_packet.net_id)

        if pkt.game_packet.type == packet.PacketType.PACKET_STATE then
            logger.info(string.format("[State] Pos: %.1f, %.1f", pkt.game_packet.pos_x, pkt.game_packet.pos_y))
        end

        if pkt.game_packet.flags & packet.PacketFlag.PACKET_FLAG_ON_JUMP ~= 0 then
            logger.info("[Game] Player Jumped!")
        end
    elseif pkt.variant then
        logger.info("[Variant] Function: " .. tostring(pkt.variant:get(0)))
    end
end)

event.on("OnSendToServer", function(ctx)
    local pkt = ctx:get_packet()
    if not pkt then return end

    logger.info("[OnSendToServer] Port: " .. tostring(pkt.port) .. " Token: " .. tostring(pkt.token))
    logger.info("[OnSendToServer] Address: " .. pkt.address .. " User: " .. tostring(pkt.user))
end)

event.on("Input", function(ctx)
    local pkt = ctx:get_packet()
    if not pkt then return end

    logger.info("[Chat] " .. pkt.text)

    if pkt.text == "!hello" then
        ctx:cancel()
        local response = LogPacket.new()
        response.msg = "Hello from Lua!"
        send.to_client(response)
    elseif pkt.text == "!rawtest" then
        if pkt:has_raw_data() then
            logger.info("[Raw] " .. #pkt.raw .. " bytes, modified=" .. tostring(pkt:is_modified()))
        end
        ctx:cancel()
    elseif pkt.text == "!modifytest" then
        pkt.text = "modified message"
        pkt:mark_modified()
        logger.info("[Modified] Packet marked as modified")
    end
end)

logger.info("Event test script loaded")
Key Features:
  • Multi-type packet handling
  • Text parse packet inspection
  • Game packet type checking
  • Packet flag checking (jump detection)
  • Variant packet handling
  • Chat interception and auto-response
  • Packet cancellation
  • Packet modification

Best Practices Summary

Check arguments, player state, and data before using:
if #ctx.args < 1 then
    ctx:reply("Usage: /command <arg>")
    return false
end

local player = world:get_local_player()
if not player then
    ctx:reply("`4Error: ``Not in a world")
    return false
end
Cancel tasks and clean up state:
event.on("server:Disconnect", function(ctx)
    if current_task_id then
        scheduler.cancel(current_task_id)
        current_task_id = nil
    end
end)
Keep variables local to avoid conflicts:
local my_state = {}
local my_task_id = nil

-- Not:
my_state = {}  -- Global!
Always inform users of success/failure:
ctx:reply("`2Success: ``Operation completed")
ctx:reply("`4Error: ``Invalid input")

See Also

Logger API

Logging reference

Events API

Event handling

Commands API

Command registration

Scheduler API

Task scheduling

World API

World and player data

Items API

Item database