Do stuff later without blocking
Published: Feb 18 2024 by Last documented edit: Feb 18 2024 by
You can get Cron.lua from psiberx's cp2077-cet-kit on github. Add it to your sources, and then define it as a global:
-- don't overwrite it if it's already defined in the global scope. Or do, up to you.
Cron = Cron or require('External/Cron.lua')
Cron offers a bunch of functions for executing code later. The code you want to execute has to be inside a function (=> callback) and is passed among the arguments.
For periodically running functions, you need to include logic when to stop the execution.
To save processor load, it is best practice to define your callback outside of the function call. This can have a noticable performance impact, so you're gonna want to do this.
local callback = function()
-- do a little dance, make a little love
end
Cron.Every(0.1, callback)
The callback is defined once.
Cron.Every(0.1, function()
-- do a little dance, make a little love
end)
The callback is defined every time cron runs.
To pass arguments to your callback function, you pass them as arguments to cron:
local callback = function(timer)
timer.tick = timer.tick +1
-- do a little dance, make a little love
end
Cron.Every(0.1, { tick = 1 }, callback)
Execute the callback after X seconds
local callback = function() --[[ do a little dance, make a little love ]] end
Cron.After(0.2, callback)
Executes a function every X seconds.
The function will run forever unless you stop it!
local callback = function(timer)
timer.tick = timer.tick +1
spdlog.info("Timer is running: " .. tostring(timer.tick))
-- do a little dance, make a little love
if timer.tick > 150 then
Cron.Halt(timer)
end
end
-- define outside of function scope to re-use this in Cron.Pause example below
local timerArg = { tick = 1 }
-- start timer
Cron.Every(0.2, timerArg, callback)
You can pause a timer by passing its reference to cron:
-- callback and timer have been defined in the example above
-- the timer is running
Cron.Pause(timerArg)
spdlog.info("Timer is paused after " .. tostring(timerArg.tick) .. " ticks")
-- wait 20 seconds, then resume our function
Cron.After(20, function () Cron.Resume(timerArg) end)
Prints one or multiple objects into CET Console
Prints one or multiple objects into Individual Mod Logs file
Handle table
with indentation
Handle boolean
, nil
, string
and other classic types
Parse userdata
via GameDump()
or Dump()
if possible
Parse CName
via NameToString()
if possible
Prints to CET Console, similar to print() function.
debug.print(obj: object [, obj2, ...]) -> nil
Write in Individual Mod Logs file. eg, <
>/mods/my_mod/my_mod.log
. Similar to spdlog.
debug.log(obj: object [, obj2, ...]) -> nil
local myTable = {
var = true,
enabled = false,
subTable = {
player = Game.GetPlayer(),
stats = Game.GetStatsSystem()
}
}
debug.log('myTable:', myTable)
[13:49:03] myTable: {
["var"] = true,
["enabled"] = false,
["subTable"] = {
["player"] = PlayerPuppet[ customCameraTarget:ECCTV_OnlyOnscreen, ...]
["stats"] = gameStatsSystem[ ],
},
}
debug = {
-- log
log = function(...)
spdlog.info(debug.parse(...))
end,
-- print
print = function(...)
print(debug.parse(...))
end,
-- parse arguments
parse = function(...)
local args = {}
for _, v in pairs{debug.null(...)} do
table.insert(args, debug.parseDeep(v))
end
local output = ""
for i, value in pairs(args) do
output = output .. tostring(value)
if i ~= #args then
output = output .. " "
end
end
return output
end,
-- parse deep table
parseDeep = function(t, max, depth)
-- convert nil to 'nil'
t = debug.null(t)
if type(t) ~= 'table' then
if type(t) == 'userdata' then
t = debug.parseUserData(t)
end
return t
end
max = max or 63
depth = depth or 4
local dumpStr = '{\n'
local indent = string.rep(' ', depth)
for k, v in pairs(t) do
-- vars
local ktype = type(k)
local vtype = type(v)
local vstr = ''
-- key
local kstr = ''
if ktype == 'string' then
kstr = string.format('[%q] = ', k)
end
-- string
if vtype == 'string' then
vstr = string.format('%q', v)
-- table
elseif vtype == 'table' then
if depth < max then
vstr = debug.parseDeep(v, max, depth + 4)
end
-- userdata
elseif vtype == 'userdata' then
vstr = debug.parseUserData(v)
-- thread (do nothing)
elseif vtype == 'thread' then
-- else
else
vstr = tostring(v)
end
-- format dump
if vstr ~= '' then
dumpStr = string.format('%s%s%s%s,\n', dumpStr, indent, kstr, vstr)
end
end
-- unindent
local unIndent = indent:sub(1, -5)
-- return
return string.format('%s%s}', dumpStr, unIndent)
end,
-- parse userdata
parseUserData = function(t)
local tstr = tostring(t)
if tstr:find('^ToCName{') then
tstr = NameToString(t)
elseif tstr:find('^userdata:') or tstr:find('^sol%.') then
local gdump = false
local ddump = false
pcall(function() gdump = GameDump(t) end)
pcall(function() ddump = Dump(t, true) end)
if gdump then
tstr = GameDump(t)
elseif ddump then
tstr = ddump
end
end
return tstr
end,
-- convert nil into 'nil'
null = function(...)
local t, n = {...}, select('#', ...)
for k = 1, n do
local v = t[k]
if v == debug.null then t[k] = 'nil'
elseif v == nil then t[k] = debug.null
end
end
return (table.unpack or unpack)(t, 1, n)
end,
}
Access and write tables using dot "." notation, also known as "flat path". Similar to TweakDB Flat logic but for any kind of LUA table.
tFlat.get(obj: table, path: string, opt default: any) -> value/default
tFlat.has(obj: table, path: string) -> boolean
tFlat.set(obj: table, path: string, value: any) -> table
tFlat.insert(obj: table, path: string, value: any) -> table
tFlat.delete(obj: table, path: string) -> table
All code examples below use this predefined table data. Note that items
is an associative object and elements
is a sequential table.
local myTable = {
ready = true,
config = {
items = {
item1 = true,
item2 = false
},
elements = {
'element1',
'element2'
}
}
}
Get value
local item2 = tFlat.get(myTable, 'config.items.item2')
-- false
local element2 = tFlat.get(myTable, 'config.elements.2')
-- element2
Has value
local hasItem2 = tFlat.has(myTable, 'config.items.item2')
-- true
local hasElement2 = tFlat.has(myTable, 'config.elements.2')
-- true
Set value
tFlat.set(myTable, 'config.items.item2', true)
tFlat.set(myTable, 'config.elements.2', 'newElement2')
myTable = {
["ready"] = true,
["config"] = {
["items"] = {
["item1"] = true,
["item2"] = true,
},
["elements"] = {
"element1",
"newElement2",
},
},
}
Insert sequential table value
tFlat.insert(myTable, 'config.elements', 'element3')
myTable = {
["ready"] = true,
["config"] = {
["items"] = {
["item1"] = true,
["item2"] = false,
},
["elements"] = {
"element1",
"element2",
"element3",
},
},
}
Delete value
tFlat.delete(myTable, 'config.items.item1')
tFlat.delete(myTable, 'config.elements.1')
myTable = {
["ready"] = true,
["config"] = {
["items"] = {
["item2"] = false,
},
["elements"] = {
"element2",
},
},
}
tFlat = {
get = function(obj, path, default)
-- get path array
path = tFlat.split(path)
-- vars
local length = #path
local current = obj
local key
-- loop through path
for index = 1, length, 1 do
-- current key
key = path[ index ]
-- convert key to number (sequential table)
if tonumber(key) then
key = tonumber(key)
end
-- stop searching if a child object is missing
if current[ key ] == nil then
return default
end
current = current[ key ]
end
return current
end,
has = function(obj, path)
return tFlat.get(obj, path) ~= nil
end,
set = function(obj, path, val)
-- get path array
path = tFlat.split(path)
-- vars
local length = #path
local current = obj
local key
-- loop through path
for index = 1, length, 1 do
-- current key
key = path[ index ]
-- convert key to number (sequential table)
if tonumber(key) then
key = tonumber(key)
end
-- set value on last key
if index == length then
current[ key ] = val
-- current key exists
elseif current[ key ] then
if type(current[ key ]) ~= 'table' then
current[ key ] = {}
end
current = current[ key ]
-- current key doesn't exist
else
current[ key ] = {}
current = current[ key ]
end
end
-- return
return obj
end,
insert = function(obj, path, val)
-- get target
local target = tFlat.get(obj, path)
-- check if table and sequential
if type(target) == 'table' and tFlat.isSequential(target) then
table.insert(target, val)
end
-- return
return obj
end,
delete = function(obj, path)
-- get path array
path = tFlat.split(path)
-- vars
local length = #path
local current = obj
local key
-- loop through path
for index = 1, length, 1 do
-- current key
key = path[ index ]
-- convert key to number (sequential table)
if tonumber(key) then
key = tonumber(key)
end
-- set value on last key
if index == length then
current[ key ] = nil
else
current = current[ key ] or {}
end
end
-- return
return obj
end,
split = function(s)
s = tostring(s)
local fields = {}
local pattern = string.format("([^%s]+)", '.')
string.gsub(s, pattern, function(c)
fields[ #fields + 1 ] = c
end)
return fields
end,
isSequential = function(array)
for k, _ in pairs(array) do
if type(k) ~= "number" then
return false
end
end
return true
end,
}
A custom hooks system for your LUA script
doAction(name: string, context: any [, context2 ...]) -> nil
addAction(name: string, callback: function, opt priority: integer) -> nil
registerForEvent('oninit', function()
-- execute all addAction() using this name
doAction('my_action', 'init', 'something')
end)
addAction('my_action', function(context, context2)
-- context = 'init'
-- context2 = 'something'
-- do something
end, 10)
addAction('my_action', function(context, context2)
-- context = 'init'
-- context2 = 'something'
-- do something before (note the priority: 5)
end, 5)
applyFilters(name: string, value: any, context: any [, context2 ...]) -> nil
addFilter(name: string, callback: function, opt priority: integer) -> nil
registerForEvent('oninit', function()
local myVar = 'yes'
myVar = applyFilters('my_filter', myVar, 'init', 'something')
print(myVar)
-- no
end)
addFilter('my_filter', function(myVar, context, context2)
-- myVar = 'maybe' (set by the other filter with priortiy: 5)
-- context = 'init'
-- context2 = 'something'
-- update myVar to 'no'
return 'no'
end, 10)
addFilter('my_filter', function(myVar, context, context2)
-- myVar = 'yes' (set by initial applyFilters())
-- context = 'init'
-- context2 = 'something'
-- update myVar to 'maybe' (note the priority: 5)
return 'maybe'
end, 5)
local this
this = {
storage = {
actions = {},
filters = {}
},
addAction = function(action, callback, priority, context)
if type(action) == 'string' and type(callback) == 'function' then
priority = tonumber(priority or 10)
this._addHook('actions', action, callback, priority, context)
end
return this
end,
doAction = function(...)
local args = { ... }
local action = table.remove(args, 1)
if type(action) == 'string' then
this._runHook('actions', action, args)
end
return this
end,
removeAction = function(action, callback)
if type(action) == 'string' then
this._removeHook('actions', action, callback)
end
return this
end,
addFilter = function(filter, callback, priority, context)
if type(filter) == 'string' and type(callback) == 'function' then
priority = tonumber(priority or 10)
this._addHook('filters', filter, callback, priority, context)
end
return this
end,
applyFilters = function(...)
local args = { ... }
local filter = table.remove(args, 1)
if type(filter) == 'string' then
return this._runHook('filters', filter, args)
end
return this
end,
removeFilter = function(filter, callback)
if type(filter) == 'string' then
this._removeHook('filters', filter, callback)
end
return this
end,
_removeHook = function(type, hook, callback, context)
if not this.storage[type][hook] then
return
end
if not callback then
this.storage[type][hook] = {}
else
local handlers = this.storage[type][hook]
--local i
if not context then
for i = #handlers, 1, -1 do
if handlers[i].callback == callback then
table.remove(handlers, i)
end
end
else
for i = #handlers, 1, -1 do
local handler = handlers[i]
if handler.callback == callback and handler.context == context then
table.remove(handlers, i)
end
end
end
end
end,
_addHook = function(type, hook, callback, priority, context)
local hookObject = {
callback = callback,
priority = priority,
context = context
}
local hooks = this.storage[type][hook]
if hooks then
table.insert(hooks, hookObject)
hooks = this._hookInsertSort(hooks)
else
hooks = { hookObject }
end
this.storage[type][hook] = hooks
end,
_hookInsertSort = function(hooks)
local tmpHook, j, prevHook
for i = 2, #hooks do
tmpHook = hooks[i]
j = i
prevHook = hooks[j - 1]
while prevHook and prevHook.priority > tmpHook.priority do
hooks[j] = hooks[j - 1]
j = j - 1
prevHook = hooks[j - 1]
end
hooks[j] = tmpHook
end
return hooks
end,
_runHook = function(type, hook, args)
local handlers = this.storage[type][hook]
if not handlers then
return type == 'filters' and args[1] or false
end
local i = 1
local len = #handlers
if type == 'filters' then
for i = 1, len do
args[1] = handlers[i].callback(unpack(args))
end
else
for i = 1, len do
handlers[i].callback(unpack(args))
end
end
return type == 'filters' and args[1] or true
end
}
-- history storage
local actionHistory = {}
addAction = function(...)
this.addAction(...)
return this
end
removeAction = function(...)
this.removeAction(...)
return this
end
doAction = function(...)
local args = {...}
local action = args[1]
actionHistory[action] = 1
this.doAction(...)
actionHistory[action] = 0
return this
end
doingAction = function(action)
return actionHistory[action] == 1
end
didAction = function(action)
return actionHistory[action] ~= nil
end
currentAction = function()
for k in pairs(actionHistory) do
if actionHistory[k] then
return k
end
end
return false
end
addFilter = function(...)
this.addFilter(...)
return this
end
removeFilter = function(...)
this.removeFilter(...)
return this
end
applyFilters = function(...)
return this.applyFilters(...)
end
fileExists(path: string) -> boolean
print(fileExists('my_file.lua'))
-- true
function fileExists(path)
local f = io.open(path, 'r')
return f ~= nil and io.close(f)
end
getFilesRecursive(path: string, maxDepth: integer) -> table
--
-- getFilesRecursive()
--
-- @param string path The starting path: Default: '.' (mod's root folder)
-- @param integer maxDepth The maximum depth allowed. Default: -1 (unlimited)
--
getFilesRecursive('.', -1)
local files = getFilesRecursive('.', 2)
files = {
"./includes/module-1.lua",
"./includes/module-2.lua",
"./includes/sub-folder/sub-module-1.lua",
"./init.lua",
}
function getFilesRecursive(path, maxDepth, depth, storage)
-- vars
path = path or '.'
maxDepth = maxDepth or -1
depth = depth or 0
storage = storage or {}
-- validate maxDepth
if maxDepth ~= -1 and depth > maxDepth then
return storage
end
-- sanitize path leading "."
if not string.find(path, '^%.') then
path = '.' .. path
end
-- sanitize path leading double "//"
if string.find(path, '^%.//') then
path = string.gsub(path, '^%.//', './')
end
-- get dir/files
local files = dir(path)
-- validate files
if not files or (type(files) == 'table' and #files == 0) then
return storage
end
-- loop files
for _, file in ipairs(files) do
-- directory
if file.type == 'directory' then
getFilesRecursive(path .. '/' .. file.name, maxDepth, depth + 1, storage)
-- lua file
elseif string.find(file.name, '%.lua$') then
table.insert(storage, path .. '/' .. file.name)
end
end
return storage
end
getFoldersRecursive(path: string, maxDepth: int) -> table
--
-- getFoldersRecursive()
--
-- @param string path The starting path: Default: '.' (mod's root folder)
-- @param integer maxDepth The maximum depth allowed. Default: -1 (unlimited)
--
getFoldersRecursive('.', -1)
local folders = getFoldersRecursive('.', 2)
folders = {
"./includes",
"./includes/sub-folder",
}
function getFoldersRecursive(path, maxDepth, depth, storage)
-- vars
path = path or '.'
maxDepth = maxDepth or -1
depth = depth or 0
storage = storage or {}
-- validate maxDepth
if maxDepth ~= -1 and depth > maxDepth then
return storage
end
-- sanitize path leading "."
if not string.find(path, '^%.') then
path = '.' .. path
end
-- sanitize path leading double "//"
if string.find(path, '^%.//') then
path = string.gsub(path, '^%.//', './')
end
-- get dir/files
local files = dir(path)
-- validate files
if not files or (type(files) == 'table' and #files == 0) then
return storage
end
-- loop files
for _, file in ipairs(files) do
-- directory
if file.type == 'directory' then
table.insert(storage, path .. '/' .. file.name)
getFoldersRecursive(path .. '/' .. file.name, maxDepth, depth + 1, storage)
end
end
return storage
end
A collection of LUA scripting utilities to make your life easier