Skip to content

Mod Tools

REghZy edited this page Nov 20, 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

These are not in release builds yet.

-- 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

These are instance methods, so the invisible first parameter is the element itself

-- 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)

-- These 6 functions accept nil (to clear the fixed size value) or a normal number.
-- Width and Height are nil by default, which means auto-size
-- MinWidth and MinHeight are 0 by default
-- MaxWidth and MaxHeight are math.huge by default

-- By default, all controls will try to take up as much space as possible, 
-- which is typically better than using a fixed width or height.
-- It's also better to use a grid instead of fixed size/offsets, since it will resize properly
set_width(value : number?)
set_height(value : number?)
set_minwidth(value : number?)
set_maxwidth(value : number?)
set_minheight(value : number?)
set_maxheight(value : number?)

Panel Functions

Stack, Dock and Grid panels contain these functions. These are instance methods, so the invisible first parameter is the panel itself

-- Returns the index of the element within the panel. The index is zero-based, so 0 is the first element. If the element was not found, -1 is returned.
index_of(element : table) -> number

-- Removes an element from the panel at the given index
remove_at(index : number)

GridPanel functions

All column/row parameters pass column first and row next, so that it behaves like X and Y coordinates. These are instance methods, so the invisible first parameter is the panel itself

-- 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)

-- Removes the row at the given index. The index is zero-based, so 0 is the first row.
remove_row(index : number)

-- Removes the column at the given index. The index is zero-based, so 0 is the first column.
remove_column(index : number)

DockPanel functions

These are instance methods, so the invisible first parameter is the panel itself

-- 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

These are instance methods, so the invisible first parameter is the panel itself

-- Adds an element
add(element : table)

Button functions

These are instance methods, so the invisible first parameter is the button itself.

The callback function is invoked with the Button (table) as the first parameter, which lets you access and modify the button itself during its own callback(s).

-- Sets the function that runs when the user clicks then releases their mouse on the button.
-- Provide nil to remove the callback
set_press_function(callback : function(sender : table)?)

