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.

141 lines
7.7 KiB

# 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