You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

Module Authoring Guide

Guide for developing modules for the ARZ Web Helper framework.

Module Structure

A module is a directory containing at minimum init.lua:

modules/mymodule/
├── init.lua              -- Required: module entry point
└── static/               -- Optional: custom web UI
    ├── index.html
    ├── style.css
    └── app.js

Modules placed in http_framework/modules/ are bundled with the framework. Modules in more_modules/ are separate (gitignored). Both are loaded identically at runtime.

Module Interface

local M = {}

function M.init(fw)
    -- Required. Called once when module is loaded.
    -- fw: framework API object (namespaced per module)
end

function M.render(ui, state)
    -- Optional. Generates UI for both web and in-game imgui.
    -- ui: abstract widget builder
    -- state: persistent table (survives re-renders, cleared on module reset)
end

function M.unload()
    -- Optional. Cleanup when module is unloaded/reloaded.
    -- Cancel timers, reset globals, release resources.
end

return M

Framework API (fw object)

Logging

fw.log(level, tag, message, ...)
-- level: "INFO", "WARN", "ERROR", "DEBUG"
-- tag: short identifier (e.g. module name)
-- Additional args are concatenated with spaces
-- Also broadcasts to WebSocket clients as __log event

JSON

local tbl = fw.json_decode(json_string)
local str = fw.json_encode(table)
-- Uses cjson if available, falls back to naive parser
-- Fallback only handles flat {"key":"value"} — always ensure cjson is present

Encoding (Win-1251 / UTF-8)

local utf8 = fw.to_utf8(win1251_string)
local win  = fw.to_win1251(utf8_string)
-- Game uses CP1251 for Cyrillic text
-- Web and DB use UTF-8
-- Convert before sending to game APIs (sampProcessChatInput, etc.)

API Handlers

Register HTTP endpoints accessible at POST /api/{module}/{action}:

fw.on_api("mymod", "do_something", function(body)
    -- body: raw request body string (JSON)
    local data = fw.json_decode(body)
    -- ... process ...
    return fw.json_encode({ok = true, result = "done"})
end)

The handler receives the POST body as a string and must return a JSON string. Errors in handlers are caught by the framework and returned as {"error":"..."}.

Timeout: 2 seconds. If the game thread is blocked, the request returns {"error":"lua timeout"}.

Event Interception

Hook into SA-MP network events:

fw.on_event(event_name, handler, priority)
-- event_name: SAMP event (e.g. "onServerMessage", "onShowDialog", "onSendChat")
-- handler: function receiving event arguments
-- priority: lower = fires first (default 50)
-- Return false to block the event from propagating
-- Return nil/true to allow propagation

Example — block outgoing messages containing "spam":

fw.on_event("onSendChat", function(message)
    if message:lower():find("spam") then
        fw.log("WARN", "MYMOD", "Blocked: " .. message)
        return false  -- cancel the event
    end
end, 10)  -- priority 10: runs before default (50)

Common events:

  • onServerMessage(color, text) — incoming chat message
  • onSendChat(message) — outgoing chat message
  • onShowDialog(id, style, title, button1, button2, text) — server dialog
  • All SAMP RPCs and packets from samp.events.INTERFACE

Events are also broadcast to WebSocket clients as JSON:

{"type":"event", "event":"onServerMessage", "args":[color, text]}

Commands

fw.command("mycommand", function(args)
    -- args: string after the command (e.g. "/mycommand hello world" → "hello world")
    fw.add_chat_message("You said: " .. args, -1)
end)
-- Registers /mycommand with SA-MP and the framework's command list
-- Visible in admin panel Commands tab

Chat / Game Interaction

fw.chat(text)
-- Process text as chat input (sends message or executes /command)

fw.add_chat_message(text, color)
-- Show a local-only chat message (not sent to server)
-- color: ARGB integer or -1 for default white

fw.respond_dialog(id, button, listbox_id, input)
-- Send a dialog response to the server
-- id: dialog ID, button: 0=close/1=confirm, listbox_id: selected row, input: text

fw.unlock_dialog()
-- Sends /mm then auto-closes the menu dialog (id=722)
-- Used to unblock the dialog system after automated dialog interaction

Timers

local cancel = fw.timer_once(milliseconds, callback)
-- Schedules a one-shot callback after delay
-- Returns a cancel function: call cancel() to abort
-- Runs in a separate lua_thread coroutine
-- Use for deferred dialog responses, delayed actions

Pattern — deferred dialog response (from btc module):