-- 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. 
-- Remarks about the callback's return value: When no value is available or it returns false, the loop continues. When true, it breaks the loop (as if 
the user released their LMB). Return nothing or false to truly loop until the user stops pressing.
-- Provide nil to remove the callback
set_holding_function(callback : function(sender : table, 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.

Text Block functions

These are instance methods, so the invisible first parameter is the text block itself

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

Text Box functions

Text Boxes allow users to input data into your mod tool, e.g. specify a target address or offset, a number or plain text.

Text Boxes have three modes:

  • text The user types text, and you access that text in the script.
  • address The value is an unsigned 32-bit integer. This doesn't have to be used for an address, it could be an offset or anything that requires a non-negative integer up to 4.2~ billion
  • number The value is a regular lua number.

These are instance methods, so the invisible first parameter is the text block itself

-- Sets the value as plain text.
set_text(value : string)

-- Sets the value as an unsigned 32-bit integer value
set_address(value : string|number)

-- Sets the value as a decimal number
set_number(value : number)

-- Gets the value in the text box. This returns either a string ("text" mode) or number ("address" or "number" mode).
get_value() -> string|number

-- Sets the callback that runs when the text box's value changes. This includes both from code (e.g. set_number), or when the user types something and presses enter or moves focus away from the text box.
-- This is not called for each key pressed. Use set_keypress_function instead.
-- The sender parameter is the text box
set_value_change_function(callback : function(sender : table)?)

-- Sets the callback that runs when the user presses or releases a key.
-- sender is the text box.
-- isPressed specifies if it was a key press instead of release.
-- symbol specifies the key symbol (e.g. "w" for the W key)
-- keyCode specifies the exact Avalonia key code value
set_keypress_function(callback : function(sender : table, isPressed : boolean, symbol : string, keyCode : number)?)

For more info about key codes, see Key Codes

Warning

Avoid using loops within callback functions, and instead, use timers. If you absolutely must, then call pump.try_run_messages() often. Looping in a callback stops the main thread from communicating with the lua thread if you don't pump the messages, so UI interactions won't get dispatched.

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(gui.create_button(side .. " left btn 1"))
    bottom_spL:add(gui.create_button(side .. " left btn 2"))
    bottom_spL:add(gui.create_button(side .. " left btn 3"))  

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

    -- add panels to the dock
    dock:add("left", bottom_spL)
    dock:add("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)

-- For some reason we have to set these as local variables before passing to root:add(), otherwise we get 511 arguments???
local top_panel = CreateTestDock("Top ")
local btm_panel = CreateTestDock("Bottom ")

root:add("top", top_panel)
root:add("bottom", btm_panel)

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

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

tmpstack:add(tmpbutton1)

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

tmpstack:add(tmpbutton2)

root:add(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 (sender, 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 (sender, 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 (sender, 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 (sender, 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 GetSensorData()
    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})

    return engine.readbytes(addr_XamBuffer, 12)
end

function GetTemperature(sensorType, data)
    if sensorType < 0 or sensorType > 3 then
        error("Sensor type must be 0, 1, 2, or 3")
    end
    
    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()
    local data = GetSensorData()
    s_CpuLabel:set_text("CPU: " .. GetTemperature(0, data) .. "c")
    s_GpuLabel:set_text("GPU: " .. GetTemperature(1, data) .. "c")
    s_MemLabel:set_text("MEMORY: " .. GetTemperature(2, data) .. "c")
    s_BrdLabel:set_text("CHASSIS: " .. GetTemperature(3, data) .. "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

Handling key presses and value changes for text boxes

s_TbValChange = gui.create_text("")
s_TbKeyPressed = gui.create_text("")

local tb = gui.create_textbox("address")
tb:set_value_change_function(function (sender)
    s_TbValChange:set_text(tostring(sender:get_value()))
end)

tb:set_keypress_function(function (sender, press, symbol, code)
    s_TbKeyPressed:set_text(tostring(press) .. ", " .. symbol .. ", " .. tostring(code))
end)

local stack = gui.create_stackpanel(true)
stack:add(tb)
stack:add(s_TbValChange)
stack:add(s_TbKeyPressed)

gui.set_root_panel(stack)

while true do
    pump.run_messages()
end

Key Codes

In key callbacks, the Code column corresponds to the keyCode parameter.

Source: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Base/Input/Key.cs

Key Code Name Code Description
None 0 No key pressed.
Cancel 1 The Cancel key.
Back 2 The Back key.
Tab 3 The Tab key.
LineFeed 4 The Linefeed key.
Clear 5 The Clear key.
Enter 6 The Enter key.
Return 6 The Return key.
Pause 7 The Pause key.
Capital 8 The Caps Lock key.
CapsLock 8 The Caps Lock key.
HangulMode 9 The IME Hangul mode key.
KanaMode 9 The IME Kana mode key.
JunjaMode 10 The IME Junja mode key.
FinalMode 11 The IME Final mode key.
HanjaMode 12 The IME Hanja mode key.
KanjiMode 12 The IME Kanji mode key.
Escape 13 The Escape key.
ImeConvert 14 The IME Convert key.
ImeNonConvert 15 The IME NonConvert key.
ImeAccept 16 The IME Accept key.
ImeModeChange 17 The IME Mode change key.
Space 18 The space bar.
PageUp 19 The Page Up key.
Prior 19 The Page Up key.
Next 20 The Page Down key.
PageDown 20 The Page Down key.
End 21 The End key.
Home 22 The Home key.
Left 23 The Left arrow key.
Up 24 The Up arrow key.
Right 25 The Right arrow key.
Down 26 The Down arrow key.
Select 27 The Select key.
Print 28 The Print key.
Execute 29 The Execute key.
PrintScreen 30 The Print Screen key.
Snapshot 30 The Print Screen key.
Insert 31 The Insert key.
Delete 32 The Delete key.
Help 33 The Help key.
D0 34 The 0 key.
D1 35 The 1 key.
D2 36 The 2 key.
D3 37 The 3 key.
D4 38 The 4 key.
D5 39 The 5 key.
D6 40 The 6 key.
D7 41 The 7 key.
D8 42 The 8 key.
D9 43 The 9 key.
A 44 The A key.
B 45 The B key.
C 46 The C key.
D 47 The D key.
E 48 The E key.
F 49 The F key.
G 50 The G key.
H 51 The H key.
I 52 The I key.
J 53 The J key.
K 54 The K key.
L 55 The L key.
M 56 The M key.
N 57 The N key.
O 58 The O key.
P 59 The P key.
Q 60 The Q key.
R 61 The R key.
S 62 The S key.
T 63 The T key.
U 64 The U key.
V 65 The V key.
W 66 The W key.
X 67 The X key.
Y 68 The Y key.
Z 69 The Z key.
LWin 70 The left Windows key.
RWin 71 The right Windows key.
Apps 72 The Application key.
Sleep 73 The Sleep key.
NumPad0 74 The 0 key on the numeric keypad.
NumPad1 75 The 1 key on the numeric keypad.
NumPad2 76 The 2 key on the numeric keypad.
NumPad3 77 The 3 key on the numeric keypad.
NumPad4 78 The 4 key on the numeric keypad.
NumPad5 79 The 5 key on the numeric keypad.
NumPad6 80 The 6 key on the numeric keypad.
NumPad7 81 The 7 key on the numeric keypad.
NumPad8 82 The 8 key on the numeric keypad.
NumPad9 83 The 9 key on the numeric keypad.
Multiply 84 The Multiply key.
Add 85 The Add key.
Separator 86 The Separator key.
Subtract 87 The Subtract key.
Decimal 88 The Decimal key.
Divide 89 The Divide key.
F1 90 The F1 key.
F2 91 The F2 key.
F3 92 The F3 key.
F4 93 The F4 key.
F5 94 The F5 key.
F6 95 The F6 key.
F7 96 The F7 key.
F8 97 The F8 key.
F9 98 The F9 key.
F10 99 The F10 key.
F11 100 The F11 key.
F12 101 The F12 key.
F13 102 The F13 key.
F14 103 The F14 key.
F15 104 The F15 key.
F16 105 The F16 key.
F17 106 The F17 key.
F18 107 The F18 key.
F19 108 The F19 key.
F20 109 The F20 key.
F21 110 The F21 key.
F22 111 The F22 key.
F23 112 The F23 key.
F24 113 The F24 key.
NumLock 114 The Numlock key.
Scroll 115 The Scroll key.
LeftShift 116 The left Shift key.
RightShift 117 The right Shift key.
LeftCtrl 118 The left Ctrl key.
RightCtrl 119 The right Ctrl key.
LeftAlt 120 The left Alt key.
RightAlt 121 The right Alt key.
BrowserBack 122 The browser Back key.
BrowserForward 123 The browser Forward key.
BrowserRefresh 124 The browser Refresh key.
BrowserStop 125 The browser Stop key.
BrowserSearch 126 The browser Search key.
BrowserFavorites 127 The browser Favorites key.
BrowserHome 128 The browser Home key.
VolumeMute 129 The Volume Mute key.
VolumeDown 130 The Volume Down key.
VolumeUp 131 The Volume Up key.
MediaNextTrack 132 The media Next Track key.
MediaPreviousTrack 133 The media Previous Track key.
MediaStop 134 The media Stop key.
MediaPlayPause 135 The media Play/Pause key.
LaunchMail 136 The Launch Mail key.
SelectMedia 137 The Select Media key.
LaunchApplication1 138 The Launch Application 1 key.
LaunchApplication2 139 The Launch Application 2 key.
Oem1 140 The OEM 1 key.
OemSemicolon 140 The OEM Semicolon key.
OemPlus 141 The OEM Plus key.
OemComma 142 The OEM Comma key.
OemMinus 143 The OEM Minus key.
OemPeriod 144 The OEM Period key.
Oem2 145 The OEM 2 key.
OemQuestion 145 The OEM Question Mark key.
Oem3 146 The OEM 3 key.
OemTilde 146 The OEM Tilde key.
AbntC1 147 The ABNT_C1 (Brazilian) key.
AbntC2 148 The ABNT_C2 (Brazilian) key.
Oem4 149 The OEM 4 key.
OemOpenBrackets 149 The OEM Open Brackets key.
Oem5 150 The OEM 5 key.
OemPipe 150 The OEM Pipe key.
Oem6 151 The OEM 6 key.
OemCloseBrackets 151 The OEM Close Brackets key.
Oem7 152 The OEM 7 key.
OemQuotes 152 The OEM Quotes key.
Oem8 153 The OEM 8 key.
Oem102 154 The OEM 3 key.
OemBackslash 154 The OEM Backslash key.
ImeProcessed 155 A special key masking the real key being processed by an IME.
System 156 A special key masking the real key being processed as a system key.
DbeAlphanumeric 157 The DBE_ALPHANUMERIC key.
OemAttn 157 The OEM ATTN key.
DbeKatakana 158 The DBE_KATAKANA key.
OemFinish 158 The OEM Finish key.
DbeHiragana 159 The DBE_HIRAGANA key.
OemCopy 159 The OEM Copy key.
DbeSbcsChar 160 The DBE_SBCSCHAR key.
OemAuto 160 The OEM Auto key.
DbeDbcsChar 161 The DBE_DBCSCHAR key.
OemEnlw 161 The OEM ENLW key.
DbeRoman 162 The DBE_ROMAN key.
OemBackTab 162 The OEM BackTab key.
Attn 163 The ATTN key.
DbeNoRoman 163 The DBE_NOROMAN key.
CrSel 164 The CRSEL key.
DbeEnterWordRegisterMode 164 The DBE_ENTERWORDREGISTERMODE key.
DbeEnterImeConfigureMode 165 The DBE_ENTERIMECONFIGMODE key.
ExSel 165 The EXSEL key.
DbeFlushString 166 The DBE_FLUSHSTRING key.
EraseEof 166 The ERASE EOF Key.
DbeCodeInput 167 The DBE_CODEINPUT key.
Play 167 The Play key.
DbeNoCodeInput 168 The DBE_NOCODEINPUT key.
Zoom 168 The Zoom key.
DbeDetermineString 169 The DBE_DETERMINESTRING key.
NoName 169 Reserved for future use.
DbeEnterDialogConversionMode 170 The DBE_ENTERDLGCONVERSIONMODE key.
Pa1 170 The PA1 key.
OemClear 171 The OEM Clear key.
DeadCharProcessed 172 The key is used with another key to create a single combined character.
FnLeftArrow 10001 OSX Platform-specific Fn+Left key
FnRightArrow 10002 OSX Platform-specific Fn+Right key
FnUpArrow 10003 OSX Platform-specific Fn+Up key
FnDownArrow 10004 OSX Platform-specific Fn+Down key
MediaHome 100000 Remove control home button
MediaChannelList 100001 TV Channel up
MediaChannelRaise 100002 TV Channel up
MediaChannelLower 100003 TV Channel down
MediaRecord 100005 TV Channel down
MediaRed 100010 Remote control Red button
MediaGreen 100011 Remote control Green button
MediaYellow 100012 Remote control Yellow button
MediaBlue 100013 Remote control Blue button
MediaMenu 100020 Remote control Menu button
MediaMore 100021 Remote control dots button
MediaOption 100022 Remote control option button
MediaInfo 100023 Remote control channel info button
MediaSearch 100024 Remote control search button
MediaSubtitle 100025 Remote control subtitle/caption button
MediaTvGuide 100026 Remote control Tv guide detail button
MediaPreviousChannel 100027 Remote control Previous Channel

Clone this wiki locally