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); ]] 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 admin_visible = false local recent_logs = {} local MAX_LOGS = 100 local notifications = {} -- {text, level, time, start} local module_errors = {} -- ["module_name"] = "last error" 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") setup_framework() 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 cjson_ok then framework.json_encode = cjson.encode framework.json_decode = cjson.decode else framework.json_decode = function(s) local t = {} for k,v in s:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do t[k]=v end return t end framework.json_encode = function(t) local p = {} for k,v in pairs(t) do p[#p+1]='"'..k..'":"'..tostring(v)..'"' end return "{"..table.concat(p,",").."}" end end 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 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 ok, mod = pcall(dofile, path) if not ok then _current_module = nil; return false, "load: " .. 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"} 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, "") loaded_modules[name] = nil log("INFO", "MODS", "Unloaded: " .. name) return true end function load_all_modules() local dir = framework.modules_dir local ls = io.popen('ls "' .. dir .. '" 2>/dev/null') if not ls then return end for name in ls:lines() do local path = dir .. "/" .. name .. "/init.lua" local f = io.open(path) if f then f:close(); local ok,err = load_single_module(name) if not ok then log("ERROR", "MODS", name .. ": " .. err) end end end ls:close() 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() local dir = framework.modules_dir local ls = io.popen('ls "' .. dir .. '" 2>/dev/null') if ls then for name in ls:lines() do if not loaded_modules[name] then local path = dir .. "/" .. name .. "/init.lua" local f = io.open(path) if f then f:close() 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 end ls:close() 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 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 ) -- BTC module window (and any other module with render()) local btc_window = imgui.new.bool() imgui.OnFrame( function() return btc_visible end, function() btc_window[0] = true imgui.SetNextWindowSize(imgui.ImVec2(450 * dpi, 400 * dpi), imgui.Cond.FirstUseEver) imgui.Begin("BTC Miner", btc_window, imgui.WindowFlags.NoCollapse) local mod = loaded_modules and loaded_modules["btc"] if mod and mod.mod and mod.mod.render then local ui = create_ui_imgui() if ui then local rok, rerr = pcall(mod.mod.render, ui, module_states["btc"] 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("BTC module not loaded") end imgui.End() if not btc_window[0] then btc_visible = false 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