Modules in OTClient (OTC) are self-contained folders containing Lua logic and UI definition files (.lua and .otui) that extend the game's client functionality. Examples include hotkey bars, health trackers, battle lists, and custom UIs.
modules/
hello_world/
hello_world.lua
hello_world.otui
hello_world.otmod
Every module must include a .otmod file which defines its metadata and how it integrates with the client.
Important: Some forks of OTClient require OTUI files to be listed under the scripts: array to ensure they are loaded. Always test if your .otui files need to be explicitly listed in .otmod.
Compatibility clarification: This guide does not treat that "some forks" statement as a confirmed mehah requirement. For mehah, mark it as unknown until validated in your specific build.
name: "hello_world" -- Name of the module
description: "Displays a Hello World message on Ctrl+I" -- Description
author: "Your Name" -- Who created it
sandboxed: true -- Restricts global access
otuis: [ "hello_world.otui" ] -- Better provide it here, otherwise you might have problems with styles
scripts: ["hello_world.lua"] -- Lua files to load
autoload: true -- Load automatically
autoload-priority: 1001 -- Load order priority
@onLoad: init -- Lua function to run on load
@onUnload: terminate -- Lua function to run on unload
This script binds Ctrl+I to toggle a small window with a Hello World message:
local window = nil
local hotkey = nil
function init()
-- Import base and custom styles
g_ui.importStyle('hello_world.otui')
window = g_ui.createWidget('HelloWindow', rootWidget)
window:hide()
-- Bind Ctrl+I to show the window
hotkey = g_keyboard.bindKeyPress('Ctrl+I', function()
toggleHelloWindow()
end)
end
function terminate()
if window then
window:destroy()
window = nil
end
if hotkey then
g_keyboard.unbindKeyPress('Ctrl+I')
hotkey = nil
end
end
This defines a simple window named HelloWindow with a label inside it:
HelloWindow < MainWindow
id: HelloWindow
size: 200 100
text: "Hello"
draggable: true
visible: true
Label
id: HelloLabel
anchors.centerIn: parent
text: "Hello, World!"
font: verdana-11px-rounded
Note: Fork differences are possible (classic/OTCv8/mehah), but this OTUI snippet stays aligned with the same baseline reference.
Download a working example module:
These are the most commonly used attributes across OTUI widget types.
width height, e.g. size: 100 20.position: 20 30.top, bottom, left, right, or fill.margin: 10 or margin: 5 10 5 10.false to hide widget by default.false to disable interaction (e.g. greys out buttons).left, center, right.text-offset: 2 2.verdana-11px-rounded.#FFFFFF./images/myicon.png).x y width height.verticalBox, horizontalBox, or grid.width color, e.g. 1 #FF0000.border-width: 1 2 1 2.border-color: #000 #222 #000 #222.Besides @onLoad and @onUnload, some OTClient forks support additional lifecycle hooks. These control what happens during loading, unloading, focusing, and reloading modules.
@onLoad: init -- Called when the module is loaded
@onUnload: terminate -- Called when the module is unloaded
@onReload: refreshUI -- Called when module is reloaded manually
@onFocus: handleFocus -- Called when the module UI is focused
@onBlur: handleBlur -- Called when the module UI is unfocused
Note: These additional events are supported in some forks like OTCv8, but not guaranteed in all versions. For mehah compatibility in this documentation, this is unknown until directly tested on the target build.
You can define reusable widget styles using inheritance. This lets you apply common properties across multiple widgets.
MyCustomLabel < Label
color: #FF0000
font: verdana-11px-rounded
Then, use this custom style in your layout:
MyCustomLabel
id: warningText
text: "Warning: Something went wrong!"
You can nest containers and use layout managers to create complex UIs:
Panel
id: mainPanel
layout: verticalBox
spacing: 4
Label
text: "Player Stats"
Panel
id: gridPanel
layout: grid
cell-size: 50 20
spacing: 2
Label
text: "Health"
ProgressBar
id: healthBar
value: 75
Widgets can be shown or hidden dynamically from your Lua logic:
local myWidget = rootWidget:getChildById("premiumPanel")
myWidget:setVisible(player:isPremium())
This allows you to show certain UI elements only for premium users, quest progress, etc.
g_ui.loadUI path and parent widget.nil from getChildById? Ensure the id is spelled correctly and the widget is loaded.g_game connects.Note on .lua Extensions in .otmod Files: In classic OTClient, script filenames in the scripts: array must include the .lua extension. However, in modern forks like OTCv8, the client automatically appends .lua if it's missing. Example:
scripts: ["example.lua"] -- always works
scripts: ["example"] -- works only in OTCv8 and similar forks
For maximum compatibility across clients, always include the full filename with the .lua extension.
fit-children: true on layout panels to allow auto-sizing for nested widgets.anchors.fill: parent to make widgets automatically stretch and fit their container.margin and padding for clean spacing without needing nested panels.focusable: false for labels or non-interactive elements to avoid them intercepting keyboard focus.id in Lua: myWidget = rootWidget:getChildById("myId")UICreature or UIItem for creature/item previews and inventory widgets.The table below links each feature to a concrete module API area and states current compatibility confidence for mehah fork.
| Feature | Status | API area reference | Notes |
|---|---|---|---|
Module lifecycle hooks (@onLoad, @onUnload) |
supported | .otmod lifecycle hooks |
Core module load/unload path, expected baseline behavior on mehah. |
Optional lifecycle hooks (@onReload, @onFocus, @onBlur) |
unknown | .otmod lifecycle hooks |
Previously described as available in “some forks”; in this guide this is not confirmed for mehah. |
OTUI declaration in descriptor (otuis: [...]) |
supported | .otmod + OTUI/scripts loading |
Prefer explicit OTUI registration for stable packaging across forks, including mehah. |
Skipping .lua extension in scripts |
unknown | .otmod script loader |
Documented here for OTCv8-like clients only; mehah behavior requires direct test. |
UI loading with g_ui.loadUI / g_ui.importStyle |
supported | OTUI/scripts integration | Standard module API flow. |
Hotkeys with g_keyboard.bindKeyPress |
supported | Hotkeys API | Bind in init and unbind in terminate for safe reload behavior. |
Extended opcodes (ProtocolGame.registerExtendedOpcode, g_game.sendExtendedOpcode) |
partial | Extended opcodes API | Client-side API is present; production behavior also depends on server-side opcode scripts. |
.lua" → mehah: unknown unless confirmed by tests..lua filenames in scripts:.Extended opcodes allow OTClient and the server to exchange custom messages, beyond the default protocol. They're extremely useful for adding features like UI-server communication, achievements, or custom task systems.
function onExtendedOpcode(player, opcode, buffer)
-- Called automatically when client sends an extended opcode
if opcode == 42 then
print("Received from client:", buffer)
-- Respond back to the client using the same opcode
player:sendExtendedOpcode(42, "response from server")
end
return true
end
Explanation: This function should be registered in TFS's creaturescripts.xml. You can define multiple opcodes and use buffer as the custom message content.
function init()
-- Register a client-side handler for opcode 42
ProtocolGame.registerExtendedOpcode(42, onOpcode42)
end
function onOpcode42(protocol, opcode, buffer)
-- This is called when server sends an extended opcode 42
g_logger.info("Received from server: " .. buffer)
end
function sendMyOpcode()
-- Send data to the server only if connected
if g_game.isOnline() then
g_game.sendExtendedOpcode(42, "hello from client")
end
end
Key Points:
ProtocolGame.registerExtendedOpcode: Registers a Lua function to handle server responses.g_game.sendExtendedOpcode(opcode, buffer): Sends a custom message to the server.buffer can be plain text, JSON, CSV, or any string — it's your protocol.This is useful for implementing:
Tips:
g_game.isOnline() before sending opcodesresources/hello_world.otmod, resources/hello_world.lua, resources/hello_world.otui.