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
.
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 -- Global reference to the window
local hotkey = nil -- Reference to the bound hotkey
function init()
g_ui.importStyle('hello_world.otui') -- Load UI style definition
window = g_ui.createWidget('HelloWindow', rootWidget) -- Create the widget
window:hide() -- Start hidden
hotkey = g_keyboard.bindKeyPress('Ctrl+I', function() -- Bind Ctrl+I
toggleHelloWindow()
end)
end
function terminate()
if window then
window:destroy() -- Remove the window
window = nil
end
if hotkey then
g_keyboard.unbindKeyPress('Ctrl+I') -- Unbind hotkey
hotkey = nil
end
end
function toggleHelloWindow()
if window and window:isVisible() then
window:hide() -- Hide if visible
else
window:show() -- Show if hidden
window:raise() -- Bring to front
window:focus() -- Focus it
end
end
This defines a simple window named HelloWindow
with a label inside it:
HelloWindow < MainWindow -- Inherit from MainWindow style
id: HelloWindow -- ID used in Lua
size: 200 100 -- Width and height
text: "Hello" -- Window title
draggable: true -- Allow window to be dragged
visible: false -- Start hidden
Label -- Child label inside window
id: HelloLabel -- Label ID
anchors.centerIn: parent -- Center in window
text: "Hello, World!" -- Text displayed
color: #00A -- Text color
font: verdana-11px-rounded -- Font style
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. Always test for compatibility with your specific client 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.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 opcodes