diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..373df2e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dml.zip diff --git a/scripts/mods/dml/class.lua b/scripts/mods/dml/class.lua new file mode 100644 index 0000000..19b0c91 --- /dev/null +++ b/scripts/mods/dml/class.lua @@ -0,0 +1,22 @@ +Mods.original_class = Mods.original_class or class + +local _G = _G +local rawget = rawget +local rawset = rawset + +_G.CLASS = _G.CLASS or setmetatable({}, { + __index = function(_, key) + return key + end +}) + +class = function(class_name, super_name, ...) + local result = Mods.original_class(class_name, super_name, ...) + if not rawget(_G, class_name) then + rawset(_G, class_name, result) + end + if not rawget(_G.CLASS, class_name) then + rawset(_G.CLASS, class_name, result) + end + return result +end \ No newline at end of file diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua new file mode 100644 index 0000000..f74822f --- /dev/null +++ b/scripts/mods/dml/hook.lua @@ -0,0 +1,274 @@ +--[[ + Mods Hook v2: + New version with better control +--]] + +-- Hook structure +MODS_HOOKS = MODS_HOOKS or {} +MODS_HOOKS_BY_FILE = MODS_HOOKS_BY_FILE or {} + +local item_template = { + name = "", + func = EMPTY_FUNC, + hooks = {}, +} + +local item_hook_template = { + name = "", + func = EMPTY_FUNC, + enable = false, + exec = EMPTY_FUNC, +} +local Log = Log + +local print_log_info = function(mod_name, message) + Log = Log or rawget(_G, "Log") + if Log then + Log._info(mod_name, message) + else + print("[" .. mod_name .. "]: " .. message) + end +end + +local print_log_warning = function(mod_name, message) + Log = Log or rawget(_G, "Log") + if Log then + Log._warning(mod_name, message) + else + print("[" .. mod_name .. "]: " .. message) + end +end + +Mods.hook = { + -- + -- Set hook + -- + set = function(mod_name, func_name, hook_func) + local item = Mods.hook._get_item(func_name) + local item_hook = Mods.hook._get_item_hook(item, mod_name) + + print_log_info(mod_name, "Hooking " .. func_name) + + item_hook.enable = true + item_hook.func = hook_func + + Mods.hook._patch() + end, + + -- + -- Set hook on every instance of the given file + -- + set_on_file = function(mod_name, filepath, func_name, hook_func) + -- Add hook create function to list for the file + MODS_HOOKS_BY_FILE[filepath] = MODS_HOOKS_BY_FILE[filepath] or {} + local hook_create_func = function(this_filepath, this_index) + local dynamic_func_name = "Mods.require_store[\"" .. this_filepath .. "\"][" .. tostring(this_index) .. "]." .. func_name + Mods.hook.set(mod_name, dynamic_func_name, hook_func, false) + end + table.insert(MODS_HOOKS_BY_FILE[filepath], hook_create_func) + + -- Add the new hook to every instance of the file + local all_file_instances = Mods.require_store[filepath] + if all_file_instances then + for i, item in ipairs(all_file_instances) do + if item then + hook_create_func(filepath, i) + end + end + end + end, + + -- + -- Enable/Disable hook + -- + enable = function(value, mod_name, func_name) + for _, item in ipairs(MODS_HOOKS) do + if item.name == func_name or func_name == nil then + for _, hook in ipairs(item.hooks) do + if hook.name == mod_name then + hook.enable = value + Mods.hook._patch() + end + end + end + end + + return + end, + + -- + -- Enable all hooks on a stored file + -- + enable_by_file = function(filepath, store_index) + local all_file_instances = Mods.require_store[filepath] + local file_instance = all_file_instances and all_file_instances[store_index] + + local all_file_hooks = MODS_HOOKS_BY_FILE[filepath] + + if all_file_hooks and file_instance then + for i, hook_create_func in ipairs(all_file_hooks) do + hook_create_func(filepath, store_index) + end + end + end, + + -- + -- Remove hook from chain + -- + ["remove"] = function(func_name, mod_name) + for i, item in ipairs(MODS_HOOKS) do + if item.name == func_name then + if mod_name ~= nil then + for j, hook in ipairs(item.hooks) do + if hook.name == mod_name then + table.remove(item.hooks, j) + + Mods.hook._patch() + end + end + else + local item_name = "MODS_HOOKS[" .. tostring(i) .. "]" + + -- Restore orginal function + assert(loadstring(item.name .. " = " .. item_name .. ".func"))() + + -- Remove hook function + table.remove(MODS_HOOKS, i) + + return + end + end + end + + return + end, + + -- + -- Move hook to front of the hook chain + -- + front = function(mod_name, func_name) + for _, item in ipairs(MODS_HOOKS) do + if item.name == func_name or func_name == nil then + for i, hook in ipairs(item.hooks) do + if hook.name == mod_name then + local saved_hook = table.clone(hook) + table.remove(item.hooks, i) + table.insert(item.hooks, saved_hook) + + Mods.hook._patch() + end + end + end + end + + return + end, + + -- + -- Get function by function name + -- + _get_func = function(func_name) + return assert(loadstring("return " .. func_name))() + end, + + -- + -- Get item by function name + -- + _get_item = function(func_name) + -- Find existing item + for _, item in ipairs(MODS_HOOKS) do + if item.name == func_name then + return item + end + end + + -- Create new item + local item = table.clone(item_template) + item.name = func_name + item.func = Mods.hook._get_func(func_name) + + -- Save + table.insert(MODS_HOOKS, item) + + return item + end, + + -- + -- Get item hook by mod name + -- + _get_item_hook = function(item, mod_name) + -- Find existing item + for _, hook in ipairs(item.hooks) do + if hook.name == mod_name then + return hook + end + end + + -- Create new item + local item_hook = table.clone(item_hook_template) + item_hook.name = mod_name + + -- Save + table.insert(item.hooks, 1, item_hook) + + return item_hook + end, + + -- + -- If settings are changed the hook itself needs to be updated + -- + _patch = function(mods_hook_item) + for i, item in ipairs(MODS_HOOKS) do + local item_name = "MODS_HOOKS[" .. i .. "]" + + local last_j = 1 + for j, hook in ipairs(item.hooks) do + local hook_name = item_name .. ".hooks[" .. j .. "]" + local before_hook_name = item_name .. ".hooks[" .. (j - 1) .. "]" + + if j == 1 then + if hook.enable then + assert( + loadstring( + hook_name .. ".exec = function(...)" .. + " return " .. hook_name .. ".func(" .. item_name .. ".func, ...)" .. + "end" + ) + )() + else + assert( + loadstring( + hook_name .. ".exec = function(...)" .. + " return " .. item_name .. ".func(...)" .. + "end" + ) + )() + end + else + if hook.enable then + assert( + loadstring( + hook_name .. ".exec = function(...)" .. + " return " .. hook_name .. ".func(" .. before_hook_name .. ".exec, ...)" .. + "end" + ) + )() + else + assert( + loadstring( + hook_name .. ".exec = function(...)" .. + " return " .. before_hook_name .. ".exec(...)" .. + "end" + ) + )() + end + end + + last_j = j + end + + -- Patch orginal function call + assert(loadstring(item.name .. " = " .. item_name .. ".hooks[" .. last_j .. "].exec"))() + end + end, +} diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 8b13789..d0d22d4 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -1 +1,16 @@ +-- The loader object that is used during game boot +-- to initialize the modding environment. +local loader = {} +Mods = { + hook = {}, + lua = setmetatable({}, { + __index = { debug = debug, io = io, ffi = ffi, os = os }, + }), +} + +dofile("scripts/mods/dml/require") +dofile("scripts/mods/dml/class") +dofile("scripts/mods/dml/hook") + +return loader diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua new file mode 100644 index 0000000..6e876f7 --- /dev/null +++ b/scripts/mods/dml/require.lua @@ -0,0 +1,35 @@ +Mods.require_store = Mods.require_store or {} +Mods.original_require = Mods.original_require or require + +local can_insert = function(filepath, new_result) + local store = Mods.require_store[filepath] + if not store or #store == 0 then + return true + end + + if store[#store] ~= new_result then + return true + end +end + +require = function(filepath, ...) + local Mods = Mods + + local result = Mods.original_require(filepath, ...) + if result and type(result) == "table" then + + if can_insert(filepath, result) then + Mods.require_store[filepath] = Mods.require_store[filepath] or {} + local store = Mods.require_store[filepath] + + table.insert(store, result) + + --print("[Require] #" .. tostring(#store) .. " of " .. filepath) + if Mods.hook then + Mods.hook.enable_by_file(filepath, #store) + end + end + end + + return result +end \ No newline at end of file