Complete Guide to Creating OTClient Modules

What Are OTClient Modules?

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.

Folder Structure

modules/
  hello_world/
    hello_world.lua
    hello_world.otui
    hello_world.otmod

OTMod File (.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

Lua Logic File (hello_world.lua)

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

OTUI File (hello_world.otui)

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:

Module preview

Attribute Reference

These are the most commonly used attributes across OTUI widget types.

Common Attributes

Text Attributes

Image Attributes

Layout & Container Attributes

Style & Border

Event Handlers

Lifecycle Events in OTMod

Besides @onLoad and @onUnload, some OTClient forks support additional lifecycle hooks. These control what happens during loading, unloading, focusing, and reloading modules.

Standard Events

@onLoad: init         -- Called when the module is loaded
@onUnload: terminate   -- Called when the module is unloaded

Optional Events (Client-dependent)

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

Deeper Dive Into OTUI Features

Widget Inheritance

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

Nested Layouts

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

Conditional Visibility from Lua

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.

Common Mistakes & Troubleshooting

Advanced Tips & Tricks

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.

Using Extended Opcodes

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.

Server-Side (Lua - TFS)

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.

Client-Side (Lua - OTClient)

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:

This is useful for implementing:

Tips: