Rename ws_framework → rgl_framework, add docs, improve Rust logging

- Rename http_framework/ws_server.lua → rgl_framework.lua and update
  all internal references (log path, script name, init message)
- Replace debug eprintln!() in bridge.rs with logging::log() calls
  so bridge diagnostics go to log file and admin panel instead of stderr
- Add DEBUG logs: bridge requests/polls, WS connect/disconnect,
  API timeout warnings, DB batch JSON parse errors
- Move log file from /sdcard/Download/ to getWorkingDirectory()/logs/
  with auto-creation of logs/ directory in logging::init()
- Remove unused debug_pending_count() from bridge.rs
- Add project documentation: CLAUDE.md, docs/MODULE_GUIDE.md,
  docs/LIB_REFERENCE.md, docs/TASKS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main
Regela 1 day ago
parent 9e2fdd4c5b
commit e92bbdb62c

@ -0,0 +1,140 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ARZ Web Helper is a modular web-based control panel for Arizona RP (SA-MP/MoonLoader game mod). It has two layers:
- **Rust core** (`rust_core/`) — an Axum HTTP/WebSocket server compiled as a shared library (`libarz_core.so`). Exposes a C FFI (`rgl_*` functions) for the Lua side. Handles HTTP routing, WebSocket event broadcasting, SQLite persistence, and static file serving.
- **Lua framework** (`http_framework/rgl_framework.lua`) — the main MoonLoader script (~1058 lines). Loads the Rust `.so` via LuaJIT FFI, sets up the framework API, loads modules, and runs the game-thread event loop.
## Build Commands
### Rust core (cross-compile for Android aarch64)
```bash
cd rust_core
cargo build --release --target aarch64-linux-android
```
The output `.so` is deployed to the game device at `lib/libarz_core.so` relative to the MoonLoader working directory.
### Rust tests
```bash
cd rust_core
cargo test
```
Tests exist in `rust_core/src/db.rs` (7 tests covering batch DB operations).
### Lua modules
No build step. Lua files are loaded at runtime by MoonLoader from the `modules/` directory on the device.
## Architecture
### Rust ↔ Lua Bridge
The Rust server and Lua game thread communicate through a poll-based bridge (`rust_core/src/bridge.rs`):
- **Lua → Rust**: `rgl_push_event()` sends game events (SAMP network events) to connected WebSocket clients via broadcast channel (capacity 256).
- **Rust → Lua**: HTTP API requests become Lua code strings queued via `bridge::request_lua_exec()`. Lua's `main()` loop calls `rgl_poll()` each frame, executes the code, and returns results via `rgl_respond()`. API timeout is 2 seconds.
- All strings crossing the FFI boundary may be Win-1251 encoded (game uses CP1251). The Rust side converts to UTF-8 via `encoding_rs`.
### Module System
Modules live in `http_framework/modules/<name>/init.lua` (bundled with framework) or `more_modules/<name>/init.lua` (separate, gitignored). Both are loaded identically at runtime. Each module exports:
- `M.init(fw)` — required. Receives the framework API table (namespaced per module).
- `M.render(ui, state)` — optional. Provides a UI via the abstract widget builder (works both as web JSON and in-game imgui).
- `M.unload()` — optional cleanup.
The framework wraps each module's DB calls with a `<module_name>.` key prefix for isolation. Modules register API handlers via `fw.on_api(module, action, handler)` and game event interceptors via `fw.on_event(event_name, handler, priority)`.
See `docs/MODULE_GUIDE.md` for the full module authoring reference.
### Dual UI Rendering
Modules write UI using an abstract `ui` API (`ui.text()`, `ui.button()`, `ui.tab_bar()`, etc.). Two implementations exist:
- `create_ui_builder()` (rgl_framework.lua:129) — records widgets as JSON for the web frontend (`/m/<module>/` served via `ui_page.html`).
- `create_ui_imgui()` (rgl_framework.lua:267) — wraps `mimgui` (ImGui) for in-game overlay rendering.
Modules with `render()` but no `static/` directory get auto-registered as `__render__` modules, which are served the generic `ui_page.html` template.
### HTTP Routes (server.rs)
- `GET /` — dashboard listing all registered modules
- `GET /admin` — admin panel (module management, DB browser, logs)
- `GET /m/<module>/` — module UI (static files or auto-rendered via ui_page.html)
- `POST /api/<module>/<action>` — module API endpoint (routed to Lua via bridge)
- `GET /api/modules` — JSON list of loaded modules
- `GET /api/commands` — JSON list of registered commands
- `GET /ws` — WebSocket upgrade for real-time game events
### Framework API Quick Reference
Core `fw.*` methods available to modules:
| Method | Purpose |
|--------|---------|
| `fw.log(level, tag, ...)` | Log message (also broadcasts to WS as `__log`) |
| `fw.on_api(mod, action, fn)` | Register HTTP API handler |
| `fw.on_event(name, fn, priority)` | Hook SA-MP event (return false to block) |
| `fw.command(name, fn)` | Register `/name` chat command |
| `fw.db_get(key)` / `fw.db_set(key, val)` | Single key DB ops (auto-prefixed) |
| `fw.db_get_many(keys)` / `fw.db_set_many(kv)` | Batch DB ops |
| `fw.db_get_prefix(prefix)` | Get all keys with prefix |
| `fw.timer_once(ms, fn)` | One-shot timer, returns cancel function |
| `fw.chat(text)` | Send chat message/command |
| `fw.add_chat_message(text, color)` | Local-only chat message |
| `fw.respond_dialog(id, btn, list, input)` | Respond to server dialog |
| `fw.unlock_dialog()` | Send /mm + auto-close to unblock dialog |
| `fw.json_encode(t)` / `fw.json_decode(s)` | JSON serialization (cjson) |
| `fw.to_utf8(s)` / `fw.to_win1251(s)` | Encoding conversion |
| `fw.register_module(name, dir)` | Register static file serving |
| `fw.push_event(name, json)` | Broadcast custom event to WS |
### UI Widget Types
Available in `render(ui, state)`:
| Widget | Signature | Returns |
|--------|-----------|---------|
| `text` | `ui.text(str)` | — |
| `text_colored` | `ui.text_colored(r,g,b,a, str)` | — |
| `separator` | `ui.separator()` | — |
| `spacing` | `ui.spacing()` | — |
| `sameline` | `ui.sameline()` | — |
| `button` | `ui.button(label)` | `true` if clicked |
| `input` | `ui.input(label, value)` | new string value |
| `input_int` | `ui.input_int(label, value)` | new int value |
| `checkbox` | `ui.checkbox(label, bool)` | new bool |
| `slider_int` | `ui.slider_int(label, val, min, max)` | new int |
| `slider_float` | `ui.slider_float(label, val, min, max)` | new float |
| `combo` | `ui.combo(label, idx, items)` | new index (0-based) |
| `progress` | `ui.progress(fraction, label)` | — |
| `collapsing` | `ui.collapsing(label)` | true (call `collapsing_end()`) |
| `tab_bar` | `ui.tab_bar(id)` | true |
| `tab_item` | `ui.tab_item(label)` | true (call `tab_end()`) |
### Key Conventions
- Game chat commands: short names without prefix for modules (`/btc`, `/btcc`), `rgl_` prefix for core (`/rgl_admin`).
- Dialog IDs are hardcoded SA-MP server dialog identifiers (e.g., 7238, 25244). Handlers return `false` to hide dialog from player.
- Event interceptors sorted by priority (lower = earlier). Return `false` to block event propagation.
- DB operations support async batching via `rgl_db_submit()`/`rgl_db_poll()` for bulk reads/writes.
- Win-1251 Cyrillic matching in dialog text uses hex escapes (e.g. `\xc2\xea\xeb\xfe\xf7` for "Включ").
### Key Files
| File | Lines | Purpose |
|------|-------|---------|
| `http_framework/rgl_framework.lua` | ~1058 | Main framework: init, module loading, UI builders, event hooks, admin panel |
| `rust_core/src/lib.rs` | ~169 | FFI exports (all `rgl_*` functions) |
| `rust_core/src/server.rs` | ~306 | Axum HTTP/WS server, routing, static files |
| `rust_core/src/bridge.rs` | ~149 | Lua↔Rust request/response bridge + event broadcast |
| `rust_core/src/db.rs` | ~397 | SQLite key-value store with async batch API |
| `rust_core/src/events.rs` | ~77 | Win-1251↔UTF-8 conversion, SA-MP color parsing |
| `rust_core/src/logging.rs` | ~45 | File + WS broadcast logging |
| `rust_core/static/ui_page.html` | ~293 | Generic auto-UI renderer for modules with render() |
| `rust_core/static/index.html` | ~55 | Dashboard page |
### Known Issues
- `.gitignore` is minimal — missing `target/`, `*.so`, `*.log`, `rgl_data.db`
- Broadcast channel (capacity 256) silently drops events when full
- Fallback JSON encoder only handles flat `{"key":"value"}` — cjson must be present
### Additional Documentation
- `docs/MODULE_GUIDE.md` — complete module authoring reference with examples
- `docs/LIB_REFERENCE.md` — analysis of available standard libraries (../lib)
- `docs/TASKS.md` — prioritized improvement backlog

