Skip to content

Mod Tools

REghZy edited this page Nov 3, 2025 · 24 revisions

Mod tools are similar to scripts except they can create GUIs. Accessible via the Mod Tools sub menu in the Tools menu. Loaded mod tools show in the submenu, and any opened files are reloaded on startup and appear in that submenu.

Mod tools support all the functions that scripts support (e.g. engine.readnumber() and jrpc.callvoidat())

Functions

-- These 4 functions return a table that represent the GUI element.
gui.create_dockpanel(lastChildFill : boolean) -> table
gui.create_stackpanel(isVertical : boolean)   -> table
gui.create_gridpanel()                        -> table
gui.create_button(text : string)              -> table
gui.create_text(text : string)                -> table

-- Sets the GUI's root panel to draw
gui.set_root_panel(panel : table)

-- Blocks until there's pending messages queued on the lua thread, then processes them, and returns.
-- For example, when the user presses a button, it dispatches a click event to the lua thread,
-- then this method will process that press event (which starts an internal loop until the click
-- release event is sent, and this inner loop calls the equivalent of pump.try_run_messages())
pump.run_messages()

-- Processes any pending messages queued on the lua thread, or returns if the queue is empty.
-- This is the same as `pump.run_messages()` except doesn't block if the queue is empty.
-- Note, this may block anyway (e.g. pressing a button which has a holding function set)
pump.try_run_messages()

Timers

-- Creates a timer that invokes the callback function at the specified interval (in seconds). The interval is also the delay for invoking the callback for the first time (aka due time)
gui.create_timer(interval : number, callback : function) -> TimerObject

-- Destroys the timer returned by create_timer, which stops the callback from being invoked.
gui.destroy_timer(timer : TimerObject)

Functions for all GUI elements

-- Horizontally aligns this element relative to the parent. Can be "stretch", "left", "center" or "right"
set_align_h(align : string)

-- Vertically aligns this element relative to the parent. Can be "stretch", "top", "center" or "bottom"
set_align_v(align : string)

GridPanel functions

All column/row parameters pass column first and row next, so that it behaves like X and Y coordinates

-- Note: Row and column indices are zero-based and cannot be negative
--       RowSpan and ColumnSpan must be >= 1
add(column : number, row : number, element : table)
add(column : number, row : number, columnSpan : number, rowSpan : number, element : table)
-- Adds a row with the given format for the pixel height. E.g. "25", "3*", "Auto"
add_row(layout: string)

-- Adds a column with the given format for the pixel width. E.g. "25", "3*", "Auto"
add_column(layout: string)

DockPanel functions

-- Adds an element docked to either "left", "top", "right" or "bottom"
add(dock : string, element : table)
-- Adds an element docked to the left. Ideally only use for the last child when lastChildFill is true
add(element : table)

StackPanel functions

-- Adds an element
add(element : table)

Button functions

-- Sets the function that runs when the user clicks then releases their mouse on the button
set_press_function(callback : function)

-- Sets the function that is called in a loop while the user has left-click pressed.
-- The 'delta' parameter is the amount of time since the callback was last called, in seconds. 
set_holding_function(callback : function(delta : number) -> boolean)

-- Sets the button's text
set_text(text : string)

The holding function is provided the delta time between each callback invocation. On the first run, it will be very very small.

You should use this if you need to, for example, increase a float on the console at a fixed rate (e.g. 25.0 per second).

Example: engine.writenumber(addr, "float", 25.0 * delta)

In the demo script at the bottom of the page, the text increments by 3000 per second on a modern PC. This is because setting the text requires posting a message to the UI dispatcher to set the text, because the caller is the lua thread. If you only used lua code and no network operations, it would run as fast as the lua virtual machine can execute, which is significantly faster than 3000 times per second.

Warning

Do not create your own loop inside of this, because the outer loop will pump any queued messages automatically. If you need a loop, call pump.try_run_messages() often

Text Block functions

-- Sets the text of the text block
set_text(text : string)

Examples

This is a demo of UI layout

press_btn_counter = 0
holding_btn_counter = 0

function CreateTestDock(side)
    -- create a dock panel that won't fill the last child control, 
    -- since we want a left and right dock
    local dock = gui.create_dockpanel(false)

    -- create a horizontal stack panel
    local bottom_spL = gui.create_stackpanel(true)
    bottom_spL.add(bottom_spL, gui.create_button(side .. " left btn 1"))
    bottom_spL.add(bottom_spL, gui.create_button(side .. " left btn 2"))
    bottom_spL.add(bottom_spL, gui.create_button(side .. " left btn 3"))  

    -- create a horizontal stack panel
    local bottom_spR = gui.create_stackpanel(true)
    bottom_spR.add(bottom_spR, gui.create_button(side .. " right btn 1"))
    bottom_spR.add(bottom_spR, gui.create_button(side .. " right btn 2"))
    bottom_spR.add(bottom_spR, gui.create_button(side .. " right btn 3"))

    -- add panels to the dock
    dock.add(dock, "left", bottom_spL)
    dock.add(dock, "right", bottom_spR)

    return dock
end