fw.on_event("onShowDialog", function(id, style, title, b1, b2, text)
    if id == 7238 then
        fw.timer_once(5, function()
            fw.respond_dialog(id, 1, 0, "")
        end)
        return false  -- hide dialog from player
    end
end, 20)

Database (Key-Value, Auto-Namespaced)

All keys are automatically prefixed with "{module}." for isolation between modules.

Simple Operations

local value = fw.db_get(key)          -- Returns string or nil
local ok    = fw.db_set(key, value)   -- Returns true/false
local ok    = fw.db_delete(key)       -- Returns true/false
-- Get multiple keys at once
local results = fw.db_get_many({"key1", "key2", "key3"})
-- Returns: {key1 = "val1", key2 = "val2"}  (missing keys omitted)

-- Set multiple keys at once
fw.db_set_many({key1 = "val1", key2 = "val2", key3 = "val3"})

-- Get all keys with a prefix
local items = fw.db_get_prefix("settings.")
-- Returns: {["settings.delay"] = "5", ["settings.mode"] = "auto"}

These batch methods use fw.db_batch() internally, which sends operations to the async SQLite backend. They block until results are available (safe in coroutines with wait()).

Persistence Patterns

Settings pattern (from btc module):

local settings = {delay = 5, mode = "auto"}

local function save_settings()
    local kv = {}
    for k, v in pairs(settings) do kv["s." .. k] = tostring(v) end
    fw.db_set_many(kv)
end

local function load_settings()
    local all = fw.db_get_prefix("s.")
    for k, default in pairs(settings) do
        local val = all["s." .. k]
        if val then
            if type(default) == "number" then settings[k] = tonumber(val) or default
            else settings[k] = val end
        end
    end
end

Module Registration

fw.register_module(name, static_dir)
-- Register a module's static file directory for web serving at /m/{name}/
-- static_dir: absolute path to directory containing index.html etc.
-- Use fw.modules_dir .. "/mymod/static" for the standard path

Other

fw.modules_dir    -- Path to the modules directory (string)
fw.rust           -- Direct FFI reference to libarz_core.so (advanced use only)
fw.push_event(name, json_args)  -- Broadcast custom event to WebSocket clients
fw.fire_event(event_name, ...)  -- Manually trigger event interceptors

UI Rendering (render function)

Modules can provide a render(ui, state) function for automatic UI generation. The same code renders both in the web browser (via JSON widgets) and in-game (via mimgui/ImGui).

How It Works

  1. Module defines M.render(ui, state)
  2. Framework detects render() and registers the module as __render__
  3. Web requests to /m/{module}/ serve a generic UI page (ui_page.html)
  4. The page periodically calls POST /api/{module}/__render with user interactions
  5. Framework calls M.render() with a UI builder that records widgets as JSON
  6. Browser renders the JSON widget tree as HTML

For imgui: the framework calls M.render() with an imgui wrapper that translates widget calls to real ImGui commands.

Widget API

Text & Layout

ui.text("Plain text")
ui.text_colored(r, g, b, a, "Colored text")  -- RGBA 0.0-1.0
ui.separator()     -- Horizontal line
ui.spacing()       -- Vertical gap
ui.sameline()      -- Next widget on same line

Interactive Widgets

-- Button: returns true on the frame it was clicked
if ui.button("Click me") then
    -- handle click
end

-- Text input: returns current value
local name = ui.input("Label", current_value)

-- Integer input
local count = ui.input_int("Count", current_value)

-- Checkbox: returns boolean
local enabled = ui.checkbox("Enable feature", current_bool)

-- Integer slider
local level = ui.slider_int("Level", current_value, min, max)

-- Float slider
local alpha = ui.slider_float("Alpha", current_value, 0, 1)

-- Dropdown
local idx = ui.combo("Choose", selected_index, {"Option A", "Option B", "Option C"})
-- selected_index is 0-based

Progress

ui.progress(0.75, "75%")  -- fraction 0.0-1.0

Containers

-- Collapsible section
if ui.collapsing("Details") then
    ui.text("Hidden content")
    ui.collapsing_end()
end

-- Tab bar with tabs
if ui.tab_bar("MyTabs") then
    if ui.tab_item("Tab 1") then
        ui.text("Tab 1 content")
        ui.tab_end()
    end
    if ui.tab_item("Tab 2") then
        ui.text("Tab 2 content")
        ui.tab_end()
    end
end

State Persistence