@ -0,0 +1,205 @@
# Standard Library Reference
Analysis of the shared Lua library at `../lib` (`/home/regela/Workspace/lua/lib/`). Contains ~163 files across 17 directories (6.4MB total).
## Currently Used by Project
| Module | Import | Purpose |
|--------|--------|---------|
| `ffi` | Built-in LuaJIT | FFI to load libarz_core.so, imgui bindings |
| `encoding` | `require("encoding")` | UTF-8 / CP1251 conversion |
| `cjson` | `require("cjson")` | JSON encode/decode |
| `mimgui` | `require("mimgui")` | ImGui binding for in-game UI |
| `samp.events` | `require("samp.events")` | SA-MP event hooking (RPC/packets) |
| `lua_thread` | Global | Coroutine-based threading (MonetLoader built-in) |
---
## High-Value Unused Libraries
### SAMemory — Game Memory Access
**Path:** `lib/SAMemory/`
**Size:** 50+ files covering all game entity types
Direct FFI-based access to GTA SA game structures: CPed, CVehicle, CEntity, CCamera, CWorld, etc. Read player position, health, vehicle state, weapon data directly from memory.
**Use cases for modules:**
- Read player coordinates for location-based automation
- Check vehicle state (speed, health, passengers)
- Access weapon/ammo data
- Read game time, weather, gravity
**Key files:**
- `SAMemory/init.lua` — main entry point
- `SAMemory/game/CPed.lua` — player/NPC structures
- `SAMemory/game/CVehicle.lua` — vehicle structures
- `SAMemory/game/CCamera.lua` — camera state
### requests.lua — HTTP Client
**Path:** `lib/requests.lua` (~270 lines)
Full HTTP/HTTPS client built on LuaSocket + LuaSec. Supports:
- GET, POST, PUT, DELETE methods
- Basic and Digest authentication
- Automatic HTTPS
- Cookie handling
- Custom headers
**Use cases:** External API calls, webhooks, data sync with remote servers.
### inicfg.lua — INI Config Files
**Path:** `lib/inicfg.lua` (~150 lines)
Read/write `.ini` config files with sections. Auto-creates defaults.
```lua
local inicfg = require("inicfg")
local cfg = inicfg.load({section = {key = "default"}}, "mymod.ini")
cfg.section.key = "new_value"
inicfg.save(cfg, "mymod.ini")
```
**Note:** The framework already has DB persistence, but inicfg is useful for human-editable config files.
### jsoncfg.lua — JSON Config Files
**Path:** `lib/jsoncfg.lua` (~150 lines)
Similar to inicfg but uses JSON format. Supports deep merge of defaults.
### game/ — Game Constants
**Path:** `lib/game/`
| File | Content |
|------|---------|
| `globals.lua` (~650 lines) | Script variables $0-$2000 |
| `models.lua` (~500 lines) | Object/vehicle model IDs |
| `weapons.lua` (~100 lines) | Weapon IDs and names |
| `keys.lua` (~50 lines) | Control key constants |
**Use cases:** Reference weapon names, vehicle model IDs, key bindings in modules.
### Crypto & Encoding
| Module | Path | Purpose |
|--------|------|---------|
| `md5.lua` | `lib/md5.lua` | MD5 hashing |
| `sha1/` | `lib/sha1/` | SHA-1 hashing + HMAC (multiple implementations) |
| `basexx.lua` | `lib/basexx.lua` | Base16, Base32, Base64 encoding/decoding |
**Use cases:** Data integrity checks, API signatures, obfuscation.
### copas.lua — Async I/O
**Path:** `lib/copas.lua` (~1500 lines) + submodules
Coroutine-based async TCP/IP dispatcher. Non-blocking HTTP, FTP, SMTP.
Submodules:
- `copas/http.lua` — async HTTP client
- `copas/smtp.lua` — async email sending
- `copas/ftp.lua` — async FTP
- `copas/timer.lua` — timer support
- `copas/lock.lua`, `queue.lua`, `semaphore.lua` — async primitives
**Note:** The framework already uses Rust's tokio for async. Copas is an alternative for pure-Lua async if needed.
### timerwheel.lua — Efficient Timers
**Path:** `lib/timerwheel.lua` (~300 lines)
O(1) timer wheel for managing many concurrent timers efficiently. Better than individual `lua_thread` coroutines when you have dozens of timers.
### monethook.lua — Function Hooking
**Path:** `lib/monethook.lua` (~80 lines)
Inline function hooking for MonetLoader >= 2.8.0. Intercept game function calls at the assembly level.
---
## Medium-Value Libraries
### Math
| Module | Path | Purpose |
|--------|------|---------|
| `vector3d.lua` | `lib/vector3d.lua` | 3D vector ops (normalize, dot, cross) |
| `matrix3x3.lua` | `lib/matrix3x3.lua` | 3x3 matrix ops |
### Networking Stack
| Module | Path | Purpose |
|--------|------|---------|
| `socket.lua` | `lib/socket.lua` | LuaSocket TCP wrapper |
| `ssl.lua` | `lib/ssl.lua` | LuaSec TLS/SSL |
| `socket/http.lua` | `lib/socket/http.lua` | Low-level HTTP client |
| `socket/url.lua` | `lib/socket/url.lua` | URL parsing (RFC 3986) |
| `socket/ftp.lua` | `lib/socket/ftp.lua` | FTP client |
| `socket/smtp.lua` | `lib/socket/smtp.lua` | SMTP client |
| `socket/headers.lua` | `lib/socket/headers.lua` | HTTP header utils |
Prefer `requests.lua` over raw socket modules for HTTP.
### Data Processing
| Module | Path | Purpose |
|--------|------|---------|
| `ltn12.lua` | `lib/ltn12.lua` | Source/sink/pump data streaming |
| `mime.lua` | `lib/mime.lua` | Base64, quoted-printable MIME encoding |
| `binaryheap.lua` | `lib/binaryheap.lua` | Priority queue (O(log n)) |
---
## Low-Value / Specialized
### Android-Specific
| Module | Path | Purpose |
|--------|------|---------|
| `android/arizona.lua` | MonetLoader Arizona RP API | Only on Android |
| `android/jnienv.lua` | JNI environment helpers | Java/Lua interop |
| `android/jni-raw.lua` | Raw JNI access | Low-level |
| `android/jar/arzapi.jar` | Arizona API JAR | Binary |
### Platform / Runtime
| Module | Path | Purpose |
|--------|------|---------|
| `MoonMonet/` | Material Design 3 color generation | Theming |
| `monetloader.lua` | MonetLoader constants | TAG types |
| `moonloader.lua` | MoonLoader compat layer | Logging codes |
| `sa_renderfix.lua` | Screen scaling fixes | Rendering |
| `bitex.lua` | Bit extraction utilities | Bit manipulation |
| `widgets.lua` | SAMP widget constants | Vehicle/player keys |
| `sampfuncs.lua` | RPC/packet ID constants | SAMP protocol |
### UI
| Module | Path | Purpose |
|--------|------|---------|
| `imgui_piemenu.lua` | Circular pie menu for imgui | Alternative menu |
| `webviews/` | WebView integration | Android WebView + JAR |
---
## SAMP Event System Deep Dive
**Path:** `lib/samp/`
The framework hooks all SAMP events via `samp.events`. Key files:
| File | Purpose |
|------|---------|
| `samp/events.lua` | Main entry point, event registration |
| `samp/events/core.lua` | Event dispatcher (~200 lines) |
| `samp/events/handlers.lua` | Specialized RPC handlers (~150 lines) |
| `samp/events/bitstream_io.lua` | Binary data serialization (~300 lines) |
| `samp/events/utils.lua` | Helper functions |
| `samp/events/extra_types.lua` | Extended data types |
| `samp/raknet.lua` | RakNet protocol constants |
| `samp/synchronization.lua` | Player/vehicle sync state |
| `sampfuncs.lua` | RPC and packet ID constants |
The framework's `setup_event_hooks()` iterates over all entries in `sampev.INTERFACE` (OUTCOMING_RPCS, INCOMING_RPCS, OUTCOMING_PACKETS, INCOMING_PACKETS) and creates hooks for each event name. This gives modules access to all ~200+ SAMP events without manual registration.
## Summary: Recommended for Integration
Priority order for adding to new modules:
1. **SAMemory** — most powerful, enables location/state-aware automation
2. **game/*** — essential constants for any game-interacting module
3. **requests.lua** — if modules need external HTTP APIs
4. **md5/sha1/basexx** — if data integrity or encoding needed
5. **inicfg/jsoncfg** — if human-editable config files needed
6. **timerwheel** — if module manages many concurrent timers

@ -0,0 +1,508 @@
# 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()`

@ -0,0 +1,105 @@
# Tasks & Improvements
Prioritized backlog of issues, improvements, and feature ideas.
---
## Critical
*No critical issues currently.*
---
## High
### Add proper .gitignore
Current `.gitignore` only has `more_modules`. Missing:
- `rust_core/target/`
- `*.so`
- `rgl_data.db`
- `*.log`
- `.DS_Store`
### Create build script
No Makefile or build automation exists. Need a script for:
- Cross-compile Rust for aarch64-linux-android
- Deploy .so to device via ADB
- Optional: rebuild on file change
### Improve Rust error handling
50+ `unwrap()` calls across Rust codebase. Key areas:
- `bridge.rs`: mutex locks can panic on poisoning — use `unwrap_or_else()` with recovery
- `db.rs`: 30+ unwrap/unwrap_or_default — silent failures on DB errors
- `server.rs`: 15+ unwraps in HTTP handlers
Should introduce proper error types (thiserror crate) or at minimum `unwrap_or_else()` with logging.
### Silent JSON parse failures in db.rs
`execute_batch()` returns `"[]"` on JSON parse error without logging. Should log the error for debugging.
---
## Medium
### Fix BTC module global state
`more_modules/btc/init.lua` uses `btc_visible = false` as a global variable (for mimgui OnFrame). Should be moved to a proper module state mechanism to avoid global namespace pollution.
### Implement WebSocket backpressure
`bridge.rs` broadcast channel has capacity 256. Events are silently dropped when full. Should either increase capacity, add warning logging, or implement backpressure.
### Improve fallback JSON encoder
`rgl_framework.lua` fallback JSON decoder only handles flat `{"key":"value"}` — fails on nested objects, arrays, numbers, booleans. Since cjson is always expected to be present, consider making it a hard requirement instead of silently degrading.
### Add integration tests
Only `db.rs` has tests (6 batch operation tests). Missing:
- Bridge request/response cycle
- Event system overflow/blocking
- HTTP handler edge cases
- Module loading/unloading
- Win-1251 encoding conversion
### Module loading error handling
`load_all_modules()` uses `io.popen('ls ...')` which can fail if directory is deleted between listing and loading. Use `pcall(io.open)` instead.
### Add auth/CORS for web API
Currently any network client can call all APIs. Consider at minimum:
- Bind to localhost only (or configurable)
- Basic auth token
- CORS headers for web clients
---
## Low
### Add rate limiting to API handlers
No protection against API spam. Could add simple per-endpoint rate limits.
### Add request/response logging middleware
No HTTP access logging in Rust. Add optional access log for debugging API calls.
### Optimize broadcast channel capacity
Current 256 capacity is arbitrary. Profile actual event rates and set appropriately.
### Add module versioning
No way to track which version of a module is loaded. Could add `M.version` field and display in admin panel.
### Add deployment documentation
No docs on how to deploy the .so to an Android device, set up the framework, or configure MoonLoader to load the script.
---
## Feature Ideas
### WebSocket command execution
Allow web clients to send commands via WebSocket instead of only HTTP POST. Would enable real-time bidirectional communication.
### Module dependency system
Currently modules load independently. A dependency system would let modules declare required modules and load in order.
### Persistent notification history
In-game notifications disappear after timeout. Could persist to DB and show in admin panel.
### SAMemory integration module
Create a utility module that exposes player position, health, vehicle state via the web UI using the SAMemory library from ../lib.
### Config file support
Use `inicfg.lua` or `jsoncfg.lua` from ../lib for human-editable configuration alongside the DB-based persistence.

@ -1,4 +1,4 @@
script_name("WS_FRAMEWORK")
script_name("rgl_framework")
script_author("Regela")
local ffi = require("ffi")
@ -77,8 +77,8 @@ function main()
pcall(function() rust.rgl_stop() end)
wait(300)
rust.rgl_log_init("/sdcard/Download/ws_framework.log")
log("INFO", "INIT", "WS Framework starting...")
rust.rgl_log_init(getWorkingDirectory() .. "/logs/rgl_framework.log")
log("INFO", "INIT", "RGL Framework starting...")
if rust.rgl_start(8081) ~= 0 then log("ERROR", "RUST", "rgl_start failed"); return end
log("INFO", "RUST", "Server on :8081")
@ -931,7 +931,7 @@ function setup_event_hooks()
end
function onScriptTerminate(s, quitGame)
if s.name == "WS_FRAMEWORK" and rust then
if s.name == "RGL_FRAMEWORK" and rust then
pcall(function() rust.rgl_stop() end)
end
end

@ -13,6 +13,8 @@ use std::sync::{
Condvar, Mutex, OnceLock,
};
use crate::logging;
/// A request from Rust to Lua (e.g., "execute sampSendChat('hello')")
#[derive(Debug, Clone, serde::Serialize)]
pub struct LuaRequest {
@ -88,9 +90,8 @@ pub fn push_event(event_name: &str, json_args: &str) -> Option<String> {
pub fn request_lua_exec(code: String) -> u32 {
let s = state();
let id = s.next_id.fetch_add(1, Ordering::Relaxed);
eprintln!("arz: request_lua_exec id={id} code={}", &code[..code.len().min(80)]);
logging::log("DEBUG", "BRIDGE", &format!("request id={id} code={}", &code[..code.len().min(80)]));
s.pending_requests.lock().unwrap().push(LuaRequest { id, code });
eprintln!("arz: pending_requests len={}", s.pending_requests.lock().unwrap().len());
id
}
@ -123,15 +124,10 @@ pub fn poll_requests() -> Option<String> {
return None;
}
let requests: Vec<LuaRequest> = pending.drain(..).collect();
eprintln!("arz: poll_requests returning {} requests", requests.len());
logging::log("DEBUG", "BRIDGE", &format!("poll: {} requests", requests.len()));
Some(serde_json::to_string(&requests).unwrap_or_else(|_| "[]".to_string()))
}
/// Debug: return count of pending requests.
pub fn debug_pending_count() -> usize {
state().pending_requests.lock().unwrap().len()
}
/// Non-blocking check for a result (used by async polling in api_handler).
pub fn try_get_result(id: u32) -> Option<String> {
state().results.lock().unwrap().remove(&id)

@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
use std::collections::HashMap;
use serde_json::Value;
use crate::server;
use crate::logging;
static DB: OnceLock<Connection> = OnceLock::new();
static BATCH_RESULTS: OnceLock<Mutex<HashMap<u32, String>>> = OnceLock::new();
@ -145,7 +146,10 @@ pub fn poll_result(id: u32) -> Option<String> {
async fn execute_batch_async(ops_json: &str) -> String {
let ops: Vec<Value> = match serde_json::from_str(ops_json) {
Ok(v) => v,
Err(_) => return "[]".to_string(),
Err(e) => {
logging::log("ERROR", "DB", &format!("batch JSON parse error: {e}"));
return "[]".to_string();
}
};
let Some(conn) = DB.get() else { return "[]".to_string() };

@ -15,6 +15,9 @@ fn log_file() -> &'static Mutex<Option<std::fs::File>> {
/// Initialize logging with a file path.
pub fn init(path: &str) {
if let Some(parent) = std::path::Path::new(path).parent() {
let _ = std::fs::create_dir_all(parent);
}
let file = OpenOptions::new()
.create(true)
.append(true)

@ -12,7 +12,8 @@ use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::{Mutex, OnceLock};
use std::collections::HashMap;
use crate::{bridge, logging};
use crate::bridge;
use crate::logging;
const BUILD_TS: &str = match option_env!("ARZ_BUILD_TS") {
Some(v) => v,
@ -198,10 +199,13 @@ async fn api_handler(
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r))
.unwrap(),
Err(_) => Response::builder()
Err(_) => {
logging::log("WARN", "API", &format!("timeout: {module}/{action}"));
Response::builder()
.status(StatusCode::GATEWAY_TIMEOUT)
.body(Body::from(r#"{"error":"lua timeout"}"#))
.unwrap(),
.unwrap()
}
}
}
@ -274,6 +278,8 @@ async fn handle_ws(mut socket: WebSocket) {
None => return,
};
logging::log("DEBUG", "WS", "client connected");
loop {
tokio::select! {
Ok(event) = event_rx.recv() => {
@ -302,4 +308,6 @@ async fn handle_ws(mut socket: WebSocket) {
}
}
}
logging::log("DEBUG", "WS", "client disconnected");
}

Loading…
Cancel
Save