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.
1058 lines
38 KiB
1058 lines
38 KiB
|
1 day ago
|
script_name("WS_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("/sdcard/Download/ws_framework.log")
|
||
|
|
log("INFO", "INIT", "WS 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 == "WS_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
|
||
|
|
|