-- create our root panel, which will fill the last child (tmpstack)
-- to the window contents (inbetween the top and bottom docks ofc)
local root = gui.create_dockpanel(true)
root.add(root, "top", CreateTestDock("Top "))
root.add(root, "bottom", CreateTestDock("Bottom "))

-- create a vertical stack panel
local tmpstack = gui.create_stackpanel(true)
tmpstack.add(tmpstack, gui.create_text("Hello!"))

local tmpbutton1 = gui.create_button("Click to incr per press")
tmpbutton1.set_press_function(tmpbutton1, function ()
    press_btn_counter = press_btn_counter + 1
    tmpbutton1.set_text(tmpbutton1, press_btn_counter)
end)

tmpstack.add(tmpstack, tmpbutton1)

local tmpbutton2 = gui.create_button("Click to incr while holding")
tmpbutton2.set_holding_function(tmpbutton2, function (delta)
    holding_btn_counter = holding_btn_counter + 1
    tmpbutton2.set_text(tmpbutton2, holding_btn_counter)
end)

tmpstack.add(tmpstack, tmpbutton2)

root.add(root, tmpstack)

-- assign root panel to the GUI
gui.set_root_panel(root)

-- the important part. In order for button clicks to work, 
-- pump.run_messages() must be called often
while true do 
    pump.run_messages()
end

MW2

Here is a small script to move the player around using 4 buttons:

local tb_x = gui.create_text("Pos X: ???");
local tb_y = gui.create_text("Pos Y: ???");

local grid = gui.create_gridpanel()
grid:add_row("40")
grid:add_row("40")
grid:add_row("40")
grid:add_column("40")
grid:add_column("40")
grid:add_column("40")

local btn_fwd = gui.create_button("+Y");
local btn_lft = gui.create_button("-X");
local btn_rht = gui.create_button("+X");
local btn_bck = gui.create_button("-Y");

btn_fwd:set_holding_function(function (delta)
    local num = engine.readnumber(0x830CBFA0, "float") + (100 * delta)
    engine.writenumber(0x830CBFA0, "float", num)
    tb_y:set_text("Pos Y: " .. tostring(num))
end)

btn_lft:set_holding_function(function (delta)
    local num = engine.readnumber(0x830CBF9C, "float") - (100 * delta)
    engine.writenumber(0x830CBF9C, "float", num)
    tb_x:set_text("Pos X: " .. tostring(num))
end)

btn_rht:set_holding_function(function (delta)
    local num = engine.readnumber(0x830CBF9C, "float") + (100 * delta)
    engine.writenumber(0x830CBF9C, "float", num)
    tb_x:set_text("Pos X: " .. tostring(num))
end)

btn_bck:set_holding_function(function (delta)
    local num = engine.readnumber(0x830CBFA0, "float") - (100 * delta)
    engine.writenumber(0x830CBFA0, "float", num)
    tb_y:set_text("Pos Y: " .. tostring(num))
end)

grid:add(1, 0, btn_fwd)
grid:add(0, 1, btn_lft)
grid:add(2, 1, btn_rht)
grid:add(1, 2, btn_bck)

local stack = gui.create_stackpanel(true)
stack:add(grid)
stack:add(tb_x)
stack:add(tb_y)

-- assign root panel to the GUI
gui.set_root_panel(stack)

-- the important part. In order for button clicks to work, 
-- pump.run_messages() must be called often
while true do 
    pump.run_messages()
end

Temperature Display

function GetTemperature(sensorType)
    if sensorType < 0 or sensorType > 3 then
        error("Sensor type must be 0, 1, 2, or 3")
    end
    
    local addr_HalSendSMCMessage = jrpc.getprocaddress("xboxkrnl.exe", 0x29)

    -- XamSwapDiscPatchAddress + 0x3000
    local addr_XamBuffer = jrpc.getprocaddress("xam.xex", 0xA29) + 12288
    
    local smcMsg = "7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"
    jrpc.callat("int", addr_HalSendSMCMessage, {"byte[]", smcMsg}, {"int", addr_XamBuffer})

    local data = engine.readbytes(addr_XamBuffer, 12)
    local lo = data[sensorType * 2 + 2]
    local hi = data[sensorType * 2 + 3]
    return bit32.bor(lo, bit32.lshift(hi, 8)) / 256.0
end

s_CpuLabel = gui.create_text("CPU: ? c")
s_GpuLabel = gui.create_text("GPU: ? c")
s_MemLabel = gui.create_text("MEMORY: ? c")
s_BrdLabel = gui.create_text("CHASSIS: ? c")

function UpdateTemps()
    s_CpuLabel:set_text("CPU: " .. GetTemperature(0) .. "c")
    s_GpuLabel:set_text("GPU: " .. GetTemperature(1) .. "c")
    s_MemLabel:set_text("MEMORY: " .. GetTemperature(2) .. "c")
    s_BrdLabel:set_text("CHASSIS: " .. GetTemperature(3) .. "c")
end

local stack = gui.create_stackpanel(true)
stack:add(s_CpuLabel)
stack:add(s_GpuLabel)
stack:add(s_MemLabel)
stack:add(s_BrdLabel)

gui.set_root_panel(stack)

gui.create_timer(1, UpdateTemps)
while (true) do
   pump.run_messages() 
end

Clone this wiki locally