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.

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

Lua Logic File (hello_world.lua)

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

OTUI File (hello_world.otui)

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:

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. For mehah compatibility in this documentation, this is unknown until directly tested on the target 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.

mehah fork compatibility

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.

Clarification of prior “some forks” wording

Before shipping module on mehah client

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:

Compatibility notes