# 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 ```lua 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 ```lua 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 ```lua 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) ```lua 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}`: ```lua 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: ```lua 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": ```lua 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: ```json {"type":"event", "event":"onServerMessage", "args":[color, text]} ``` ### Commands ```lua 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 ```lua 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 ```lua 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): ```lua 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 ```lua 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 ``` #### Batch Operations (Recommended for Multiple Keys) ```lua -- 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): ```lua 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 ```lua 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 ```lua 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 ```lua 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 ```lua -- 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 ```lua ui.progress(0.75, "75%") -- fraction 0.0-1.0 ``` #### Containers ```lua -- 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: ```lua 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) ```lua 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. ```lua 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. ```lua 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 ``` ```html