Script Manager - Lua
Script Manager - Lua
Script Manager - Lua
0
-- Reference script: Script Manager
--
-- For script developers:
-- If you want to allow your script to be toggled from Script Manager, you must
implement the following API in EXPORTS:
-- 1. For "Enabled" checkbox:
-- a. canToggle: return true
-- b. getToggle: return <your toggled status variable>
-- c. toggle: execute any code, and switch <your toggled status variable>
(switching is optional)
-- 2. For "Activate button":
-- a. canToggle: return false
-- b. getToggle: return false
-- c. toggle: execute any code
--
-- Simple example that implements "Enabled" checkbox:
-- local toggled = false
-- EXPORTS = {
-- canToggle = function() return true end,
-- getToggle = function() return toggled end,
-- toggle = function() toggled = not toggled end
-- }
--
-- script info
script_name('Script Manager')
script_version('1.2')
script_version_number(3)
script_author('The MonetLoader Team')
script_description('Script manager that opens on left swipe on radar and provides
ability to manage scripts, view logs, execute Lua code in REPL-like mode and
receive script notifications.')
script_properties('work-in-pause', 'forced-reloading-only') -- work even in pause
and don't reload ourselves on reloadScripts()
-- libs
local levels = require('moonloader').message_prefix
local ffi = require('ffi')
local widgets = require('widgets') -- for WIDGET_(...)
local imgui = require('mimgui')
local faicons = require('fAwesome6')
local cfg = require('jsoncfg')
function prettyPrintTable(node)
local cache, stack, output = {},{},{}
local depth = 1
local output_str = "{"
while true do
local size = 0
for k,v in pairs(node) do
size = size + 1
end
local cur_index = 1
for k,v in pairs(node) do
if (cache[node] == nil) or (cur_index >= cache[node]) then
if (string.find(output_str,"}",output_str:len())) then
output_str = output_str .. ","
end
-- This is necessary for working with HUGE tables otherwise we run out of
memory using concat on huge strings
table.insert(output,output_str)
output_str = ""
local key
if (type(k) == "string") then
key = "['"..tostring(k).."']"
else
key = "["..tostring(k).."]"
end
cur_index = cur_index + 1
end
if (size == 0) then
output_str = output_str .. "}"
end
-- This is necessary for working with HUGE tables otherwise we run out of memory
using concat on huge strings
table.insert(output,output_str)
output_str = table.concat(output)
return output_str
end
-- pretty prints arguments, expanding tables (also supports multiple nils without
omitting them)
function prettyPrint(...)
-- we use select instead of table unpacking in order to handle nil values
correctly
local argc = select('#', ...)
if argc == 0 then
return 'nil'
end
return output_str
end
function stateless_iter(a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end
function any_ipairs(a)
return stateless_iter, a, 0
end
function circular_buffer.reverse_iter(a, i)
i = i - 1
local v = a[i]
if v then
return i, v
end
end
function circular_buffer.reverse_ipairs(self)
return circular_buffer.reverse_iter, self, 0
end
function circular_buffer.filled(self)
return #(self.history) == self.max_length
end
function circular_buffer.clear(self)
self.history = {}
self.oldest = 1
end
circular_buffer.metatable = {}
function circular_buffer.metatable.__len(self)
return #(self.history)
end
function circular_buffer.new(max_length)
if type(max_length) ~= 'number' or max_length <= 1 then
error("Buffer length must be a positive integer")
end
local instance = {
history = {},
oldest = 1,
max_length = max_length,
push = circular_buffer.push,
filled = circular_buffer.filled,
clear = circular_buffer.clear
}
setmetatable(instance, circular_buffer.metatable)
return instance
end
-- notifications (https://www.blast.hk/threads/132205/)
Notifications = {
_version = '0.2',
_list = {},
_COLORS = {
[0] = {back = {0.26, 0.71, 0.81, 1}, text = {1, 1, 1, 1}, icon = {1, 1, 1,
1}, border = {1, 0, 0, 0}},
[1] = {back = {0.26, 0.81, 0.31, 1}, text = {1, 1, 1, 1}, icon = {1, 1, 1,
1}, border = {1, 0, 0, 0}},
[2] = {back = {1, 0.39, 0.39, 1}, text = {1, 1, 1, 1}, icon = {1, 1, 1,
1}, border = {1, 0, 0, 0}},
[3] = {back = {0.97, 0.57, 0.28, 1}, text = {1, 1, 1, 1}, icon = {1, 1, 1,
1}, border = {1, 0, 0, 0}},
[4] = {back = {0, 0, 0, 1}, text = {1, 1, 1, 1}, icon = {1, 1, 1,
1}, border = {1, 0, 0, 0}},
},
TYPE = {
INFO = 0,
OK = 1,
ERROR = 2,
WARN = 3,
DEBUG = 4
},
ICON = {
[0] = faicons('CIRCLE_INFO'),
[1] = faicons('CHECK'),
[2] = faicons('XMARK'),
[3] = faicons('EXCLAMATION'),
[4] = faicons('WRENCH')
}
}
imgui.OnFrame(
function() return #Notifications._list > 0 end,
function(self)
self.HideCursor = true
local c = imgui.GetCursorPos()
local p = imgui.GetCursorScreenPos()
local DL = imgui.GetWindowDrawList()
imgui.PushStyleVarFloat(imgui.StyleVar.Alpha, data.alpha)
imgui.PushStyleVarFloat(imgui.StyleVar.ChildRounding, fiveSc)
imgui.PushStyleColor(imgui.Col.ChildBg,
Notifications._TableToImVec(data.colors.back or
Notifications._COLORS[data.type].back))
imgui.PushStyleColor(imgui.Col.Border,
Notifications._TableToImVec(data.colors.border or
Notifications._COLORS[data.type].border))
imgui.BeginChild('toastNotf:'..tostring(k)..tostring(data.text), size, true,
imgui.WindowFlags.NoScrollbar + imgui.WindowFlags.NoScrollWithMouse)
imgui.PushStyleColor(imgui.Col.Text,
Notifications._TableToImVec(data.colors.icon or
Notifications._COLORS[data.type].icon))
imgui.SetCursorPos(imgui.ImVec2(fiveSc, size.y / 2 - iconSize.y / 2))
imgui.Text(Notifications.ICON[data.type] or faicons('XMARK'))
imgui.PopStyleColor()
imgui.PushStyleColor(imgui.Col.Text,
Notifications._TableToImVec(data.colors.text or
Notifications._COLORS[data.type].text))
imgui.SetCursorPos(imgui.ImVec2(fiveSc + iconSize.x + fiveSc, size.y / 2 -
textSize.y / 2))
imgui.Text(data.text)
imgui.PopStyleColor()
imgui.EndChild()
imgui.PopStyleColor(2)
imgui.PopStyleVar(2)
------------------------------------------------
end
imgui.End()
end
)
-- global vars
-- utils
-- formats time in seconds into format: xxh xxm xxs (hours and minutes are omitted
if not present)
function formatClock(diff)
diff = math.floor(diff)
local seconds = diff % 60
diff = math.floor(diff / 60)
local minutes = diff % 60
diff = math.floor(diff / 60)
local hours = diff
return (hours > 0 and tostring(hours) .. 'h ' or '') .. (minutes > 0 and
tostring(minutes) .. 'm ' or '') .. tostring(seconds) .. 's'
end
-- https://www.blast.hk/threads/111224/
imgui.OnInitialize(function()
imgui.GetIO().IniFilename = nil
imgui.GetIO().Fonts:AddFontFromMemoryCompressedBase85TTF(faicons.get_font_data_base
85('solid'), 14 * MONET_DPI_SCALE, config, glyphRanges[0].Data) -- load scaled DPI
font
-- rendering
-- main window
imgui.OnFrame(
function() return windowState[0] end,
function(self)
imgui.SetNextWindowSize(imgui.ImVec2(530 * MONET_DPI_SCALE, 330 *
MONET_DPI_SCALE), imgui.Cond.FirstUseEver)
imgui.Begin('Script Manager for MonetLoader v' .. script.this.version,
windowState, imgui.WindowFlags.NoCollapse)
if imgui.BeginTabBar('Tabs') then
local didLogRender = false
local didShellRender = false
imgui.End()
end
imgui.NextColumn()
if imgui.Button('Unload') then
scr:unload()
Notifications.Show(scr.name .. ':\nUnloaded!', Notifications.TYPE.OK)
end
imgui.SameLine()
if imgui.Button('Reload') then
scr:reload()
Notifications.Show(scr.name .. ':\nReloaded!', Notifications.TYPE.OK)
end
imgui.Columns(1)
imgui.EndChild()
imgui.EndTabItem()
end
for i, v in any_ipairs(messages) do
if v:lower():find(logSearchText, 1, true) then
imgui.TextWrapped('%s', v)
end
end
imgui.EndChild()
imgui.EndTabItem()
didLogRender = true
end
imgui.AlignTextToFramePadding()
imgui.Text('Script name')
imgui.NextColumn()
imgui.AlignTextToFramePadding()
imgui.Text('Time since crash')
imgui.NextColumn()
imgui.AlignTextToFramePadding()
imgui.Text('Actions')
imgui.Separator()
imgui.NextColumn()
for i, v in circular_buffer.reverse_ipairs(lastCrashes) do
if not v.hidden then
imgui.AlignTextToFramePadding()
imgui.Text('%s', v.name)
imgui.NextColumn()
imgui.AlignTextToFramePadding()
imgui.Text('%s', formatClock(os.clock() - v.time))
imgui.NextColumn()
if not v.reloaded then
if imgui.Button('Reload##' .. tostring(i)) then
reloadLastCrashInfos[v.path] = v
script.load(v.path)
lua_thread.create(function()
wait(0)
if not v.reloaded then
Notifications.Show(v.name .. ':\nReload failed!',
Notifications.TYPE.ERROR)
reloadLastCrashInfos[v.path] = nil
end
end)
end
imgui.SameLine()
end
imgui.Separator()
imgui.NextColumn()
end
end
imgui.Columns(1)
imgui.EndChild()
imgui.EndTabItem()
end
if imgui.Button('Up') then
shellInputHistoryPos = shellInputHistoryPos - 1
if shellInputHistory[shellInputHistoryPos] ~= nil then
imgui.StrCopy(shellInputBuffer,
shellInputHistory[shellInputHistoryPos])
else
shellInputHistoryPos = shellInputHistoryPos + 1
end
end
imgui.SameLine()
if imgui.Button('Down') then
shellInputHistoryPos = shellInputHistoryPos + 1
if shellInputHistoryPos >= 0 then
shellInputHistoryPos = 0
imgui.StrCopy(shellInputBuffer, '')
else
imgui.StrCopy(shellInputBuffer,
shellInputHistory[shellInputHistoryPos])
end
end
imgui.SameLine()
if imgui.Button('Clear') then
imgui.StrCopy(shellInputBuffer, '')
shellInputHistoryPos = 0
end
imgui.SameLine()
if imgui.Button('Clear history') then
imgui.StrCopy(shellInputBuffer, '')
shellInputHistoryPos = 0
shellHistory:clear()
shellInputHistory:clear()
end
for i, v in any_ipairs(shellHistory) do
local doPop = false
if v:sub(1, 3) == '<!>' then
doPop = true
imgui.PushStyleColor(imgui.Col.Text, imgui.ImVec4(1.0, 0.0, 0.0, 1.0))
end
imgui.TextWrapped('%s', v)
if doPop then
imgui.PopStyleColor()
end
end
imgui.EndChild()
imgui.EndTabItem()
didShellRender = true
end
config.messagesCount = imMessagesCount[0]
cfg.save(config)
for i, v in any_ipairs(messages) do
newBuffer:push(v)
end
messages = newBuffer
end
config.lastCrashesCount = imLastCrashesCount[0]
cfg.save(config)
for i, v in any_ipairs(lastCrashes) do
newBuffer:push(v)
end
lastCrashes = newBuffer
end
config.shellHistoryCount = imShellHistoryCount[0]
cfg.save(config)
local newInputBuffer =
circular_buffer.new(math.ceil(config.shellHistoryCount / 2))
for i, v in any_ipairs(shellInputHistory) do
newInputBuffer:push(v)
end
shellInputHistory = newInputBuffer
end
imgui.EndTabItem()
end
imgui.EndTabBar()
wasInLog = didLogRender
wasInShell = didShellRender
end
imgui.End()
end
)
-- custom events
lastCrashes:push({
name = scr.name,
path = scr.path,
time = os.clock(),
reloaded = false,
hidden = false
})
messages:push('(crash) ' .. scr.name .. ': ' .. msg)
end
-- events