The state table passed to render() persists between renders (it's stored in module_states[name]). Use it for UI state that doesn't need database persistence:

function M.render(ui, state)
    state.counter = state.counter or 0
    ui.text("Count: " .. state.counter)
    if ui.button("Increment") then
        state.counter = state.counter + 1
    end
end

State is cleared on module reset (admin panel "Reset" button).

Interactions Model

The web UI sends interactions as a JSON object mapping widget IDs to values:

  • Buttons: {id: "click"}
  • Inputs: {id: "new text value"}
  • Checkboxes: {id: "true"/"false"}
  • Sliders/combos: {id: "number_as_string"}

Widget IDs are auto-generated (b1, i2, c3, etc.) in order of creation. This means widget order must be stable — conditional widgets that change position will break ID mapping.

Module Patterns

Pattern 1: Minimal Module (API only)

local M = {}

function M.init(fw)
    fw.on_api("ping", "check", function()
        return '{"ok":true,"time":' .. os.time() .. '}'
    end)
    fw.command("ping", function()
        fw.add_chat_message("Pong!", -1)
    end)
    fw.log("INFO", "PING", "Module loaded")
end

return M

Pattern 2: Auto-UI Module (render)

No static files needed. Framework generates the web page automatically.

local M = {}
local fw

function M.init(f)
    fw = f
    fw.log("INFO", "MYMOD", "Loaded")
end

function M.render(ui, state)
    state.name = state.name or ""
    state.name = ui.input("Your name", state.name)

    if ui.button("Greet") then
        fw.add_chat_message("Hello, " .. state.name .. "!", -1)
    end
end

return M

Pattern 3: Custom HTML Module

For rich UIs beyond what the widget builder offers.

local M = {}

function M.init(fw)
    local static_dir = fw.modules_dir .. "/mymod/static"
    fw.register_module("mymod", static_dir)

    fw.on_api("mymod", "getData", function()
        return fw.json_encode({items = {"a", "b", "c"}})
    end)
end

return M
<!-- modules/mymod/static/index.html -->
<!DOCTYPE html>
<html>
<head><title>My Module</title></head>
<body>
  <div id="app"></div>
  <script>
    // Connect to WebSocket for real-time events
    const ws = new WebSocket('ws://' + location.host + '/ws');
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      if (msg.event === 'onServerMessage') {
        // Handle chat message
      }
    };

    // Call module API
    async function loadData() {
      const res = await fetch('/api/mymod/getData', {method: 'POST'});
      const data = await res.json();
      document.getElementById('app').textContent = JSON.stringify(data);
    }
    loadData();
  </script>
</body>
</html>

Pattern 4: Event Interceptor with Dialog Automation

From the btc module — intercept server dialogs, parse content, respond automatically:

function M.init(fw)
    fw.on_event("onShowDialog", function(id, style, title, b1, b2, text)
        if id == 12345 then  -- your target dialog ID
            -- Parse dialog text
            for line in text:gmatch("[^\n]+") do
                local value = line:match("Balance: (%d+)")
                if value then
                    fw.log("INFO", "MYMOD", "Balance: " .. value)
                end
            end
            -- Auto-respond after small delay
            fw.timer_once(5, function()
                fw.respond_dialog(id, 1, 0, "")  -- click OK
            end)
            return false  -- hide dialog from player
        end
    end, 20)  -- priority 20
end

Key points:

  • Dialog IDs are hardcoded SA-MP server values — find them by logging all dialogs first
  • Always use fw.timer_once() for dialog responses to avoid blocking the game thread
  • Return false to hide the dialog from the player
  • Use fw.unlock_dialog() after a chain of automated dialogs to unblock the dialog system
  • Dialog text uses Win-1251 encoding — use hex escapes for Cyrillic matching (e.g. \xc2\xea\xeb\xfe\xf7 for "Включ")

Error Handling

Module errors are caught by the framework via pcall(). If a module crashes:

  • Error is logged via fw.log("ERROR", ...)
  • Toast notification appears in-game
  • Error is stored and visible in admin panel
  • Module continues to be loaded (other handlers still work)

Best practices:

  • Use pcall() for operations that might fail (JSON decode, game API calls)
  • Always validate API body data before using it
  • Log meaningful error messages with context
  • Implement M.unload() to clean up state (cancel timers, reset globals)

Hot Reloading

Modules can be reloaded at runtime via the admin panel or API:

  • Reload: calls unload(), removes all API handlers and event interceptors owned by module, then loads fresh
  • Reset: same as reload but also deletes all module DB data and clears state

On reload, all API handlers and event interceptors registered by the module are automatically removed before re-init. This means modules don't need to manually clean up handlers.

Things that are NOT auto-cleaned on reload:

  • Global variables (avoid using them)
  • SA-MP chat commands (registered once, cannot be unregistered)
  • Running lua_thread coroutines from fw.timer_once()