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.

1142 lines
41 KiB

script_name("rgl_framework")
script_author("Regela")
local ffi = require("ffi")
ffi.cdef[[
int rgl_start(int port);
void rgl_stop();
int rgl_hello();
void rgl_log_init(const char* path);
void rgl_log(const char* level, const char* tag, const char* msg);
void rgl_db_init(const char* path);
char* rgl_db_get(const char* key);
int rgl_db_set(const char* key, const char* value);
int rgl_db_delete(const char* key);
void rgl_register_module(const char* name, const char* static_dir);
void rgl_register_command(const char* name, const char* owner);
char* rgl_get_commands();
char* rgl_push_event(const char* event_name, const char* json_args);
char* rgl_poll();
void rgl_respond(int request_id, const char* result_json);
char* rgl_db_list(const char* prefix);
int rgl_db_delete_prefix(const char* prefix);
unsigned int rgl_db_submit(const char* ops_json);
char* rgl_db_poll(unsigned int id);
void rgl_free(char* s);
void rgl_auth_init(const char* secret_dir);
void rgl_auth_set(const char* login, const char* password);
void rgl_auth_clear();
int rgl_auth_enabled();
]]
local rust = nil
local sampev = nil
local api_handlers = {}
local event_interceptors = {}
local loaded_modules = {}
local module_states = {} -- persistent state per module for render()
local command_handlers = {}
local framework = {}
local _current_module = nil
local admin_visible = false
local recent_logs = {}
local MAX_LOGS = 100
local notifications = {} -- {text, level, time, start}
local module_errors = {} -- ["module_name"] = "last error"
local module_windows = {} -- {name = {visible, title, width, height}}
local function log(level, tag, ...)
if not rust then return print("[" .. level .. "][" .. tag .. "]", ...) end
local args = {...}
local parts = {}
for i, v in ipairs(args) do parts[i] = tostring(v) end
local msg = table.concat(parts, " ")
rust.rgl_log(level, tag, msg)
-- Keep recent logs for admin UI
recent_logs[#recent_logs + 1] = {level = level, tag = tag, msg = msg}
if #recent_logs > MAX_LOGS then table.remove(recent_logs, 1) end
end
local function notify(text, level, duration)
notifications[#notifications + 1] = {
text = text, level = level or "ERROR",
time = duration or 5, start = os.clock()
}
end
local function module_error(mod_name, context, err)
local msg = mod_name .. " [" .. context .. "]: " .. tostring(err)
log("ERROR", "MOD", msg)
notify(msg, "ERROR", 8)
module_errors[mod_name] = msg
end
function main()
while not isSampAvailable() do wait(100) end
local so_path = getWorkingDirectory() .. "/lib/libarz_core.so"
local ok, lib = pcall(ffi.load, so_path)
if not ok then print("RGL: Failed to load .so: " .. tostring(lib)); return end
rust = lib
pcall(function() rust.rgl_stop() end)
wait(300)
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")
rust.rgl_db_init(getWorkingDirectory() .. "/rgl_data.db")
log("INFO", "INIT", "DB initialized")
rust.rgl_auth_init(getWorkingDirectory())
log("INFO", "INIT", "Auth initialized")
if setup_framework() == false then return end
log("INFO", "INIT", "Framework ready")
register_admin()
log("INFO", "INIT", "Admin registered")
load_all_modules()
log("INFO", "INIT", "Modules loaded")
sampev = require("samp.events")
setup_event_hooks()
-- Global API handler
_G.__arz_handle_api = function(module, action, body)
local key = module .. "." .. action
local entry = api_handlers[key]
if entry then
local hok, result = pcall(entry.fn, body)
if hok then return result or '{"ok":true}' end
module_error(entry.owner, "api:" .. action, result)
local safe_err = tostring(result):gsub('"', '\\"')
return '{"error":"' .. safe_err .. '"}'
end
return '{"error":"not found: ' .. key .. '"}'
end
log("INFO", "INIT", "Ready")
while true do
local ptr = rust.rgl_poll()
if ptr ~= nil then
local json_str = ffi.string(ptr)
rust.rgl_free(ptr)
process_requests(json_str)
end
wait(0)
end
end
----------------------------------------------------------------
-- UI Builder (records widgets → JSON for web)
----------------------------------------------------------------
function create_ui_builder(interactions)
interactions = interactions or {}
local widgets = {}
local id_n = 0
local collapse_stack = {}
local ui = {}
local function next_id(prefix)
id_n = id_n + 1
return prefix .. id_n
end
function ui.text(str) widgets[#widgets+1] = {t="text", text=tostring(str)} end
function ui.text_colored(r,g,b,a, str) widgets[#widgets+1] = {t="text_colored", r=r,g=g,b=b,a=a,text=tostring(str)} end
function ui.separator() widgets[#widgets+1] = {t="separator"} end
function ui.spacing() widgets[#widgets+1] = {t="spacing"} end
function ui.sameline() widgets[#widgets+1] = {t="sameline"} end
function ui.button(label)
local id = next_id("b")
widgets[#widgets+1] = {t="button", id=id, label=label}
return interactions[id] == "click"
end
function ui.input(label, value)
local id = next_id("i")
local new_val = interactions[id]
if new_val ~= nil then value = new_val end
widgets[#widgets+1] = {t="input", id=id, label=label, value=value or ""}
return value or ""
end
function ui.checkbox(label, value)
local id = next_id("c")
local iv = interactions[id]
if iv ~= nil then value = (iv == "true") end
widgets[#widgets+1] = {t="checkbox", id=id, label=label, value=value}
return value
end
function ui.slider_int(label, value, min, max)
local id = next_id("si")
local iv = interactions[id]
if iv ~= nil then value = tonumber(iv) or value end
widgets[#widgets+1] = {t="slider_int", id=id, label=label, value=value, min=min, max=max}
return value
end
function ui.input_int(label, value)
local id = next_id("ii")
local iv = interactions[id]
if iv ~= nil then value = tonumber(iv) or value end
widgets[#widgets+1] = {t="input_int", id=id, label=label, value=value}
return value
end
function ui.slider_float(label, value, min, max)
local id = next_id("sf")
local iv = interactions[id]
if iv ~= nil then value = tonumber(iv) or value end
widgets[#widgets+1] = {t="slider_float", id=id, label=label, value=value, min=min, max=max}
return value
end
function ui.combo(label, selected, items)
local id = next_id("co")
local iv = interactions[id]
if iv ~= nil then selected = tonumber(iv) or selected end
widgets[#widgets+1] = {t="combo", id=id, label=label, value=selected, items=items}
return selected
end
function ui.progress(fraction, label)
widgets[#widgets+1] = {t="progress", fraction=fraction, label=label or ""}
end
function ui.collapsing(label)
local children = {}
widgets[#widgets+1] = {t="collapsing", label=label, open=true, children=children}
collapse_stack[#collapse_stack+1] = widgets
widgets = children
return true
end
function ui.collapsing_end()
if #collapse_stack > 0 then
widgets = collapse_stack[#collapse_stack]
collapse_stack[#collapse_stack] = nil
end
end
function ui.tab_bar(id)
widgets[#widgets+1] = {t="tab_bar", id=id, tabs={}}
collapse_stack[#collapse_stack+1] = widgets
return true
end
function ui.tab_item(label)
-- Find parent tab_bar
local parent = collapse_stack[#collapse_stack]
if parent then
local tb = parent[#parent]
if tb and tb.t == "tab_bar" then
local tab = {label=label, children={}}
tb.tabs[#tb.tabs+1] = tab
collapse_stack[#collapse_stack+1] = widgets
widgets = tab.children
return true
end
end
return false
end
function ui.tab_end()
if #collapse_stack > 0 then
widgets = collapse_stack[#collapse_stack]
collapse_stack[#collapse_stack] = nil
end
end
function ui._get_widgets() return widgets end
return ui
end
----------------------------------------------------------------
-- mimgui wrapper (thin layer over imgui for in-game rendering)
----------------------------------------------------------------
local imgui_loaded, imgui
do
local ok, result = pcall(require, "mimgui")
if ok then
imgui_loaded = true
imgui = result
else
imgui_loaded = false
end
end
function create_ui_imgui()
if not imgui then return nil end
local ui = {}
local input_bufs = {}
function ui.text(str) imgui.TextUnformatted(tostring(str)) end
function ui.text_colored(r,g,b,a, str) imgui.TextColored(imgui.ImVec4(r,g,b,a), tostring(str)) end
function ui.separator() imgui.Separator() end
function ui.spacing() imgui.Spacing() end
function ui.sameline() imgui.SameLine() end
function ui.button(label) return imgui.Button(label) end
function ui.input(label, value)
if not input_bufs[label] then
input_bufs[label] = ffi.new('char[256]')
end
local buf = input_bufs[label]
if ffi.string(buf) ~= (value or "") then
ffi.copy(buf, value or "")
end
imgui.InputText(label, buf, 256)
return ffi.string(buf)
end
function ui.checkbox(label, value)
local b = ffi.new('bool[1]', value or false)
imgui.Checkbox(label, b)
return b[0]
end
function ui.slider_int(label, value, min, max)
local v = ffi.new('int[1]', value or 0)
imgui.SliderInt(label, v, min, max)
return v[0]
end
function ui.input_int(label, value)
local v = ffi.new('int[1]', value or 0)
imgui.InputInt(label, v)
return v[0]
end
function ui.slider_float(label, value, min, max)
local v = ffi.new('float[1]', value or 0)
imgui.SliderFloat(label, v, min, max)
return v[0]
end
function ui.combo(label, selected, items)
local v = ffi.new('int[1]', selected or 0)
local str = table.concat(items, "\0") .. "\0"
imgui.Combo(label, v, str)
return v[0]
end
function ui.progress(fraction, label)
imgui.ProgressBar(fraction or 0, imgui.ImVec2(-1, 0), label or "")
end
function ui.collapsing(label) return imgui.CollapsingHeader(label) end
function ui.collapsing_end() end
function ui.tab_bar(id) return imgui.BeginTabBar(id) end
function ui.tab_item(label) return imgui.BeginTabItem(label) end
function ui.tab_end()
imgui.EndTabItem()
end
return ui
end
----------------------------------------------------------------
-- Framework API
----------------------------------------------------------------
function setup_framework()
local enc_ok, encoding = pcall(require, "encoding")
if enc_ok then
encoding.default = "CP1251"
local u8 = encoding.UTF8
framework.to_utf8 = function(s) return u8:encode(s) end
framework.to_win1251 = function(s) return u8:decode(s) end
else
framework.to_utf8 = function(s) return s end
framework.to_win1251 = function(s) return s end
end
local cjson_ok, cjson = pcall(require, "cjson")
if not cjson_ok then
log("ERROR", "INIT", "cjson not found — framework cannot start")
return false
end
framework.json_encode = cjson.encode
framework.json_decode = cjson.decode
framework.rust = rust
framework.log = log
framework.modules_dir = getWorkingDirectory() .. "/modules"
framework.register_module = function(name, dir) rust.rgl_register_module(name, dir) end
framework.on_api = function(mod, action, handler)
api_handlers[mod.."."..action] = {fn = handler, owner = _current_module or mod}
end
framework.push_event = function(name, json) rust.rgl_push_event(name, json) end
framework.fire_event = function(event_name, ...)
local interceptors = event_interceptors[event_name]
if interceptors then
for _, entry in ipairs(interceptors) do
local iok, iresult = pcall(entry.fn, ...)
if not iok then
module_error(entry.owner, "event:" .. event_name, iresult)
elseif iresult == false then
return false
end
end
end
return true
end
framework.on_event = function(event_name, handler, priority)
priority = priority or 50
if not event_interceptors[event_name] then event_interceptors[event_name] = {} end
local list = event_interceptors[event_name]
list[#list+1] = {fn = handler, priority = priority, owner = _current_module or "unknown"}
table.sort(list, function(a, b) return a.priority < b.priority end)
end
framework.db_get = function(key)
local full = (_current_module or "global") .. "." .. key
local ptr = rust.rgl_db_get(full)
if ptr == nil then return nil end
local val = ffi.string(ptr); rust.rgl_free(ptr); return val
end
framework.db_set = function(key, value)
return rust.rgl_db_set((_current_module or "global") .. "." .. key, tostring(value)) == 0
end
framework.db_delete = function(key)
return rust.rgl_db_delete((_current_module or "global") .. "." .. key) == 0
end
-- Raw db access (for admin, no prefix)
framework.db_raw_list = function(prefix)
local ptr = rust.rgl_db_list(prefix)
if ptr == nil then return {} end
local json = ffi.string(ptr); rust.rgl_free(ptr)
local ok, result = pcall(framework.json_decode, json)
return ok and result or {}
end
framework.db_raw_set = function(key, value)
return rust.rgl_db_set(key, tostring(value)) == 0
end
framework.db_raw_delete = function(key)
return rust.rgl_db_delete(key) == 0
end
framework.db_raw_delete_prefix = function(prefix)
return rust.rgl_db_delete_prefix(prefix)
end
-- Async batch DB API
framework.db_batch = function(operations)
local json = framework.json_encode(operations)
local id = rust.rgl_db_submit(json)
-- Check if we're in a coroutine (wait() only works in coroutines)
local in_coroutine = coroutine.running() ~= nil
while true do
if in_coroutine then wait(1) end
local ptr = rust.rgl_db_poll(id)
if ptr ~= nil then
local r = ffi.string(ptr); rust.rgl_free(ptr)
local ok, decoded = pcall(framework.json_decode, r)
return ok and decoded or {}
end
end
end
-- Delayed one-shot (returns cancel function)
framework.timer_once = function(ms, callback)
local cancelled = false
lua_thread.create(function()
wait(ms)
if not cancelled then
local ok, err = pcall(callback)
if not ok then log("ERROR", "TIMER", tostring(err)) end
end
end)
return function() cancelled = true end
end
-- Dialog response helper
framework.respond_dialog = function(id, button, listboxId, input)
sampSendDialogResponse(id, button, listboxId, input or "")
end
-- Send chat/command helper
framework.chat = function(text)
sampProcessChatInput(text)
end
-- Add chat message (local, only visible to player)
framework.add_chat_message = function(text, color)
sampAddChatMessage(text, color or -1)
end
-- Dialog unlock: send /mm and auto-close the menu dialog to unblock inventory
local _waiting_unlock = false
framework.unlock_dialog = function()
_waiting_unlock = true
sampSendChat("/mm")
end
-- Internal interceptor for /mm dialog (id=722), priority 1 (before all modules)
if not event_interceptors["onShowDialog"] then event_interceptors["onShowDialog"] = {} end
table.insert(event_interceptors["onShowDialog"], 1, {
fn = function(id)
if _waiting_unlock and id == 722 then
_waiting_unlock = false
lua_thread.create(function()
wait(1)
sampSendDialogResponse(722, 0, 0, "")
end)
return false
end
end,
priority = 1,
owner = "__core",
})
framework.command = function(name, handler)
local owner = _current_module or "__core"
command_handlers[name] = {fn = handler, owner = owner}
sampRegisterChatCommand(name, function(args)
local ok2, err = pcall(handler, args)
if not ok2 then module_error(owner, "cmd:/" .. name, err) end
end)
rust.rgl_register_command(name, owner)
log("INFO", "CMD", "Registered /" .. name .. " (" .. owner .. ")")
end
-- Module imgui window registry
framework.register_window = function(opts)
opts = opts or {}
local mod_name = _current_module or "__unknown"
module_windows[mod_name] = {
visible = false,
title = opts.title or mod_name,
width = opts.width or 450,
height = opts.height or 400,
}
end
framework.toggle_window = function(name)
local w = module_windows[name or _current_module]
if w then w.visible = not w.visible end
end
framework.show_window = function(name)
local w = module_windows[name or _current_module]
if w then w.visible = true end
end
framework.hide_window = function(name)
local w = module_windows[name or _current_module]
if w then w.visible = false end
end
end
----------------------------------------------------------------
-- Module lifecycle
----------------------------------------------------------------
function load_single_module(name)
if loaded_modules[name] then unload_single_module(name) end
local path = framework.modules_dir .. "/" .. name .. "/init.lua"
local f = io.open(path); if not f then return false, "not found" end; f:close()
_current_module = name
local chunk, cerr = loadfile(path)
if not chunk then _current_module = nil; return false, "load: " .. tostring(cerr) end
-- Sandbox: module gets its own environment, reads fall through to _G
local sandbox = setmetatable({}, {__index = _G})
setfenv(chunk, sandbox)
local ok, mod = pcall(chunk)
if not ok then _current_module = nil; return false, "exec: " .. tostring(mod) end
if not mod or not mod.init then _current_module = nil; return false, "no init()" end
-- Create per-module framework wrapper with bound db prefix
local mod_fw = setmetatable({}, {__index = framework})
mod_fw.db_get = function(key)
local ptr = rust.rgl_db_get(name .. "." .. key)
if ptr == nil then return nil end
local val = ffi.string(ptr); rust.rgl_free(ptr); return val
end
mod_fw.db_set = function(key, value)
return rust.rgl_db_set(name .. "." .. key, tostring(value)) == 0
end
mod_fw.db_delete = function(key)
return rust.rgl_db_delete(name .. "." .. key) == 0
end
mod_fw.db_get_many = function(keys)
local ops = {}
for i, k in ipairs(keys) do ops[i] = {op = "get", key = name .. "." .. k} end
local results = framework.db_batch(ops)
local out = {}
for i, k in ipairs(keys) do
local r = results[i]
if r and type(r.v) == "string" then out[k] = r.v end
end
return out
end
mod_fw.db_set_many = function(kv)
local ops = {}
for k, v in pairs(kv) do
ops[#ops + 1] = {op = "set", key = name .. "." .. k, value = tostring(v)}
end
framework.db_batch(ops)
end
mod_fw.db_get_prefix = function(prefix)
local full = name .. "." .. prefix
local results = framework.db_batch({{op = "get_prefix", prefix = full}})
local items = results[1] and results[1].items or {}
local out = {}
local strip = #name + 2
for _, item in ipairs(items) do
out[item.key:sub(strip)] = item.value
end
return out
end
local iok, ierr = pcall(mod.init, mod_fw)
_current_module = nil
if not iok then return false, "init: " .. tostring(ierr) end
loaded_modules[name] = {mod = mod, status = "loaded", sandbox = sandbox}
module_states[name] = module_states[name] or {}
register_module_render(name)
log("INFO", "MODS", "Loaded: " .. name)
return true
end
function unload_single_module(name)
local entry = loaded_modules[name]
if not entry then return false, "not loaded" end
if entry.mod and entry.mod.unload then pcall(entry.mod.unload, framework) end
local rm = {}
for key, h in pairs(api_handlers) do if h.owner == name then rm[#rm+1] = key end end
for _, key in ipairs(rm) do api_handlers[key] = nil end
for ev, list in pairs(event_interceptors) do
local new = {}
for _, e in ipairs(list) do if e.owner ~= name then new[#new+1] = e end end
event_interceptors[ev] = new
end
rust.rgl_register_module(name, "")
module_windows[name] = nil
loaded_modules[name] = nil
log("INFO", "MODS", "Unloaded: " .. name)
return true
end
-- List module directories safely (wraps io.popen in pcall)
function list_module_dirs()
local dir = framework.modules_dir
local result = {}
local ok, ls = pcall(io.popen, 'ls "' .. dir .. '" 2>/dev/null')
if not ok or not ls then
log("WARN", "MODS", "Failed to list modules dir: " .. tostring(ls))
return result
end
for name in ls:lines() do
local path = dir .. "/" .. name .. "/init.lua"
local f = io.open(path)
if f then
f:close()
result[#result + 1] = name
end
end
ls:close()
return result
end
function load_all_modules()
for _, name in ipairs(list_module_dirs()) do
local ok, err = load_single_module(name)
if not ok then log("ERROR", "MODS", name .. ": " .. err) end
end
end
----------------------------------------------------------------
-- Built-in Admin (render-based, works in web + imgui)
----------------------------------------------------------------
function admin_render(ui, state)
if ui.tab_bar("AdminTabs") then
if ui.tab_item("Modules") then
for name, entry in pairs(loaded_modules) do
if module_errors[name] then
ui.text_colored(1, 0.3, 0.3, 1, name .. " (error)")
else
ui.text(name)
end
ui.sameline()
if ui.button("Reload##" .. name) then
module_errors[name] = nil
unload_single_module(name)
load_single_module(name)
end
ui.sameline()
if ui.button("Unload##" .. name) then
module_errors[name] = nil
unload_single_module(name)
end
ui.sameline()
if ui.button("Reset##" .. name) then
module_errors[name] = nil
unload_single_module(name)
framework.db_raw_delete_prefix(name .. ".")
module_states[name] = {}
load_single_module(name)
log("INFO", "ADMIN", "Reset module: " .. name)
end
if module_errors[name] then
ui.text_colored(0.7, 0.3, 0.3, 1, " " .. module_errors[name])
end
end
ui.separator()
for _, name in ipairs(list_module_dirs()) do
if not loaded_modules[name] then
ui.text_colored(0.5, 0.5, 0.5, 1, name)
ui.sameline()
if ui.button("Load##" .. name) then load_single_module(name) end
end
end
ui.tab_end()
end
if ui.tab_item("Commands") then
for name, entry in pairs(command_handlers) do
ui.text("/" .. name)
ui.sameline()
ui.text_colored(0.5, 0.5, 0.5, 1, entry.owner)
end
ui.tab_end()
end
if ui.tab_item("Data") then
-- Collect module names that have db data
local all_mods = {}
for name in pairs(loaded_modules) do all_mods[#all_mods+1] = name end
table.sort(all_mods)
if #all_mods == 0 then
ui.text_colored(0.5, 0.5, 0.5, 1, "No modules loaded")
else
for _, mod_name in ipairs(all_mods) do
local keys = framework.db_raw_list(mod_name .. ".")
if type(keys) == "table" and #keys > 0 then
if ui.collapsing(mod_name .. " (" .. #keys .. " keys)") then
for _, kv in ipairs(keys) do
local short_key = kv.key:sub(#mod_name + 2) -- strip "module."
ui.text(short_key .. " = " .. tostring(kv.value))
ui.sameline()
if ui.button("Del##" .. kv.key) then
framework.db_raw_delete(kv.key)
end
end
ui.separator()
if ui.button("Delete all##" .. mod_name) then
framework.db_raw_delete_prefix(mod_name .. ".")
log("INFO", "ADMIN", "Deleted all data for " .. mod_name)
end
ui.collapsing_end()
end
end
end
end
ui.tab_end()
end
if ui.tab_item("Logs") then
local start = math.max(1, #recent_logs - 80)
for i = start, #recent_logs do
local l = recent_logs[i]
if l.level == "ERROR" then
ui.text_colored(1, 0.3, 0.3, 1, "[" .. l.tag .. "] " .. l.msg)
elseif l.level == "WARN" then
ui.text_colored(1, 0.8, 0.2, 1, "[" .. l.tag .. "] " .. l.msg)
else
ui.text("[" .. l.tag .. "] " .. l.msg)
end
end
ui.tab_end()
end
if ui.tab_item("Auth") then
if rust.rgl_auth_enabled() == 1 then
ui.text_colored(0.3, 0.8, 0.3, 1, "Auth: ON")
ui.spacing()
if ui.button("Disable Auth") then
rust.rgl_auth_clear()
log("INFO", "AUTH", "Credentials cleared from admin UI")
end
else
ui.text_colored(0.5, 0.5, 0.5, 1, "Auth: OFF")
end
ui.separator()
ui.text("Set credentials:")
state.auth_login = ui.input("Login", state.auth_login or "")
state.auth_pass = ui.input("Password", state.auth_pass or "")
if ui.button("Save Credentials") then
if #(state.auth_login or "") > 0 and #(state.auth_pass or "") > 0 then
rust.rgl_auth_set(state.auth_login, state.auth_pass)
log("INFO", "AUTH", "Credentials set from admin UI")
state.auth_login = ""
state.auth_pass = ""
end
end
ui.tab_end()
end
end
end
function register_admin()
-- __render for web UI
api_handlers["admin.__render"] = {fn = function(body)
local interactions = {}
if body and #body > 2 then
pcall(function() interactions = framework.json_decode(body) end)
end
local ui = create_ui_builder(interactions)
admin_render(ui, {})
return framework.json_encode({widgets = ui._get_widgets()})
end, owner = "__admin"}
-- Module management API (for backward compat)
api_handlers["admin.modules"] = {fn = function()
local list = {}
for name, entry in pairs(loaded_modules) do list[#list+1] = {name=name, status=entry.status} end
return framework.json_encode(list)
end, owner = "__admin"}
api_handlers["admin.load"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.name then return '{"ok":false}' end
local ok,err = load_single_module(data.name)
return ok and '{"ok":true}' or framework.json_encode({ok=false,error=err})
end, owner = "__admin"}
api_handlers["admin.unload"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.name then return '{"ok":false}' end
local ok,err = unload_single_module(data.name)
return ok and '{"ok":true}' or framework.json_encode({ok=false,error=err})
end, owner = "__admin"}
api_handlers["admin.reload"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.name then return '{"ok":false}' end
unload_single_module(data.name); local ok,err = load_single_module(data.name)
return ok and '{"ok":true}' or framework.json_encode({ok=false,error=err})
end, owner = "__admin"}
api_handlers["admin.db_keys"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.module then return '[]' end
local ptr = rust.rgl_db_list(data.module .. ".")
if ptr == nil then return '[]' end
local json = ffi.string(ptr); rust.rgl_free(ptr)
return json
end, owner = "__admin"}
api_handlers["admin.db_edit"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.key or not data.value then return '{"ok":false}' end
rust.rgl_db_set(data.key, data.value)
return '{"ok":true}'
end, owner = "__admin"}
api_handlers["admin.db_delete_key"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.key then return '{"ok":false}' end
rust.rgl_db_delete(data.key)
return '{"ok":true}'
end, owner = "__admin"}
api_handlers["admin.reset_module"] = {fn = function(body)
local ok2,data = pcall(framework.json_decode, body)
if not ok2 or not data or not data.name then return '{"ok":false}' end
unload_single_module(data.name)
rust.rgl_db_delete_prefix(data.name .. ".")
module_states[data.name] = {}
local ok,err = load_single_module(data.name)
log("INFO", "ADMIN", "Reset: " .. data.name)
return ok and '{"ok":true}' or framework.json_encode({ok=false,error=err})
end, owner = "__admin"}
-- Register /rgl_admin command
framework.command("rgl_admin", function()
admin_visible = not admin_visible
log("INFO", "ADMIN", "Toggled: " .. tostring(admin_visible))
end)
end
----------------------------------------------------------------
-- Module __render API (for web auto-UI)
----------------------------------------------------------------
function setup_module_render_api()
-- Called when a new module with render() is loaded
-- api_handlers[module..".__render"] is set automatically
end
-- Called from load_single_module after init
-- Check if module has render(), if so register __render API
function register_module_render(name)
local entry = loaded_modules[name]
if not entry or not entry.mod or not entry.mod.render then return end
-- Register in Rust so it shows on dashboard and serves ui_page.html
rust.rgl_register_module(name, "__render__")
api_handlers[name .. ".__render"] = {fn = function(body)
local interactions = {}
if body and #body > 2 then
pcall(function() interactions = framework.json_decode(body) end)
end
local ui = create_ui_builder(interactions)
local state = module_states[name] or {}
module_states[name] = state
_current_module = name
local ok, err = pcall(entry.mod.render, ui, state)
_current_module = nil
if not ok then return framework.json_encode({error = tostring(err)}) end
return framework.json_encode({widgets = ui._get_widgets()})
end, owner = name}
end
----------------------------------------------------------------
-- Request processing
----------------------------------------------------------------
function process_requests(json_str)
local ok, requests = pcall(framework.json_decode, json_str)
if not ok or type(requests) ~= "table" then return end
for _, req in ipairs(requests) do
local code = req.code
if not code then goto continue end
local exec_ok, result = pcall(function()
local fn = loadstring(code)
if not fn then return nil end
return fn()
end)
local response = '"ok"'
if exec_ok and type(result) == "string" then response = result
elseif not exec_ok then log("ERROR", "EXEC", tostring(result)) end
rust.rgl_respond(req.id, response)
::continue::
end
end
----------------------------------------------------------------
-- Event system
----------------------------------------------------------------
function setup_event_hooks()
local function ja(...)
local args = {...}
local p = {}
for _, v in ipairs(args) do
local t = type(v)
if t == "string" then p[#p+1] = '"' .. v:gsub('\\','\\\\'):gsub('"','\\"'):gsub('%c','') .. '"'
elseif t == "number" then p[#p+1] = tostring(v)
elseif t == "boolean" then p[#p+1] = v and "true" or "false"
elseif t == "table" then
local tp = {}
for k2,v2 in pairs(v) do
local vs
if type(v2) == "string" then vs = '"' .. v2:gsub('\\','\\\\'):gsub('"','\\"'):gsub('%c','') .. '"'
elseif type(v2) == "number" or type(v2) == "boolean" then vs = tostring(v2)
else vs = '"' .. tostring(v2) .. '"' end
tp[#tp+1] = '"' .. tostring(k2) .. '":' .. vs
end
p[#p+1] = "{" .. table.concat(tp,",") .. "}"
else p[#p+1] = "null" end
end
return "[" .. table.concat(p,",") .. "]"
end
local function make_hook(event_name)
return function(...)
local interceptors = event_interceptors[event_name]
if interceptors and #interceptors > 0 then
for _, entry in ipairs(interceptors) do
local iok, iresult = pcall(entry.fn, ...)
if not iok then
module_error(entry.owner, "event:" .. event_name, iresult)
elseif iresult == false then
return false
end
end
end
rust.rgl_push_event(event_name, ja(...))
end
end
local interface = sampev.INTERFACE
local count = 0
local function reg(tbl)
for _, entry in pairs(tbl) do
if type(entry) == "table" and entry[1] then
local names = entry[1]
if type(names) == "string" then names = {names} end
for _, name in ipairs(names) do
if type(name) == "string" and not sampev[name] then
sampev[name] = make_hook(name)
count = count + 1
end
end
end
end
end
reg(interface.OUTCOMING_RPCS or {})
reg(interface.INCOMING_RPCS or {})
reg(interface.OUTCOMING_PACKETS or {})
reg(interface.INCOMING_PACKETS or {})
log("INFO", "EVENTS", "Hooked " .. count .. " events")
end
function onScriptTerminate(s, quitGame)
if s.name == "RGL_FRAMEWORK" and rust then
pcall(function() rust.rgl_stop() end)
end
end
-- mimgui — top level, outside main()
if imgui_loaded and imgui then
local admin_window = imgui.new.bool()
local dpi = MONET_DPI_SCALE or 1
-- Admin window
imgui.OnFrame(
function() return admin_visible end,
function()
admin_window[0] = true
imgui.SetNextWindowSize(imgui.ImVec2(530 * dpi, 400 * dpi), imgui.Cond.FirstUseEver)
imgui.Begin("RGL Admin", admin_window, imgui.WindowFlags.NoCollapse)
local ui = create_ui_imgui()
if ui then
local rok, rerr = pcall(admin_render, ui, {})
if not rok then
imgui.TextColored(imgui.ImVec4(1, 0.3, 0.3, 1), "Render error:")
imgui.TextWrapped(tostring(rerr))
end
imgui.EndTabBar()
end
imgui.End()
if not admin_window[0] then admin_visible = false end
end
)
-- Dynamic module windows (registered via fw.register_window)
local mod_window_bool = imgui.new.bool()
imgui.OnFrame(
function()
for _, w in pairs(module_windows) do
if w.visible then return true end
end
return false
end,
function()
for name, w in pairs(module_windows) do
if w.visible then
mod_window_bool[0] = true
imgui.SetNextWindowSize(
imgui.ImVec2(w.width * dpi, w.height * dpi),
imgui.Cond.FirstUseEver
)
imgui.Begin(w.title, mod_window_bool, imgui.WindowFlags.NoCollapse)
local entry = loaded_modules and loaded_modules[name]
if entry and entry.mod and entry.mod.render then
local ui = create_ui_imgui()
if ui then
local rok, rerr = pcall(entry.mod.render, ui, module_states[name] or {})
if not rok then
imgui.TextColored(imgui.ImVec4(1, 0.3, 0.3, 1), "Render error:")
imgui.TextWrapped(tostring(rerr))
end
imgui.EndTabBar()
end
else
imgui.Text(name .. " module not loaded")
end
imgui.End()
if not mod_window_bool[0] then w.visible = false end
end
end
end
)
-- Notification toasts (errors, warnings)
imgui.OnFrame(
function() return #notifications > 0 end,
function(self)
self.HideCursor = true
local resX, resY = getScreenResolution()
imgui.SetNextWindowPos(imgui.ImVec2(resX - 10 * dpi, resY * 0.3), imgui.Cond.Always, imgui.ImVec2(1, 0))
imgui.Begin("##rgl_notifications", nil,
imgui.WindowFlags.AlwaysAutoResize +
imgui.WindowFlags.NoTitleBar +
imgui.WindowFlags.NoResize +
imgui.WindowFlags.NoMove +
imgui.WindowFlags.NoBackground +
imgui.WindowFlags.NoFocusOnAppearing +
imgui.WindowFlags.NoNav
)
local now = os.clock()
local i = 1
while i <= #notifications do
local n = notifications[i]
local elapsed = now - n.start
if elapsed > n.time then
table.remove(notifications, i)
else
-- Fade
local alpha = 1
if elapsed < 0.3 then alpha = elapsed / 0.3
elseif elapsed > n.time - 0.5 then alpha = (n.time - elapsed) / 0.5 end
local color
if n.level == "ERROR" then color = imgui.ImVec4(1, 0.3, 0.3, alpha)
elseif n.level == "WARN" then color = imgui.ImVec4(1, 0.7, 0.2, alpha)
else color = imgui.ImVec4(0.3, 0.8, 1, alpha) end
local bg = imgui.ImVec4(0.1, 0.1, 0.15, 0.9 * alpha)
local p = imgui.GetCursorScreenPos()
local textSize = imgui.CalcTextSize(n.text, nil, false, 280 * dpi)
local pad = 8 * dpi
local dl = imgui.GetWindowDrawList()
dl:AddRectFilled(
imgui.ImVec2(p.x - pad, p.y - pad),
imgui.ImVec2(p.x + textSize.x + pad, p.y + textSize.y + pad),
imgui.ColorConvertFloat4ToU32(bg), 6 * dpi
)
imgui.PushTextWrapPos(imgui.GetCursorPosX() + 280 * dpi)
imgui.TextColored(color, n.text)
imgui.PopTextWrapPos()
imgui.Spacing()
i = i + 1
end
end
imgui.End()
end
)
end