diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..83d1b7c --- /dev/null +++ b/CLAUDE.md @@ -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//init.lua` (bundled with framework) or `more_modules//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 `.` 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//` 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 UI (static files or auto-rendered via ui_page.html) +- `POST /api//` — 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 diff --git a/docs/LIB_REFERENCE.md b/docs/LIB_REFERENCE.md new file mode 100644 index 0000000..3faac2d --- /dev/null +++ b/docs/LIB_REFERENCE.md @@ -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 diff --git a/docs/MODULE_GUIDE.md b/docs/MODULE_GUIDE.md new file mode 100644 index 0000000..b3a959e --- /dev/null +++ b/docs/MODULE_GUIDE.md @@ -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 + + + +My Module + +
+ + + +``` + +### 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()` diff --git a/docs/TASKS.md b/docs/TASKS.md new file mode 100644 index 0000000..e3bc568 --- /dev/null +++ b/docs/TASKS.md @@ -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. diff --git a/http_framework/ws_server.lua b/http_framework/rgl_framework.lua similarity index 99% rename from http_framework/ws_server.lua rename to http_framework/rgl_framework.lua index 0059ddb..da68fcf 100644 --- a/http_framework/ws_server.lua +++ b/http_framework/rgl_framework.lua @@ -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 diff --git a/rust_core/src/bridge.rs b/rust_core/src/bridge.rs index 91d41cd..24e1cfb 100644 --- a/rust_core/src/bridge.rs +++ b/rust_core/src/bridge.rs @@ -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 { 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 { return None; } let requests: Vec = 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 { state().results.lock().unwrap().remove(&id) diff --git a/rust_core/src/db.rs b/rust_core/src/db.rs index d9ce8cd..1c9c06f 100644 --- a/rust_core/src/db.rs +++ b/rust_core/src/db.rs @@ -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 = OnceLock::new(); static BATCH_RESULTS: OnceLock>> = OnceLock::new(); @@ -145,7 +146,10 @@ pub fn poll_result(id: u32) -> Option { async fn execute_batch_async(ops_json: &str) -> String { let ops: Vec = 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() }; diff --git a/rust_core/src/logging.rs b/rust_core/src/logging.rs index af5d23b..8152571 100644 --- a/rust_core/src/logging.rs +++ b/rust_core/src/logging.rs @@ -15,6 +15,9 @@ fn log_file() -> &'static Mutex> { /// 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) diff --git a/rust_core/src/server.rs b/rust_core/src/server.rs index c558d60..1e141c5 100644 --- a/rust_core/src/server.rs +++ b/rust_core/src/server.rs @@ -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() - .status(StatusCode::GATEWAY_TIMEOUT) - .body(Body::from(r#"{"error":"lua timeout"}"#)) - .unwrap(), + Err(_) => { + logging::log("WARN", "API", &format!("timeout: {module}/{action}")); + Response::builder() + .status(StatusCode::GATEWAY_TIMEOUT) + .body(Body::from(r#"{"error":"lua timeout"}"#)) + .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"); }