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.

509 lines
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
```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
<!-- 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:
```lua
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()`