From a67c28ca94c319f0416d5e4f080bb27482500ce8 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:41:28 +0100 Subject: [PATCH 01/34] chore: Initialize mod --- .luacheckrc | 44 +++++++++++++++++++++++++++++++++++++++ dtmt.cfg | 16 ++++++++++++++ packages/dml.package | 3 +++ scripts/mods/dml/init.lua | 1 + 4 files changed, 64 insertions(+) create mode 100644 .luacheckrc create mode 100644 dtmt.cfg create mode 100644 packages/dml.package create mode 100644 scripts/mods/dml/init.lua diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..552b17e --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,44 @@ +max_line_length = 120 + +include_files = { + "scripts/", +} + +ignore = { + "12.", -- ignore "Setting a read-only global variable/Setting a read-only field of a global variable." + "542", -- disable warnings for empty if branches. These are useful sometime and easy to notice otherwise. + "212/self", -- Disable unused self warnings. +} + +std = "+DT" + +stds["DT"] = { + read_globals = { + string = { fields = { "split" }}, + table = { fields = { + "merge", "table_to_array", "mirror_table", "tostring", "is_empty", "array_to_table", "reverse", "shuffle", + "merge_recursive", "unpack_map", "remove_unordered_items", "append", "mirror_array_inplace", "size", "dump", + "clear_array", "append_varargs", "find", "for_each", "crop", "mirror_array", "set", "create_copy", "clone", + "contains", "add_meta_logging", "table_as_sorted_string_arrays", "clone_instance", "max", "clear", "find_by_key", + }}, + math = { fields = { + "ease_exp", "lerp", "polar_to_cartesian", "smoothstep", "easeCubic", "round", "point_is_inside_2d_triangle", + "radians_to_degrees", "circular_to_square_coordinates", "uuid", "easeInCubic", "round_with_precision", + "clamp", "get_uniformly_random_point_inside_sector", "angle_lerp", "ease_out_exp", "rand_normal", + "bounce", "point_is_inside_2d_box", "catmullrom", "clamp_direction", "ease_in_exp", "random_seed", + "sign", "degrees_to_radians", "sirp", "ease_pulse", "cartesian_to_polar", "ease_out_quad", + "easeOutCubic", "radian_lerp", "auto_lerp", "rand_utf8_string", "point_is_inside_oobb", + }}, + Managers = { fields = { + "mod", "event", "chat" + }}, + Mods = { fields = { + lua = { fields = { "debug", "io", "ffi", "os" }}, + "original_require", + "require_store", + }}, + "Crashify","Keyboard","Mouse","Application","Color","Quarternion","Vector3","Vector2","RESOLUTION_LOOKUP", + "ModManager", "Utf8", "StateGame", "ResourcePackage", "class", "Gui", "fassert", "printf", "__print", "ffi", + }, +} + diff --git a/dtmt.cfg b/dtmt.cfg new file mode 100644 index 0000000..3831232 --- /dev/null +++ b/dtmt.cfg @@ -0,0 +1,16 @@ +id = "dml" +name = "Darktide Mod Loader" +description = "This is my new mod 'Darktide Mod Loader'!" +version = "0.1.0" + +resources = { + init = "scripts/mods/dml/init" +} + +packages = [ + "packages/dml" +] + +depends = [ + "dmf" +] diff --git a/packages/dml.package b/packages/dml.package new file mode 100644 index 0000000..1bd97ef --- /dev/null +++ b/packages/dml.package @@ -0,0 +1,3 @@ +lua = [ + "scripts/mods/dml/*" +] diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/scripts/mods/dml/init.lua @@ -0,0 +1 @@ + From d816cefae8c779be428c31fa97314695a5121806 Mon Sep 17 00:00:00 2001 From: Aussiemon Date: Sun, 26 Feb 2023 23:14:26 -0700 Subject: [PATCH 02/34] Initialize repo from files --- README.md | 21 ++ binaries/mod_loader | 470 ++++++++++++++++++++++++++++++ bundle/9ba626afa44a3aa3.patch_999 | Bin 0 -> 894 bytes mods/base/function/class.lua | 22 ++ mods/base/function/hook.lua | 276 ++++++++++++++++++ mods/base/function/require.lua | 35 +++ mods/base/mod_manager.lua | 417 ++++++++++++++++++++++++++ mods/mod_load_order.txt | 6 + toggle_darktide_mods.bat | 10 + tools/README.md | 5 + tools/dtkit-patch.exe | Bin 0 -> 199168 bytes 11 files changed, 1262 insertions(+) create mode 100644 README.md create mode 100644 binaries/mod_loader create mode 100644 bundle/9ba626afa44a3aa3.patch_999 create mode 100644 mods/base/function/class.lua create mode 100644 mods/base/function/hook.lua create mode 100644 mods/base/function/require.lua create mode 100644 mods/base/mod_manager.lua create mode 100644 mods/mod_load_order.txt create mode 100644 toggle_darktide_mods.bat create mode 100644 tools/README.md create mode 100644 tools/dtkit-patch.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f4fb26 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +Contains a small set of basic functionality required for loading other mods. It also handles initial setup and contains a mod_load_order.txt file for mod management. + +Installation: + 1. Copy the Darktide Mod Loader files to your game directory and overwrite existing. + 2. Run the "toggle_darktide_mods.bat" script in your game folder. + 3. Copy the Darktide Mod Framework files to your "mods" directory (/mods) and overwrite existing. + 3. Install other mods by downloading them from the Nexus site (https://www.nexusmods.com/warhammer40kdarktide) then adding them to "/mods/mod_load_order.txt" with a text editor. + +Disable mods: + * Disable individual mods by removing their name from your mods/mod_load_order.txt file. + * Run the "toggle_darktide_mods.bat" script at your game folder and choose to unpatch the bundle database to disable all mod loading. + +Uninstallation: + 1. Run the "toggle_darktide_mods.bat" script at your game folder and choose to unpatch the bundle database. + 2. Delete the mods and tools folders from your game directory. + 3. Delete the "mod_loader" file from /binaries. + 4. Delete the "9ba626afa44a3aa3.patch_999" file from /bundle. + +Mods will automatically disable when the game updates, so re-run the "toggle_darktide_mods.bat" script to re-enable mods after an update. + +This mod does not need to be added to your mod_load_order.txt file. \ No newline at end of file diff --git a/binaries/mod_loader b/binaries/mod_loader new file mode 100644 index 0000000..d9c7d65 --- /dev/null +++ b/binaries/mod_loader @@ -0,0 +1,470 @@ +local mod_directory = "./../mods" + +-- Mod initialization code -- +local debug = debug +local io = io +local ffi = ffi + +local assert = assert +local ipairs = ipairs +local loadstring = loadstring +local pairs = pairs +local pcall = pcall +local print = print +local rawget = rawget +local rawset = rawset +local select = select +local setmetatable = setmetatable +local string = string +local table = table +local tonumber = tonumber +local tostring = tostring + +Mods = { + file = {}, + message = {}, + lua = { + debug = debug, + io = io, + ffi = ffi, + loadstring = loadstring, + os = os + } +} + +local chat_sound = "wwise/events/ui/play_ui_click" + +local notify = function(message) + local event_manager = Managers and Managers.event + + if event_manager then + event_manager:trigger("event_add_notification_message", "default", message, nil, chat_sound) + end + + print(message) +end +Mods.message.notify = notify + +local echo = function(message, sender) + local chat_manager = Managers and Managers.chat + local event_manager = Managers and Managers.event + + if chat_manager and event_manager then + local message_obj = { + message_body = message, + is_current_user = false, + } + + local participant = { + displayname = sender or "SYSTEM", + } + + local message_sent = false + + local channel_handle, channel = next(chat_manager:connected_chat_channels()) + if channel then + event_manager:trigger("chat_manager_message_recieved", channel_handle, participant, message_obj) + message_sent = true + end + + if not message_sent then + notify(message) + return + end + end + + print(message) +end +Mods.message.echo = echo + +local get_file_path = function(local_path, file_name, file_extension) + local file_path = mod_directory + + if local_path and local_path ~= "" then + file_path = file_path .. "/" .. local_path + end + + if file_name and file_name ~= "" then + file_path = file_path .. "/" .. file_name + end + + if file_extension and file_extension ~= "" then + file_path = file_path .. "." .. file_extension + else + file_path = file_path .. ".lua" + end + + if string.find(file_path, "\\") then + file_path = string.gsub(file_path, "\\", "/") + end + + return file_path +end + +local function read_or_execute(file_path, args, return_type) + local f = io.open(file_path, "r") + + local result + if return_type == "lines" then + result = {} + for line in f:lines() do + if line then + -- Trim whitespace + line = line:gsub("^%s*(.-)%s*$", "%1") + + -- Handle empty lines and single-line comments + if line ~= "" and line:sub(1, 2) ~= "--" then + table.insert(result, line) + end + end + end + else + result = f:read("*all") + + -- Either execute the data or leave it unmodified + if return_type == "exec_result" or return_type == "exec_boolean" then + local func = loadstring(result, file_path) + result = func(args) + end + end + + f:close() + if return_type == "exec_boolean" then + return true + else + return result + end +end + +local function handle_io(local_path, file_name, file_extension, args, safe_call, return_type) + + local file_path = get_file_path(local_path, file_name, file_extension) + print("[Mod] Loading " .. file_path) + + -- Check for the existence of the path + local ff, err_io = io.open(file_path, "r") + if ff ~= nil then + ff:close() + + -- Initialize variables + local status, result + + -- If this is a safe call, wrap it in a pcall + if safe_call then + status, result = pcall(function () + return read_or_execute(file_path, args, return_type) + end) + + -- If status is failed, notify the user and return false + if not status then + notify("[Mod] Error processing '" .. file_path .. "': " .. tostring(result)) + return false + end + + -- If this isn't a safe call, load without a pcall + else + result = read_or_execute(file_path, args, return_type) + end + + return result + + -- If the initial open failed, report failure + else + notify("[Mod] Error opening '" .. file_path .. "': " .. tostring(err_io)) + return false + end +end + +local function exec(local_path, file_name, file_extension, args) + return handle_io(local_path, file_name, file_extension, args, true, "exec_boolean") +end +Mods.file.exec = exec + +local function exec_unsafe(local_path, file_name, file_extension, args) + return handle_io(local_path, file_name, file_extension, args, false, "exec_boolean") +end +Mods.file.exec_unsafe = exec_unsafe + +local function exec_with_return(local_path, file_name, file_extension, args) + return handle_io(local_path, file_name, file_extension, args, true, "exec_result") +end +Mods.file.exec_with_return = exec_with_return + +local function exec_unsafe_with_return(local_path, file_name, file_extension, args) + return handle_io(local_path, file_name, file_extension, args, false, "exec_result") +end +Mods.file.exec_unsafe_with_return = exec_unsafe_with_return + +local function mod_dofile(file_path, args) + return handle_io(file_path, nil, nil, args, true, "exec_result") +end +Mods.file.dofile = mod_dofile + +local function read_content(file_path, file_extension) + return handle_io(file_path, nil, file_extension, nil, true, "data") +end +Mods.file.read_content = read_content + +local function read_content_to_table(file_path, file_extension) + return handle_io(file_path, nil, file_extension, nil, true, "lines") +end +Mods.file.read_content_to_table = read_content_to_table + +local file_exists = function(name) + print(name) + local f = io.open(name,"r") + + if f ~= nil then + print("[Mod]: File exists") + io.close(f) + return true + else + print("[Mod]: File does not exist") + return false + end +end +Mods.file.exists = file_exists + +-- Load remaining base modules +exec("base/function", "require") + +local init_mod_framework = function() + + print("[DMF]: Initializing basic mod hook system...") + exec("base/function", "hook") + + -- The mod manager isn't in the bundles, so load our version from the mods folder + local ModManager = exec_with_return("base", "mod_manager") + + -- Initialize mods after loading managers and state_game files + Mods.hook.set("Base", "StateRequireScripts._require_scripts", function (req_func, ...) + local req_result = req_func(...) + + Managers.mod = Managers.mod or ModManager:new() + + -- Update mod manager + Mods.hook.set("Base", "StateGame.update", function (func, self, dt, ...) + Managers.mod:update(dt) + + return func(self, dt, ...) + end) + + -- Skip splash view + Mods.hook.set("Base", "StateSplash.on_enter", function (func, self, ...) + local result = func(self, ...) + + self._should_skip = true + self._continue = true + + return result + end) + + -- Trigger state change events + Mods.hook.set("Base", "GameStateMachine._change_state", function (func, self, ...) + local old_state = self._state + local old_state_name = old_state and self:current_state_name() + + if old_state_name then + Managers.mod:on_game_state_changed("exit", old_state_name, old_state) + end + + local result = func(self, ...) + + local new_state = self._state + local new_state_name = new_state and self:current_state_name() + + if new_state_name then + Managers.mod:on_game_state_changed("enter", new_state_name, new_state) + end + + return result + end) + + -- Trigger ending state change event + Mods.hook.set("Base", "GameStateMachine.destroy", function (func, self, ...) + local old_state = self._state + local old_state_name = old_state and self:current_state_name() + + if old_state_name then + Managers.mod:on_game_state_changed("exit", old_state_name) + end + + return func(self, ...) + end) + + return req_result + end) + + -- Set crashify modded property + Mods.hook.set("Base", "StateRequireScripts._get_is_modded", function () + return true + end) +end + +-- Original main script +Main = Main or {} + +require("scripts/boot_init") +require("scripts/foundation/utilities/class") + +-- Expose classes at the global table +exec("base/function", "class") + +require("scripts/foundation/utilities/patches") +require("scripts/foundation/utilities/settings") +require("scripts/foundation/utilities/table") + +local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") +local LocalizationManager = require("scripts/managers/localization/localization_manager") +local PackageManager = require("scripts/foundation/managers/package/package_manager") +local PackageManagerEditor = require("scripts/foundation/managers/package/package_manager_editor") +local ParameterResolver = require("scripts/foundation/utilities/parameters/parameter_resolver") +local StateBoot = require("scripts/game_states/state_boot") +local StateLoadAudioSettings = require("scripts/game_states/boot/state_load_audio_settings") +local StateLoadBootAssets = require("scripts/game_states/boot/state_load_boot_assets") + +-- Unused right now, but might be useful in case we want to re-add the state at some point +local StateLoadMods = require("scripts/game_states/boot/state_load_mods") + +local StateLoadRenderSettings = require("scripts/game_states/boot/state_load_render_settings") +local StateRequireScripts = require("scripts/game_states/boot/state_require_scripts") + +Main.init = function (self) + Script.configure_garbage_collection(Script.ACCEPTABLE_GARBAGE, 0.1, Script.MAXIMUM_GARBAGE, 0.5, Script.FORCE_FULL_COLLECT_GARBAGE_LEVEL, 1, Script.MINIMUM_COLLECT_TIME_MS, 0.1, Script.MAXIMUM_COLLECT_TIME_MS, 0.5) + ParameterResolver.resolve_command_line() + ParameterResolver.resolve_game_parameters() + ParameterResolver.resolve_dev_parameters() + Application.set_time_step_policy("throttle", DEDICATED_SERVER and 1 / GameParameters.fixed_time_step or 30) + + if type(GameParameters.window_title) == "string" and GameParameters.window_title ~= "" then + Window.set_title(GameParameters.window_title) + end + + local package_manager = LEVEL_EDITOR_TEST and PackageManagerEditor:new() or PackageManager:new() + local localization_manager = LocalizationManager:new() + local params = { + next_state = "StateGame", + index_offset = 1, + states = { + { + StateLoadBootAssets, + { + package_manager = package_manager, + localization_manager = localization_manager + } + }, + { + StateRequireScripts, + { + package_manager = package_manager + } + }, + { + StateLoadAudioSettings, + {} + } + }, + package_manager = package_manager, + localization_manager = localization_manager + } + + if PLATFORM == "win32" and not LEVEL_EDITOR_TEST then + table.insert(params.states, 1, { + StateLoadRenderSettings, + {} + }) + end + + if LEVEL_EDITOR_TEST then + Wwise.load_bank("wwise/world_sound_fx") + end + + self._package_manager = package_manager + self._sm = GameStateMachine:new(nil, StateBoot, params) + + -- ####################### + -- ## Mod intialization ## + init_mod_framework() + -- ####################### +end + +Main.update = function (self, dt) + self._sm:update(dt) +end + +Main.render = function (self) + self._sm:render() +end + +Main.on_reload = function (self, refreshed_resources) + self._sm:on_reload(refreshed_resources) +end + +Main.on_close = function (self) + local should_close = self._sm:on_close() + + return should_close +end + +Main.shutdown = function (self) + Application.force_silent_exit_policy() + if rawget(_G, "Crashify") then + Crashify.print_property("shutdown", true) + end + + local owns_package_manager = true + + if rawget(_G, "Managers") and Managers.package then + Managers.package:shutdown_has_started() + + owns_package_manager = false + end + + self._sm:destroy() + + if owns_package_manager then + self._package_manager:delete() + end +end + +function init() + Main:init() +end + +function update(dt) + Main:update(dt) +end + +function render() + Main:render() +end + +function on_reload(refreshed_resources) + Main:on_reload(refreshed_resources) +end + +function on_activate(active) + print("LUA window => " .. (active and "ACTIVATED" or "DEACTIVATED")) + + if active and rawget(_G, "Managers") and Managers.dlc then + Managers.dlc:evaluate_consumables() + end +end + +function on_close() + local should_close = Main:on_close() + + if should_close then + Application.force_silent_exit_policy() + + if rawget(_G, "Crashify") then + Crashify.print_property("shutdown", true) + end + end + + return should_close +end + +function shutdown() + Main:shutdown() +end diff --git a/bundle/9ba626afa44a3aa3.patch_999 b/bundle/9ba626afa44a3aa3.patch_999 new file mode 100644 index 0000000000000000000000000000000000000000..a0e417c3603d7b637debd4e25337c888a003c94d GIT binary patch literal 894 zcmd;JVEDkyz`(!=#4b9{>~9WR?pozPn?7+zIzo<+G;sp)6g$DVxgk-(kVY7fir30Nn)a?2()y4e-*xUnb(eLG^<;=!q; z`l@$ZuBb#f%Pys)hbx2910+wybP6~y2??s`&rH!$P}bOaz@bfI6^n?>k2OjgW_@4t z%=R>En^vQ~xWFfl37_Y^>&n?{`23t{XwIa`uXl83%kGXS=MTMeB~dIYHG8#O_{_;c zKbyrbW^CDQKF?>_=Gx%bJ2S84FAUGHei6!6zu)$(9rFSH2i6TQr+;5py~g~B;sK!t zQye*2+!@>zcCs{ox+d~Yk=y9{4;P)5PrjSoCbn+<)0}R`SjcpNDM&@2=JsZvli!0F zw>nsO2&`bWDP>uq>~Qy6qj=(JZ_N)uN->NU+srGY?!-q#+lLeug{QpkwJd*^89d!; z%>?Pi%&~Lx#KSg+WC^$|zWDB5h0)A&-&S8$4qx4O$~m2XbJ_8yY3vNiF+8^yhBIrV z2ep-ITcw5B-t4#B*mS~sbc(6e(LeTNsITps9 z*5{{Q&06^|O?B(0Eva?uHgC^x*)c^>EY-+CS4H(fqeesI{s;Swwmkb(HXoFBl=sQ} Y|F3WU 0 and Managers.chat then + for i = 1, num_delayed_prints, 1 do + Mods.message.echo(chat_print_buffer[i]) + + chat_print_buffer[i] = nil + end + end + + local old_state = self._state + + if self._settings.developer_mode and self:_check_reload() then + self._reload_requested = true + end + + if self._reload_requested and self._state == "done" then + self:_reload_mods() + end + + if self._state == "done" then + if self._num_mods then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.enabled and not mod.callbacks_disabled then + self:_run_callback(mod, "update", dt) + end + end + end + + elseif self._state == "scanning" then + self:_build_mod_table() + self._state = self:_load_mod(1) + self._ui_time = 0 + + elseif self._state == "loading" then + local mod = self._mods[self._mod_load_index] + local mod_data = mod.data + + mod.state = "running" + local ok, object = pcall(mod_data.run) + + if not ok then + self:print("error", "%s", object) + end + + local name = mod.name + mod.object = object or {} + + self:_run_callback(mod, "init", self._reload_data[mod.id]) + self:print("info", "%s loaded.", name) + + self._state = self:_load_mod(self._mod_load_index + 1) + end + + local gui = self._gui + + if gui then + self:_draw_state_to_gui(gui, dt) + end + + if old_state ~= self._state then + self:print("info", "%s -> %s", old_state, self._state) + end +end + +ModManager.all_mods_loaded = function (self) + return self._state == "done" +end + +ModManager.destroy = function (self) + self:unload_all_mods() +end + +ModManager._run_callback = function (self, mod, callback_name, ...) + local object = mod.object + local cb = object[callback_name] + + if not cb then + return + end + + local success, val = pcall(cb, object, ...) + + if success then + return val + else + self:print("error", "%s", val or "[unknown error]") + self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", + callback_name, mod.name, mod.id) + + mod.callbacks_disabled = true + end +end + +ModManager._build_mod_table = function (self) + fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") + + -- Get the mods' load order from mod_load_order file + local mod_load_order = _io.read_content_to_table("mod_load_order", "txt") + if not mod_load_order then + print("ERROR executing mod_load_order: " .. tostring(mod_load_order)) + mod_load_order = {} + end + + -- Add DMF to the mod load order + table.insert(mod_load_order, 1, "dmf") + + -- Read the .mod files of given mods and, if everything's fine, add mods' entries to the mods list. + for i = 1, #mod_load_order do + local mod_name = mod_load_order[i] + self._mods[i] = { + state = "not_loaded", + callbacks_disabled = false, + id = i, + name = mod_name, + enabled = true, + handle = mod_name, + loaded_packages = {} + } + end + + self._num_mods = #self._mods +end + +ModManager._load_mod = function (self, index) + self._ui_time = 0 + local mods = self._mods + local mod = mods[index] + + while mod and not mod.enabled do + index = index + 1 + mod = mods[index] + end + + if not mod then + table.clear(self._reload_data) + return "done" + end + + local id = mod.id + local mod_name = mod.name + + self:print("info", "loading mod %s", id) + Crashify.print_property("modded", true) + + local mod_data = _io.exec_with_return(mod_name, mod_name, "mod") + + if not mod_data then + self:print("error", "Mod file is invalid or missing. Mod %q with id %d skipped.", mod.name, mod.id) + + mod.enabled = false + + return self:_load_mod(index + 1) + end + self:print("spew", "\n%s\n", mod_data) + + mod.data = mod_data + mod.name = mod.name or mod_data.NAME or "Mod " .. mod.id + mod.state = "loading" + + Crashify.print_property(string.format("Mod:%s:%s", id, mod.name), true) + + self._mod_load_index = index + + return "loading" +end + +ModManager.unload_all_mods = function (self) + if self._state ~= "done" then + self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + + return + end + + self:print("info", "Unload all mod packages") + + for i = self._num_mods, 1, -1 do + local mod = self._mods[i] + + if mod and mod.enabled then + self:unload_mod(i) + end + + self._mods[i] = nil + end + + self._num_mods = nil + self._state = "unloaded" +end + +ModManager.unload_mod = function (self, index) + local mod = self._mods[index] + + if mod then + self:print("info", "Unloading %q.", mod.name) + self:_run_callback(mod, "on_unload") + + mod.state = "not_loaded" + else + self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + end +end + +ModManager._reload_mods = function (self) + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.state == "running" then + self:print("info", "reloading %s", mod.name) + + self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") + else + self:print("info", "not reloading mod, state: %s", mod.state) + end + end + + self:unload_all_mods() + self._state = "scanning" + self._reload_requested = false + + Mods.message.notify("Mods reloaded.") +end + +ModManager.on_game_state_changed = function (self, status, state_name, state_object) + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.enabled and not mod.callbacks_disabled then + self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) + end + end + else + self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + end +end + +ModManager._visit = function (self, mod_list, visited, sorted, mod_data) + self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id) + + if visited[mod_data] then + return mod_data.enabled + end + + if visited[mod_data] ~= nil then + self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id) + + return false + end + + visited[mod_data] = false + local enabled = mod_data.enabled or false + + for i = 1, mod_data.num_children or 0 do + local child_id = mod_data.children[i] + local child_index = table.find_by_key(mod_list, "id", child_id) + local child_mod_data = mod_list[child_index] + + if not child_mod_data then + self:print("warning", "Mod with id %d not found", child_id) + elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then + self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.", + mod_data.name, mod_data.id, child_id) + + enabled = false + end + end + + mod_data.enabled = enabled + visited[mod_data] = true + sorted[#sorted + 1] = mod_data + + return enabled +end + +ModManager.print = function (self, level, str, ...) + local message = string.format("[ModManager][" .. level .. "] " .. tostring(str), ...) + local log_level = LOG_LEVELS[level] or 99 + + if log_level <= 2 then + print(message) + end + + if log_level <= self._settings.log_level then + self._chat_print_buffer[#self._chat_print_buffer + 1] = message + end +end + +ModManager.network_bind = function (self) + return +end + +ModManager.network_unbind = function (self) + return +end + +ModManager.network_is_occupied = function (self) + return false +end + +ModManager.network_send = function (self) + return +end + +ModManager.rpc_mod_user_data = function (self) + return +end + +ModManager.register_network_event_delegate = function (self) + return +end + +ModManager.unregister_network_event_delegate = function (self) + return +end + +ModManager.network_context_created = function (self) + return +end + +return ModManager diff --git a/mods/mod_load_order.txt b/mods/mod_load_order.txt new file mode 100644 index 0000000..086a2e7 --- /dev/null +++ b/mods/mod_load_order.txt @@ -0,0 +1,6 @@ +-- ################################################################ +-- Enter user mod names below, separated by line. +-- Order in the list determines the order in which mods are loaded. +-- Do not rename a mod's folders. +-- You do not need to include 'base' or 'dmf' mod folders. +-- ################################################################ diff --git a/toggle_darktide_mods.bat b/toggle_darktide_mods.bat new file mode 100644 index 0000000..abfed86 --- /dev/null +++ b/toggle_darktide_mods.bat @@ -0,0 +1,10 @@ +@echo off +echo Starting Darktide patcher... +.\tools\dtkit-patch --toggle .\bundle +if errorlevel 1 goto failure +pause +exit +:failure +echo Error patching the Darktide bundle database. See logs. +pause +exit \ No newline at end of file diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..bb184d4 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,5 @@ +# dtkit-patch + +Simple tool based on Aussiemon's nodejs script for patching `bundle_database.data` to load Darktide mods. + +https://github.com/ManShanko/dtkit-patch \ No newline at end of file diff --git a/tools/dtkit-patch.exe b/tools/dtkit-patch.exe new file mode 100644 index 0000000000000000000000000000000000000000..2306a5c2c4273307e5c40de8254580d273234728 GIT binary patch literal 199168 zcmeFa33OCd*7#eM0TM{O0f~a56k1^7fQh0c8Z-q7+|ml5GCAWXB973c3UB}mD$!iZ z>ZWb|+8+Gc|LaNn({5}P0uH1CBmqSRClH5#1Gg-Z-~a)HdcS?{t)vo%w(YmpTW_rg zD|PQZ_ndw9*=L`9_TFcobE~de>PUAu92xv25)MZ_SNT`2et-Cz%%hgD1|srP?)n&a9i~+;E5M#vAUubC%!rUpKkt1nzX*a;K|e{1vV{W=+3oSZ;1F zw+;I3=l_)bt2Z`WmHhkdQ&(Q~8=g;m_LZxDsji!@zF%EmyZX24x?ZKXy!gu15A*!1 zH(t4_MO`;sCHWs(*>d$mTra=n#+d@MdkZF1IvmsQ&vcBBAHFg9uFY|Zqj%ad$2rEO zIUK93)TI}3pQrB2opOIw1qm+qLhC|SrZOR5DVd69f}irhNN_M|yl z)Sdm42bBDjgramup1@z3=9rk0oV=a6D$P;A!!+J!DMalz)6yL6;QM|5hWT%r@8{*K zRdze2?RGnoWn6$AHfQ<`{u>;Q`HzwcOvi7zUfvaOIqWjb%I0`fy@Dok{p?%$8ivib zGO9MxR>yJV`}|w^%7@Ll>GoOlkZLJQF8RnRJD|=(DM?QzrM)ns918o9v+jYhbOyM$}sV0%iq@{BahO#`i;Z?pgWKHjJR&>O?;w<8ZvYv z8b3>qT;kMgx9VEO2HhFeL(#1GfN!Ox#WP`Uhr?GI(;oPhK#Pw-wJmT$hxFSxG-N#cxAg zJ>4gjzM|JA{CP{#?FMKMH)OTM(~2AP$nV|#C0kf!>+Lgc%#s#ugBH@F7HQG8b?tpF zb@4MA!jCg`EwZBSLAQvGx*GRy6u^UeWTJbTS>tg69rk(0yo=i@qOGcUs}zaWs>uikKldywkbC05-4m%1yKSQy2= zKAK3B^@2T{J*$PWDzu9Bc)Djrnoh|AJyhcG_tV3K3N8_1#^ux^J{l4(X>wIYrJybTr;iq`T2y*2<%S2PW)suKz4>h^; z(k}xactaOC{DZ2(WBT*@Vf7j@;JTV-(Ih=Fq(4wZG9;7MOr9d*LJyZ3?V;B6P`fiQ zUXRR6=;87FjqRC$1;_T(L-Vs7{*!#+9^B>inUhf!E^nuXb5sq9{KiYcEu4F*dh{A? zR8VmJbyW8xFO(p<{<<~6yZWbTH4i2rLa3DPP(%ple$I?iyp_BQ$O}3`i3z$;;u0D$ zioQGO^yllf>jV5W1Q1jw?RbnLiTH&)3+v~l@plRQl6SdFIFX@KD367t7m{S`Gq1eB z;fS9JIGO|0`B49$IlN8yb&GEF(2dEYyyG?6%@2xbim@<{CQN{AN`TtkqZ@DO#wK6* zE|>I>FZ{Fq-blZbz2TmE;by(GZBBvieBT=$pQSrr(Va33s*E=C5e09a!0Vv7&o~H? z%-ct$IpVJ>EUzq$h~e|H+t&E^;t8T$D(yK zj}>`>{PZFHj6-Ib3;b*M`}@)#zlsZKy~eggzUvZ_n=9Npc^%&HF?!hVF3}OSkmrla z;s(#k0Zi{^pCM@uqgf=Uu7~b*7daH1OKmu9iTrI|A4!Zc1jd>QP$*;m>y=M25{13&#SCToeNCz|VW^7KR_ zX@NGcaUDG}S;$skL6vU2z(eif$$9bSZu8m`(15;CEoAry^+@J_qy31u4bouUNGIi+ zD#3j;gc~h{8?6W@0(FAV*eO!WF^CM!N+Iz=9}tikq&W(NU>7oddMadE2RHW8!&VLP z-a4~a^h2ofN}7;vfm|RryDUJ z1qugSL=mDn0&`>%o^3kpL}>|>T7i)Efk6XL45108^j{*sbxfKg_B#>ak=!cUvuY^s z%^TxR2UD@pXx9B2Spq!9l(Wf0@4f{a%G^UjUQ;;%6#)ug038j08M1jQ~b_XeI-a;iZoRPSvsAl<$ zBGs(NRdZ4r{){9ekk&>wzF^b_gN`~JkTW-DRZdb@1G#8!7Y|C)0@HA(* z9a#eAcK5x_SapKG~{LxXP|_wpC@ot};`qR72tGldQaIC|Kw

NyTwexNtEqN_H(QK0J zmUJ{%$vr@Ha~XF_(WjoEmM;2~lxr1mh?k{Y)wq^E=or^^HA*&gQs^A2e(fWRd`x0j zOYJsKEx8I|31!UB2BtZJhjX-=O^nWXUzM25WNzZ6fD2L$gZn9aaM$q!=xPNzk)ICS z*izn2>YP@@S~5jKGa`dz2g&Cr$a8b8&V|g0ME+K0aNTG^uQzJN(&l9r7dVyVTCsDP zinSUolgd|IG=sUaCY^hD(ieHYm5HXx=;3W1lO@BmDmhMzcaYJ4EjnG=j8G!m-%ney zDRj`~&j}qoC6KfK_27rj`1$+SIQ14&!H|GxyVwxUO<-R1u()Sm6%tA}5bB6_51nP?Daoh4)JrT&6_BqGf!-f9+1 zcQQ8rEKj0~XL;#s#Ki^_flNJe?J_zuoxe*FAANjz@|(e29`zbzQ_X$aMbRpw(&25c zwA)*m_kuJW!{fa5@F2z=`00^xjGrcK$u@KcMKUkq3`(35D6Fi3g1?8c$SA~=H_{x8 z&oqilo{h0G^&XOC`?H~&R{L`*k&?BaNxy_2D^M+y#=c{gpD84JM0M4Lk4W zgRitr3$5b?vmu=?y5<$b5ujm(KgSGpc*FVaG9&{inh zPf8103{y5nB2QcV7e%;(KUlsxqqreHRuAp^=8{OxXdD)^i=}Vf)hXs)W=>L22BHt3NIKsDrf86U;#C2N+-N+(BM-qH<$&#Ig) zzS2!v%_2w@+O5qP1x>FJn(B#7fqk)Cg@XSt;8a!mn*Uf&=#vzLyouKW$QQ*|<@=kR zTFpc>FX7#b9NHsmy7S9pOzd{Qme+ltM}{x#U^c}zu?Nu;l_y6@FP~AOHs57*93+p^4}vh)PA>CTapVF&K;^Qy>yMPjoyaw z6Dy@oZSlGMR+X;R7EhC0k%$`2#;4|r(?sArFONun88%R@vZ1N6#c8xzUI4B3K}n5_fkLV@o+w827%b1{Tc}n10=06Zhs!PZ z0LV)%-{B?5QCKAMe}AEj``jb%S`#`Z(GpVPLzw_nI~gUF^aK0+oQr;ct&r*^nQ5t1iEbu~Xlpmw+%pe=89ntx6Z|KVJ@yh!s-g??nGJ9xbO8~+ zhG`%4S zOW@kzJ>tl@y%#m1G5}rtuwJNL7?DXt*j8}ksuG?5befxv6_G$1t@>@r7d zC|>{ISubqSqh==JJYzMeI2beSx*9+Xm-OQOdTF!vP=gj4C6y+c8KL4AE3a#U=hMYm zoC|W&GqZ;8PJzUkG8#?F}$i&^pQBgO$e^`l%up)q=4=^SSL?Ls4wAZ-E zYmC977$cUnkj`#PUb1a6@k2{rctL|Nd`G>{Ncal(>Y?UV&x=x`ct5kqFkk6Gt>#H- zh&VfpM?}V0gep`*Z^@PY!C~ z=vNfE>aHm&39Axe1%Mo-#>ApBZ(^;___N|!@dtuZY&PU$%Jo89HCrRbcbR`jOa@v!`yrbsqGC1ij+nx{)DRVf&y$>3K*6LiQ!Zf`!s%w=S;=bVFJ%PbI`7a@tB>O$VyPbc~9!LWQ zIq9q|@u3|>Dz;!ymo{pTM0Mwu;NiA8y>SkfMzu#8c@~AUG#Xf{hmQ6P++2UwS+j5O zoZEBd4gMQvdd?l6o*mU{pMvNhoId9iZ+Mbk6|S5>&k8$=F?0$pdQWg6>{~kva{3|x z+Pu$v9yYO(Ycx2XjG!J+~)5XIy@veKYm! zXrMSAl9&EjH{X2wP2SR;0at7mT|m+uH-V`~TQNRk(LVnT4hOi_Y!v8`%rjY+)GZ|O zMfxJ=y&XdfGv;RTu0Y;l8Ssd`lNlW;S!F1)Hh>2U0K8VZI%CwhP&cYwmdD6v+=Cgs z-Ms#HNU}&~iGtI^hPq3@0D?^Ylcio3?)4ef_y(+@K17u~@00nGx0<}wlGhg*c}<;o z#qV+HQiYwl{kN)`!KxYoY-R42Ypdi+fz|ExvlfuD9|jV!a*g}pTsQx4Rk{V_U*LO+ zmFv_WuIiiL{9vozlh={Vqa8H=PSUKfa^?MSRll@yo$|xEZnJV#{BW*HE7x`3lPhdn zfK|)P6Uk{y>|bUxTkVk5EpafP^QO#QEEXp6SBp|_uyj>mnaPXRnhZK|-P-FZ*RJ^v zVX8!&Oc+X4lTkhLJ4f|(!0Hr&1(sw~laD>e6mO0FS{!3CKF56T_|$6i{}mv!iK!|! zL10DlA1)P6SI+v^`&&BS8w|Pc!X%cfidaME^meD3B?GKEG3^>^4r!{D$vG|V@r5hR zP8mb%lT(YzriT`^I{X^cedBdYGnjr3YMYwbky{hcd{+X;VFnOpfEGc`3?qWNIqlBC z?YHBEkU2o|*rftF$yt5=C(Ubbh@4R~w)yVIG{9cocy6oEM=fu7Q1}^ReM{tGiO1CzqS-z) z+6CNJ8=NI?&qX?Rf$+ZyLgpKGE`UpQK>1>E3d*p%xhSQs(juisD%VXo_pe(lW*H&t zO(8XX(SYc&^Ruc1f^kHA;=+F5hR8gq*i{d@Y(Vt{1diHc(N-KCdy^j-fOsqSC* zo$!q5`bhPiR&$dq6ohlX+T789i_Qj>agrR#{rEZtT_p4KyYaFYzyjvG$73a+5218L z)DZC+(%BmsoZ=LBQKqa$%H(cE2(3Cp$~=04tkRNeHDFlMNAK|(4dx<1X}!(U2jZS# z#YU-&8}qHKxwl&tKg|NKRq>4~w_UL`!K%27isQ99E8=-w9EPFK(7OK?q=;8?0qKzK@yczW!+e)?n=w4^}bi;L#>4>?18cR}VjBuZ6s*0#RgHqLm!7 zwn;Z|ea>2@VEx!7OG}Kj*7IfDKd2yy$7P!cCR;8K+}JU$pFD$XEP9HvkZC8kC-NVX zhh-xRSd~pyMhJlEgOA-)H__c65dViH zmZ|>4#e=*X;6d^Xj5$@$SQSX^E_pV@6z^7-K*I|!mkhU=^F9;y20+PjOb2pOs~DH= za(F@NV{P#=Zlz9cCt`)VOF(CW@Pss4RkJKvYBsQAPOgMV>8zIN=2rpXS=32a)i%q#*FzyEX#fMX^{&Q90R=FZA%adU!PJ$Sf9u$7tLj;4nc5A+sN0TjG&F z-L6Hel`idQ>Lou^lZ^ih?m&h|`VdRGfk@Vo5k} zxTuN{A8&YmUX@W)Wz5WC#sz?Ej3&?XPCddhD)DF?W_r$Z3(CG+kv8)U4 zB*o`@%|e?h12J{$P?p7N_9v_N50LeDA^J*{$0m9Oxsrr^T)m)A6ZvPU!h8G!Eh2Uf zqc{o{iS!0=>pk0Hu|hy?Zapn0L_ zm!bQzKujc*%s-E-SAOC=56!Y7N!A^_PX&D5PKX6opB7prS@6=#8D8I0ZVTI9o+#Iq zf4tz5KmB9qUw)^TRspeDct5dDkMt{&WjnmXp+ge`AJWi`(n2C39A{BR4GK*Dv?C=;B-7sQD)f^>Q&TbCDzx z$ziTBoR0G4&e#|MzDoQ_Gp{c#g+97r>fsW-_-$?ZRD4Tcmg?DuSrJaG*I6E7VY7NC z4Lu=AGHr1tKBjdCXJtNgTAG7v?r*uO1=QHOgL^OQwQk?u0qb_gx31lF*YR7mn$3y= zEiBLCZ#|*<-B=SHUs>~!pUA)b<1ser{mZ!&ulFc>Fp)oJh%9aN2n<^ry?a(q`58}# zvHDIYFn=Aq&uRWb+F-1~*piizsYulLmCzzfXo1FI)2$DLmB&Yg6xeG&L@7qI#SorO ziv6-qQLK1JY`+qh-zX;Q4>Xu;l!A6Ulavbg5f1Y#p}z0}BIqU&u*W3hQ{o@RsQG%; zu~^%<<3&B0vDOkvjGQkec#5fqSmw<8R8P}XELhc<(sQ!lkI=}!JRq&R%oV!^Y*@K| ziq838?SW~uUZi`uUiyW$_(pk(+%DmHRe1^-qElw;gCb!e#0ljA zTM^}x5xasF2tVi!N+I#Pmp1xc5}TQRSyu5Ly>u@j(3OaQQZo=sPERmBYl<$_;7C6&T%3+%4_!f2Y5plJN(C1g~vk3+&k>IMqIluAyAF27K0FHQ-)B;5!?z!On3N zIT-sR5qHR_;o}9RFU+U1Wj-R|RF$yL{9R8;c(sdhzUVV)RWh1~WzPu+J=R8E)tRn{ zy6!SvWt}cU@Q%$mJ*hx6q;_bZDr?>%hwb~ebG#)v!fFH(UvVm8?Q6 zKpSDjF|wdd1v*YDF{fNEtK=gjWKBekpY@kEofu%B5pe0owRos1i&$9^o;s7P$PHcx z+Jym;8K`bwc+|peBL9w)#WU$2k#x#Fwz`9pPv=#V!SA<|wU#j$k{{$6vj=al}IqAlXMY^*k(Yj=89@btY^OXW2@+9aX@q=$RLI!Jw z?W^oZ2=>DhZ2ZzCW3@N|w+HoT%w;}|o1>;7aA7=KH`-Zlbg`Hf$vmzbtW^0vXBG&^ zj4i`2e=*iysLajKt3o=+&^hO2i*p+XO1}2w&`EWAf0u^`Z>Dk+(dRC6+yMvia(0+= zhp+qy+&Na*oWJY%do|^s#n)`TW*^Bg=0tl?+ZefD7?xRh1wl~YvQ*v5tkdA z?VSeK=@E&*s3a=<60Var6X>pX98_{=hZV5q2HSsY=Ry687UlI5(NZ(i$Oz~DQ6(~D z68S}>ucR%GRnnj4m1u;D4Dg_%*>T4Wx7)ljMSx4Drbxg^-Bq zckoW)js&B?xD+f$JF#eFg#u`;cI_tjZB{Q-ZleHup_;T>tSYOj_+WuI3V4<{@EPPX zu5G212__EDa%V+dU>+V&iFO+WSIBfjutzxaJ$mORJ)ARSD*5zqMpRoKEa8?tApSE% ziR%Z;G*!-ZnkT-3O`rL6_xw#NyOZjcg)@KA{jDCVFAxm0S~M++-}$W!%|Xckzp`vo zUu5N-rt*&ZR$k*#$)H9n8GnqzhL%`Dd1-#5WaK1{UZmPWPYHROxV1 ztQ@F+@d{MS{d^S}P(XW6luKnf@scH(gbRs4knw4`eA$3=t<3ay%;-c%RV~9(p^%w& zn|h_NvXb)0OKw4Kf5PdAv96TL)_o=IBhmmnY;q?)cQX-!&xvC!d{*TF%&bj^yH=37 zsj>`FAYCox&R69u>U2Y=`qr-K_zcKPJpUjH8kMcw7V9bplV_!E%ic;8a2LFa>gKDJs50wusFdc#X{gv~a z{sQKtR1lD5n|T9tkjd&>FY3PI#lNmEoH=zxHzA})>O?A-BjAwkHT{_OFEdBdCx|UZ zY!@_tO#6ci<~sb@w6wuI@KfqHi;>`3Z=v>&qPOY%H}h|C{Cn{K&%c+S*^HUD+v?E9 zwXjUMs@+^moNf0m5w)bt_Bhf1o`;cZ*W;}U9qm&+OqOWd=?vNa#x}oCj!h#J(d1Xe zcA3%-GGov2NbWgXb?1TN9jZto{{=e}`-cfW930gPUz6EO%&Ur`c7*c|u~#g}YdRGq zuhD`}r*oI$g8)}M3{M0e*}TRU$wK^p^2H_A3mb%Nh%|rwtc*C7!t+#y?#96~KJg)l z9Z+r!-U_!ctB8QHH(77d1lXrzCFcK4ieRl5m?eigz`bh0XWC1`m))S)tH~yG+Yxw2 zK3n=z|B08_E+RVyUpl+nd!cO_%7mMIr8`?oL*M2dSZIb9lR-l|{+18UZU0Eh^Oc}Ep%q~>MS9%HX4Jd-Fsj&0c{rx z`wSo~73fe{dZtKdUm`t&iZ8cEvM zi|!4uxn24`N<-JGc2rVo?Z~8L`Da2y$N;?wG%2-+@%Al?> zPz&im*@>Cqjg%M6(DpX)w_Y3M_)?oSp_QMhZ8NmnU!S49(a1xdB;?r%8?1o!=5GS8 z`<#vBXqlnSj(*NfbcU`iY-WFZs{Bg78z*EV{*(=YFBNY76G2*=*VC)^1!wBcM0Iv? zLg5-eSGeUgt?vJTW(Ut~mpy7Q({UXsQ!o*<-l~WlxyC9*(&J-X+m2S*rseg}^2X(9 zd6QjQ;iOiruxhhbSlNJm5*3vWiy3XJ!F-YPCa~+W>7)6`7^P%AicOo;&dRuFMt+Gh zl7H2?U}nxcB=wsv9N)IU?lwPKor_)okTln>wHUc$zpKw^l09jVETrCb0 zd7+21&FSE6mFPNvct2A{3?raG%S+SP3$3vgS}TOJwL-XGE0nPj$q!@YDRJbBkYLsz zSfnX4yMnXjd6AM2iw*^;wYpGt-Rc(}M5O)oEK{(E5q(g zE-1f?5)KA!^+{2)M@~Z>$>qV_7ncV&1_32SFC#O|18SUhE zUz`)<;jGjW!&G2!d_>gj>+YG;mdu-z%$r(pSZcwuSE~`j-u3Wx(k1LhnS)Wuuy$@2 z-{P6#-!Z?CPC2X_WU=%HIz;wx%gYzOln%KrFTP%NNQ(U(`m9_=eL17P9dW@Jrr6tT z+ZMSx*k<go{gt_9R4PMHD94V8>%DSP1B4M{{ifEqtE)Z{-GzAH z#7-EzMLW$Zk;kP6{8th3&Bm^t+KSD_nnfQv+8vHXhaKApU4L!Sp~Mb{W8Nit={|p@ zwxUmDiNhrul-4gg==jFrm{Ux!aK?}X_THjh@=GxN2?&-vJ-gB8JQRO~0l<#zF|bu- zt4Hh8BYu+XS`!qlXf^gGR+*}8=6y6(n5w*+_L(YAzNNpq7-i+aXvIBXnq@@N2?$Xb zh#pjSQ&QH-RaTy?(OIq=uM6jgXEQkG=jmbgW6Kgu{>j6W2619Se_4PofM1GOwl7BO zNccw*WII<8SI!!d#lNoln(U2FG^u4mwTb`kp|q53IUj4w0rQL#X>+o8h#8ht%qc37 z{HM=NsnLjv3S!Yfj*>{l{#87V+2K(|Pvix1wB^}PxB|JcB^*z{eQawv-utuo=~BST zY~>W|i1M+S71q^(y#)oj@JIY;m*V#Piq_)`52!y40NzE1^SEk@U*KV-N~yo}%KJ;+ zT6f2AUnKu_#6&7ReC*sm&Wob z?s-)v=du}w&n^?m$zZ!x@hWMdUWkmEC`y2jt-+DZUti5^V66YAv$A--N&L5xgfhij z{MB)0PUtgT9hkFGR`u)2SF_cBab>u&sKSVbb~;Jv<5(>rXB;@>wD8@+N}Hru26m8h#A_QJ7#Fp zx6ja~?f62Qz2gmLc=_4!4zv6=vRpGFjXZ0}6D3arc^V{-{Oo8{?=yJ+A@6tc{v+Oh zB=6;C$1Y&B^7o<2*W>%)kY_DhA5kCHeWTUhh=dev$m??+)twE+dj#{Ny)4Xos1^)@ zuR(v<52DW!);e|)n1MGzs72J47ExPdKb^nO=S*B0Ia5oKH^n=G7dPmUDNgM%jE8Lq zXub5)K#bFgD!65ZYZ>J9h8Ca$`+TrRNmcl!{@%h@=T^}-Sz7Jmy!M2vN`eQ|mA;}? zMr-Ix7NfPABfQg=H{dhVYLTRBWe^b&%aCpk2l%^|E$Oga9KG6l6v2UWqk?ciHN$v} z_9~oCLy@QDD-bRIW0kq2cU^%Ak4BC$A#pTQ8=|GlTRqXr=@XuC<#17gJqgVQZ#Y{LJ#3C^Z5OMx@D&zW=bgps zYu3EeFyXv~+i*2!%{kE*891QQhH0Z3NaKoc#z2l4J|DeuI9`ro?7Fx;IT0 zP{p0kk`Ud2S>1Y62?56Xto&zL_u_ie3+gJiK#<;IJPjYfc=|+J;U6Glo&htR5!`J| zGty;DPyduRdl>EOWVD+y+T*;5NnsHP2zmbi8S$@?xA9w&19|esTHb8n&3d4$7bxYjAT6*8z%3E7tGjf|p8WH&Nh(x|$`2LO>Ri0=-$obOWD z&N`_;U8PIP>5q(J8r({Qi={tIn(-6*qoP>)qd}lZe_+D>l>QLe+- {zj`i1Uu;t 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 Date: Sun, 26 Feb 2023 23:16:01 -0700 Subject: [PATCH 03/34] Updated readme markdown --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3f4fb26..8c7e76e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -Contains a small set of basic functionality required for loading other mods. It also handles initial setup and contains a mod_load_order.txt file for mod management. +### Contains a small set of basic functionality required for loading other mods. It also handles initial setup and contains a mod_load_order.txt file for mod management. -Installation: +## Installation: 1. Copy the Darktide Mod Loader files to your game directory and overwrite existing. 2. Run the "toggle_darktide_mods.bat" script in your game folder. 3. Copy the Darktide Mod Framework files to your "mods" directory (/mods) and overwrite existing. 3. Install other mods by downloading them from the Nexus site (https://www.nexusmods.com/warhammer40kdarktide) then adding them to "/mods/mod_load_order.txt" with a text editor. -Disable mods: +## Disable mods: * Disable individual mods by removing their name from your mods/mod_load_order.txt file. * Run the "toggle_darktide_mods.bat" script at your game folder and choose to unpatch the bundle database to disable all mod loading. -Uninstallation: +## Uninstallation: 1. Run the "toggle_darktide_mods.bat" script at your game folder and choose to unpatch the bundle database. 2. Delete the mods and tools folders from your game directory. 3. Delete the "mod_loader" file from /binaries. 4. Delete the "9ba626afa44a3aa3.patch_999" file from /bundle. -Mods will automatically disable when the game updates, so re-run the "toggle_darktide_mods.bat" script to re-enable mods after an update. +### Mods will automatically disable when the game updates, so re-run the "toggle_darktide_mods.bat" script to re-enable mods after an update. -This mod does not need to be added to your mod_load_order.txt file. \ No newline at end of file +### This mod does not need to be added to your mod_load_order.txt file. \ No newline at end of file From e369e692af276735cfd48e2836340031a1f20320 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:43:52 +0100 Subject: [PATCH 04/34] chore: Import base mod from DMF Co-authored-by: Aussiemon --- .gitignore | 1 + scripts/mods/dml/class.lua | 22 +++ scripts/mods/dml/hook.lua | 274 +++++++++++++++++++++++++++++++++++ scripts/mods/dml/init.lua | 15 ++ scripts/mods/dml/require.lua | 35 +++++ 5 files changed, 347 insertions(+) create mode 100644 .gitignore create mode 100644 scripts/mods/dml/class.lua create mode 100644 scripts/mods/dml/hook.lua create mode 100644 scripts/mods/dml/require.lua 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 From c2c2710a681e8dbe82e535bd0e95371f8c7f57fb Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:45:12 +0100 Subject: [PATCH 05/34] chore: Add ModManager class from VT2 --- scripts/mods/dml/mod_loader.lua | 628 ++++++++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 scripts/mods/dml/mod_loader.lua diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua new file mode 100644 index 0000000..406f707 --- /dev/null +++ b/scripts/mods/dml/mod_loader.lua @@ -0,0 +1,628 @@ +-- Copyright on this file is owned by Fatshark. +-- It is extracted, used and modified with permission only for +-- the purpose of loading mods within Warhammer 40,000: Darktide. +require("scripts/managers/mod/mod_shim") + +ModManager = class(ModManager) + +ModManager.init = function (self, boot_gui) + self._mods = {} + self._num_mods = nil + self._state = "not_loaded" + self._settings = Application.user_setting("mod_settings") or { + toposort = false, + log_level = 1, + developer_mode = false + } + self._chat_print_buffer = {} + self._reload_data = {} + self._gui = boot_gui + self._ui_time = 0 + self._network_callbacks = {} + local in_modded_realm = script_data["eac-untrusted"] + + Crashify.print_property("realm", (in_modded_realm and "modded") or "official") + + if rawget(_G, "Presence") then + Presence.set_presence("status", (in_modded_realm and "Modded Realm") or "Official Realm") + end + + ModShim.start() + + local has_enabled_mods = self:_has_enabled_mods() + local is_bundled = Application.bundled() + + printf("[ModManager] Mods enabled: %s // Bundled: %s", has_enabled_mods, is_bundled) + + if has_enabled_mods and is_bundled then + print("[ModManager] Fetching mod metadata ...") + + if in_modded_realm then + self._mod_metadata = {} + self._state = "fetching_metadata" + else + self:_fetch_mod_metadata() + end + else + self._state = "done" + self._num_mods = 0 + end +end + +ModManager.developer_mode_enabled = function (self) + return self._settings.developer_mode +end + +ModManager._draw_state_to_gui = function (self, gui, dt) + local state = self._state + local t = self._ui_time + dt + self._ui_time = t + local status_str = "Loading mods" + + if state == "scanning" then + status_str = "Scanning for mods" + elseif state == "loading" then + local mod = self._mods[self._mod_load_index] + status_str = string.format("Loading mod %q", mod.name) + elseif state == "fetching_metadata" then + status_str = "Fetching mod metadata" + end + + Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) +end + +ModManager.remove_gui = function (self) + assert(self._gui, "Trying to remove gui without setting gui first.") + + self._gui = nil +end + +ModManager._has_enabled_mods = function (self, in_modded_realm) + local mod_settings = Application.user_setting("mods") + + if not mod_settings then + return false + end + + for i = 1, #mod_settings, 1 do + if mod_settings[i].enabled then + return true + end + end + + return false +end + +local Keyboard = Keyboard +local BUTTON_INDEX_R = Keyboard.button_index("r") +local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") +local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") + +ModManager._check_reload = function (self) + return Keyboard.pressed(BUTTON_INDEX_R) and Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +end + +ModManager.update = function (self, dt) + local chat_print_buffer = self._chat_print_buffer + local num_delayed_prints = #chat_print_buffer + + if num_delayed_prints > 0 and Managers.chat then + for i = 1, num_delayed_prints, 1 do + Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) + + chat_print_buffer[i] = nil + end + end + + local old_state = self._state + + if self._settings.developer_mode and self:_check_reload() then + self._reload_requested = true + end + + if self._reload_requested and self._state == "done" then + self:_reload_mods() + end + + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.enabled and not mod.callbacks_disabled then + self:_run_callback(mod, "update", dt) + end + end + elseif self._state == "fetching_metadata" then + if self._mod_metadata then + self:_start_scan() + end + elseif self._state == "scanning" and not Mod.is_scanning() then + local mod_handles = Mod.mods() + + self:_build_mod_table(mod_handles) + + self._state = self:_load_mod(1) + self._ui_time = 0 + elseif self._state == "loading" then + local handle = self._loading_resource_handle + + if ResourcePackage.has_loaded(handle) then + ResourcePackage.flush(handle) + + local mod = self._mods[self._mod_load_index] + local next_index = mod.package_index + 1 + local mod_data = mod.data + + if next_index > #mod_data.packages then + mod.state = "running" + local ok, object = pcall(mod_data.run) + + if not ok then + self:print("error", "%s", object) + end + + local name = mod.name + mod.object = object or {} + + self:_run_callback(mod, "init", self._reload_data[mod.id]) + self:print("info", "%s loaded.", name) + + self._state = self:_load_mod(self._mod_load_index + 1) + else + self:_load_package(mod, next_index) + end + end + end + + local gui = self._gui + + if gui then + self:_draw_state_to_gui(gui, dt) + end + + if old_state ~= self._state then + self:print("info", "%s -> %s", old_state, self._state) + end +end + +ModManager.all_mods_loaded = function (self) + return self._state == "done" +end + +ModManager.destroy = function (self) + self:unload_all_mods() +end + +ModManager._run_callback = function (self, mod, callback_name, ...) + local object = mod.object + local cb = object[callback_name] + + if not cb then + return + end + + local success, val = pcall(cb, object, ...) + + if success then + return val + else + self:print("error", "%s", val or "[unknown error]") + self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + + mod.callbacks_disabled = true + end +end + +ModManager._fetch_mod_metadata = function (self) + local url = "http://cdn.fatsharkgames.se/mod_metadata.txt" + local headers = { + ["User-Agent"] = "Warhammer: Vermintide 2" + } + + Managers.curl:get(url, headers, callback(self, "_cb_mod_metadata")) + + self._state = "fetching_metadata" +end + +ModManager._cb_mod_metadata = function (self, success, return_code, headers, data, userdata) + printf("[ModManager] Metadata request completed. success=%s code=%s", success, return_code) + + local mod_metadata = {} + + if success and return_code >= 200 and return_code < 300 then + local line_number = 0 + + for line in string.gmatch(data, "[^\n\r]+") do + line_number = line_number + 1 + line = string.gsub(line, "#(.*)$", "") + + if line ~= "" then + local key, value = string.match(line, "(%d+)%s*=%s*(%w+)") + + if key then + printf("[ModManager] Metadata set: [%s] = %s", key, value) + + mod_metadata[key] = value + else + printf("[ModManager] Malformed metadata entry near line %d", line_number) + end + end + end + end + + self._mod_metadata = mod_metadata +end + +ModManager._start_scan = function (self) + self:print("info", "Starting mod scan") + + self._state = "scanning" + + Mod.start_scan(not script_data["eac-untrusted"]) +end + +ModManager._build_mod_table = function (self, mod_handles) + fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") + + local user_settings_mod_list = Application.user_setting("mods") or {} + + if self._settings.toposort then + user_settings_mod_list = self:_topologically_sorted(user_settings_mod_list) + end + + table.dump(mod_handles, "mod_handles", 3) + + local mod_metadata = self._mod_metadata + + print("user_setting.mods =>") + + for i, mod_data in ipairs(user_settings_mod_list) do + local id = mod_data.id or -9999 + local handle = mod_handles[id] + local enabled = mod_data.enabled + + if not handle then + self:print("warning", "Mod %q with id %d was not found in the workshop folder.", mod_data.name, id) + self:print("warning", "Did you try loading an unsanctioned mod in Official?") + + enabled = false + end + + local metadata = mod_metadata[id] + + if enabled and metadata then + local last_updated_string = mod_data.last_updated + local month, day, year, hour, minute, second, am_pm = string.match(last_updated_string, "(%d+)/(%d+)/(%d+) (%d+):(%d+):(%d+) ([AP]M)") + + if month then + if am_pm == "PM" then + hour = tonumber(hour) + 12 + end + + local last_updated = string.format("%04d%02d%02dT%02d%02d%02dZ", year, month, day, hour, minute, second) + + printf("[ModManager] id=%s last_updated=%s metadata=%s", id, last_updated, metadata) + + if last_updated < metadata then + enabled = false + end + else + printf("[ModManager] Could not parse date for %s", id) + + enabled = false + end + end + + self._mods[i] = { + state = "not_loaded", + callbacks_disabled = false, + id = id, + name = mod_data.name, + enabled = enabled, + handle = handle, + loaded_packages = {} + } + end + + for i, mod_data in ipairs(user_settings_mod_list) do + printf("[ModManager] mods[%d] = (id=%d, name=%q, enabled=%q, last_updated=%q)", i, mod_data.id, mod_data.name, mod_data.enabled, mod_data.last_updated) + end + + self._num_mods = #self._mods + + self:print("info", "Found %i mods", #self._mods) +end + +ModManager._load_mod = function (self, index) + self._ui_time = 0 + local mods = self._mods + local mod = mods[index] + + while mod and not mod.enabled do + index = index + 1 + mod = mods[index] + end + + if not mod then + table.clear(self._reload_data) + + return "done" + end + + local id = mod.id + local handle = mod.handle + + self:print("info", "loading mod %s", id) + + local info = Mod.info(handle) + + self:print("spew", "\n%s\n", info) + Crashify.print_property("modded", true) + + local chunk, err_msg = loadstring(info) + + if not chunk then + self:print("error", "Syntax error in .mod file. Mod %q with id %d skipped.", mod.name, mod.id) + self:print("info", err_msg) + + mod.enabled = false + + return self:_load_mod(index + 1) + end + + local ok, data_or_error = pcall(chunk) + + if not ok then + self:print("error", "Error in .mod file return table. Mod %q with id %d skipped.", mod.name, mod.id) + self:print("info", data_or_error) + + mod.enabled = false + + return self:_load_mod(index + 1) + end + + mod.data = data_or_error + mod.name = mod.name or data_or_error.NAME or "Mod " .. id + mod.state = "loading" + + Crashify.print_property(string.format("Mod:%s:%s", id, mod.name), true) + + self._mod_load_index = index + + self:_load_package(mod, 1) + + return "loading" +end + +ModManager._load_package = function (self, mod, index) + mod.package_index = index + local package_name = mod.data.packages[index] + + if not package_name then + return + end + + self:print("info", "loading package %q", package_name) + + local resource_handle = Mod.resource_package(mod.handle, package_name) + self._loading_resource_handle = resource_handle + + ResourcePackage.load(resource_handle) + + mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle +end + +ModManager.unload_all_mods = function (self) + if self._state ~= "done" then + self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + + return + end + + self:print("info", "Unload all mod packages") + + for i = self._num_mods, 1, -1 do + local mod = self._mods[i] + + if mod and mod.enabled then + self:unload_mod(i) + end + + self._mods[i] = nil + end + + self._num_mods = nil + self._state = "unloaded" +end + +ModManager.unload_mod = function (self, index) + local mod = self._mods[index] + + if mod then + self:print("info", "Unloading %q.", mod.name) + self:_run_callback(mod, "on_unload") + + for _, handle in ipairs(mod.loaded_packages) do + Mod.release_resource_package(handle) + end + + mod.state = "not_loaded" + else + self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + end +end + +ModManager._reload_mods = function (self) + self:print("info", "reloading mods") + + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.state == "running" then + self:print("info", "reloading %s", mod.name) + + self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") + else + self:print("info", "not reloading mod, state: %s", mod.state) + end + end + + self:unload_all_mods() + self:_start_scan() + + self._reload_requested = false +end + +ModManager.on_game_state_changed = function (self, status, state_name, state_object) + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.enabled and not mod.callbacks_disabled then + self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) + end + end + else + self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + end +end + +ModManager._topologically_sorted = function (self, mod_list) + local visited = {} + local sorted = {} + + for _, mod_data in ipairs(mod_list) do + if not visited[mod_data] then + self:_visit(mod_list, visited, sorted, mod_data) + end + end + + return sorted +end + +ModManager._visit = function (self, mod_list, visited, sorted, mod_data) + self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id) + + if visited[mod_data] then + return mod_data.enabled + end + + if visited[mod_data] ~= nil then + self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id) + + return false + end + + visited[mod_data] = false + local enabled = mod_data.enabled or false + slot6 = 1 + slot7 = mod_data.num_children or 0 + + for i = slot6, slot7, 1 do + local child_id = mod_data.children[j] + local child_index = table.find_by_key(mod_list, "id", child_id) + local child_mod_data = mod_list[child_index] + + if not child_mod_data then + self:print("warning", "Mod with id %d not found", id) + elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then + self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.", mod_data.name, mod_data.id, child_id) + + enabled = false + end + end + + mod_data.enabled = enabled + visited[mod_data] = true + sorted[#sorted + 1] = mod_data + + return enabled +end + +local LOG_LEVELS = { + spew = 4, + info = 3, + warning = 2, + error = 1 +} + +ModManager.print = function (self, level, str, ...) + local message = string.format("[ModManager][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 + + if log_level <= 2 then + print(message) + end + + if log_level <= self._settings.log_level then + self._chat_print_buffer[#self._chat_print_buffer + 1] = message + end +end + +ModManager.network_bind = function (self, port, callback) + local ncbs = self._network_callbacks + + fassert(not ncbs[port], "Port %d already in use", port) + + ncbs[port] = callback +end + +ModManager.network_unbind = function (self, port) + local ncbs = self._network_callbacks + + fassert(ncbs[port], "Port %d not in use", port) + + ncbs[port] = nil +end + +ModManager.network_is_occupied = function (self, port) + return self._network_callbacks[port] ~= nil +end + +ModManager.network_send = function (self, destination_peer_id, port, payload) + if destination_peer_id == self._my_peer_id then + Managers.state.network.network_transmit:queue_local_rpc("rpc_mod_user_data", port, payload) + end + + local channel_id = PEER_ID_TO_CHANNEL[(self._is_server and destination_peer_id) or self._host_peer_id] + + if channel_id then + RPC.rpc_mod_user_data(channel_id, self._my_peer_id, destination_peer_id, port, payload) + end +end + +ModManager.rpc_mod_user_data = function (self, relay_channel_id, source_peer_id, destination_peer_id, port, payload) + if destination_peer_id == self._my_peer_id then + local cb = self._network_callbacks[port] + + if cb then + cb(source_peer_id, payload) + end + elseif self._is_server then + local channel_id = PEER_ID_TO_CHANNEL[destination_peer_id] + + if channel_id then + RPC.rpc_mod_user_data(channel_id, source_peer_id, destination_peer_id, port, payload) + end + end +end + +ModManager.register_network_event_delegate = function (self, network_event_delegate) + network_event_delegate:register(self, "rpc_mod_user_data") + + self._network_event_delegate = network_event_delegate +end + +ModManager.unregister_network_event_delegate = function (self) + self._network_event_delegate:unregister(self) + + self._network_event_delegate = nil +end + +ModManager.network_context_created = function (self, host_peer_id, my_peer_id, is_server) + self._host_peer_id = host_peer_id + self._my_peer_id = my_peer_id + self._is_server = is_server +end + +return From 41564b65787ef3a3e35b89fd88151766b753df09 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:55:27 +0100 Subject: [PATCH 06/34] feat: Implement mod loader Co-authored-by: Aussiemon --- scripts/mods/dml/init.lua | 16 + scripts/mods/dml/mod_loader.lua | 906 +++++++++++--------------------- 2 files changed, 324 insertions(+), 598 deletions(-) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index d0d22d4..34583d1 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -13,4 +13,20 @@ dofile("scripts/mods/dml/require") dofile("scripts/mods/dml/class") dofile("scripts/mods/dml/hook") +function loader:init(boot_gui, mod_data) + local ModLoader = dofile("scripts/mods/dml/mod_loader") + local mod_loader = ModLoader:init(boot_gui, mod_data) + self._mod_loader = mod_loader + + Mods.hook.set(StateGame, "update", function(func, dt, ...) + mod_loader:update(dt) + return func(dt, ...) + end) + +end + +function loader:update(dt) + self._mod_loader:update(dt) +end + return loader diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 406f707..b7440f5 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -1,628 +1,338 @@ -- Copyright on this file is owned by Fatshark. -- It is extracted, used and modified with permission only for -- the purpose of loading mods within Warhammer 40,000: Darktide. -require("scripts/managers/mod/mod_shim") +local ModLoader = class("ModLoader") -ModManager = class(ModManager) - -ModManager.init = function (self, boot_gui) - self._mods = {} - self._num_mods = nil - self._state = "not_loaded" - self._settings = Application.user_setting("mod_settings") or { - toposort = false, - log_level = 1, - developer_mode = false - } - self._chat_print_buffer = {} - self._reload_data = {} - self._gui = boot_gui - self._ui_time = 0 - self._network_callbacks = {} - local in_modded_realm = script_data["eac-untrusted"] - - Crashify.print_property("realm", (in_modded_realm and "modded") or "official") - - if rawget(_G, "Presence") then - Presence.set_presence("status", (in_modded_realm and "Modded Realm") or "Official Realm") - end - - ModShim.start() - - local has_enabled_mods = self:_has_enabled_mods() - local is_bundled = Application.bundled() - - printf("[ModManager] Mods enabled: %s // Bundled: %s", has_enabled_mods, is_bundled) - - if has_enabled_mods and is_bundled then - print("[ModManager] Fetching mod metadata ...") - - if in_modded_realm then - self._mod_metadata = {} - self._state = "fetching_metadata" - else - self:_fetch_mod_metadata() - end - else - self._state = "done" - self._num_mods = 0 - end -end - -ModManager.developer_mode_enabled = function (self) - return self._settings.developer_mode -end - -ModManager._draw_state_to_gui = function (self, gui, dt) - local state = self._state - local t = self._ui_time + dt - self._ui_time = t - local status_str = "Loading mods" - - if state == "scanning" then - status_str = "Scanning for mods" - elseif state == "loading" then - local mod = self._mods[self._mod_load_index] - status_str = string.format("Loading mod %q", mod.name) - elseif state == "fetching_metadata" then - status_str = "Fetching mod metadata" - end - - Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) -end - -ModManager.remove_gui = function (self) - assert(self._gui, "Trying to remove gui without setting gui first.") - - self._gui = nil -end - -ModManager._has_enabled_mods = function (self, in_modded_realm) - local mod_settings = Application.user_setting("mods") - - if not mod_settings then - return false - end - - for i = 1, #mod_settings, 1 do - if mod_settings[i].enabled then - return true - end - end - - return false -end +local LOG_LEVELS = { + spew = 4, + info = 3, + warning = 2, + error = 1 +} +local DEFAULT_SETTINGS = { + log_level = LOG_LEVELS.error, + developer_mode = false +} local Keyboard = Keyboard local BUTTON_INDEX_R = Keyboard.button_index("r") local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") -ModManager._check_reload = function (self) - return Keyboard.pressed(BUTTON_INDEX_R) and Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +ModLoader.init = function(self, boot_gui, mod_data) + self._mod_data = mod_data + self._gui = boot_gui + + self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS + + self._mods = {} + self._num_mods = nil + self._chat_print_buffer = {} + self._reload_data = {} + self._ui_time = 0 + + if Crashify then + Crashify.print_property("modded", true) + end + + self._state = "scanning" end -ModManager.update = function (self, dt) - local chat_print_buffer = self._chat_print_buffer - local num_delayed_prints = #chat_print_buffer +ModLoader.developer_mode_enabled = function(self) + return self._settings.developer_mode +end - if num_delayed_prints > 0 and Managers.chat then - for i = 1, num_delayed_prints, 1 do - Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) +ModLoader._draw_state_to_gui = function(self, gui, dt) + local state = self._state + local t = self._ui_time + dt + self._ui_time = t + local status_str = "Loading mods" - chat_print_buffer[i] = nil - end - end + if state == "scanning" then + status_str = "Scanning for mods" + elseif state == "loading" then + local mod = self._mods[self._mod_load_index] + status_str = string.format("Loading mod %q", mod.name) + end - local old_state = self._state + Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) +end - if self._settings.developer_mode and self:_check_reload() then - self._reload_requested = true - end +ModLoader.remove_gui = function(self) + self._gui = nil +end - if self._reload_requested and self._state == "done" then - self:_reload_mods() - end +ModLoader._check_reload = function() + return Keyboard.pressed(BUTTON_INDEX_R) and + Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +end - if self._state == "done" then +ModLoader.update = function(self, dt) + local chat_print_buffer = self._chat_print_buffer + local num_delayed_prints = #chat_print_buffer + + if num_delayed_prints > 0 and Managers.chat then + for i = 1, num_delayed_prints, 1 do + -- TODO: Use new chat system + -- Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) + + chat_print_buffer[i] = nil + end + end + + local old_state = self._state + + if self._settings.developer_mode and self:_check_reload() then + self._reload_requested = true + end + + if self._reload_requested and self._state == "done" then + self:_reload_mods() + end + + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "update", dt) + end + end + elseif self._state == "scanning" then + self:_build_mod_table() + + self._state = self:_load_mod(1) + self._ui_time = 0 + elseif self._state == "loading" then + local handle = self._loading_resource_handle + + if ResourcePackage.has_loaded(handle) then + ResourcePackage.flush(handle) + + local mod = self._mods[self._mod_load_index] + local next_index = mod.package_index + 1 + local mod_data = mod.data + + if next_index > #mod_data.packages then + mod.state = "running" + local ok, object = pcall(mod_data.run) + + if not ok then self:print("error", "%s", object) end + + local name = mod.name + mod.object = object or {} + + self:print("info", "%s loaded.", name) + + self._state = self:_load_mod(self._mod_load_index + 1) + else + self:_load_package(mod, next_index) + end + end + end + + local gui = self._gui + + if gui then + self:_draw_state_to_gui(gui, dt) + end + + if old_state ~= self._state then + self:print("info", "%s -> %s", old_state, self._state) + end +end + +ModLoader.all_mods_loaded = function(self) + return self._state == "done" +end + +ModLoader.destroy = function(self) + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "on_destroy") + end + end + + self:unload_all_mods() +end + +ModLoader._run_callback = function (self, mod, callback_name, ...) + local object = mod.object + local cb = object[callback_name] + + if not cb then + return + end + + local success, val = pcall(cb, object, ...) + + if success then + return val + else + self:print("error", "%s", val or "[unknown error]") + self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + + mod.callbacks_disabled = true + end +end + +ModLoader._start_scan = function(self) + self:print("info", "Starting mod scan") + self._state = "scanning" +end + +ModLoader._build_mod_table = function(self) + fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") + + for i, mod_data in ipairs(self._mod_data) do + printf("[ModLoader] mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) + + self._mods[i] = { + id = mod_data.id, + state = "not_loaded", + callbacks_disabled = false, + name = mod_data.name, + loaded_packages = {}, + packages = mod_data.packages, + } + end + + self._num_mods = #self._mods + + self:print("info", "Found %i mods", #self._mods) +end + +ModLoader._load_mod = function(self, index) + self._ui_time = 0 + local mods = self._mods + local mod = mods[index] + + if not mod then + table.clear(self._reload_data) + + return "done" + end + + self:print("info", "loading mod %i", mod.id) + + mod.state = "loading" + + Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true) + + self._mod_load_index = index + + self:_load_package(mod, 1) + + return "loading" +end + +ModLoader._load_package = function(self, mod, index) + mod.package_index = index + local package_name = mod.packages[index] + + if not package_name then + return + end + + self:print("info", "loading package %q", package_name) + + local resource_handle = Application.resource_package(package_name) + self._loading_resource_handle = resource_handle + + ResourcePackage.load(resource_handle) + + mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle +end + +ModLoader.unload_all_mods = function(self) + if self._state ~= "done" then + self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + + return + end + + self:print("info", "Unload all mod packages") + + for i = self._num_mods, 1, -1 do + local mod = self._mods[i] + + if mod then + self:unload_mod(i) + end + + self._mods[i] = nil + end + + self._num_mods = nil + self._state = "unloaded" +end + +ModLoader.unload_mod = function(self, index) + local mod = self._mods[index] + + if mod then + self:print("info", "Unloading %q.", mod.name) + + for _, handle in ipairs(mod.loaded_packages) do + ResourcePackage.unload(handle) + Application.release_resource_package(handle) + end + + mod.state = "not_loaded" + else + self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + end +end + +ModLoader._reload_mods = function(self) + self:print("info", "reloading mods") + + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.state == "running" then + self:print("info", "reloading %s", mod.name) + + self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") + else + self:print("info", "not reloading mod, state: %s", mod.state) + end + end + + self:unload_all_mods() + self:_start_scan() + + self._reload_requested = false +end + +ModLoader.on_game_state_changed = function(self, status, state_name, state_object) + if self._state == "done" then for i = 1, self._num_mods, 1 do local mod = self._mods[i] - if mod and mod.enabled and not mod.callbacks_disabled then - self:_run_callback(mod, "update", dt) - end - end - elseif self._state == "fetching_metadata" then - if self._mod_metadata then - self:_start_scan() - end - elseif self._state == "scanning" and not Mod.is_scanning() then - local mod_handles = Mod.mods() - - self:_build_mod_table(mod_handles) - - self._state = self:_load_mod(1) - self._ui_time = 0 - elseif self._state == "loading" then - local handle = self._loading_resource_handle - - if ResourcePackage.has_loaded(handle) then - ResourcePackage.flush(handle) - - local mod = self._mods[self._mod_load_index] - local next_index = mod.package_index + 1 - local mod_data = mod.data - - if next_index > #mod_data.packages then - mod.state = "running" - local ok, object = pcall(mod_data.run) - - if not ok then - self:print("error", "%s", object) - end - - local name = mod.name - mod.object = object or {} - - self:_run_callback(mod, "init", self._reload_data[mod.id]) - self:print("info", "%s loaded.", name) - - self._state = self:_load_mod(self._mod_load_index + 1) - else - self:_load_package(mod, next_index) - end - end - end - - local gui = self._gui - - if gui then - self:_draw_state_to_gui(gui, dt) - end - - if old_state ~= self._state then - self:print("info", "%s -> %s", old_state, self._state) - end -end - -ModManager.all_mods_loaded = function (self) - return self._state == "done" -end - -ModManager.destroy = function (self) - self:unload_all_mods() -end - -ModManager._run_callback = function (self, mod, callback_name, ...) - local object = mod.object - local cb = object[callback_name] - - if not cb then - return - end - - local success, val = pcall(cb, object, ...) - - if success then - return val - else - self:print("error", "%s", val or "[unknown error]") - self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) - - mod.callbacks_disabled = true - end -end - -ModManager._fetch_mod_metadata = function (self) - local url = "http://cdn.fatsharkgames.se/mod_metadata.txt" - local headers = { - ["User-Agent"] = "Warhammer: Vermintide 2" - } - - Managers.curl:get(url, headers, callback(self, "_cb_mod_metadata")) - - self._state = "fetching_metadata" -end - -ModManager._cb_mod_metadata = function (self, success, return_code, headers, data, userdata) - printf("[ModManager] Metadata request completed. success=%s code=%s", success, return_code) - - local mod_metadata = {} - - if success and return_code >= 200 and return_code < 300 then - local line_number = 0 - - for line in string.gmatch(data, "[^\n\r]+") do - line_number = line_number + 1 - line = string.gsub(line, "#(.*)$", "") - - if line ~= "" then - local key, value = string.match(line, "(%d+)%s*=%s*(%w+)") - - if key then - printf("[ModManager] Metadata set: [%s] = %s", key, value) - - mod_metadata[key] = value - else - printf("[ModManager] Malformed metadata entry near line %d", line_number) - end - end - end - end - - self._mod_metadata = mod_metadata -end - -ModManager._start_scan = function (self) - self:print("info", "Starting mod scan") - - self._state = "scanning" - - Mod.start_scan(not script_data["eac-untrusted"]) -end - -ModManager._build_mod_table = function (self, mod_handles) - fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") - - local user_settings_mod_list = Application.user_setting("mods") or {} - - if self._settings.toposort then - user_settings_mod_list = self:_topologically_sorted(user_settings_mod_list) - end - - table.dump(mod_handles, "mod_handles", 3) - - local mod_metadata = self._mod_metadata - - print("user_setting.mods =>") - - for i, mod_data in ipairs(user_settings_mod_list) do - local id = mod_data.id or -9999 - local handle = mod_handles[id] - local enabled = mod_data.enabled - - if not handle then - self:print("warning", "Mod %q with id %d was not found in the workshop folder.", mod_data.name, id) - self:print("warning", "Did you try loading an unsanctioned mod in Official?") - - enabled = false - end - - local metadata = mod_metadata[id] - - if enabled and metadata then - local last_updated_string = mod_data.last_updated - local month, day, year, hour, minute, second, am_pm = string.match(last_updated_string, "(%d+)/(%d+)/(%d+) (%d+):(%d+):(%d+) ([AP]M)") - - if month then - if am_pm == "PM" then - hour = tonumber(hour) + 12 - end - - local last_updated = string.format("%04d%02d%02dT%02d%02d%02dZ", year, month, day, hour, minute, second) - - printf("[ModManager] id=%s last_updated=%s metadata=%s", id, last_updated, metadata) - - if last_updated < metadata then - enabled = false - end - else - printf("[ModManager] Could not parse date for %s", id) - - enabled = false - end - end - - self._mods[i] = { - state = "not_loaded", - callbacks_disabled = false, - id = id, - name = mod_data.name, - enabled = enabled, - handle = handle, - loaded_packages = {} - } - end - - for i, mod_data in ipairs(user_settings_mod_list) do - printf("[ModManager] mods[%d] = (id=%d, name=%q, enabled=%q, last_updated=%q)", i, mod_data.id, mod_data.name, mod_data.enabled, mod_data.last_updated) - end - - self._num_mods = #self._mods - - self:print("info", "Found %i mods", #self._mods) -end - -ModManager._load_mod = function (self, index) - self._ui_time = 0 - local mods = self._mods - local mod = mods[index] - - while mod and not mod.enabled do - index = index + 1 - mod = mods[index] - end - - if not mod then - table.clear(self._reload_data) - - return "done" - end - - local id = mod.id - local handle = mod.handle - - self:print("info", "loading mod %s", id) - - local info = Mod.info(handle) - - self:print("spew", "\n%s\n", info) - Crashify.print_property("modded", true) - - local chunk, err_msg = loadstring(info) - - if not chunk then - self:print("error", "Syntax error in .mod file. Mod %q with id %d skipped.", mod.name, mod.id) - self:print("info", err_msg) - - mod.enabled = false - - return self:_load_mod(index + 1) - end - - local ok, data_or_error = pcall(chunk) - - if not ok then - self:print("error", "Error in .mod file return table. Mod %q with id %d skipped.", mod.name, mod.id) - self:print("info", data_or_error) - - mod.enabled = false - - return self:_load_mod(index + 1) - end - - mod.data = data_or_error - mod.name = mod.name or data_or_error.NAME or "Mod " .. id - mod.state = "loading" - - Crashify.print_property(string.format("Mod:%s:%s", id, mod.name), true) - - self._mod_load_index = index - - self:_load_package(mod, 1) - - return "loading" -end - -ModManager._load_package = function (self, mod, index) - mod.package_index = index - local package_name = mod.data.packages[index] - - if not package_name then - return - end - - self:print("info", "loading package %q", package_name) - - local resource_handle = Mod.resource_package(mod.handle, package_name) - self._loading_resource_handle = resource_handle - - ResourcePackage.load(resource_handle) - - mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle -end - -ModManager.unload_all_mods = function (self) - if self._state ~= "done" then - self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) - - return - end - - self:print("info", "Unload all mod packages") - - for i = self._num_mods, 1, -1 do - local mod = self._mods[i] - - if mod and mod.enabled then - self:unload_mod(i) - end - - self._mods[i] = nil - end - - self._num_mods = nil - self._state = "unloaded" -end - -ModManager.unload_mod = function (self, index) - local mod = self._mods[index] - - if mod then - self:print("info", "Unloading %q.", mod.name) - self:_run_callback(mod, "on_unload") - - for _, handle in ipairs(mod.loaded_packages) do - Mod.release_resource_package(handle) - end - - mod.state = "not_loaded" - else - self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) - end -end - -ModManager._reload_mods = function (self) - self:print("info", "reloading mods") - - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.state == "running" then - self:print("info", "reloading %s", mod.name) - - self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") - else - self:print("info", "not reloading mod, state: %s", mod.state) - end - end - - self:unload_all_mods() - self:_start_scan() - - self._reload_requested = false -end - -ModManager.on_game_state_changed = function (self, status, state_name, state_object) - if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.enabled and not mod.callbacks_disabled then + if mod and not mod.callbacks_disabled then self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) end end - else - self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) - end + else + self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + end end -ModManager._topologically_sorted = function (self, mod_list) - local visited = {} - local sorted = {} +ModLoader.print = function(self, level, str, ...) + local message = string.format("[ModLoader][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 - for _, mod_data in ipairs(mod_list) do - if not visited[mod_data] then - self:_visit(mod_list, visited, sorted, mod_data) - end - end + if log_level <= 2 then + print(message) + end - return sorted + if log_level <= self._settings.log_level then + self._chat_print_buffer[#self._chat_print_buffer + 1] = message + end end -ModManager._visit = function (self, mod_list, visited, sorted, mod_data) - self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id) - - if visited[mod_data] then - return mod_data.enabled - end - - if visited[mod_data] ~= nil then - self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id) - - return false - end - - visited[mod_data] = false - local enabled = mod_data.enabled or false - slot6 = 1 - slot7 = mod_data.num_children or 0 - - for i = slot6, slot7, 1 do - local child_id = mod_data.children[j] - local child_index = table.find_by_key(mod_list, "id", child_id) - local child_mod_data = mod_list[child_index] - - if not child_mod_data then - self:print("warning", "Mod with id %d not found", id) - elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then - self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.", mod_data.name, mod_data.id, child_id) - - enabled = false - end - end - - mod_data.enabled = enabled - visited[mod_data] = true - sorted[#sorted + 1] = mod_data - - return enabled -end - -local LOG_LEVELS = { - spew = 4, - info = 3, - warning = 2, - error = 1 -} - -ModManager.print = function (self, level, str, ...) - local message = string.format("[ModManager][" .. level .. "] " .. str, ...) - local log_level = LOG_LEVELS[level] or 99 - - if log_level <= 2 then - print(message) - end - - if log_level <= self._settings.log_level then - self._chat_print_buffer[#self._chat_print_buffer + 1] = message - end -end - -ModManager.network_bind = function (self, port, callback) - local ncbs = self._network_callbacks - - fassert(not ncbs[port], "Port %d already in use", port) - - ncbs[port] = callback -end - -ModManager.network_unbind = function (self, port) - local ncbs = self._network_callbacks - - fassert(ncbs[port], "Port %d not in use", port) - - ncbs[port] = nil -end - -ModManager.network_is_occupied = function (self, port) - return self._network_callbacks[port] ~= nil -end - -ModManager.network_send = function (self, destination_peer_id, port, payload) - if destination_peer_id == self._my_peer_id then - Managers.state.network.network_transmit:queue_local_rpc("rpc_mod_user_data", port, payload) - end - - local channel_id = PEER_ID_TO_CHANNEL[(self._is_server and destination_peer_id) or self._host_peer_id] - - if channel_id then - RPC.rpc_mod_user_data(channel_id, self._my_peer_id, destination_peer_id, port, payload) - end -end - -ModManager.rpc_mod_user_data = function (self, relay_channel_id, source_peer_id, destination_peer_id, port, payload) - if destination_peer_id == self._my_peer_id then - local cb = self._network_callbacks[port] - - if cb then - cb(source_peer_id, payload) - end - elseif self._is_server then - local channel_id = PEER_ID_TO_CHANNEL[destination_peer_id] - - if channel_id then - RPC.rpc_mod_user_data(channel_id, source_peer_id, destination_peer_id, port, payload) - end - end -end - -ModManager.register_network_event_delegate = function (self, network_event_delegate) - network_event_delegate:register(self, "rpc_mod_user_data") - - self._network_event_delegate = network_event_delegate -end - -ModManager.unregister_network_event_delegate = function (self) - self._network_event_delegate:unregister(self) - - self._network_event_delegate = nil -end - -ModManager.network_context_created = function (self, host_peer_id, my_peer_id, is_server) - self._host_peer_id = host_peer_id - self._my_peer_id = my_peer_id - self._is_server = is_server -end - -return +return ModLoader From 2247c81700f0750f43d150d040a1833a5cded780 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 10:23:06 +0100 Subject: [PATCH 07/34] refactor: Move indexed objects to local and change whitespace --- .luacheckrc | 31 ++- scripts/mods/dml/class.lua | 24 +- scripts/mods/dml/hook.lua | 515 ++++++++++++++++++----------------- scripts/mods/dml/init.lua | 3 +- scripts/mods/dml/require.lua | 56 ++-- 5 files changed, 327 insertions(+), 302 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 552b17e..f213500 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -10,7 +10,30 @@ ignore = { "212/self", -- Disable unused self warnings. } -std = "+DT" +std = "+DT+DML" + +stds["DML"] = { + read_globals = { + "MODS_HOOKS", "MODS_HOOKS_BY_FILE", "Log", + Mods = { fields = { + lua = { fields = { "debug", "io", "ffi", "os" }}, + hook = { fields = { + "set", + "set_on_file", + "enable", + "enable_by_file", + "remove", + "front", + "_get_item", + "_get_item_hook", + "_patch", + }}, + "original_require", + "require_store", + "original_class", + }}, + }, +} stds["DT"] = { read_globals = { @@ -32,13 +55,9 @@ stds["DT"] = { Managers = { fields = { "mod", "event", "chat" }}, - Mods = { fields = { - lua = { fields = { "debug", "io", "ffi", "os" }}, - "original_require", - "require_store", - }}, "Crashify","Keyboard","Mouse","Application","Color","Quarternion","Vector3","Vector2","RESOLUTION_LOOKUP", "ModManager", "Utf8", "StateGame", "ResourcePackage", "class", "Gui", "fassert", "printf", "__print", "ffi", + "class", }, } diff --git a/scripts/mods/dml/class.lua b/scripts/mods/dml/class.lua index 19b0c91..53896de 100644 --- a/scripts/mods/dml/class.lua +++ b/scripts/mods/dml/class.lua @@ -1,9 +1,13 @@ -Mods.original_class = Mods.original_class or class +local original_class = Mods.original_class or class +Mods.original_class = original_class local _G = _G local rawget = rawget local rawset = rawset +-- The `__index` metamethod maps a proper identifier `CLASS.MyClassName` to the +-- stringified version of the key: `"MyClassName"`. +-- This allows using LuaCheck for the stringified class names in hook parameters. _G.CLASS = _G.CLASS or setmetatable({}, { __index = function(_, key) return key @@ -11,12 +15,12 @@ _G.CLASS = _G.CLASS or setmetatable({}, { }) 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 + local result = 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 diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua index f74822f..7f17888 100644 --- a/scripts/mods/dml/hook.lua +++ b/scripts/mods/dml/hook.lua @@ -1,274 +1,275 @@ ---[[ - 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 function NOOP() end + local item_template = { - name = "", - func = EMPTY_FUNC, - hooks = {}, + name = "", + func = NOOP, + hooks = {}, } local item_hook_template = { - name = "", - func = EMPTY_FUNC, - enable = false, - exec = EMPTY_FUNC, + name = "", + func = NOOP, + enable = false, + exec = NOOP, } 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 +local function print_log_info(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 +-- +-- Get function by function name +-- +local function get_func(func_name) + return assert(loadstring("return " .. func_name))() +end + +-- +-- Get item by function name +-- +local function get_item(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 = get_func(func_name) + + -- Save + table.insert(MODS_HOOKS, item) + + return item +end + +-- +-- Get item hook by mod name +-- +local function get_item_hook(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 +-- +local function patch() + 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 + +-- +-- Set hook +-- +local function set(mod_name, func_name, hook_func) + local item = get_item(func_name) + local item_hook = get_item_hook(item, mod_name) + + print_log_info(mod_name, "Hooking " .. func_name) + + item_hook.enable = true + item_hook.func = hook_func + + patch() +end + +-- +-- Set hook on every instance of the given file +-- +local function set_on_file(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 = string.format( + "Mods.require_store[\"%s\"][%i].%s", + this_filepath, this_index, func_name + ) + 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 +-- +local function enable(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 + patch() + end + end + end + end + + return +end + +-- +-- Enable all hooks on a stored file +-- +local function enable_by_file(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 _, hook_create_func in ipairs(all_file_hooks) do + hook_create_func(filepath, store_index) + end + end +end + +-- +-- Remove hook from chain +-- +local function remove(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) + + 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 +-- +local function front(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) + + patch() + end + end + end + end + + return 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, + set = set, + set_on_file = set_on_file, + enable = enable, + enable_by_file = enable_by_file, + remove = remove, + front = front, + _get_item = get_item, + _get_item_hook = get_item_hook, + _patch = patch, } diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 34583d1..71f299d 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -3,7 +3,6 @@ local loader = {} Mods = { - hook = {}, lua = setmetatable({}, { __index = { debug = debug, io = io, ffi = ffi, os = os }, }), @@ -11,7 +10,7 @@ Mods = { dofile("scripts/mods/dml/require") dofile("scripts/mods/dml/class") -dofile("scripts/mods/dml/hook") +Mods.hook = dofile("scripts/mods/dml/hook") function loader:init(boot_gui, mod_data) local ModLoader = dofile("scripts/mods/dml/mod_loader") diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua index 6e876f7..d311b56 100644 --- a/scripts/mods/dml/require.lua +++ b/scripts/mods/dml/require.lua @@ -1,35 +1,37 @@ -Mods.require_store = Mods.require_store or {} -Mods.original_require = Mods.original_require or require +local require_store = Mods.require_store or {} +Mods.require_store = require_store + +local original_require = Mods.original_require or require +Mods.original_require = original_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 + local store = require_store[filepath] + local num_store = #store + if not store or num_store == 0 then + return true + end + + if store[num_store] ~= new_result then + return true + end end require = function(filepath, ...) - local Mods = Mods + local result = original_require(filepath, ...) + if result and type(result) == "table" then + if can_insert(filepath, result) then + require_store[filepath] = require_store[filepath] or {} + local store = require_store[filepath] - local result = Mods.original_require(filepath, ...) - if result and type(result) == "table" then + table.insert(store, result) - if can_insert(filepath, result) then - Mods.require_store[filepath] = Mods.require_store[filepath] or {} - local store = Mods.require_store[filepath] + --print("[Require] #" .. tostring(#store) .. " of " .. filepath) + local Mods = Mods + if Mods.hook then + Mods.hook.enable_by_file(filepath, #store) + end + end + end - 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 + return result +end From 108c0dd0ce780f45b0494c66feb042b92b200ea5 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 11:55:34 +0100 Subject: [PATCH 08/34] fix: Add missing game state hooks Co-authored-by: Aussiemon --- .luacheckrc | 2 + scripts/mods/dml/init.lua | 81 ++++++++++++++++++++++++++++++------ scripts/mods/dml/message.lua | 47 +++++++++++++++++++++ 3 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 scripts/mods/dml/message.lua diff --git a/.luacheckrc b/.luacheckrc index f213500..e04e4da 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -8,6 +8,7 @@ ignore = { "12.", -- ignore "Setting a read-only global variable/Setting a read-only field of a global variable." "542", -- disable warnings for empty if branches. These are useful sometime and easy to notice otherwise. "212/self", -- Disable unused self warnings. + "432/self", -- Allow shadowing `self`, often happens when creating hooks in functions } std = "+DT+DML" @@ -28,6 +29,7 @@ stds["DML"] = { "_get_item_hook", "_patch", }}, + message = { fields = { "echo", "notify" }}, "original_require", "require_store", "original_class", diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 71f299d..dcfa892 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -2,30 +2,87 @@ -- to initialize the modding environment. local loader = {} -Mods = { - lua = setmetatable({}, { - __index = { debug = debug, io = io, ffi = ffi, os = os }, - }), -} +Mods = {} -dofile("scripts/mods/dml/require") -dofile("scripts/mods/dml/class") -Mods.hook = dofile("scripts/mods/dml/hook") +function loader:init(libs, mod_data, boot_gui) + -- The metatable prevents overwriting these + self._libs = setmetatable({}, { __index = libs }) + Mods.lua = self._libs + + dofile("scripts/mods/dml/message") + dofile("scripts/mods/dml/require") + dofile("scripts/mods/dml/class") + dofile("scripts/mods/dml/hook") -function loader:init(boot_gui, mod_data) local ModLoader = dofile("scripts/mods/dml/mod_loader") - local mod_loader = ModLoader:init(boot_gui, mod_data) + local mod_loader = ModLoader:new(boot_gui, mod_data) self._mod_loader = mod_loader - Mods.hook.set(StateGame, "update", function(func, dt, ...) + -- The mod loader needs to remain active during game play, to + -- enable reloads + Mods.hook.set("DML", "StateGame.update", function(func, dt, ...) mod_loader:update(dt) return func(dt, ...) end) + -- Skip splash view + Mods.hook.set("Base", "StateSplash.on_enter", function(func, self, ...) + local result = func(self, ...) + + self._should_skip = true + self._continue = true + + return result + end) + + -- Trigger state change events + Mods.hook.set("Base", "GameStateMachine._change_state", function(func, self, ...) + local old_state = self._state + local old_state_name = old_state and self:current_state_name() + + if old_state_name then + mod_loader:on_game_state_changed("exit", old_state_name, old_state) + end + + local result = func(self, ...) + + local new_state = self._state + local new_state_name = new_state and self:current_state_name() + + if new_state_name then + mod_loader:on_game_state_changed("enter", new_state_name, new_state) + end + + return result + end) + + -- Trigger ending state change event + Mods.hook.set("Base", "GameStateMachine.destroy", function(func, self, ...) + local old_state = self._state + local old_state_name = old_state and self:current_state_name() + + if old_state_name then + mod_loader:on_game_state_changed("exit", old_state_name) + end + + return func(self, ...) + end) end function loader:update(dt) - self._mod_loader:update(dt) + local mod_loader = self._mod_loader + mod_loader:update(dt) + + local done = mod_loader:all_mods_loaded() + if done then + mod_loader:_remove_gui() + end + + return done +end + +function loader:done() + return self._mod_loader:all_mods_loaded() end return loader diff --git a/scripts/mods/dml/message.lua b/scripts/mods/dml/message.lua new file mode 100644 index 0000000..3ea27fe --- /dev/null +++ b/scripts/mods/dml/message.lua @@ -0,0 +1,47 @@ +local chat_sound = "wwise/events/ui/play_ui_click" + +local notify = function(message) + local event_manager = Managers and Managers.event + + if event_manager then + event_manager:trigger("event_add_notification_message", "default", message, nil, chat_sound) + end + + print(message) +end + +local echo = function(message, sender) + local chat_manager = Managers and Managers.chat + local event_manager = Managers and Managers.event + + if chat_manager and event_manager then + local message_obj = { + message_body = message, + is_current_user = false, + } + + local participant = { + displayname = sender or "SYSTEM", + } + + local message_sent = false + + local channel_handle, channel = next(chat_manager:connected_chat_channels()) + if channel then + event_manager:trigger("chat_manager_message_recieved", channel_handle, participant, message_obj) + message_sent = true + end + + if not message_sent then + notify(message) + return + end + end + + print(message) +end + +Mods.message = { + echo = echo, + notify = notify, +} From 698a01c2598aeaaf0ee69455e2ed9e1094f2c877 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 14:48:47 +0100 Subject: [PATCH 09/34] refactor: Drop loadstring for hooks --- scripts/mods/dml/hook.lua | 65 ++++++++++++++------------------------- scripts/mods/dml/init.lua | 8 ++--- 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua index 7f17888..deed632 100644 --- a/scripts/mods/dml/hook.lua +++ b/scripts/mods/dml/hook.lua @@ -30,23 +30,24 @@ end -- -- Get function by function name -- -local function get_func(func_name) - return assert(loadstring("return " .. func_name))() +local function get_func(obj, func_name) + return obj[func_name] end -- -- Get item by function name -- -local function get_item(func_name) +local function get_item(obj, func_name) -- Find existing item for _, item in ipairs(MODS_HOOKS) do - if item.name == func_name then + if item.obj == obj and item.name == func_name then return item end end -- Create new item local item = table.clone(item_template) + item.obj = obj item.name = func_name item.func = get_func(func_name) @@ -82,48 +83,30 @@ end -- local function patch() 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 + local is_first_hook = j == 1 + if is_first_hook then if hook.enable then - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. hook_name .. ".func(" .. item_name .. ".func, ...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + local mod_hook = MODS_HOOKS[i] + return mod_hook.hooks[j].func(mod_hook.func, ...) + end else - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. item_name .. ".func(...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + return MODS_HOOKS[i].func(...) + end end else if hook.enable then - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. hook_name .. ".func(" .. before_hook_name .. ".exec, ...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + local mod_hook = MODS_HOOKS[i] + return mod_hook.hooks[j].func(mod_hook.hooks[j - 1].exec, ...) + end else - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. before_hook_name .. ".exec(...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + return MODS_HOOKS[i].hooks[j - 1].exec(...) + end end end @@ -131,7 +114,7 @@ local function patch() end -- Patch orginal function call - assert(loadstring(item.name .. " = " .. item_name .. ".hooks[" .. last_j .. "].exec"))() + item.obj[item.name] = MODS_HOOKS[i].hooks[last_j].exec end end @@ -225,10 +208,8 @@ local function remove(func_name, mod_name) end end else - local item_name = "MODS_HOOKS[" .. tostring(i) .. "]" - -- Restore orginal function - assert(loadstring(item.name .. " = " .. item_name .. ".func"))() + item.obj[item.name] = MODS_HOOKS[i].func -- Remove hook function table.remove(MODS_HOOKS, i) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index dcfa892..9a716f5 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -20,13 +20,13 @@ function loader:init(libs, mod_data, boot_gui) -- The mod loader needs to remain active during game play, to -- enable reloads - Mods.hook.set("DML", "StateGame.update", function(func, dt, ...) + Mods.hook.set("DML", GameState, "update", function(func, dt, ...) mod_loader:update(dt) return func(dt, ...) end) -- Skip splash view - Mods.hook.set("Base", "StateSplash.on_enter", function(func, self, ...) + Mods.hook.set("Base", StateSplash, "on_enter", function(func, self, ...) local result = func(self, ...) self._should_skip = true @@ -36,7 +36,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger state change events - Mods.hook.set("Base", "GameStateMachine._change_state", function(func, self, ...) + Mods.hook.set("Base", GameStateMachine, "_change_state", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() @@ -57,7 +57,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger ending state change event - Mods.hook.set("Base", "GameStateMachine.destroy", function(func, self, ...) + Mods.hook.set("Base", GameStateMachine, "destroy", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() From fb687fbab1fe5ea3175222e8b4e805f6f7c23c38 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 18:30:14 +0100 Subject: [PATCH 10/34] fix: Fix initial loading --- scripts/mods/dml/hook.lua | 6 +++--- scripts/mods/dml/init.lua | 19 ++++++++++++------- scripts/mods/dml/mod_loader.lua | 18 +++++++++++------- scripts/mods/dml/require.lua | 5 ++--- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua index deed632..f64a158 100644 --- a/scripts/mods/dml/hook.lua +++ b/scripts/mods/dml/hook.lua @@ -49,7 +49,7 @@ local function get_item(obj, func_name) local item = table.clone(item_template) item.obj = obj item.name = func_name - item.func = get_func(func_name) + item.func = get_func(obj, func_name) -- Save table.insert(MODS_HOOKS, item) @@ -121,8 +121,8 @@ end -- -- Set hook -- -local function set(mod_name, func_name, hook_func) - local item = get_item(func_name) +local function set(mod_name, obj, func_name, hook_func) + local item = get_item(obj, func_name) local item_hook = get_item_hook(item, mod_name) print_log_info(mod_name, "Hooking " .. func_name) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 9a716f5..f455539 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -1,10 +1,14 @@ +local StateGame = require("scripts/game_states/state_game") +local StateSplash = require("scripts/game_states/game/state_splash") +local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") + -- The loader object that is used during game boot -- to initialize the modding environment. local loader = {} Mods = {} -function loader:init(libs, mod_data, boot_gui) +function loader:init(mod_data, libs, boot_gui) -- The metatable prevents overwriting these self._libs = setmetatable({}, { __index = libs }) Mods.lua = self._libs @@ -15,18 +19,19 @@ function loader:init(libs, mod_data, boot_gui) dofile("scripts/mods/dml/hook") local ModLoader = dofile("scripts/mods/dml/mod_loader") - local mod_loader = ModLoader:new(boot_gui, mod_data) + local mod_loader = ModLoader:new(mod_data, libs, boot_gui) self._mod_loader = mod_loader + Managers.mod = mod_loader -- The mod loader needs to remain active during game play, to -- enable reloads - Mods.hook.set("DML", GameState, "update", function(func, dt, ...) + Mods.hook.set("DML", StateGame, "update", function(func, dt, ...) mod_loader:update(dt) return func(dt, ...) end) -- Skip splash view - Mods.hook.set("Base", StateSplash, "on_enter", function(func, self, ...) + Mods.hook.set("DML", StateSplash, "on_enter", function(func, self, ...) local result = func(self, ...) self._should_skip = true @@ -36,7 +41,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger state change events - Mods.hook.set("Base", GameStateMachine, "_change_state", function(func, self, ...) + Mods.hook.set("DML", GameStateMachine, "_change_state", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() @@ -57,7 +62,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger ending state change event - Mods.hook.set("Base", GameStateMachine, "destroy", function(func, self, ...) + Mods.hook.set("DML", GameStateMachine, "destroy", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() @@ -75,7 +80,7 @@ function loader:update(dt) local done = mod_loader:all_mods_loaded() if done then - mod_loader:_remove_gui() + mod_loader:remove_gui() end return done diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index b7440f5..1331473 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -3,6 +3,10 @@ -- the purpose of loading mods within Warhammer 40,000: Darktide. local ModLoader = class("ModLoader") +local ScriptGui = require("scripts/foundation/utilities/script_gui") + +local FONT_MATERIAL = "content/ui/fonts/arial" + local LOG_LEVELS = { spew = 4, info = 3, @@ -19,8 +23,9 @@ local BUTTON_INDEX_R = Keyboard.button_index("r") local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") -ModLoader.init = function(self, boot_gui, mod_data) +ModLoader.init = function(self, mod_data, libs, boot_gui) self._mod_data = mod_data + self._libs = libs self._gui = boot_gui self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS @@ -31,10 +36,6 @@ ModLoader.init = function(self, boot_gui, mod_data) self._reload_data = {} self._ui_time = 0 - if Crashify then - Crashify.print_property("modded", true) - end - self._state = "scanning" end @@ -55,7 +56,9 @@ ModLoader._draw_state_to_gui = function(self, gui, dt) status_str = string.format("Loading mod %q", mod.name) end - Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) + local msg = status_str .. string.rep(".", (2 * t) % 4) + Log.info("ModLoader", msg) + ScriptGui.text(gui, msg, FONT_MATERIAL, 48, Vector3(20, 30, 1), Color.white()) end ModLoader.remove_gui = function(self) @@ -120,9 +123,9 @@ ModLoader.update = function(self, dt) if not ok then self:print("error", "%s", object) end - local name = mod.name mod.object = object or {} + self:_run_callback(mod, "init", self._reload_data[mod.id]) self:print("info", "%s loaded.", name) self._state = self:_load_mod(self._mod_load_index + 1) @@ -197,6 +200,7 @@ ModLoader._build_mod_table = function(self) name = mod_data.name, loaded_packages = {}, packages = mod_data.packages, + data = mod_data, } end diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua index d311b56..bc808a9 100644 --- a/scripts/mods/dml/require.lua +++ b/scripts/mods/dml/require.lua @@ -6,12 +6,11 @@ Mods.original_require = original_require local can_insert = function(filepath, new_result) local store = require_store[filepath] - local num_store = #store - if not store or num_store == 0 then + if not store or #store then return true end - if store[num_store] ~= new_result then + if store[#store] ~= new_result then return true end end From 2ec7d2599aba3b0b69f0b6d5eb5a438fde21f648 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 10:05:20 +0100 Subject: [PATCH 11/34] feat: Improve mod loader logging --- scripts/mods/dml/mod_loader.lua | 73 +++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 1331473..48e941f 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -3,6 +3,9 @@ -- the purpose of loading mods within Warhammer 40,000: Darktide. local ModLoader = class("ModLoader") +local table_unpack = table.unpack or unpack +local table_pack = table.pack or pack + local ScriptGui = require("scripts/foundation/utilities/script_gui") local FONT_MATERIAL = "content/ui/fonts/arial" @@ -58,7 +61,7 @@ ModLoader._draw_state_to_gui = function(self, gui, dt) local msg = status_str .. string.rep(".", (2 * t) % 4) Log.info("ModLoader", msg) - ScriptGui.text(gui, msg, FONT_MATERIAL, 48, Vector3(20, 30, 1), Color.white()) + ScriptGui.text(gui, msg, FONT_MATERIAL, 25, Vector3(20, 30, 1), Color.white()) end ModLoader.remove_gui = function(self) @@ -119,14 +122,17 @@ ModLoader.update = function(self, dt) if next_index > #mod_data.packages then mod.state = "running" - local ok, object = pcall(mod_data.run) + local ok, object = xpcall(mod_data.run, self._libs.debug.traceback) - if not ok then self:print("error", "%s", object) end + if not ok then + Log.error("ModLoader", "Failed 'run' for %q: %s", mod.name, object) + end mod.object = object or {} self:_run_callback(mod, "init", self._reload_data[mod.id]) - self:print("info", "%s loaded.", name) + + Log.info("ModLoader", "Finished loading %q", mod.name) self._state = self:_load_mod(self._mod_load_index + 1) else @@ -142,7 +148,7 @@ ModLoader.update = function(self, dt) end if old_state ~= self._state then - self:print("info", "%s -> %s", old_state, self._state) + Log.info("ModLoader", "%s -> %s", old_state, self._state) end end @@ -170,20 +176,26 @@ ModLoader._run_callback = function (self, mod, callback_name, ...) return end - local success, val = pcall(cb, object, ...) + local args = table_pack(...) + + local success, val = xpcall(function() return cb(object, table_unpack(args)) end, self._libs.debug.traceback) if success then return val else - self:print("error", "%s", val or "[unknown error]") - self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + Log.error("ModLoader", "Failed to run callback %q for mod %q with id %q. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + if type(val) == "table" then + Log.error("ModLoader", "<>\n<>\n%s\n<>\n<>\n%s\n<>\n<>\n%s\n<>", val.error, val.traceback, val.locals, val.self) + else + Log.error("ModLoader", "Error: %s", val or "[unknown error]") + end mod.callbacks_disabled = true end end ModLoader._start_scan = function(self) - self:print("info", "Starting mod scan") + Log.info("ModLoader", "Starting mod scan") self._state = "scanning" end @@ -191,7 +203,7 @@ ModLoader._build_mod_table = function(self) fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") for i, mod_data in ipairs(self._mod_data) do - printf("[ModLoader] mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) + Log.info("ModLoader", "mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) self._mods[i] = { id = mod_data.id, @@ -206,7 +218,7 @@ ModLoader._build_mod_table = function(self) self._num_mods = #self._mods - self:print("info", "Found %i mods", #self._mods) + Log.info("ModLoader", "Found %i mods", #self._mods) end ModLoader._load_mod = function(self, index) @@ -220,11 +232,11 @@ ModLoader._load_mod = function(self, index) return "done" end - self:print("info", "loading mod %i", mod.id) + Log.info("ModLoader", "Loading mod %q", mod.id) mod.state = "loading" - Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true) + Crashify.print_property(string.format("Mod:%s:%s", mod.id, mod.name), true) self._mod_load_index = index @@ -241,7 +253,7 @@ ModLoader._load_package = function(self, mod, index) return end - self:print("info", "loading package %q", package_name) + Log.info("ModLoader", "Loading package %q", package_name) local resource_handle = Application.resource_package(package_name) self._loading_resource_handle = resource_handle @@ -253,12 +265,12 @@ end ModLoader.unload_all_mods = function(self) if self._state ~= "done" then - self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + Log.error("ModLoader", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) return end - self:print("info", "Unload all mod packages") + Log.info("ModLoader", "Unload all mod packages") for i = self._num_mods, 1, -1 do local mod = self._mods[i] @@ -278,7 +290,7 @@ ModLoader.unload_mod = function(self, index) local mod = self._mods[index] if mod then - self:print("info", "Unloading %q.", mod.name) + Log.info("ModLoader", "Unloading %q.", mod.name) for _, handle in ipairs(mod.loaded_packages) do ResourcePackage.unload(handle) @@ -287,22 +299,22 @@ ModLoader.unload_mod = function(self, index) mod.state = "not_loaded" else - self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + Log.error("ModLoader", "Mod index %i can't be unloaded, has not been loaded", index) end end ModLoader._reload_mods = function(self) - self:print("info", "reloading mods") + Log.info("ModLoader", "reloading mods") for i = 1, self._num_mods, 1 do local mod = self._mods[i] if mod and mod.state == "running" then - self:print("info", "reloading %s", mod.name) + Log.info("ModLoader", "reloading %s", mod.name) self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") else - self:print("info", "not reloading mod, state: %s", mod.state) + Log.info("ModLoader", "not reloading mod, state: %s", mod.state) end end @@ -322,20 +334,21 @@ ModLoader.on_game_state_changed = function(self, status, state_name, state_objec end end else - self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + Log.warning("ModLoader", "Ignored on_game_state_changed call due to being in state %q", self._state) end end ModLoader.print = function(self, level, str, ...) - local message = string.format("[ModLoader][" .. level .. "] " .. str, ...) - local log_level = LOG_LEVELS[level] or 99 + local f = Log[level] + if f then + f("ModLoader", str, ...) + else + local message = string.format("[ModLoader][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 - if log_level <= 2 then - print(message) - end - - if log_level <= self._settings.log_level then - self._chat_print_buffer[#self._chat_print_buffer + 1] = message + if log_level <= 2 then + print(message) + end end end From b0932a5968ce289319e1f4cf51431c2d64f82c90 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 10:06:49 +0100 Subject: [PATCH 12/34] feat: Add API for DMF to get mod data --- scripts/mods/dml/mod_loader.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 48e941f..3dc2734 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -68,6 +68,25 @@ ModLoader.remove_gui = function(self) self._gui = nil end +ModLoader.mod_data = function(self, id) + -- Since this primarily exists for DMF, + -- we can optimize the search for its use case of looking for the + -- mod currently being loaded + local mod_data = self._mods[self._mod_load_index] + + if mod_data.id ~= id then + mod_data = nil + + for _, v in ipairs(self._mods) do + if v.id == id then + mod_data = v + end + end + end + + return mod_data +end + ModLoader._check_reload = function() return Keyboard.pressed(BUTTON_INDEX_R) and Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + From ff6cc9c8c7e23062962bf6e70ef9633a4eed57b8 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 10:07:25 +0100 Subject: [PATCH 13/34] WIP --- scripts/mods/dml/mod_loader.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 3dc2734..bb35d77 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -27,6 +27,7 @@ local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") ModLoader.init = function(self, mod_data, libs, boot_gui) + table.dump(mod_data, nil, 5, function(...) Log.info("ModLoader", ...) end) self._mod_data = mod_data self._libs = libs self._gui = boot_gui From ac46c311d485a1c1e3bac59717f27ec6737a3173 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 00:23:41 +0100 Subject: [PATCH 14/34] feat: Move class and require hooks into early loading --- scripts/mods/dml/class.lua | 26 ------------------------ scripts/mods/dml/init.lua | 18 +++++------------ scripts/mods/dml/mod_loader.lua | 8 ++++---- scripts/mods/dml/require.lua | 36 --------------------------------- 4 files changed, 9 insertions(+), 79 deletions(-) delete mode 100644 scripts/mods/dml/class.lua delete mode 100644 scripts/mods/dml/require.lua diff --git a/scripts/mods/dml/class.lua b/scripts/mods/dml/class.lua deleted file mode 100644 index 53896de..0000000 --- a/scripts/mods/dml/class.lua +++ /dev/null @@ -1,26 +0,0 @@ -local original_class = Mods.original_class or class -Mods.original_class = original_class - -local _G = _G -local rawget = rawget -local rawset = rawset - --- The `__index` metamethod maps a proper identifier `CLASS.MyClassName` to the --- stringified version of the key: `"MyClassName"`. --- This allows using LuaCheck for the stringified class names in hook parameters. -_G.CLASS = _G.CLASS or setmetatable({}, { - __index = function(_, key) - return key - end -}) - -class = function(class_name, super_name, ...) - local result = 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 diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index f455539..21a16a6 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -1,3 +1,6 @@ +dofile("scripts/mods/dml/message") +dofile("scripts/mods/dml/hook") + local StateGame = require("scripts/game_states/state_game") local StateSplash = require("scripts/game_states/game/state_splash") local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") @@ -6,20 +9,9 @@ local GameStateMachine = require("scripts/foundation/utilities/game_state_machin -- to initialize the modding environment. local loader = {} -Mods = {} - -function loader:init(mod_data, libs, boot_gui) - -- The metatable prevents overwriting these - self._libs = setmetatable({}, { __index = libs }) - Mods.lua = self._libs - - dofile("scripts/mods/dml/message") - dofile("scripts/mods/dml/require") - dofile("scripts/mods/dml/class") - dofile("scripts/mods/dml/hook") - +function loader:init(mod_data, boot_gui) local ModLoader = dofile("scripts/mods/dml/mod_loader") - local mod_loader = ModLoader:new(mod_data, libs, boot_gui) + local mod_loader = ModLoader:new(mod_data, boot_gui) self._mod_loader = mod_loader Managers.mod = mod_loader diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index bb35d77..d2e5b16 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -26,10 +26,10 @@ local BUTTON_INDEX_R = Keyboard.button_index("r") local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") -ModLoader.init = function(self, mod_data, libs, boot_gui) +ModLoader.init = function(self, mod_data, boot_gui) table.dump(mod_data, nil, 5, function(...) Log.info("ModLoader", ...) end) + self._mod_data = mod_data - self._libs = libs self._gui = boot_gui self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS @@ -142,7 +142,7 @@ ModLoader.update = function(self, dt) if next_index > #mod_data.packages then mod.state = "running" - local ok, object = xpcall(mod_data.run, self._libs.debug.traceback) + local ok, object = xpcall(mod_data.run, Script.callstack) if not ok then Log.error("ModLoader", "Failed 'run' for %q: %s", mod.name, object) @@ -198,7 +198,7 @@ ModLoader._run_callback = function (self, mod, callback_name, ...) local args = table_pack(...) - local success, val = xpcall(function() return cb(object, table_unpack(args)) end, self._libs.debug.traceback) + local success, val = xpcall(function() return cb(object, table_unpack(args)) end, Script.callstack) if success then return val diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua deleted file mode 100644 index bc808a9..0000000 --- a/scripts/mods/dml/require.lua +++ /dev/null @@ -1,36 +0,0 @@ -local require_store = Mods.require_store or {} -Mods.require_store = require_store - -local original_require = Mods.original_require or require -Mods.original_require = original_require - -local can_insert = function(filepath, new_result) - local store = require_store[filepath] - if not store or #store then - return true - end - - if store[#store] ~= new_result then - return true - end -end - -require = function(filepath, ...) - local result = original_require(filepath, ...) - if result and type(result) == "table" then - if can_insert(filepath, result) then - require_store[filepath] = require_store[filepath] or {} - local store = require_store[filepath] - - table.insert(store, result) - - --print("[Require] #" .. tostring(#store) .. " of " .. filepath) - local Mods = Mods - if Mods.hook then - Mods.hook.enable_by_file(filepath, #store) - end - end - end - - return result -end From ecb1b5372907a105aeb4b743bfedf11429beeaf7 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 9 Mar 2023 17:40:22 +0100 Subject: [PATCH 15/34] chore: Update config with new fields --- dtmt.cfg | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dtmt.cfg b/dtmt.cfg index 3831232..55a5fc8 100644 --- a/dtmt.cfg +++ b/dtmt.cfg @@ -1,7 +1,12 @@ id = "dml" name = "Darktide Mod Loader" -description = "This is my new mod 'Darktide Mod Loader'!" +summary = "The low-level facilities that enable loading mods from specially prepared bundles." version = "0.1.0" +author = "SirAiedail" + +categories = [ + Tools +] resources = { init = "scripts/mods/dml/init" @@ -10,7 +15,3 @@ resources = { packages = [ "packages/dml" ] - -depends = [ - "dmf" -] From 130407d3421832c12722b3dd5691ca529caa17a4 Mon Sep 17 00:00:00 2001 From: Aussiemon Date: Sun, 19 Mar 2023 00:39:46 -0600 Subject: [PATCH 16/34] Update to dtkit-patch 0.1.4, update readme --- README.md | 26 ++++++++++++++++++++++++-- tools/dtkit-patch.exe | Bin 199168 -> 201216 bytes 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c7e76e..8ef6333 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ ### Contains a small set of basic functionality required for loading other mods. It also handles initial setup and contains a mod_load_order.txt file for mod management. +### Game updates will automatically disable all mods. Re-run "toggle_darktide_mods.bat" to enable them again. + +### This mod does not need to be added to your mod_load_order.txt file. + ## Installation: 1. Copy the Darktide Mod Loader files to your game directory and overwrite existing. 2. Run the "toggle_darktide_mods.bat" script in your game folder. @@ -16,6 +20,24 @@ 3. Delete the "mod_loader" file from /binaries. 4. Delete the "9ba626afa44a3aa3.patch_999" file from /bundle. -### Mods will automatically disable when the game updates, so re-run the "toggle_darktide_mods.bat" script to re-enable mods after an update. +## Updating the mod loader: + 1. Run the "toggle_darktide_mods.bat" script at your game folder and choose to unpatch the bundle database. + 2. Copy the Darktide Mod Loader files to your game directory and overwrite existing (except for mod_load_order.txt, if you wish to preserve your mod list). + 3. Run "toggle_darktide_mods.bat" at your game folder to re-enable mods. -### This mod does not need to be added to your mod_load_order.txt file. \ No newline at end of file +## Updating any other mod: + 1. Delete the mod's directory from your mods folder. + 2. Extract the updated mod to your mods folder. All settings will remain intact. + +## Troubleshooting: + * Make sure your game folder, mods folder, and mod_load_order.txt look like the images on this page: + * Make sure your mods have their dependencies listed above them in the load order. + * Remove all mods from the load order (or add '--' before each line). + * If all else fails, re-verify your game files and start the mod installation from the beginning. + +## Creating mods: + 1. Download the latest Darktide Mod Builder release: . + 2. Add the unzipped folder to your environment path: . + 3. Run create_mod.bat or "dmb create " in the mods folder. This generates a mod folder with the same name. + 4. Add the new mod name to your mod_load_order.txt. + 5. Reload mods or restart the game. diff --git a/tools/dtkit-patch.exe b/tools/dtkit-patch.exe index 2306a5c2c4273307e5c40de8254580d273234728..177d2d74548f82cc8b0446fee73557f107b4f2b6 100644 GIT binary patch delta 65905 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 GWB(t9D8;G( delta 63924 zcmb5X30xFM^FO@ZunH(Fh^we5prGIho}j3pqO-bqpYe#sGu{Un6E%wg64!N&+M0<` z6BA?Z!-Oc_0a5Tw)WrKfVrC^OYNDu#`~OzYf+kO%=l#9;JP$ilU0q#WRb5?O-P2}# zSUF=w<)x){dlf$(ZSee8VyJ7V!~?1sdUAtdn+{ysLDXaoSd!?(Iy6bV$qoC;u<19@ z%P`XX3?SCa5a^Bft=!OmSwwK13EQ|K8t)?k@L|s&j^u_?zAU0ld7Ekh^bUUeiq<^L*WhI^0ATn{jNmm}%VyEMp&CH92>$fHB!eNT>CCZ{ zr=e$(9k6AnfO7AgulOp)|L7nr-UoP0ha9tUG;5NT%nXYCj$%Kfy^P02S&F7 z#E)Ov$V9)i8H)X(V!x|>nj$t-cqstw#T~Ocxq>@<}t|Z(`zi7$qU`Pu~b#x6u@d{b2WbO$c`uvFG5y{Uh?x+w@S^5m9VBjDn z^S5ZlksvzywCvSfR&kqm_dsI~|9bM(VlP3B=;ueE{--FQ`gkkIcsYcdvKG7DZY49p zkXBW)?C`(!8*v5P`8`CTPc%QfR+RX}HXhQ}U?^^^I64I?4sbUIy?dy&EFn=IRqPcM z`#?a>SnQ?R#aLnW?a5chiZ#A9s!#634aEl)5=tK*G!aNrv89Sbz8z{LhJ(%Q`)O6c zt0<;sE%sAxb9i@A&o8Q8P!u=VixkJONJYt<7a41ics)FLC2sR6iSHVcZZVlu8Zwje256rD*@a8+tpazg7_+Rhq;*)D^*% zQ~i&oqQ1RLWN*|=5K}70a;W;f$}#+#*5W|rVbx#-v>L-w>(?1pTw$%r&HoBM@;`S@{-P%G`Wcm;D-$u6JX=`XzIly>WI+5kRjxHL|z%Td4ReuonL@RMZR#^_wNJ-Da=OA}Od^k_PvpIKPO@0PVe{F`IhR zdr`K6ABfrQHXnn1DfYs+JjI@E&xWF(|Ntx!lz2dwi^}uBdtjjGTEIW zgz9usjksf4^cZHaIycap1;gCO0{&sB2bBCPM#VXNqgYU-Zdf=a7_Ln!~U zac^m0%0~Q2&D&cVWc3NeUvD49y<4&CM2fOwg){hh7A|fE*Rb_#2@mARjzxXfCU9YG zmrWd|SFze%R@jr}f5V zzWT(Vi0rCW;;BJvOrDZZI%}NXEIEKLT95_ECOr%k_faz4-1M3%t#EC0&W}~!V(p3b zr29q8l-2)Fw>!=~4&KkSvf!UAtqmz>DQV$F#o6*zA}&6t*18&LF(vMPoZGUXltC4< zOjhTVjpA0d2;MzT1XT~_A>N{X_3(y-XtDpP*dNF3cbor8ghlR$`%Wfz^8j!5!tA{C z0Jr&GqL^O2s^58lGD}}EWygyp)uVcj^q0mWduzX;dWiNsz$(B<# zDn%(<$kpIEuP|jFvzKIE^2#h_rtHN8b#`^bzNxn2=;>A*T`MZ~P5aP@8xA^~9u!AX zMJ02N5AjNPX|=763IGHK&GfdVI+98;nk!2-+~zgyNM-(iozsUZoZ})uitEF)WxrsG zDO((Rhn&LJCRzH#C3eNP9{n&Bjxozkcso22Pu_Q}8W58JM2lg3?9aTh|D z1~BK^iB6?L3Fee6cRIT8EREt}7)K;##hJQMi>WWguaB!uR5`txhXb0kYf2XMwmnq- zlLcFlwciyArcc$=8hH_nHx`lc-iJ+%`Q5hU-P0qdGOIF-K~i zVpq^kH2JF_LzjiqLmW;Lj$`FjG7hDNy?3b9DV1Zfcd*#I`dEs(29i0`h|!q}EMt=V z9r|x|%+Iqrrfjj=-PV}fN@mfedV3FF~@cW33M)`W+mpjK4%pQss24ZD-Z8^%o9 zbZa|!ZsD#KS%W~1m4sop#wOVN$0l0bd#v^ifg&+9wErFI=~r-@7$827OYy3n&cvpc z%%X6zE35rwA8m3m=5EJM14^xqFA2$#`6nFLqrcp4iz7W2+B0Q!ML$v<^P??J?>6G= z(0Im$7TVUPWIm~wK7LD+CeueHx2U*vRNCkX$t`+$`R1Flzd*-9$ZKX@i(`P2>PYDg z;gA%p_B&eLlgyBu@hViAJq?^&o$27^j7VFq?Il$UBXVJ3vr6mx%AI&Q>Q~m@e$IeNYxnW%jOtc*P`c%x{lZ zn>5VMfl5*55Kut5oMd6K_I8SWR(SIFG*?>fbEC<8pI?nZ>@>zpLKQq@uMi!3XONv6 zQdVmh8I9FGE4s|^qh!ri*XI!KEa1)}T&vU8e%L~a$>)YE)P`r6l)fX|iID9OWgYt; zSjPzK8Djq^&e4e^{!bz{&8egH{GVvr`2XFOq7(W5V7RW1RqvUmY$Sz$F#hMN|Jkqe zRO~c9DHr>{3MW^`3Vf!me`#u&O-HNyztJY?SY7@%R*H@_;=f=yJmDKAgId7UeV9#K z<_l+iR>^kDg+lY=H(#@|RWgIig`HlXFCRRmFk%#V#FhVRpQ?4NL3d zWUC`Zd+JueZJ9%scuMB{ONKNP=DMGM(kH<&%@L%?Ner&4H%9YT^unN}!+(J``AkHr z6JZ`T8uX5I`vHYK&-{J&6?XyF3{sN(%b5!Yn>GS^&sthU+yDL&Ojb0Gv2Ya-Kj8gvt3U_8N*ED{rR^RTMj({41sse+oVRJO+l~9I#O& zM}^r)t=hR?z=vlE`rR?=r(A+2#}T5jN<96LFgpi+PmTz#B_!^?VsC-M4Vh$Box|Zc zq)Tfh$Q1Q1IgRPmLAxGDIE}705;w5i35F;(dyDc(U>1lx^;tKU)3DUdPS+V)tN%B8 z$2|0OPuT))?Qax5mlTXYd$5RWo#(r+mGe9mJ`w|H!845|#LC!5BSAX+KR&9fy-h6Y zWXd@aU^(RkgORBGN>U9_30284dbnN+EO6 z!mmlCn6k!V+6DRFkCZEBK{K?_lfX{@Uq)b9IE}Yn!9qKlEb&}*#H<#wb*od_u*FVV zJdEK(%k6N~Nz9@;{>R`Ewl$(UZ`b4oQzc-14>H#N_Iv~h7JHty0JKb%w1%MuDz=}d z(DJZZ$Mv7AxBO+RvX)0nY){M72fgJ)v|OAmV(ZuQ)7P5Xjy_B*Y0xf650?b? z8a?1rw|O~%ojW3r$l4FJrdLElj1r@Gh<@TBn)D3OtV23c)DCJSK8p#j_6CWr3n}2$ zAIq{M>*tC~G3ILZE|PY2?pzZw>Js5@){06EM^qy%BLog<7lg;!LAdJ}q6uG_GBB>0Xb!eTwj% zc&>5ZkcOTDQ#Pd-%WHQ>VW_y&*r!v|{lpltIoNI9+5p@+(jrHudQ=i9OXWk+F(na%4Qs*QQKLjN{2? zX3H%V=`p5cSAY85y1s4q55KPe(P||j6%>oe6y19jXVq9r*c$ajcO|!wJR79<7jy(KCN-4iI7|_)O#P%|xBJn!?$p zvP~;DdZX|ZQ_?R|K=kKf)qw`=UZTzqWLm6xj@Gi4o- zz1(M0-1$moY0X*ZT!qkYO4hZsUWikwBS3(T`{E~Vf3lpy-R7A{<7SptNN>6)|CMPK z=}#}H1&KdM8q3m$Hm1Evov;^RVx`b#Fh*eU5cJmvzFx8GYQbv|gh#K~DyRUp8>>ih z*c7J+!=4nxf~Wq7V&Xs8FJM+#Y?{VKCgQ%ME_yVVZy)Tddd`S8GDwbYi+s zxN8_1R-8S*fY3iTy)jbGsr^AS;jZbm(ewrmVikKTavf+FHaK00xvSXwN6t~~dcG63 z(^rZB!ou^CrbO8qIoRrK@>`lnY+l2*>=Z^=Z_){7!zLI>G3cQyQBIGOBS#__c2*5r z?w|gv;`BzO1b?%QQoKsSPie7A4AS?vAQP?PNt$;1lh5>U z;+P+(B%Gu;l6WCMRh*(Q8#4dC@a4?m@OOAHY5^lD>wa1dOJ+wyT5zhP8)V`?#Z&rMnE0Y)Xn04OshMPo`jK!SeXk||mvBqwVO~aOfTzMh5V}$uov`07qt)&y zt$io~V@Ze)6`E|xjfBc#IU#>a#o##@V50@24bltg2jf3Rc%ZmD-bkh<)ybP_EqeqNaKYgVdXSdYb5g8)ND9&8pyygVSN;> znO|c_NF_#M0#C&lAlqraOIP&nP~jclmiK%hlHw!158*$tFrlSwa40beTh4^M;gFMou2?DTS7|hQcIoSZYe3gga8Gj1=u7V%vq;{LvCk z5CfBCBQ&Ys$u)2<)lAKGcoZ z49eURtHVc1c&F_mD>1TR&l7I9X4;Rb1SUTa&7M-6^TFVJ*oE;yYcgX<(P_L^TO3Pl zFrmdO1bL`whum(t)-LAA9XgD2ah3qFhF4mX!W09u+Q%lL&PNt9W_?Nb91t+8SL#nc zDM{GQi#K9Nr&L+6hceE3nnxyiP;tsQai@I^8=pk=Q$|8m6Nq(KYOiqskrm!|GlL76 zlV|Os3c;=c1`M(JA40m*EcBj#scTm0UQ_l3CqXzl*zCj#L?&NoP4|?KAE!Fmj8l4*S2;Bt!viD{(?6-Sq%Co{h8K~9~NZj2AQFUEIG49KVH%9&aM4drPY}o z>%EQHv1~Xd;u%WZNwXm^wzWVL+DnhbuU+Tx`M--%-NLx_5Aj;JL4gj;6|z;U6H6{F zE63HX67$mTGIhovR+DePCxR_~_~3gv<1E7&uhUZO?VieB5y3s`@-JJ6Ha(h0RyJ++ z{Ukiy-?a#*Q}FCMmW24X3I52Q%Rgd%k6C0e96B>DK0aQ!VkSwJ}$Rij&sE zep@PW!#LA4b6zF!Rq95+Cw}^Cg-SUSdM11M{->++3l+rD zzF+u-BNC+2=nA51znxw~jCyG=BT8F|{{7E!t9MTG0S`E@RYf!!9M5a~BE}Bx!Vd1MN8rsC0zvUzLJwKP%@DV8^@A3z= zMZ%~^?yex7AJv#&?kb$4YFB;-tZd65X)in{HjnD;Rp7(b&}1GTdbZpaU>y!W%$}I= zlR}{Qsa~S@3z?0FLKDewqcrG`VwceOhTFXq{*%@Q1&H1Hz@h4-JD$yPzreWv9&f1E zzMC>-ai@j<*zSC7Le7w}ZsvUswxl-g!fU9TeVExhUMo2+mW=Pj7aODsd<~=muj6he zKC?FBxgACBgqB{T{$k?Rgt=b#{$yhEi&>{ zOm54)BE;s&&AfsiGoei$ACdlurVcD7;Cqn1)!JhLl!cY#Zkc;9Tf)WkDQ&&_JYwR@ zDG$9~eZ+D;of^ozCOu?g$FyD{)lD>mQtykgz#-9KS~7Qy6!oUh^6dfjroI#(KG9N&BX8GnalXD ziXv=Q4Zg5OPTN`6yu7B~Wg_AwbA!!T+RM~qtGhb?;dBrLOOEJNtTEn}rQ?=bZZ1UvyJ{}a1> z?ihq&H7>#PT~pw0f-jfw+Xraiyd|HUunkTZ{8s$kexlIZ7b{QX~3-ZquQt7i3I6S83Txb;CfQ& zz1`ybxeLwNx0#_hm=@2NCutGxY*nSP7&h-6KJd0EohSHELGr8W$<6zSRw?v|*{b;e@milP9(++2q zzV%qtMW_fXv*HFQFP6`Dw6ouN3eNxlvz3*v>xEXV$G6MMF$bJ7(xE3_Bj|p2Hvzv{}(=}!T(@m5JCTy4KEKL%@<%8%(l;cpL*mrp9z5Z!h;km|ZSIJN&U!rQjHt7u36H2LQkR5QK6|>X#{REG z`jWbQ+cmLjNwn8aILJLq+J{7x&=CEnuj*|SHQo&O_tKy!3e=tc;hO0B<^n$cw%Gq> zSmj}-%BXh#O8oxjEM9a=ytp*1@(yg>>HK~6rC70a7H@D%_$~{p99jHToMY;xvtKb&1!hfFJ+?Zih8`(MKNhbXz*e57%HuE z6|Bw@pb>#$9ydFgd= zK^?&FTo$d~#^K2;V({A+`OQlrVbxgQUlG&;rRZAD$E(5__q!(czte=DyDCcGX~dUZ z6;Z33@}n2TfYr@->{apl>QTJpf^e-);w!I+Mr#`J*Di>`YofXFig;~J5;t5Cd)CD8 zQ1^$6OSB-+RkHWI=+jGMEg9iM$bE3%yEqSGLV)O^`{;R0Iqv~$O*<{yw@yQ3FJm{1t{2-d& zJu6B+sKF1P6qVLL?^_FY3#mLlBWA51#=Xyqqw5#)SI>x{A9mnLC&cOxJMkyS#l;V! zYV7!x^h?f7JD-DO_k3U)9)djzeK27BFm*>3rX)xm<$Hc3jHRB~m#n+!T;LBV& zr#@@z71Q?zkLlvvDKbuOZPl9|kJP)Ygr!+M9Hs+wZ#w`(Fc9W1ALX+t^9ei+P><~s*P z-JJ2f(Lv$J3FWZ|#iuzJdCLQ0(RcNEg9GB*?;Kp&pVM(uZN`V}6C*Z9b9a$gxH*nj zD-zo`x8tV^Md{`~d`3}DkKD?Pcikg~Z0XGl_U3%P#h>xXd&Rb`HTgGt#EGrjs&~Xh z=5k>1k8G7G>lRJvPV>S&;^S=*JRnaT*cQhx7l^;M#rX~bCd%3rh&J0h^4tP3XL|&X zFA%G@H|FCC#D2Wp-X(t99>)U`t=Wi(WcjRo^wS|Sfx6`9$Eqwu5Lpf4y(0RO5EY3^hzwHzU@)sFs&Y`kQ$mrN@9^zXEbX_8xPp1y z?|}Hapanm*O#-f4Wn7SXP73BQw@bFi>3 zs~5JpjNw0CewJZVRxMp8A8i(a`%?JF%fz64pYzO3Io|u5v4%lkd8)zFuY(VP-#{Dl zoRQP;@Ks*rB>4eEO&B(aybfqT&d3QnvWhqKK7nwd7QAvW9%R`Wtz;s#2v~K%5PDFg zc5Y(Mm1Fn4x>o%NF)6GKaJgjoc^u(_`GTLMu+MEMYx%kHo)U}fkMT1qk#0X}yr^@s zANTr5%sQD6>H~_jk(vRbTESSVgwv=;A?OaDiG3$0`NhBhD87taAljcg%=>1D%BMrT zlnf@8^^F(LpB~ApY!LaUBiKZ7^>iox<%godnfc}x>xiFuk>OTn)Z`E8xrx_@Ia|+! zGJf#`aq?_ke(_!L_-wTA3m;JX=1w1o#^T;@j7VLzk_-Q`X2DscS^Ul@r{(T0Fec zfS+0|>RyfHX{&{Dbu>4BD!#nhh5M}*_pXlQ-QN}iuKgVB*ek{NGY&!NYOgy)B>vKv zUt5(k@t3o_#Y-47+ROnS#jL^`W!OeZKq@|6e=-(dg@MirytvZ43M8S~PY9 zPH`&~oG3$v(y1aW_NO=KCo3`!D%2;tZ?Kgm7euG9eJ9(~LkAp(#Hox_WRIJb5v~Rr zu2&zke3MDyYuvW52h2mGC^oW*}A zoM))w#DjFc^fi%MT7TlG*O+0!!+uDPn6egtAkIgoLfOXd65a{$v?*n6Wm zIYL>dU1SgSo?&tjP+f!covMTBa8AVPHu|F->gbWp=y~W{uqhQI=1gY~Y{)!O`$+_^ z@>)*PlNTAUHAj5zZfgB`HA7Bs>_BuJIlT_}90ji79Ds0*Zu0;SyuM;D0{obS-@zuI z2Ogl<+b75+wz2I4wlaz}s|`m?S<2xBrZCXx9~n3RNxg8zIWbn7lC*LLi=afr%9Sk4 z%Yffp*4nG+K3iGBYI=QkpQ-+g)$+|scDo(M*YO(1*t=XTQ;#wh?v?pIQDzZx zR|lJ-jp68qDF`R`255@?l|aQl6Q73}uV9HbuBj<33lxZaK28jz)hSU--}o2ZV`_#M zt5rRyEA)j``GAtGu|SfHXn)45-+Hmk7P;`>`qbkzug9|$>B-n+`l7;6T#pK{wdMcq zGe`>-s1v-|DE^aOJ?PD9^WW|24R6*xU=B7UJ$<~RO;S4<*`BH&B|e2g(D5UNyM$V^ z0-IK=9!ih{DkKHq(+TWYJA9;FxO~+QE3mqCuD5f$v5dxlO7^idA53VjpLCGJc&tRd zR)KZmOG?z<6mn7 z>O>#bjOWZ(SNpJo{FYUn;LEx-{PHGvfvoX;$3yp#;!#Na*8Gjwd+1K@gdG5!oFtZw zO@32P`?C5~&lfX88a4n6lHDh06VzdRT&xEBvBc(M*GbI|e5cH_{Mmxc+Me<|HUxf0 zE}ym^_PtKpO}T`}v2Uph{8&uvD{rAWXX+=~{^oKR;KLNDRHu0zCLVb5{rCaGDdc-U z(W-V(&-k&Pv5Qd4ZLZcs=N_pFb6pkBG$MR;V0uL~(nOkvaq|sza3$8>%k~zGqdO0& zvttb*r^qcZPix=keC*RU#;HG4ViEjiOZ84AHa_;bmDJRA%QH<8Dm8Ul2R_@>5|`do zua%yr7RwR&_ohG*J=&*b#Oh7$Y@tT@v#8j~E2ybb535Q=e33by;(j1;8FwxYW7QfCFQseZLuy4|Kq z4&y;hEe>Fjd~Fjo$ix!ZYibV@tHW~Ci6*wB-l6NDhTSq#b{%-LjQJ0+Th{H{q}9w+apuUV&Q zVV${WRW|H^A=sy7FAe!WAE6f4n=#=j0R*XHht)`TE$_}JqXxR z;Al>(KUZOUS#a(-)P7&K_j3%c50^*`=YomHq-D?zrJR+nbE%u;uH2ekA(7a>f- z=*hz;s2AjXy#qZq#bC2+3z@DZA>U%^l&?72Dvnk!A6Msxvvzz>D|K5q98$z_ z^?W#M#jDI!-&lx_wRl7QY9Xs1upKi5#Y7Y$UmT-Oi(q5wENID|?o!FaFgWzsZ%ceQ zrrar}+&d90h4G#()cAUAc;(yhyij!oL>bN1_v^72`J*_sawJP=(@`4o9opcZT!Xp> zodpB7%@9Z~=(Kjd4BF%==CK;;yhzqy{5P|w$B9^e>e4=bq%W3{#q}sARd#&pITIJ*E zj%S9ysvfA%_6F2`rbG+OUE6>SL9%2>ZfP_NV!XxR+)54MRRbneFDGzX8>;qg#OekI zj>dqz3IYH40?F;JmRm)AqY+!o`y{C5#w@Bva~d4ZG+Hl1BagJZo#k1mb_r^~#;hyv z)KkxJRvyVjyID2VBaK z9raZ7iGavi>xvGPqNlqRps?|KRa-0zwXv)o`%TS{WnVF;Iz5h6<#?w^wc3oYnCf2I%ds+^kM- z&iw0j97WCRiO==VNyF>XhknZ3?^glpy5@*if8PV@*pgRIH)pf?fKh6n7HkPmt*>5g z!BTmJ0cvbZ7Q-iXR)@A^8+gn9DsRP_^QE2C)~#3^Ki*w^u@!qUzPr>HRB}Um-iHK( zaoC~zQx+Z}N`NcK>{Mp^v*14q%u0!h8ZJr&5WL4Qm!R6o@}iFHUB(u+_ZX|BHzO~<2IMFOs9rwBm% zJYoGMF~zYGQ>#HcLU4RsZLBEakF@X6!qO!6W0jql&}kN`q93@^+CR>nk<4l^-rPrB z+zAHktv>7o6)iHTN4{dAYEoxbjSb8l)S2~VevhF8T9!K==H_%^FMIK%k-1G3APiQ} zj@;oXj)Dm4(wpwnW&Y^K@0d?Wi4p%O$d?7+^He`i4LWT7c^UfMez|Kb?0{F76VSAF zw1lVt`4Zl?Qgnf{Lp#cVj|pX0*_#n7K$-^EmI3y3Qa?*&zt-+{(*Wfcx5K3!;nsHD z!qcPh9dC%m(KAGAd^7i}o@}~zoeJ%#@_}hh->QfL0FH)-BX=|SY+47H=cM_B+^aO@sF(i z^a-_b)O%w1N>|SLzf9|H-_kNQ&ts-Fr)pYckz?fCC=?K;Q`WJm_#3V zK(x7pPe%x^K-6^JJCD|bwLd|Hv^Oc7u-i_%&D&sViv6BzE?UQ0rvRnM1gF=-d@%{F zN~g7iaEQ_lNr{fotOlO@D2O=BZ_6sj(s6{>e;z$M%^gt_lT%r>^|Bf+RxnORdFBt^ z_B-xln9qlflU47_s-5(z!Ln-YvZ`;$s^Hbw7Uh@)Q9$3g=3_@w#x3p>g~khOnSm8o z{hn+JF$iYif|KfuA*^<<`qIl8Pj)Pu&URh{N?RP;D}`8n^wRgsOW&i?P*3TN-WV;| zqAf37a4q-z5R4XoI`!^Jt@gb7)=<`sr$nokp)9^`JCx8Fn+e*T%VbP?^jH4Zwd}aM zekiNYryW-h4n;`T@wobUC_7$#GOUptbS!ONlM4V9;~PEHUx%^Qyn16bY&a%}pmu8e z;VjC_?@xm|b~sz;)e8?L!{KNm+ftBIUA2s0!}oSml`k+W_iC+PeSv*daXrm^_AGVXa2BC%9nGHeO1N)O ztBqkZ`A0R?MPpc-%9&nJQwBP(%{Z+d9>YGY+V1?5@@#@9u83B%$Fk$BiaK;07;Rr) zwU1*R*e~k7aV*UC8XDOe2nw!OF+D?uBMA^hfGIklJprl^V3-aFEw9o;2RueluG46% z15N_qiqW69QlYGl16S0*Y6AEZy~^BRBM3r0!`1+TJcc3x7)SuR$H{d=2P6=nG67EO zfZ7D8M1Xxd;6c5zD#)Y~jicp&k95E{1Taw*tepvCIRG}j-7Fn46%ZFzEmWFF=yamN zg^WJ|?$re#o&W=NzAQb{nPAC-!AgNx|0p}tBknR4U19lQX%5SF*c#i;5#Pnf5XuLvzs(5y-)&Zjk zfZ)t#a4pgytq4L9aq&DczdZFGa7Hh=#984#;%#Mv&5vH7vrm2W8H9d@{6n`UlEr3!HcXh<4X$FBNJJx z(2GZKn%DpXdVoWthQPFja6`4?$g^!IRwE{{6h5w4oiK@ovnbY;spasyn2=#}VthfIX2+Z@TNAN+l(JYL#RI@r{ z7K^EK06nG1ZucS2Y`Ike@Fl?`?W+Uox>;D6z8Rwa23-DPh*~|3)#DRG)WkH_f+vNj zGt#h7s}-WYorb{iyXxwpG%VQO`QeFNu!Upp_^A3@>8wS>%ZH!1UGt?8;p&ylLKE^h zIE9H1q`Ub5t9_)}KOK~e)z!J_aHzl6Q`e_M80~yjS31%;>#M1Lv#~@RQcZ0#8#%(s z-f}I#TUS#T&&F(?S3CF9*+~5Gmny1TUt*0bKVO2BXatF&9!Uo<6;34!COvt-L*;Q(D-M>U(opA3phldUr0% z3TTb5zC1bAhgyu9HIGG9d1)_1hCmPr3mNG=_+HJO#|EQK&?m|2B;e7Hgb-9Cej|@3LMj-FOqTAa_a9qr7ehgr;y$H(0l}Hs1ytNqi`^$bc zVlkWVUm*y?A2BZ)9lBrrd@)Pp*|k*HVz#DIgFviUe6eMEp{Baj$yV{7wyTYnuw*{+ zOLgiJjExt!t1FkV20ZXfb=MO199yB*c$3Ak7t|haviP<~|B&J088o1;fcAX_=M@DB zUjzu>j*yT)Bv=qq@97maCFhB|*r?t4Lf!c$ThIU8ufDJp###7Hx%eMyTh-r}!Z<4e z_N;Nf@wwVy85`*J`~!oUv5YlnY6L#`n&9bj#g>PV1H% zoqK#4!;rGsDpz}({meM4lDq32c9w^ZM%1p2*hoiQ)2rz* zC-PPZAI*Lyb<eO|0*_L>9~X2U<=Lky?!4B*-n3EX-KFzFGrL-@2$ zR@+X+J>ShOd5?kn?dsk4*_P^k*Zqsby16Ph{{uG6tIZoOYWj*+{cR78*Kg>6YGnYY z1YE{y!mA9l{D#{9BQ~dM2qU{s(YA(^^U_|u^bu>s-(RWvf6QjErn$C{*#H)JYSlC9 zI=JeI+P@KO_cEn6mt@2v(bRpq9Uc4S%Zh0oY5#^=7N_WOd3=3U=Yw|v3U0y0*X6Md-7Qrmus zuz1Xa`r)RLB{OTI*m+xK1di!WI#n?PVvmMrOrXJjfa9jBkNWJ!0~EI2v3oiLFq zxT4ixitfZZxZjC;ue!dxjdx3gmGiJ>XRo`6Jgum5Mjn8FiCWGUdM(!LPSOcHR9cZ(qX-B!)GO{|}u-SsWY5y^j8{-E2=>1rY z6(t2j{@V~A;X^}=Cx`I@D*TsCmeoMpf-uBjH|F6HvE739s&Omq=hksGek%*-S;y7q zwzBT+eeC5tKhz#Ce1a?X;L%39E%E6A_);2S7kTk_FkbOFW4amg#>kiQ)^;vaFK=bB zUZ4MD5Y0M7;P5?=u;2F+{iI&f+GMKTwy{6VyD*(W0(p?YJ}CjGaeh0s&vrJQKR&7! zY-drGem^Sf9MK#L)O*|6wdzr?|BDOAv#Gasu%6~iSrm8`lCMTa2}cJWX~S)5pPj5a zAM>g@aVKlZMyNREHH3YqKHSOFfKXKMgd(Rkllty1_J(;pu}OQF*e4)K!QNWB)VAMa zH!b)l)$u)>9JlE$LdjkTrnTxbUguF% z&C#lR8wfrd_rUCor%Lv<)A1vd2BowDb($-}hq%o@twbTEDTdP0j1nBiz&5!{^4M6$ zQ>LkZMetZv%F{^FlaQoq~FZdp2|)3=>IM=H|Kmcc~GQTw<|&YzXtd!L@2X6FU!-n=(G zEylo<`vTYC{JRCOH6-R3!q)YgaC z>Xv64KCSr%Jv+@?PEpMlur(=lv_R^JYJQD!xA{|huJJI#`K`ZRP}}^#w%RJ6fY=PM zC%T=j7u2RN?d+Q}u$;&E{SY1&UUdVZN$Iu&PVtKX9fP0n)O@I!3EB^rHF~N|(W^}@ zujWk%%|WoAUeaGL34^|FOhpmC+wo7(i`te~Ix;nP$&Wbwz}x4mcaGv%L96^+^D*{3 zP7Y5{Xa9udQtJuohd*HnRB1x)k3TUR=S^p;y-#4}5j0z!LchPKtBdIOWP0w$C)gK^ z$E4*BIEB!|)(>h#Jecfr(KP_^HF~rRVLYr(dwH~+sM*6}FDpKcPuOoUb=!~fQ9poD zT*_vgjt9zr_0|!wW|e@4#dVYi7g63@f7N2k$_MEcC;$nSFvS^YY)3wXwhQPSPwVmO zmNRT3Tcp-I%O>#=Th*m!S$e?7Jb zcXfSN+q0&R-JP0rhlvj-``s)d*V$;KJcJbd~ZzIv<>dJ5ND34i-3#IiIMRmsp47NkA$~$u@u@8%at| zj60g~GWSd$N=u`a6Agxz%$`!>nv6J7dmXO`_Kp-9y{f7FGV2*UX1{K0-<8?g<6-LP z%go8=6{>eHWAQNHJ2mDC79)>y)S*|{6h5Y^df*C+3Y@=}TGH=ravE>vsCm~|9kupV zpgzb^dtGI7_|0H-|5et3AOBX(`;}G01-QoUd(_BlY;tVqh-ZXi!b|ywor?V=Qf)g5 z$SgL=TcPlwEC^g<5!D(Y8G}I#BQk9^*2jyI*}i@i^}#h*fuEm4@%mM z!$`(qCiR_PvGq}HBlSY%?a{!mtd+V%W33~XV$Or{efkl^7fp`vjR6?WA7X58sZ34} zcZ7VQY8umE?yr5|d~fAbm!Pz0{geV9M?T zy}8X-C#m&ruolhh=aO6ym*H-MJWcE88O6WCRfCO^&1Gy{K#8dmt{a`CF1*1;utN3M z8?0ZInS;vu>2Qt&2O3uP#jHt#7Jw&bJmKk8%Q_AnT-brGJb4Vu%Y5RE_aAns>xx-x zb|Uv=F%F3F&xLyL7JI(-fxdD{lopPdpal1$1)8E$a0r7u9^T{CiTAK?cVd7V{X6R%WgjJVM_=n> z83WzPMegbsNPV7re`k+Um;R3Iz_3y3XTP&_lQWe9Rwq?)o1;gmP5*#v*f~y7CW}W%067Ohb3BQcOc>7A>PI@xL7g2bNAt zyC~6cc>vzp;8qc2b6yH8>=r^j52qC(K3^};R;w@EXGQ#8ceT+2>;t_wBsZ!Q0dTO> zLID*P$h4mn+nFeA2kOJTPPs;i4ILu z-ls&1OvZ=WAt=yAJhq$K@iA-A>=3|+PoKRae#oD+RR+Cj&*A$}Wy^;xU+&)E(T_*5 zqf?-+c+BedEAC2NrZa-F%d~o&AnC|uk?!i~fQj}&H{zWx+;T9wAs(w2*R7XOnQpZw zsIQgDEx*UA@1N{8tE4{slXa@_Is~n~uO|P64Qb!MRCjM4Y70P{lncw(zUxRVKV8A# zDyhuhaMKL#uCdrpTI~1;HqMIaB-(ty%}Sy41m8zdx)ods9uFAUL|##yW{NXWGNQ2C zO8&3JNC&jv0qqx9WA3GzIu}~(KWmTMm$#0~A0VoQhE02SE z(-Zpon5B(N-UX&X^ClLzHZYO$4;F^~orRa<5F8u_QeaSuEN^`j>* z+zRhC6K9)?o*)s8CcDN<6MtORR-LD9k%_;I&55oc`w#gM>7WX&@n>K-t+L!drhg-E zthU-Ak~zBA&>qBw%0Z;%pd>otmIT+TzVF4W;=VvqqvvA5%tz)~Eg=bV6!O~o0Dnvs za?LHB>?5p{slv13bydVS=c)D|>9jVah+DcGS&rrM6E`q5#!<#E5sK(Fvbtj)vf4FV>4VTFOTU=V znTW|^9! zaITP&kff~W4Iyb>gaO&hu4R^1dI=}E>25VF!&l@L4;1fvT~4KIY?HHE~^s_{gZ)UfRxn^8bo6RB!YScTIet!ExsmL9Ep*s@JBut zTwN!*ki#2|uS1)}RhLM#h=w!{q!F&H<5!>x;wo+s>JZX zG|nzr+=r#iccM>L=XAsnz2TaZ;F=#p%$OY{@tv*9iJ!9C=yvmoG~ji8>mwyw~1 zgq*k=Gd+ZkaZX-L7c3qYONoQLXX>CJ1F}A;#5^#)wuiDYo{m`99*RLS-Da!jmOE4C z2CNNSjqyirsoc7e{>w~92BLZwX^W6?^%@qQQ(ogAbiVV8PGmTKc|9=># zME9n@R&U*wy>0&o8~umK<^=Wk)@u=|Ado6I)H!5uNvX~$_(;?{l1wn(0#orx#mUUS znQcay+1fmIOHRXr&;R1-j_1i_x!*-nn1b8?V4f8*IVmy4R#WG_QsCOFEuY#DO;5Uk z7QsOONIzt?-+~$9LO8|L=?TrzNj=Jx;%w}Vu;Y}){R2#Sc1y)!O9lB9xab0kZF(!@ z4LaxPjnsU$5dwuCY9ZCEW(xi9TJlyvK#)*zJ7&xy%YsL-vG~VTlJMc$ z?icrc{+Yp$?=q(*{AOC7XHEFSw0yS@J}6Jl$U3H&y5s{+t_&U6mvtzQU4f}sX*&s6)~)R1NsrFx}XW8o{vls_RO@b-OEKScluVEV=tjunq|N$2?TBl{7O#CN^xZ;-G3j0-BDPBrX=LU-tuX>%jyY1;@^R& z90~PXNd0o;ndHbb=_AjiJ2l%nw4-}&{b7Z?rNgut-#y}AL3)54o6x@GtwAmk>M>71I9A)%OoA)aZ zj$;0QhQTn&V3^Ke7)?ah05lNf9e_Xm1H!=-qnX&lSRxqHdFepBbdbDski1wWksxoP z7}P&392}UkUq)gE?h&;vdFwuv>|b}Pvh$b)H_C?~YX#XZ!q=ew6%d;5L9?*oEhsfu zVCo=~L}V-sk#U~#;1q0TD-~ivSYD{GLeBs)Wqykrk!cWQ{-LWT1-F|VhIVWqbJ-ig z?Gfs`_rIxV9mxKthS1}#NARcTCHxs!Lj9DrgRD})LH$X=G;he-G;in+X0}QyA`KhSMRBg@rC0EgrDG z#%+?aN%Eyqs^n!UA|pqNX5=CY02wErP-32#UJte*%25&)OXrnrdW+^^xhPL{ChA5T zNpkr=g+3PNFz;9hqce%62Z^PhE*2v3-vS_;jF7=?c>vUp}OnP%9*rfnV+8dyuF7fyI8#wVU9SBmBdZ zxth+Zm^SxxTWp|(Vc;1B;2DB?x2b(dD4B z+5)f~%8PZ4hvLinB7^U{WqqNqV6WUo%use&E<9k8oOCF-FlDww7hM_<{$*`cpa&ZN zQsU~>>HoF&?Qu~S|G&=;mq5{76cotIil}I1E13e`7H=&IhL?=Y3h$S^AYzeei-H9r zMP`ke`9(BEM5XkrWLoQ;)M_;|GSk(}7O^6;L^C<>&&({BO@HTgUgytq=C!l$na|98 z<}>%}^E}87T+@5*JyZbN8oT0b^)RhTK|BCjB|V&eML6E9yae+w>CyRu%h!KhA+I@8 zx~Y(BiH6Llv5;6kB|O*dM$l3>VQ6l5bV-LTQK$78R$O>P7kwl)`!p7P7^ktmx3RdW zs=58~24xJL7tuIXThH+eh|O6LK=XG`%-=nIFu{uNdK_U&>^|qF@3c{PTj8-jL5M$e!a0l)SK9M2m9`{JaDZJ+A8)IJs#D> zG{B0LL7eU~YCyf&KB`H*wRdMUGq^Wgfy;?KDhf^Wv87<-A){H-$(VzJM!nrI1J+4# zI;@krG7oYtbChz*iXXV~PLcMz{^q83AUCQd4;x$lLg_&Nh?TmehibQTMB;dX1 z`pgYHIgz#NX8E}LBSLxLhPfr`SPbS8sQ;8ln5_h4$L@1@U=T}fhyK+Ad7sXU1DMHE zAA%iBM*s`!e7sxTDRLW2#z7M3fOGN#?(t#E8u6KGdoorpJ}fUqw;K>!KRQv=GP*v& zDmv>e)7_$5(WNdrqj*pcubhU|oh>=9;N|N_g2ZAXx0M;Mov+sLN^G>EFR62-8A-+g zJC>C6#n1b(^MQS=CO=&g`>GjY&llUPGY81mRT`hRB5`%~SG*dZMj=#LOZGRh*eO`rLd2USq4=Mrd{E#k>tA;y%r+_9J?eyU+HMfHSOVO=l5f_`pkkbdW}%+J%j z@B;A4=dzDxTx_e~nWoG5xnKIt^Dp$!?@VU-okswurqD**mu#-_jLB}88?kD zHp@O5t6z6|ZXX*!iulVfqaoOL{ zUKbtId_@P4J!uL}Q#wmCti?086EAd+iNvsk8-rM&wLR8HVl2ZBdMtgS=SqC`-(pBa zZN?k}qdHCBu$6!Uc73TJ)yD^=;A5aHvt*C;!2|O<#GC_DN*~Npa2XTLw&FP<-a>um zVj(c(Jcv?_%l;uQ`)ehqRSLSuizLIYARUBY^9gTI#ss@*eR&JP!zCjQWhyI_Hs7^yhEd7WSE_yc11pEgY zuse-MMB?B;QxsXtWjSK%qAF{}rwCnY#@}B0>wF07KO=IP-k;Z^lK&pe9=1~Qd*KEB z^MWuqIv>VPQ(TTpINttFEeKV>f|vSHz9z+H#~m~XDH6oI3@a6s3{jB_%f!3g$>J*S zWNO32LkWZw$J4*-mPfOQvHsS&2TyiNkkXCUn*E+A_7|V&cay zgfNqp;f0=*cyny_pdgtuB-0>#mTwR^Qpv@Ir8!?m{h~{{)VI{%-J}axgWo}q(gqIB zz#nUBKEx5-erNO>cpw&UaV9RPY=Vo)xf~f&xwT#w)ZE&E`bjWp_7-&{UiT9(bk(?l z(owijY0yR@%~R#4bHd>T4?q9g;l|sP0~oZx?$_nSu1R-rmF$LeCU!BS#pgrl$cpa1 z&wLx*R|T!$EIBbj)IZOP%NcHn%k?wIqJhTiPQ>FF;>P#!QT=`94T;Z*upGgQ{E)KT z?&yr})TH*0S(@st%YEfDb9zmPe)$30k6z}b3z#lYJBi1q8tYL1gf=2S#Q@;hUbTART=j8K1z^ zXXXk^tk15=t(mvZDu+*Fz44_D-P@KUWAAiJ?o;8;&}tMg#;Kd#_*mp?vkBvGp*U&$` zx9?}Kv_>yq>D}#QC7ahQHXAd!1dQ4^y{1Df9AM*f4X9vNQQPQ2xaFdbh94zS6l8;bpfiduL)OZU5+Q~y{AQ7?6 z-T4ePSDF(HGgz!TtR>}eYsqdz@BW`m?^!`0v9f~q4Z`~@IxY`X{HP(tlr~cQ&U{yo zpJZ1hoa(W|(IZVy#t2?7q6*Nd8}?+v@iC6E{8`+ju;vHgJmY|czBoajF!<&S)hzT= zxJ4BO!STTgidKEFf~E=ZR3bdp1e%o(l350SM+L_R`r0c;dtLBy8GMcku7eLN9!$iG z+?u?O^Y`5l1#eo%1snNg?V@b-IsLHk63ra149&L1o^V?FFyy#CBe#jnH<-$Ze3uX)neg`>GS|(^^UNW;DYgGuusm& z;Ox=CIU`$TkH)il-dW(78~6|Qw_=Ekh-v7Hd}{|}p#xMJJbL|yN@S!Dj@d*}x+EHq zLUgom%pdU~!zhYJxaEvCSai1_G+n4zbe(+lKpm*kIP9j z(5s{4aOf0^regpjxx$^;fsh5cFLVWf`iZVe(S=U`h9hG%kpE(B^Oy_1s@Qec|vp_p_ zy_Osc@(}Z&f$(F=3D@r&q**hnA4+YWQT+gh@ESOEA*Y%{m5uNKKDMduCu_z{e|^Sp z=svog>}J#N92=-%rk?S)ul}{$qWOBJV@%HV96<9lOj~}Sfj1q|TKYsE3c$NgPK(yR zA}j$kl|&5pGKtne4|ML%Sn91aBGK=!AeK4kc4%7l8imX;(;XTNa0xV1PXrU>DD98J zW~6@3lDkY*U{qw-Y0e)`$;KUL)P83wF2d0=ovKY7CYDq;;xLDxQn%7%!VUvfh;l*% zrZ{LK;}CS5DJl1A>^_#z^jny-|+q``B3Vyw9}>V9~GJH@A0u z)LGQO^SGjT8$)|b`bJ8Wb1uFt&}|wG9n)vvN(^e!=a#4neMT%5YObfhkh&MR#w;D%M2m@T zb)Pl$>o^@g!*;B4CysWfqP83pnfX=xl1r&Acc;&+jVrc;iGIlz{KrRK z*Jt8kPHn)_fKK|I`!j7X#b@u!xX^Y0YG)^1#*h7apMT*YSO#`t`+oG!aP;#nPrp@_ z(jKHyojfuw+_&#SvwaTljEnuOzWaTTX4@;9?z`aa`yD>sJOh5NI><`-pM*HDSZ0A!?RgOp5;igmN7)!2oKKe~r1>d&vsxNOP^5tDmHga&46gn(AexV zsmG{pU@1oHCsi>}^UhGf#d!N#O~o~;6UY^6x@Hil{$p{NDVr_O1`n}1n+1GwQ)&GD z-C6Ptyy50tZs6rC)UI5wa{%WmqSC}Z4$D!qCHq{(eoX$wr6DT2%xsD&`;Rzuy+V}8 z?l^C+UEEBK&%Tb`7<|geTDi#O&76J%e(0%aN9-9jjr9Gone{iSe&$L2l0Sgi*Kk6feaE7+&;7jOA_#X&A{I#K zq;N@y(u$)s`p($lQy(K*RT+Bj{F+qIzC=jJB*y6&kgjuN|CFPg{mfH zPR30SedZdZkS0x(XiOg%nQ3x|nL~f9KGii5m2okp=&)o#1U~pGVtf8~D&qt4&AzDr zXr6(s$YG>#*ysF74;>7W5+Gxe}MGRZUx(vW403FUfZ_|^bqF=*0DmU;=Vzf;$nm0c# zCkae0rOvVBBqIEG&eh09HR|C#dneF*sF(+1UMl8TRBObJy{H4oP*G=S{G-~jRMu%% zw&o1+%^Bf`9g1Z1dvkj;)blmzm?n3vz=t`+#9CCs)RwdzFG?n-$unQk45cRUmxriq zJIsRr+8Ovfy!T3O)HywA<*z=8-Q_E!pS}W$8T&2-qYbeHWRILa$?v2lV$SZNSieQA zG4LB8vCL!YFwL;Vp*bf2vAoqU`OjZiD`Q8KW`Ik2z(SJ+J2JlZ%X;Xv(!@Lw@uMy^ z*`J9Sd^4DxM^CtB@V_k-dkJa+j*DeWFdIaLsHj*(sJkWhpiqmw(}}#V;%AU!@ii$i zW3M#UPJdmg?fYeIVvjwDAe312KS(VY0EJBQ-c()E8;L-8DoTWqAGsA9x4%30;!TU# zbr(yYAHN{pn2Of~CM%yw$PTsinF=Ga@l)ZcCUBVYv6A{fjIMMXLj2{vwE1oL@8tzsA&THSmhX$0V`0lPS6# zvAv^V$VkTUG&d$YJ~$@(iB?t|p$Elg&ostoFANuB7P1X%|9LpUd&YdX`t$kEqW;`H z+h`Sd=6G#3Pr;UstW%h{;G94{MV{;R_##RJrmwhYgGaikxzS~=n3h8NT(1(i@1}<) zu>**On=?x$E7z;%Vr&A@TOwb8L2y9j`3ISZxo&%uB5fH_%aEHt!&^vxO)SF6Yac+q zaNse)5c?>6$xU6ywmw2+?A6r-{?G+6;48s3=t|{t>z}mXM{nvL<4<&D0f8OiY{}@| zR!j9qH{F&t>0ds!>y@soC7!1b+{GWpdj#2-_PYYRF@IgtMa|_Q0d9`na)$Bl5v*6E zM1S!Tor(m1{!#>MAJ`h=%B_6yy0@tYG=eAjhY{>y-8Oz9g0mYia2Hv?dWdFw<_!p0| zpa3qdX#nTf5bucBoP+%4qpTkb>%gOXu>Q>E#b4>cc0O!AfQRFLjj&;BCHz2%uJQ(+ z0x;7VoDt81JOIZfpNpID`hf?syzW21C-h`d%(0)Z>B(9>esn+hkGxMjU4fNylapj0 zAnd7KBW#tA;3eN)Ghd=&Dy#WJZ}oZ?9`O7}0f4N8uxk%^tG-Q!_8k zGNDNl3`kmQ8;^-(Zw3_XBR)8OIUwar@5A@;%aQDX;1sx^U;7S^oij`j=~3~oA2R(seaWgsci=e0KaO_rHt4i3^)>{KX6;+e_!O04g^frH zmD5bZj1JG(csjn~$j-Il_GlKwGTZPgcxIP<-G)EVi;Z9}w&B)ZtZkFNZ4gr~_QvrQ z$2NR(FZL)~(VAcE#YV9Wt$Ac`790HDeDcy~1)vfwUYyI}O@s&A@KwE8`{3Eqd8(Up zRvUi0H|y5)i%;l7LYnr-=SRHue!>I$K&Z4&_@q88x<$-h3@jOCx`EkVJz%W7Zy?@d zyO;mB4|^#1&K|hwcmEhLH~EOyh24_L5wGue^S}GBh_0JK$td$V;`Iu- ze;qMmfnR1k;eekF0 zC@t!2Q70n46l0f~rwWMNCK?scDi<~fd@ERppXNK{K{aK$zs2aE+ zfxiU4xj*X|^w?e5#kA%X{aI)K9)hhpKkLcI3}Ai_(LrBdoZ_h@i*&1$ zPifaMCw6t@uMJ>r9{PC-rG1bcQeCB9P<(eY-T`atj>76ZJg5rEjki0O<@1vR*dX>p z2Od0-J&+g!9gIQbFX9qv_7;6=nZX#ZIY^NR0y{3N#uTA`R z@S}rSD8p-Me;v%mwB7**6<=X^pDa}EOTKfwQnsszW}xJqJMj6n9IwIKWke5d=S7bp zqQ=`PqRx#Y@vap-sGH;U?MJ-kF|gnM5pQonZS43FPq46vmh(#>2)*Qb7#`!sei=d(VyYO^>Bpg$5x9~7tQ)Hl_@%Ol#d6>|6Jz2ufTbPmkvybay!S%a+yhAKAvv2qF zDY2}L^~rddaYkhBC#b@hNQ~`6FsMwv7~n7tujr`C>!Er+-;_Qh8u(ldpA}0));HN} zCLUhFF&L|#1Qi5Ws^7oXoe_Y}9{b=TZHg!y;<<0&df zyb8KMCeb4F+OY_RpqOwk$p<)nZYc3?jHQ6V?0OPkHG~advu*tR5EkL_x9tw!w?fyP zcN)qb_Bidb%@~Tz7`%<=4P`AoI((-5>>bM5vA?!T>NlU>;U~80TEt*vo=TMwt#VXY zINDfUV>9YIVWDfZwSXAvvgDqUP6vnL^0nn zjI|GFy}6$B78+oW74gbp5d6nMb%LKS5`sHH5*Yx&Q+emd*@LX*5&q=ktR3E(_0r?4 zb)(TwqyFO2w5HGO%72{Uy%T=L;;FTk{(t^MMGdE2zml-(t_+VD&N|{H9Rb*8zvvZ@ zXW)(;u=DA|*^o(VQ4cJg%f&NPYsj4IDQ+PiT!gEGA@m;?Vjvh5V*K}IAgl}UR3rVz zX$FE(nh~D;MM!@c(tlJ4AN~YuZly&8xdTKPe}ti}unb(n&-gn@zr;w*GDBXzLH!>& z27`Db7>2+=X<97O)ZoMnPvXVWot5KpxpREPDIQ*t6BqT{+~L@W#jU>|aVz-`d~eB; zGn78~?157f97_(sc!K`17=QHT}fPd!guc$)S zbo&Kl%#ZMBON4&00~E4niU+~49UUL#)MtDTs`;-&nDzb$Q*6zVp(;PITj&RQ)U{c0 z53%;bI%O<&{px2g<)x<|{m1!H#?qB^k#V@wHOenMHhYw3IGM0e#m#kb22cMO=7cW_ z>5q`KC^O?0rJoVk!qLH#F&ua7bT71!w+o%WZov|f9`?NDd~F-8ylcGlOVFF*vUDXY z(NSY_JGk;XorsjvwHA6VwXz8c=pZOq@X+nJ3&>4*W-cC+#2q*bJ;(I5<%G4;QgclB z!gI^z&TD(Aun*LvTtybi=Ol8wN8(jzcJ#lN+^0~A*hQxv@zDSEjWm%nyzOY_8(aTc znp~sNS?qWr%wbdu9D4t))4!G`t74*fToz~5^csR0BHh=Vw<{@$eB+ZWjeS?n+a+Sg z@J^L%csNMUuDd{Hy277JWSJ~b`ZAsT?QXtiR1iP2{3=Jz74huqi5ws2N+<926dP?F z`#Vj2G%ZA77lnNl4pS&R;cslU^gmzGUsbqLVWGlL6@IDktioFg1Fi`^RxMl_v>pok zD;%Y8s>1mSvlMPnXj5occu`@sLZ3gRz($3U3M~S$Ku=VJ844FDd`DrC!aWL)Ds(FJ zxh^AUrLc#>AqvMUoUd?&!ZkM#4)JS!RhI4`0#vB7H&u8`(Jv`HsxVWb2tYhCs- z715^(PbxgC@EVZf*KR9sj#EM-3retJf-lkLYH7KC_b0m==PXUX4b)1)nJLP0M)&v z-PlNH=5wB90e&V$aRLR!Go9T0cd!R{r9Zdo4 zLbb<#2=vR1q}^wVbaxe!(HJ^6b<((*p%8+UQMtRqKc2$-k7J#zicHrQNWGh<See8hP6IG|uWdk(-Ru%5xwq!8gM>=Pqn9vc+hNV81iBPOs;x@mm=1Z-qY}mbUtDt>(_g_)}Z6*Kh2#U3);i{{fR6t+%3d2a3OizLnLwk zNvvn+_~&JWdUzq_o^QjR=hG))XUIJp3ZLIqFo})UHETLkGN3Cuq^N>TVSSlpCLc0| zjb>RhdBGIcG1#H}VQ*8Y+KBkYDGYBX3Yx`3rm}>@h6R<-LRM#2^>$(Ma9REB3ab>l z{HsVbYHp+)tNHk8tVgqe*-~16N!F@o@uF#LvNc1|gA{!ts<9E~Ve@8)mgM5=ircCJK$D zUT*YYqj1@+xWc=_xzNOAxEa&Nr;JN^Y3zjADU-&|O_?xu^32q+v-q7EtPx8~;f-dp zb6xJm!|;;iVN~dNi8q_YI))U^m6?ZaE1{PAYFZnZ3g_|>vsjOaYUOT}&e|McCunzi z4rtT?SI_0UXR%?;l2RpKS0SXN@{rlAO|w!(H^46`(1*-MQ*bIe4hMv~lggLRW_^84 z^CX`x{KOo*Jp^55>ZFueT~p?2$;zdz3eg{ZBo*=9=%II^kA{Eh*xda;>aA(ieYn#n zfbK0F`21wnH?+eGQncDWTiIPnu+QOVl97dJiry4VNxiDf z{to4@qYCH!qKw~FO`0k@IVlOXs`O+2{!Oa6{ov-GDoN324-N<}Km<7qO22lA{eO zXSchAqX*)s1l{>{2mLY0@1sTR)&7aE$#g}5M(RyvcXdH;D?8;8sa?wMFRdik{Kaha zzvQBgA(}$D$KQtCU9R?^Q+|VANCq3sfa5dRWWu8v=&FFXGT5sj8`Rujx{7}skn(3k zHeZ#Alc^2ayRKxiraJ5JESaz|3P&sKpm3zZaD^^^eU!bQ!Ux>khbntdg##6u6tJ zK(a!7iOGyoyJuf)-mTmf8b0|S{F_L+zomyHh3U7Y-8DaaT&3VUj7yU^GJYhB%;n3o zumO{wD>H|h5~+`ry}7h%hm@U0T~d+C?ivaXgC#X5SK5Uq{89Qy<;`f&l}jT2m3wE2GcQ~M=_;j9SmY~@?KuDs5gdeo-bm76Y1 zSj?L(XB|Ub1uTPOLeC}R#)=TY`IzNs)E3T{E@y)y@|3%4L`^|wKqc)?KM#6l#|E!ntZiMR`UICu^!e^ z+|eD+RuybZ=IN_&dU<{Y^JPWv@+&J?SF1xw<4WCQ$ORhS zx^|V3yStYodubu#PX7n=_IIJzfKEB@?!U2zSWsC1<#PZ5XV022iIyu+ zCGW`!;i_nFfJQThdoolNP*dn0{wTsyL%CP@eE%I%qI`uV@9`AQo@KAE<|jGp8d0`d z3RFAH$4mdNn(C^UhMy$eG@D1PMEIch`SO)mAProX=|iEDsECuQHQ8bj-B`NLb8`CcNBn7pD4R4KKJlOgr|6Z z`)sf9@9tg*cdFAfC(WFl@{*DFTgBQ12dRArSAYAEvNz@a%UDaEw~Cn~-c&SKhP^=6ZwMzLLCdARSBxQHifW8o@4ctuaWCoLPq+Ib--I8=y3A=Yi3IC{q zDU)avp=-lGqB*LigZY2Ms7ejm=6`sz)vRxq>U>!;R6R+#1duk(HG*o6+k8I!|7$}o zX)QkrIxAhv8@&&#UdxBP&jwo~3nbs}iqQ+I7ZO_`Mor67?ikpGautgESmQnh7#~7l zD0hElil3@Mjm4~;Jnnp-hpb_3LVearfm}K3zH5b&9J2;9`Ekmh>MgYq5Ihuo-01^B zZzt%xF0Nra88dI-tJY$wyLtn^xRwnwmT!;|`jQk-7nGgqJt?j$V6FI!0_GR$?$xwW z2BmmOVSrs#fDMK$MQ<<3+87|Qcc&+TMhnxdjhwARGd2|R@$1+N%@!0&zR_?al~~BH zti!C_QMhZwde%Z`O;jGjq`O9qlM?ApUj!QUCGPZ$d(b}sorHGxPxRh(^anIo#M?m} zT<1_rw}D1}AbGP5$ZX%ue9Q(^JIiLi3K;VC=7u?U6Mjk1w>R@^8!(58E9Of#Vyco> z%y%oiqA;Wob0>TXXL=#{C2rwGz-IYd8W!TFE&Ns??P4jqt0dsR|0c}U(zf!Wn;?g6 zs|**8xJi*5R8qFBJhTY$1a0FfMeI{HZr!ek&CHuwdw9tJ?j3_IUe0Xq)ucP! zh_>7KE_C`rWQ<=Vo!_pure zEn%-dWdE;B@K6=5##7Up!0ev4Jwb2D_m;4y+DCSadUWDMlh{7Sellhzvv*1+v5LvV zJnBP+_dSjs%NKpfx=-t~OS)5nNV)re88n(^k4sLTi7o!gljf#A*LB>4$;m35YukN+ zvO^Z3-1#iz?LK0icq00xx9rTor+&nY&$xTKh@g)`yVD(@(a7&kzXDqSyU@P|tqJe2 zosA4lD{WY7ZlQKO2S_XLnNl%X~k+Vc~pd2S(_rn%fGtyHQ*c1=VLmjbJkC%~Y6XX$VAU7>#Z&l<64UC;*uB?0v{HO&8fkd5T`(`nW}eU6#h0fM5Xw={Om_0`Qgnxp%w}lf z;Zh?;4jehEhtu(&=}*V*c>n3n)`NaCDb4}WIFOh)X>7{8xv692c^N_?g^jM2r+PDrW9U zHl&HLNeyTTBJ9-qG;3ml@R(emm^ey!OlwFJ{?b8nj)u@Fl%<+B6=hH3l(dJ5TtR)( zs5liZg@mJaumjac(dN&eKXLpN(5}%j(lcNq_bRH0ciw zLFP>A3~GOx19mg8E9}$@qk+`Ahm9Fy70kmy4Co3^X);y4e}S|VNLCy%zpVm|23SR- zenj*6wLt~zPBgk?l6GIMKkD?LJkWYTlk7!H61g_H)gY}6}-H&nRI!06VJ*B?OgJFty5 zS&Xh7+Dc|IK4PXv;jQ-CDoUbJ$WO?(3+dt*dO zb|Zgg4;$%CSrERT-}#D#niKZdrJ15kgxwA# zcBwo?qj2PpvdDH9e^rX+K+aJC%or%ILTU2`O2TJA5@_2Xnau&1a(lr(97w{922#L@ zk4Yhq0ZCm4QZcQt$PzarFDP8H%9C;+g(Kl9u_Q2|jb2ksDXkjHcYnruh>W)!;%!c_ zkm%$?6s1(eJPr0_Wk&)9L-TM%f{7%nvb*A(0W-z907!AJ z|D3luh`pUW*v+uJR0&?AMrp;>EfK+LdaWANT4gV3#9ukcT7*!1w-GkDxu@nA;gft{ zl1wMQ+qo6)fni5we7%8GNhSibMExI6?2Jt$doq)vNGN553GiwL)++c#RncsXWtcyK zq~7z>3ICUXq;3N#PcUN}c8IkQMSAckpMHpij=u_?E=9?|4Lk89MWHF2xvAu1046E< zeg+ct8jzG1+{kZmW6ybwJ-jcnB%LT;%Q5~d#gj~LfKZC!Nr&CN8s=j?Ks;c!-mE{)KmC!-mtzMtz~jg3gr_O$b=XTN6qJi;{s>v|s)oz*HNsAARf?Vjw8beu z@seNZFzF}saV8o*uG9;mUP+L3F*kvA5`-T>AZA`*PEv=dNUT6&^~P}Cvu#)Ia>vpRsN=#fApdvGIAimcT zzJ9|cyQ>kk`clgGAdpIT)R&BDiK69N&7hfyu~H^{3yd0)UB%-MJE?X+ikIAp;vWA> z@Tng^*=K@lsF+ftol|88?KlwQe z6+KqmR}CX3yDQ>Um?>l?kRmPy%5;xd38NV#mof**C><(_cCeG`1|$*4ohUjlrg<&b zCAp_xv-1r{S*SJKURSPEInA)Ew2JV=%W=QtZE7Xa0K~Ssh^GtOh}r{4ehY!b(XQ-h zS%c8B7Q0&33h>wIJ+#LCsVmd?--lS>kUjN;pmZOH-3%l}%!zR_Jp>G80_H&gq7^BD z&H~BxTr0len5wv=PVlDRvIu_r7&DGpaH3%(vd&Z zmq6mws;!JLv@L%DtwXewsuQB6LtT)j*J!t0u1dN9H}ff^b4 zPIHSR9d^-pu_~x7AgvDujb4&zdT$xYYd}gs@coUK&??~#u}To-b?TIuB1hkL=NnRN+lX@?WUp%Q2IBk zNbv+Wzi&x%kH)${&C}$OOr5D5Myt#Du+uEsYW%KFR+U8HyMaGee$an(&6i5}=kN%>~P(0PBI|H~Muc;Bp|Tl|Z5gH}M|S#KUK? zD_g{zAxhrVY57W!(J+%5`zBxdjcSg5-;3rrru+AG z38H0EZ`e!RnznHj!mp5mef_rN^~y?VKMf>>57P_5jqggkVHNLsnuUnWvwbg0bG)-& zTvVFc*?-RvO{-=2e1%ECWW-OI67rtRtl2Phj`+;V?LwLz4=3`C$p^i7L zESJHaAbHf5Lw=ED?f{Zg0?_;)N!z$idX8N$-4cN$$e0b%eq*Cd@AN`R4^lkBBv1ZU zIqP7hn&ER!jR#mRy2b+&Xf8#SBocNt9@Gk#{2>H@rbvgA87fKHG3GpqF=vFUNzS-S z94pSTo+1$aQ6#J3DO^#A9Wi!*VV+|8p^9YthjpF#)pMdV9}z%Ta+D%otzbuYAe4(& z@^K*Co-aL zdt{Yc3?yHfKp{Y5&nGb7EnsTJ+WmrDv5vh^mmw724A|W>B=1wnqXdZCM}ptCD(*I* zQ9MTF2Zh*RWn6rL3bAEWy=bUzoP|9ZNQ#1!h{pZsDfUVB4j`$E`}o$gtc4FXlHM2V zWtSi2S-|v(pt%(FNSUxZ6~V>Va7gm9AC#C6r1;FBCm~KUXC9E*Rt_XzXMjXs3cn=p zs)Kyi53I##lA|1P=$h3o6~rn@aQ?AQ77F77d!Cyt35StJ#7n$;z)WiJVSeTZ)=~7d zV}F!;P?>JLmcj1cE@{I!;fUmQ97yWi5&rBs7AhtnhD(h9=N#+C^UtA*i4HIFQk{s6 z;XMI%BaIqR#DK&yd?i^X1Bq4wOh8oR_9>9myPr$90wB>}J}T|$K(dcMF0=ChkU|^> zg50>K{7dQJ^A#U|UUkX0FY%@4F;@utsZKa*o84hIG^6P1gsb{aMqoOFZeI!d$k$S+ zUOu;p}dm!2M<Ayq;7v z{y1PS6XENm$~%ivg+vtFZ6GP%@1^}WAgRV@`F;l*Eh3n4nYX!sZJZUC>rzZLW;N_) z@(e|xh+q0o=rN(mJQPUk$)ETvgqO7uVeVQRe{RUz2ljLkx*qS#XwZsx6wIW0T^3VV zA?JdhY21(CNvF|0rJrV=+ItdJr*oD!3iJs)U8RFs_AKg%fR z0LlIbkc9qI*{2~aWy|x5$Gbp;uxjrqLa?9LJU@^7PI{PF?ab5UM1J6{2-uZ0(Nf+u zoF;k6R=M zVYdi2b(wCviUNfurt5&Dc3tJ~{HU6Pt%7DKN3G6^I)Q0BF8m79s@x`a=D#E#y#7=0 zGXp79KIoK4heC6;q&t**9_$V!gWs=GhNeK0=~Eymy|l5v$v}&$bYcVkD*5im47>X{V@Eu==_dHR3naBg*w=#Y97+?qGin(KP<_CK=@B5H2=nHh)m zg_M-+K7E+hzb}r)aSTrQ0(4eAyUrV%@ewS+*AoR5ijENBNXR4om^R#ulp@@4GvE0$ zZqL|%tINoyzoW|=hgs^cb+j3v(eZj@JzB|iOw2%Y)C(K%Ck9M}O{l61>-Y<;Vm!LK zdNg(eyg+kGnmkDj1fmV%fA6$inkZvUS*l-{Kq+1MOFcZ>^1vAWqbgN-3%!G8K zAq{3iy5+C}X2L7bB`_0Cj@Gn2```z7UyP=mg4qX1HwUinvoc{g1&d)nIwjzDu}BS> zfz5GVp9eFY+h;+qhndduzk@C%Kfo3^$39MefKNd?V5T$fMbOvDKk!3n!vSpP0WUy5 z2s0hT`&)4lzygaAI0Jej%*nvNptVCxGsojIbkI~x3BXm*exTYRLAL`N&A@pT%m!dKG=(hzj+}|2 zfjMCoE+x*ExtI)`G6$=A(363=&~}*f=HQyvMOY}sRlqiLWeSYItW-@aCr`jKXc7X? z@@r#WK&~7?t^oB|!k55o0Db{Yxo8LKacPZs;(2?`0Znwm(D{;X0=@=aW<}sU80@%5 zNs$r$vOt=vfNK}R4Sq_1NsDFb35zpSc>~X6%G5Z3&0obD9R7{KO-nH!qp(2x>oP|P zhrS^bnEH7aNfu454sfsJAlX6Kp>dyz+cHs6%%+ZU#37? zixd`O6-oYqON#K?ESU3v0h?vzG6J(o&`*#bV9|$Y(lD0*mu^R&LH>bnlp_9RWGU6O z6VRfZfSo>;W)rZ-C#d=GV+NkzBO`MFk9{WTc3`GWMvw;m{Y7sU0}kF3UI>_!l&_QSAhpA2gZsfpOoU`5}S?;2+S`M77h{%KsKQ zLlFbhzf(d0d;B1Int}R@(ofZoXu6kB{?vADFdV$BnikMhAq66Q{EDn-3BbJHP@xb3 z;fTLwSDFM|;G@%gaA^b&@N0*Bb+Q!zn>E4)BeeqHLTKus=mN?XXe0dNQVKrm3rlxc z?7;svmVxO?$UlBM?I7qHu4sfnpM{yOmplty1v6dc@NTNpR^ZA9UG0c~E{2(|drX5a zg*h4cH8iz#x{|R*uhUu`LoNd8nuh6^6{&~eCUkE&(7lRz(D^XaJ&TW_cfg$N4}qYm zewdo!72(ijXc6hap3RX;nCT`$*Fc>%3O5VrX2NsOsW4;c)%HVE@zJe;&=wGg`~U}8 z@plQ9Jm58GQQj?e+6d^>FA*3x4>}8GjOE%n=pY=;(=mKR5Co!Qdf*e#y!oUqV2dspZxnCa|&3UoEhbo~8r zYn?XL4mxle^a7aah@4KWvtTCdWYlR_$q#TLv>#5r(}7<=hrvuI-a+jkD9j1K589#p zsZ!9<^z&VG+8#L2QT2bI&%&G!Z22HA9?&r~(B2hShF~VV|6!CL%GC&*3@wTScoACU zqDiMsho%(cL0@ev^jL(Yqw5acblODX*$u~C(_l%1h0dORB9L`3(^>RLXa~%A&{J#u zh{`(PbI??)@kq6X$EjtV0H#0_oz9)Ve-xD(=hAczjfa1=QFPu7Y~2%ypn3zObLAaY zI=qG<6Z#a)n9perXv!7Bo6w@ndg-(?&@`^oIdOGwL_mkXeSpvwn3I6dno)dcqDjCx z{dL+((CGkpAM_rW>G1Z)A*c&D+@-@@>%^gu5EeR#9Ttz84>O(2WM3Dg}LSv6gO9oz;B%7HwS*Nv!CjTbjlggY7YPgKh-2n~gpI zVNJjp(9{Reu`V6&+Ce7_PDc4(f`yJ}JEx+eeTx7Xs&cdo1@c) zyaApt)3N6!=+!XO(Wh@7%D)sAIu~6HeF|nenq2oLvK>d4bX2J=N9&+tP2keEWK+aq zL%Rkon%N4S=D!MA3O{rlITQLi%ycx^Wi=wk(IOokJ_(%%GaVb!5n?{fK&y5NmJ(R# zu~PXzTgyXsXH7uo_d9=X*b{wq5rI%?vj$YG)bZ$R&XnU15nTk&@a7CgkQ z{R2(4THCGD`ax4?k^saz7zCYg1hnX6fY3ApiQ*>|_kVqgM`Lu%BN`^n6!(qXb=h|Lq5&}=3YcwI-ZygeFr&`6c%q%`bI{9RrrqR6|H5z#Gwn*h1FcmcVj%582f<9(;|8)DX4>|@{vBxc>6lU5%egwK0X4>IB z4sEDJRs*Y`!(hgXNVGA~)G6U*B-+B;Vt~=`LK1C1G*x6g=dAVoM~<$vpIcFb@=rrx z+TiV@F>N)>X5cPp3$+5^NoZ;j4q&A+6Q1;dUt~4mL!L-2{1Bdird<-k9$u231T2B3 zVI9v8^RIJX@o-xL`*oRG3o}Hfw94O z*yXCk&qHvo3NzvA`=vmHvsy_r;VEd$zoj8D6tx>8A)!Zmq#R5M!@|&fz=+TaP5uc> zI$&S9Oycv8A{_h>{tiuk2>bMqsV6)aC50Xw4Su~)LE*oo7s_AP8=Kj%cmf}W&Vw1R z9Mvu>GhRE2*^bN!LjOL}901Jg%e0m7Pgnp=xm67ONtvC%Zv7t+LJ<>wpv;B9i^_Zn_%}2~a2r?{gH-;4T`ph^G||KQBYOv6Hx2YO;8AFzp9KaC zlxD&N=vMGUxXh|7D}egJ@C1h{;5Fz-nCXeQ#?Zvb0Nf8vb3i-rJ_~XR#bf|xLsPrY z1HK1M{_}yGmAM3X2b$`Z7K@@7g2>_Dnh(QfXbMmQ{6v}Q$pr7AOuI@E06RcyM-U;f z7c}`d1K)urruo3gIGEwb415Bb{3HR3pv4vr@DoL+%j8#vqx^l2BgMnJZoV9u7X4_`_=I||% zThg}Jw^VQO*=pHZxYf3`Y^!~1`PQnf;oBm&+-pOl}R zpPrwWUy^Ujx92N6vWg8tEwXH#0Aw)Xz22hG9%huWvtyUZ5%7T)DvI2X7v!JTL zXPseP_&U?N(slNA&UIF8y^K6*eL4gvfdG#6)eyn-fdxXOeV}a!+F*hJ`5Q_Rb@c`V zA~z%2@{P`o)f;^ZlM2%c^9lPTrJ=1 z*zDX~wYhq;R_s%3C=Mz%7Kay`ti_SV=3+~6LUB@YT5)=DR&ic&VR1=uX|b)iyx39f zEUqf{*<#odw8gk3Yl~w`)fR25VQctSGm0i@>(s63TeDCkNOqpQPGHP8=O>~t((?0B z6jTf=*P05lkakCbXmL>3wfjfJMdU zCd;OzO;a}|Z%W&gw`t|3{7q$>sy77{nTjk$Nl0g2QDKp-sJy7E$Y-;0b0lgIrP+>j z$~1&aQM2sUt&XkQHuJWGZ6&B!c2q1aADt``WJaZ$iYk?b zloX;;m5FLqm2X;WUYm$i6{1>&qblVU6c&_<^i>xGp=2Xbu?pAO)}^7Y7&aPFOQvp2 z+bEN1E-Wo9D@3wt#kWfNBT@fLA^FtemBnSn)x{QMcG;G4NNPk2a6-g{ZCZ)ELS@L8 zH8}w_IjLYOL`W-0M;4PHB@l(GmswOUs?7m7hDD&IsgCw From a9d32a2950e5e3c525e0ae4915dd22d94d133ab6 Mon Sep 17 00:00:00 2001 From: Aussiemon Date: Thu, 23 Mar 2023 12:49:59 -0600 Subject: [PATCH 17/34] Update mod_loader for 1.0.40 --- binaries/mod_loader | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/binaries/mod_loader b/binaries/mod_loader index d9c7d65..a84d8cd 100644 --- a/binaries/mod_loader +++ b/binaries/mod_loader @@ -322,14 +322,10 @@ local ParameterResolver = require("scripts/foundation/utilities/parameters/param local StateBoot = require("scripts/game_states/state_boot") local StateLoadAudioSettings = require("scripts/game_states/boot/state_load_audio_settings") local StateLoadBootAssets = require("scripts/game_states/boot/state_load_boot_assets") - --- Unused right now, but might be useful in case we want to re-add the state at some point -local StateLoadMods = require("scripts/game_states/boot/state_load_mods") - local StateLoadRenderSettings = require("scripts/game_states/boot/state_load_render_settings") local StateRequireScripts = require("scripts/game_states/boot/state_require_scripts") -Main.init = function (self) +function Main:init() Script.configure_garbage_collection(Script.ACCEPTABLE_GARBAGE, 0.1, Script.MAXIMUM_GARBAGE, 0.5, Script.FORCE_FULL_COLLECT_GARBAGE_LEVEL, 1, Script.MINIMUM_COLLECT_TIME_MS, 0.1, Script.MAXIMUM_COLLECT_TIME_MS, 0.5) ParameterResolver.resolve_command_line() ParameterResolver.resolve_game_parameters() @@ -380,7 +376,7 @@ Main.init = function (self) end self._package_manager = package_manager - self._sm = GameStateMachine:new(nil, StateBoot, params) + self._sm = GameStateMachine:new(nil, StateBoot, params, nil, nil, "Main") -- ####################### -- ## Mod intialization ## @@ -388,26 +384,27 @@ Main.init = function (self) -- ####################### end -Main.update = function (self, dt) +function Main:update(dt) self._sm:update(dt) end -Main.render = function (self) +function Main:render() self._sm:render() end -Main.on_reload = function (self, refreshed_resources) +function Main:on_reload(refreshed_resources) self._sm:on_reload(refreshed_resources) end -Main.on_close = function (self) +function Main:on_close() local should_close = self._sm:on_close() return should_close end -Main.shutdown = function (self) +function Main:shutdown() Application.force_silent_exit_policy() + if rawget(_G, "Crashify") then Crashify.print_property("shutdown", true) end @@ -420,7 +417,9 @@ Main.shutdown = function (self) owns_package_manager = false end - self._sm:destroy() + local on_shutdown = true + + self._sm:destroy(on_shutdown) if owns_package_manager then self._package_manager:delete() @@ -446,8 +445,14 @@ end function on_activate(active) print("LUA window => " .. (active and "ACTIVATED" or "DEACTIVATED")) - if active and rawget(_G, "Managers") and Managers.dlc then - Managers.dlc:evaluate_consumables() + if active and rawget(_G, "Managers") then + if Managers.dlc then + Managers.dlc:evaluate_consumables() + end + + if Managers.account then + Managers.account:refresh_communication_restrictions() + end end end From f2b1609320d78a51b3b5cc4fe7cc1a60c93fabd0 Mon Sep 17 00:00:00 2001 From: Aussiemon Date: Wed, 5 Apr 2023 00:45:35 -0600 Subject: [PATCH 18/34] Fix FFI backup and print path in patcher script --- binaries/mod_loader | 10 +++++----- toggle_darktide_mods.bat | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/binaries/mod_loader b/binaries/mod_loader index a84d8cd..b762483 100644 --- a/binaries/mod_loader +++ b/binaries/mod_loader @@ -1,10 +1,5 @@ local mod_directory = "./../mods" --- Mod initialization code -- -local debug = debug -local io = io -local ffi = ffi - local assert = assert local ipairs = ipairs local loadstring = loadstring @@ -20,6 +15,11 @@ local table = table local tonumber = tonumber local tostring = tostring +-- Mod initialization code -- +local debug = rawget(_G, "debug") +local io = rawget(_G, "io") +local ffi = require("ffi") + Mods = { file = {}, message = {}, diff --git a/toggle_darktide_mods.bat b/toggle_darktide_mods.bat index abfed86..0862cb0 100644 --- a/toggle_darktide_mods.bat +++ b/toggle_darktide_mods.bat @@ -1,5 +1,5 @@ @echo off -echo Starting Darktide patcher... +echo Starting Darktide patcher from %~dp0... .\tools\dtkit-patch --toggle .\bundle if errorlevel 1 goto failure pause From 97a29c03f1315b0e9e88aaddefdc1ba5cf5f1d47 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:41:28 +0100 Subject: [PATCH 19/34] chore: Rework project structure Initialize as new DTMT-based project. --- .luacheckrc | 44 +++ binaries/mod_loader | 475 ------------------------------ bundle/9ba626afa44a3aa3.patch_999 | Bin 894 -> 0 bytes dtmt.cfg | 16 + mods/base/function/class.lua | 22 -- mods/base/function/hook.lua | 276 ----------------- mods/base/function/require.lua | 35 --- mods/base/mod_manager.lua | 417 -------------------------- mods/mod_load_order.txt | 6 - packages/dml.package | 3 + toggle_darktide_mods.bat | 10 - tools/README.md | 5 - tools/dtkit-patch.exe | Bin 201216 -> 0 bytes 13 files changed, 63 insertions(+), 1246 deletions(-) create mode 100644 .luacheckrc delete mode 100644 binaries/mod_loader delete mode 100644 bundle/9ba626afa44a3aa3.patch_999 create mode 100644 dtmt.cfg delete mode 100644 mods/base/function/class.lua delete mode 100644 mods/base/function/hook.lua delete mode 100644 mods/base/function/require.lua delete mode 100644 mods/base/mod_manager.lua delete mode 100644 mods/mod_load_order.txt create mode 100644 packages/dml.package delete mode 100644 toggle_darktide_mods.bat delete mode 100644 tools/README.md delete mode 100644 tools/dtkit-patch.exe diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..552b17e --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,44 @@ +max_line_length = 120 + +include_files = { + "scripts/", +} + +ignore = { + "12.", -- ignore "Setting a read-only global variable/Setting a read-only field of a global variable." + "542", -- disable warnings for empty if branches. These are useful sometime and easy to notice otherwise. + "212/self", -- Disable unused self warnings. +} + +std = "+DT" + +stds["DT"] = { + read_globals = { + string = { fields = { "split" }}, + table = { fields = { + "merge", "table_to_array", "mirror_table", "tostring", "is_empty", "array_to_table", "reverse", "shuffle", + "merge_recursive", "unpack_map", "remove_unordered_items", "append", "mirror_array_inplace", "size", "dump", + "clear_array", "append_varargs", "find", "for_each", "crop", "mirror_array", "set", "create_copy", "clone", + "contains", "add_meta_logging", "table_as_sorted_string_arrays", "clone_instance", "max", "clear", "find_by_key", + }}, + math = { fields = { + "ease_exp", "lerp", "polar_to_cartesian", "smoothstep", "easeCubic", "round", "point_is_inside_2d_triangle", + "radians_to_degrees", "circular_to_square_coordinates", "uuid", "easeInCubic", "round_with_precision", + "clamp", "get_uniformly_random_point_inside_sector", "angle_lerp", "ease_out_exp", "rand_normal", + "bounce", "point_is_inside_2d_box", "catmullrom", "clamp_direction", "ease_in_exp", "random_seed", + "sign", "degrees_to_radians", "sirp", "ease_pulse", "cartesian_to_polar", "ease_out_quad", + "easeOutCubic", "radian_lerp", "auto_lerp", "rand_utf8_string", "point_is_inside_oobb", + }}, + Managers = { fields = { + "mod", "event", "chat" + }}, + Mods = { fields = { + lua = { fields = { "debug", "io", "ffi", "os" }}, + "original_require", + "require_store", + }}, + "Crashify","Keyboard","Mouse","Application","Color","Quarternion","Vector3","Vector2","RESOLUTION_LOOKUP", + "ModManager", "Utf8", "StateGame", "ResourcePackage", "class", "Gui", "fassert", "printf", "__print", "ffi", + }, +} + diff --git a/binaries/mod_loader b/binaries/mod_loader deleted file mode 100644 index b762483..0000000 --- a/binaries/mod_loader +++ /dev/null @@ -1,475 +0,0 @@ -local mod_directory = "./../mods" - -local assert = assert -local ipairs = ipairs -local loadstring = loadstring -local pairs = pairs -local pcall = pcall -local print = print -local rawget = rawget -local rawset = rawset -local select = select -local setmetatable = setmetatable -local string = string -local table = table -local tonumber = tonumber -local tostring = tostring - --- Mod initialization code -- -local debug = rawget(_G, "debug") -local io = rawget(_G, "io") -local ffi = require("ffi") - -Mods = { - file = {}, - message = {}, - lua = { - debug = debug, - io = io, - ffi = ffi, - loadstring = loadstring, - os = os - } -} - -local chat_sound = "wwise/events/ui/play_ui_click" - -local notify = function(message) - local event_manager = Managers and Managers.event - - if event_manager then - event_manager:trigger("event_add_notification_message", "default", message, nil, chat_sound) - end - - print(message) -end -Mods.message.notify = notify - -local echo = function(message, sender) - local chat_manager = Managers and Managers.chat - local event_manager = Managers and Managers.event - - if chat_manager and event_manager then - local message_obj = { - message_body = message, - is_current_user = false, - } - - local participant = { - displayname = sender or "SYSTEM", - } - - local message_sent = false - - local channel_handle, channel = next(chat_manager:connected_chat_channels()) - if channel then - event_manager:trigger("chat_manager_message_recieved", channel_handle, participant, message_obj) - message_sent = true - end - - if not message_sent then - notify(message) - return - end - end - - print(message) -end -Mods.message.echo = echo - -local get_file_path = function(local_path, file_name, file_extension) - local file_path = mod_directory - - if local_path and local_path ~= "" then - file_path = file_path .. "/" .. local_path - end - - if file_name and file_name ~= "" then - file_path = file_path .. "/" .. file_name - end - - if file_extension and file_extension ~= "" then - file_path = file_path .. "." .. file_extension - else - file_path = file_path .. ".lua" - end - - if string.find(file_path, "\\") then - file_path = string.gsub(file_path, "\\", "/") - end - - return file_path -end - -local function read_or_execute(file_path, args, return_type) - local f = io.open(file_path, "r") - - local result - if return_type == "lines" then - result = {} - for line in f:lines() do - if line then - -- Trim whitespace - line = line:gsub("^%s*(.-)%s*$", "%1") - - -- Handle empty lines and single-line comments - if line ~= "" and line:sub(1, 2) ~= "--" then - table.insert(result, line) - end - end - end - else - result = f:read("*all") - - -- Either execute the data or leave it unmodified - if return_type == "exec_result" or return_type == "exec_boolean" then - local func = loadstring(result, file_path) - result = func(args) - end - end - - f:close() - if return_type == "exec_boolean" then - return true - else - return result - end -end - -local function handle_io(local_path, file_name, file_extension, args, safe_call, return_type) - - local file_path = get_file_path(local_path, file_name, file_extension) - print("[Mod] Loading " .. file_path) - - -- Check for the existence of the path - local ff, err_io = io.open(file_path, "r") - if ff ~= nil then - ff:close() - - -- Initialize variables - local status, result - - -- If this is a safe call, wrap it in a pcall - if safe_call then - status, result = pcall(function () - return read_or_execute(file_path, args, return_type) - end) - - -- If status is failed, notify the user and return false - if not status then - notify("[Mod] Error processing '" .. file_path .. "': " .. tostring(result)) - return false - end - - -- If this isn't a safe call, load without a pcall - else - result = read_or_execute(file_path, args, return_type) - end - - return result - - -- If the initial open failed, report failure - else - notify("[Mod] Error opening '" .. file_path .. "': " .. tostring(err_io)) - return false - end -end - -local function exec(local_path, file_name, file_extension, args) - return handle_io(local_path, file_name, file_extension, args, true, "exec_boolean") -end -Mods.file.exec = exec - -local function exec_unsafe(local_path, file_name, file_extension, args) - return handle_io(local_path, file_name, file_extension, args, false, "exec_boolean") -end -Mods.file.exec_unsafe = exec_unsafe - -local function exec_with_return(local_path, file_name, file_extension, args) - return handle_io(local_path, file_name, file_extension, args, true, "exec_result") -end -Mods.file.exec_with_return = exec_with_return - -local function exec_unsafe_with_return(local_path, file_name, file_extension, args) - return handle_io(local_path, file_name, file_extension, args, false, "exec_result") -end -Mods.file.exec_unsafe_with_return = exec_unsafe_with_return - -local function mod_dofile(file_path, args) - return handle_io(file_path, nil, nil, args, true, "exec_result") -end -Mods.file.dofile = mod_dofile - -local function read_content(file_path, file_extension) - return handle_io(file_path, nil, file_extension, nil, true, "data") -end -Mods.file.read_content = read_content - -local function read_content_to_table(file_path, file_extension) - return handle_io(file_path, nil, file_extension, nil, true, "lines") -end -Mods.file.read_content_to_table = read_content_to_table - -local file_exists = function(name) - print(name) - local f = io.open(name,"r") - - if f ~= nil then - print("[Mod]: File exists") - io.close(f) - return true - else - print("[Mod]: File does not exist") - return false - end -end -Mods.file.exists = file_exists - --- Load remaining base modules -exec("base/function", "require") - -local init_mod_framework = function() - - print("[DMF]: Initializing basic mod hook system...") - exec("base/function", "hook") - - -- The mod manager isn't in the bundles, so load our version from the mods folder - local ModManager = exec_with_return("base", "mod_manager") - - -- Initialize mods after loading managers and state_game files - Mods.hook.set("Base", "StateRequireScripts._require_scripts", function (req_func, ...) - local req_result = req_func(...) - - Managers.mod = Managers.mod or ModManager:new() - - -- Update mod manager - Mods.hook.set("Base", "StateGame.update", function (func, self, dt, ...) - Managers.mod:update(dt) - - return func(self, dt, ...) - end) - - -- Skip splash view - Mods.hook.set("Base", "StateSplash.on_enter", function (func, self, ...) - local result = func(self, ...) - - self._should_skip = true - self._continue = true - - return result - end) - - -- Trigger state change events - Mods.hook.set("Base", "GameStateMachine._change_state", function (func, self, ...) - local old_state = self._state - local old_state_name = old_state and self:current_state_name() - - if old_state_name then - Managers.mod:on_game_state_changed("exit", old_state_name, old_state) - end - - local result = func(self, ...) - - local new_state = self._state - local new_state_name = new_state and self:current_state_name() - - if new_state_name then - Managers.mod:on_game_state_changed("enter", new_state_name, new_state) - end - - return result - end) - - -- Trigger ending state change event - Mods.hook.set("Base", "GameStateMachine.destroy", function (func, self, ...) - local old_state = self._state - local old_state_name = old_state and self:current_state_name() - - if old_state_name then - Managers.mod:on_game_state_changed("exit", old_state_name) - end - - return func(self, ...) - end) - - return req_result - end) - - -- Set crashify modded property - Mods.hook.set("Base", "StateRequireScripts._get_is_modded", function () - return true - end) -end - --- Original main script -Main = Main or {} - -require("scripts/boot_init") -require("scripts/foundation/utilities/class") - --- Expose classes at the global table -exec("base/function", "class") - -require("scripts/foundation/utilities/patches") -require("scripts/foundation/utilities/settings") -require("scripts/foundation/utilities/table") - -local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") -local LocalizationManager = require("scripts/managers/localization/localization_manager") -local PackageManager = require("scripts/foundation/managers/package/package_manager") -local PackageManagerEditor = require("scripts/foundation/managers/package/package_manager_editor") -local ParameterResolver = require("scripts/foundation/utilities/parameters/parameter_resolver") -local StateBoot = require("scripts/game_states/state_boot") -local StateLoadAudioSettings = require("scripts/game_states/boot/state_load_audio_settings") -local StateLoadBootAssets = require("scripts/game_states/boot/state_load_boot_assets") -local StateLoadRenderSettings = require("scripts/game_states/boot/state_load_render_settings") -local StateRequireScripts = require("scripts/game_states/boot/state_require_scripts") - -function Main:init() - Script.configure_garbage_collection(Script.ACCEPTABLE_GARBAGE, 0.1, Script.MAXIMUM_GARBAGE, 0.5, Script.FORCE_FULL_COLLECT_GARBAGE_LEVEL, 1, Script.MINIMUM_COLLECT_TIME_MS, 0.1, Script.MAXIMUM_COLLECT_TIME_MS, 0.5) - ParameterResolver.resolve_command_line() - ParameterResolver.resolve_game_parameters() - ParameterResolver.resolve_dev_parameters() - Application.set_time_step_policy("throttle", DEDICATED_SERVER and 1 / GameParameters.fixed_time_step or 30) - - if type(GameParameters.window_title) == "string" and GameParameters.window_title ~= "" then - Window.set_title(GameParameters.window_title) - end - - local package_manager = LEVEL_EDITOR_TEST and PackageManagerEditor:new() or PackageManager:new() - local localization_manager = LocalizationManager:new() - local params = { - next_state = "StateGame", - index_offset = 1, - states = { - { - StateLoadBootAssets, - { - package_manager = package_manager, - localization_manager = localization_manager - } - }, - { - StateRequireScripts, - { - package_manager = package_manager - } - }, - { - StateLoadAudioSettings, - {} - } - }, - package_manager = package_manager, - localization_manager = localization_manager - } - - if PLATFORM == "win32" and not LEVEL_EDITOR_TEST then - table.insert(params.states, 1, { - StateLoadRenderSettings, - {} - }) - end - - if LEVEL_EDITOR_TEST then - Wwise.load_bank("wwise/world_sound_fx") - end - - self._package_manager = package_manager - self._sm = GameStateMachine:new(nil, StateBoot, params, nil, nil, "Main") - - -- ####################### - -- ## Mod intialization ## - init_mod_framework() - -- ####################### -end - -function Main:update(dt) - self._sm:update(dt) -end - -function Main:render() - self._sm:render() -end - -function Main:on_reload(refreshed_resources) - self._sm:on_reload(refreshed_resources) -end - -function Main:on_close() - local should_close = self._sm:on_close() - - return should_close -end - -function Main:shutdown() - Application.force_silent_exit_policy() - - if rawget(_G, "Crashify") then - Crashify.print_property("shutdown", true) - end - - local owns_package_manager = true - - if rawget(_G, "Managers") and Managers.package then - Managers.package:shutdown_has_started() - - owns_package_manager = false - end - - local on_shutdown = true - - self._sm:destroy(on_shutdown) - - if owns_package_manager then - self._package_manager:delete() - end -end - -function init() - Main:init() -end - -function update(dt) - Main:update(dt) -end - -function render() - Main:render() -end - -function on_reload(refreshed_resources) - Main:on_reload(refreshed_resources) -end - -function on_activate(active) - print("LUA window => " .. (active and "ACTIVATED" or "DEACTIVATED")) - - if active and rawget(_G, "Managers") then - if Managers.dlc then - Managers.dlc:evaluate_consumables() - end - - if Managers.account then - Managers.account:refresh_communication_restrictions() - end - end -end - -function on_close() - local should_close = Main:on_close() - - if should_close then - Application.force_silent_exit_policy() - - if rawget(_G, "Crashify") then - Crashify.print_property("shutdown", true) - end - end - - return should_close -end - -function shutdown() - Main:shutdown() -end diff --git a/bundle/9ba626afa44a3aa3.patch_999 b/bundle/9ba626afa44a3aa3.patch_999 deleted file mode 100644 index a0e417c3603d7b637debd4e25337c888a003c94d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 894 zcmd;JVEDkyz`(!=#4b9{>~9WR?pozPn?7+zIzo<+G;sp)6g$DVxgk-(kVY7fir30Nn)a?2()y4e-*xUnb(eLG^<;=!q; z`l@$ZuBb#f%Pys)hbx2910+wybP6~y2??s`&rH!$P}bOaz@bfI6^n?>k2OjgW_@4t z%=R>En^vQ~xWFfl37_Y^>&n?{`23t{XwIa`uXl83%kGXS=MTMeB~dIYHG8#O_{_;c zKbyrbW^CDQKF?>_=Gx%bJ2S84FAUGHei6!6zu)$(9rFSH2i6TQr+;5py~g~B;sK!t zQye*2+!@>zcCs{ox+d~Yk=y9{4;P)5PrjSoCbn+<)0}R`SjcpNDM&@2=JsZvli!0F zw>nsO2&`bWDP>uq>~Qy6qj=(JZ_N)uN->NU+srGY?!-q#+lLeug{QpkwJd*^89d!; z%>?Pi%&~Lx#KSg+WC^$|zWDB5h0)A&-&S8$4qx4O$~m2XbJ_8yY3vNiF+8^yhBIrV z2ep-ITcw5B-t4#B*mS~sbc(6e(LeTNsITps9 z*5{{Q&06^|O?B(0Eva?uHgC^x*)c^>EY-+CS4H(fqeesI{s;Swwmkb(HXoFBl=sQ} Y|F3WU 0 and Managers.chat then - for i = 1, num_delayed_prints, 1 do - Mods.message.echo(chat_print_buffer[i]) - - chat_print_buffer[i] = nil - end - end - - local old_state = self._state - - if self._settings.developer_mode and self:_check_reload() then - self._reload_requested = true - end - - if self._reload_requested and self._state == "done" then - self:_reload_mods() - end - - if self._state == "done" then - if self._num_mods then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.enabled and not mod.callbacks_disabled then - self:_run_callback(mod, "update", dt) - end - end - end - - elseif self._state == "scanning" then - self:_build_mod_table() - self._state = self:_load_mod(1) - self._ui_time = 0 - - elseif self._state == "loading" then - local mod = self._mods[self._mod_load_index] - local mod_data = mod.data - - mod.state = "running" - local ok, object = pcall(mod_data.run) - - if not ok then - self:print("error", "%s", object) - end - - local name = mod.name - mod.object = object or {} - - self:_run_callback(mod, "init", self._reload_data[mod.id]) - self:print("info", "%s loaded.", name) - - self._state = self:_load_mod(self._mod_load_index + 1) - end - - local gui = self._gui - - if gui then - self:_draw_state_to_gui(gui, dt) - end - - if old_state ~= self._state then - self:print("info", "%s -> %s", old_state, self._state) - end -end - -ModManager.all_mods_loaded = function (self) - return self._state == "done" -end - -ModManager.destroy = function (self) - self:unload_all_mods() -end - -ModManager._run_callback = function (self, mod, callback_name, ...) - local object = mod.object - local cb = object[callback_name] - - if not cb then - return - end - - local success, val = pcall(cb, object, ...) - - if success then - return val - else - self:print("error", "%s", val or "[unknown error]") - self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", - callback_name, mod.name, mod.id) - - mod.callbacks_disabled = true - end -end - -ModManager._build_mod_table = function (self) - fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") - - -- Get the mods' load order from mod_load_order file - local mod_load_order = _io.read_content_to_table("mod_load_order", "txt") - if not mod_load_order then - print("ERROR executing mod_load_order: " .. tostring(mod_load_order)) - mod_load_order = {} - end - - -- Add DMF to the mod load order - table.insert(mod_load_order, 1, "dmf") - - -- Read the .mod files of given mods and, if everything's fine, add mods' entries to the mods list. - for i = 1, #mod_load_order do - local mod_name = mod_load_order[i] - self._mods[i] = { - state = "not_loaded", - callbacks_disabled = false, - id = i, - name = mod_name, - enabled = true, - handle = mod_name, - loaded_packages = {} - } - end - - self._num_mods = #self._mods -end - -ModManager._load_mod = function (self, index) - self._ui_time = 0 - local mods = self._mods - local mod = mods[index] - - while mod and not mod.enabled do - index = index + 1 - mod = mods[index] - end - - if not mod then - table.clear(self._reload_data) - return "done" - end - - local id = mod.id - local mod_name = mod.name - - self:print("info", "loading mod %s", id) - Crashify.print_property("modded", true) - - local mod_data = _io.exec_with_return(mod_name, mod_name, "mod") - - if not mod_data then - self:print("error", "Mod file is invalid or missing. Mod %q with id %d skipped.", mod.name, mod.id) - - mod.enabled = false - - return self:_load_mod(index + 1) - end - self:print("spew", "\n%s\n", mod_data) - - mod.data = mod_data - mod.name = mod.name or mod_data.NAME or "Mod " .. mod.id - mod.state = "loading" - - Crashify.print_property(string.format("Mod:%s:%s", id, mod.name), true) - - self._mod_load_index = index - - return "loading" -end - -ModManager.unload_all_mods = function (self) - if self._state ~= "done" then - self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) - - return - end - - self:print("info", "Unload all mod packages") - - for i = self._num_mods, 1, -1 do - local mod = self._mods[i] - - if mod and mod.enabled then - self:unload_mod(i) - end - - self._mods[i] = nil - end - - self._num_mods = nil - self._state = "unloaded" -end - -ModManager.unload_mod = function (self, index) - local mod = self._mods[index] - - if mod then - self:print("info", "Unloading %q.", mod.name) - self:_run_callback(mod, "on_unload") - - mod.state = "not_loaded" - else - self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) - end -end - -ModManager._reload_mods = function (self) - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.state == "running" then - self:print("info", "reloading %s", mod.name) - - self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") - else - self:print("info", "not reloading mod, state: %s", mod.state) - end - end - - self:unload_all_mods() - self._state = "scanning" - self._reload_requested = false - - Mods.message.notify("Mods reloaded.") -end - -ModManager.on_game_state_changed = function (self, status, state_name, state_object) - if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.enabled and not mod.callbacks_disabled then - self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) - end - end - else - self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) - end -end - -ModManager._visit = function (self, mod_list, visited, sorted, mod_data) - self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id) - - if visited[mod_data] then - return mod_data.enabled - end - - if visited[mod_data] ~= nil then - self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id) - - return false - end - - visited[mod_data] = false - local enabled = mod_data.enabled or false - - for i = 1, mod_data.num_children or 0 do - local child_id = mod_data.children[i] - local child_index = table.find_by_key(mod_list, "id", child_id) - local child_mod_data = mod_list[child_index] - - if not child_mod_data then - self:print("warning", "Mod with id %d not found", child_id) - elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then - self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.", - mod_data.name, mod_data.id, child_id) - - enabled = false - end - end - - mod_data.enabled = enabled - visited[mod_data] = true - sorted[#sorted + 1] = mod_data - - return enabled -end - -ModManager.print = function (self, level, str, ...) - local message = string.format("[ModManager][" .. level .. "] " .. tostring(str), ...) - local log_level = LOG_LEVELS[level] or 99 - - if log_level <= 2 then - print(message) - end - - if log_level <= self._settings.log_level then - self._chat_print_buffer[#self._chat_print_buffer + 1] = message - end -end - -ModManager.network_bind = function (self) - return -end - -ModManager.network_unbind = function (self) - return -end - -ModManager.network_is_occupied = function (self) - return false -end - -ModManager.network_send = function (self) - return -end - -ModManager.rpc_mod_user_data = function (self) - return -end - -ModManager.register_network_event_delegate = function (self) - return -end - -ModManager.unregister_network_event_delegate = function (self) - return -end - -ModManager.network_context_created = function (self) - return -end - -return ModManager diff --git a/mods/mod_load_order.txt b/mods/mod_load_order.txt deleted file mode 100644 index 086a2e7..0000000 --- a/mods/mod_load_order.txt +++ /dev/null @@ -1,6 +0,0 @@ --- ################################################################ --- Enter user mod names below, separated by line. --- Order in the list determines the order in which mods are loaded. --- Do not rename a mod's folders. --- You do not need to include 'base' or 'dmf' mod folders. --- ################################################################ diff --git a/packages/dml.package b/packages/dml.package new file mode 100644 index 0000000..1bd97ef --- /dev/null +++ b/packages/dml.package @@ -0,0 +1,3 @@ +lua = [ + "scripts/mods/dml/*" +] diff --git a/toggle_darktide_mods.bat b/toggle_darktide_mods.bat deleted file mode 100644 index 0862cb0..0000000 --- a/toggle_darktide_mods.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -echo Starting Darktide patcher from %~dp0... -.\tools\dtkit-patch --toggle .\bundle -if errorlevel 1 goto failure -pause -exit -:failure -echo Error patching the Darktide bundle database. See logs. -pause -exit \ No newline at end of file diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index bb184d4..0000000 --- a/tools/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# dtkit-patch - -Simple tool based on Aussiemon's nodejs script for patching `bundle_database.data` to load Darktide mods. - -https://github.com/ManShanko/dtkit-patch \ No newline at end of file diff --git a/tools/dtkit-patch.exe b/tools/dtkit-patch.exe deleted file mode 100644 index 177d2d74548f82cc8b0446fee73557f107b4f2b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201216 zcmeFa33OCdw)kC@0TM{O0f`0#33h>r115-?XwVcSa7!zI%H)9J18E#VQUz#(6jY+Q zl+{gl>+A8o_WxcF_FzAJDg+!zF=PM-1_uU*04ldAk)SOEQ0o8N=iW*xacH~yTkBij zS|6;+z4zQR?6c24`|RnQTXpkFN2>ZkV$jys?MJ`fA06^-*I35%scMCf37e8-|ozx@4r9)uKV*VCft~R-`rVu4$sc+ z<+7`O`}yBy{`8G4Hz$6-{L4)@|AOapp51!OkJa_{TOLu@*KYZxy0)nF<`=i#@;J{w zePip*&FZ@4X377PwavHugzF7=&7323cJIN&N{3_CBk7KthC62^-nBW-bM#I*<1ELN z6o+G-mD)C%`y6#&?v#t%lhi{cbzU74!JZ(Gf2occ%Myj`vZPv-BrcNAPLW&37hk40 zn$?~CCqLEkRBlp2d8#9i`d782I3^_}CvNk$rZ@_C$k~$O$WVaVZ|0>q+JX0*{tfrt zxyZ-M>67h#O5g2%B+BGF95aT`pLK`t4u_-U7o<|Bh1$^j>G z{o7aaH4L9;WmJ8nua2|GckCGA)+{U

z2R|-|Z(pegHx+DjPjydmPj*k5GIjEe`m&FwK~7ahlgD^dH(vFG$}@DM!j)eYnvtW2 zpL0D)W4duvHx5-9rujQ2NyUoG_0ZTHJv1eMtxOZQw)#CiJnA%qtIr(y8{K)@Ys7Tp zNc1?9*ktf`hO)YC6>Nc2*g_&)gJw+)D|u)l&T81 zdX2@d0=;&xukVVUWw&aNN6Id7YmYa%GhUCSRu;z-aBgnT8SCWF_&%H43%#y#{#Lk( z05)H`G(ox%y4A(Qo+_i&+3wR zFQE7QwJ`CRYn7_7u*MqG&?Ldgc+jU8D9R(si^b0=26DV#En&KQ)E#euPa9` zX)ZlvmGT&qT!Sm>UUUhssC&%ybGg*GgoEhD2|YZ?HNzY^#>oTZv&Sstc14ZL{lr=zC9ce@@EVE8kqVguUIyhB@9>hAAJ5I2 zp5kx^;wjn_4f?W8GBs|Qe*10iTiw&$x4CcM=nbcD`B{p?YaEL8;{IiIzu!Dvn&QxU z_xUm&AHZ)MKe->yZ#2J~`K{x(kzbpA|2EeczcgUfLz&TwZWBCh(?fl{AvZHH620BYyqp;41>O>yYKvMU@u%2!-L zuIxXt;uP<2$KTe2jrn@%Xa4s+!K)p`$9TG z{(0mFB*Df+U9fR29U09)o^bkd_1YFce;WL7twEpsl_K%jRXoc)$VuUMEyR;^L%xtD z^QcfB3rQ~|$vA3el{p--i>Qt+QR^ax;DmYWr!pwpb)$!FOd(~D$7nZ4Tt;_{hjZx0 zL{OJJM7r@7xblV`%$HH}h91cCg!`TA3H8(qx9O#A^9yw6d!EpQ4Bfd^cgmcoGTKaY zq|~=i>g&L}*Ej*9%xlJ_IAU8BSzMn1Hgbe6&YJ+sonoP3jv|Vi!5eTpcj=)KdU&x* z*NvXKaeKz>rg4ZMP2+^Qz)ZccLgS>l!bN+v$9#PmlSfVoYCXoTc8b5T3OuSUfZ9w-kepK;xL+ojp}-Ev8%|T>bcskr!Ahl^(N`gNj4z>EIkiYa0{b7kC6=4CDYk^iT$1cnTpLryjZ<65r-6ZJ2+dUi!X| z@fe%2Let|-by+!jd;@9zHji-|BQizMR$$dC!f9|PjX2;#;l#0htu=r#5U3mgAEGBhda!~=c+K$erH2sH${ zplMVAXxa=l_R>RE3$flhOIhRqSlOB)sGDe^F7>1}1fXu^v=m3}VPBSR3~n5kmlBO2 zr^yJE=;0d0FLfhN0j&Cx+B7DEb{!UP6V^Xb(pfkHqt_F2|+|{h= zP2Z$YSK*on8b_XD2D9K!6S%GX6|Nb)7y-kR7jyKc8drdV@{jbma4)0AwUS4#@uFHt zyvA=`PukBZQQKp%zjEX%7E~$Myn215Zq$F-YIVJL0)J z5v-%X5^f&OE(aOwhVkBf?6A|px@>GPFZ>0(g!-6M&PF$*`xay<3lRx9jpZc#w@RpS zl}Oh@kGVu2QA6;uYUy#88VZJVj_P|QKvzM5lVN!2Pb&;s=zso10{yb8^YR6IJmw^b zLvK>*4X&$i={H)Y-)Ng}z36IJ zz@0E(AI#FrbgXbUrMVm#Qq8@t#Z9Ps{97vt<{v*6mi?9&@VAlZF?xE8sTm%{38f3S zXLyZSEdGYimG4z@3B1i?M9m+=7DpYTi2srbMx{cCOllBer&X?sAyr3U&E_B6fF80W zwAlX`P0aJ5stklBdAVO4Z11f-a)ESJ$sX^K9Lvxn2buq`Gyg5{EEXVok#J#HKzNcx zfYM`!?`)wnhr(a>-p?&MyGL@s6ecz@k5X31WP++DmZkV%*@~Jn1nn^w!z5D>PF*5m zV+QixT?ke88aMVg?ir{Xz3av>+DFZ~=UGh(RoP9>vznY^H`#upbXTUr7(1_;3Kn<` zlwul`*?C`4c`c#Q>?heR?r3JnJ(cL@=^?W0FWdAKt#lEtgy>tPI@rrfu4-P(7H;*w ze;I5cb*BNmg&m`Z(8jSX+%oaek|h>`VTS0Se@Nt z9=KRg_JkU$tDcbV`8A{gbMLIq^pF}XjIHlOO{ zA20^v`Mz|<(2wlgm=Ze-CZgv+IKdOQ`u|N^b4GD|w%neL?4zy8kY32MuZ_FY&=tu% zpQqJ6LvhnssUOG$=~imq*OY2X8*`qcv)t_9!>2*%*weaJ(dJ&`j9yATouKNa z?bWj_7(~QgS|Yqc4TB9$?0P+jQnvhOiZfXQ1n#uu3rVOlj(URo+B{)56tO{%97+S- zZexc#xIa}7wiju^H|Q&v?I)#$EQTu`Bc7uz|Gh%oiEk}momSis8>a{Ne{pR%YYf(l zdBqKkt%#rLSu>%Rp7|pf-CnIK5SHs>fgVAC2BE82o<$rmE7{ zd}q3YA0+|giNEHDzbLvYKi1^bY9=9h3GH6((4N@XonFcZbWmA&-TQiYUA>nQH!HA?iRa>Ww7#-bc|z1SRWfu&ZMP-XG4;<#s7yOZ>4%^SY&N%xDA$7^hi zO+}^f6rQdsZLD_b;jnN+VPS(Vmx=I_dbIc<>{#lm7q;ywZCbCc+Gym}mxNDN8K=FW zrzMBS_(C^M6}QB5+q^71E!YCwqCg$u$!J6rI3u3>5%(f@2B9vu(dI4P=pPo(-9~D# z{UNRPR5nmJ_o}w^(v7+{W*5p&w3Ifr<(Kles&tdK{D|ZVht+I04w*L>3d4C`9^o#v z!lPJnY8wD)#eWp=6sj%cuTlFHDWTen)cv2ikM)h`PFBF_k&h5RUQ2m4rHTdQEEc%h z!?JcV$)soT+%w1)IF+K+u22(3H!7dhn<{0Y25Xh88Y}CaMw?{`&}tu()bLm^q)KC! zVn&Ze^L)NuDnpU3l^Z=&ZkY%GURwDYOF{f&OSbvZbu#a>+tj0yhnA2M8^!{l`pGP* zd@5vhFf3g&D%ToSFY1Q>$-3$F;HsT8)}m`}7gW6@D=m#G*3DuOY3;_Fd*)DKT2DRR z2>nUX=u32|iXqHpHNcS2rHa_CECY#^!F=U>H5%5k@VpfEpJf9en#W6G&as=eYxzzP z=)sp{fE~WG7!%bP_K%o_od>Q3J3&RWya!KM50eM)dUB!Tn2>~wGM5L17X_rk z(zIpj+EX|z3o1AfL7}knNpHx%${Skpl-D@!Erhc-wYp!DiC28g6VAHATY6Nh`J?nj zj3dSq!jq^AYj>eS*ovF-6gD@?fO+~GVc`o*W#2sSRZk0f`aF6lETweATT~=7N`{mk zR21dc-C9%?`+-*Tr7Y#4 zhYPL?r=9P`f`rdt#ekZ<58f6y)moj+>Y1XKMs%%mOUxTMb=W_IC*QT?l4PGd@R0L2 z+M{b_g%56E$hOCZbrh-Cjt*YBL3<*iJGTc;waxF1J+U;RJ+Xmjkx@${{*|8K7d`!R z>o2}|-W~2sd#=61H*=2r(g~@V5v_J6wJMk&5lPV#nxbPw#nNHVu-)blcR3y9D}Iny zj^)_rgPYhx+`**<*9rbCqhayq*istpq1BjDYw(K^@#1Km5Qwd55!84UE5~_UB zs67QR(sQl$F7Cdz_dn?gBueyWl6H`dJEOerL1wG!%@b0MHCti7^KyV~J}CmfZ|?WM zfA5{1(w_eOXgwkeN%!3eg5BDh32DoYiqDb$)J&4>;q=R@gomkhLJzC84&$pgJZhgQ zwDNEUSuRjn#^&&df|tJ0O9|C^JR_Lws0Y)pZd|1s)%orhMSsAdg2KPs%tJAtgj-e6 z!>%qS<&7~^H#09$YCYpksUKhkv9S8yucYKgx9TUaOKy1?x9fGS6{Z_jmHY;@uD8Q+wv>Phnr zve?Vb=UFVjdlRPgt3Hs$7$qy7+dNvx1dm;TWe`}lX%|yejMTOpY5GQ{9CJJbnSIL~ z?@y`kKJmoORoc# zSo2FakSVbQ;bJ25buHa?n60o>z!dhvawn@sQ_B7Rd+)`9A`6n_u}k^0Y$@4X^C2DX z#yyUjpB{|I0}s>nE3KLzv1%5UOwD50=%`uNN2xhc;cP05E|&z`D3H*NE!GrPYbJ=% z4SaTCC79|4ciD%l1bSU=FYrSR;mJ?x#9)Qb#B2+xb(O6G~WMIFv{kPQR;5bs2Vb77M+!dksHUHKg~l za|zHE$a*r!EHLKxliDgRQZlfzJB6~}ZMBHyzH5Ly_>xcsW1@JBR&y^)9OAN!9CY=+ zcGl5kjb1MvfUaX_9H*ew4_h5f44;Th*6Fo0ldSbN z$0O%zIFOW3>Kq_xRI=-SRY{-9L?u5{xpmy~Ib95u!H-My@Sg&-gAjn>8*)?d9=yRx zN`a&VHod z$J!xK4orsfDGSO5E-9;0VpH@`cle&O;7)?;r_54;OUepd_Xw}ES$%x31s4E3c4dkL z$w<{J9(O&-C2%suKdy|mQI!m5@2Ft9SMb8TWP8H7_}XG8_)T-UaSG+33s}vVe|S%r zjfIOSHW7zZ^An0nX9U$O)inwtZXUB-wPcqef>C%-atobOt;)8NRHAVDpv}7LCb{yc z;{T9@d+fJd+-SM|-Ef|P(PKP{E9dJQkSZ|w%O%Za{_0&8SW$Yq6p*GwGj)|i0MZDK z%~NQ&UZ@M z2{YEqrpHNCh98Z;IIFrqE0zI*QpAHC%D!%owIb~mTQx|Ndd$n|j<~jj-n+TB{$jNR z#TH15P>eX4^$@{XI<7h0yZ}?SGBfmq+j$5DA~ZopLUH{K=5IH$jxGG$=svErJX_9M^`MU)+5I;da`6>{N zjUY*+$W8#pBmnq2j75XA)7g!%yfsh|jN*kWs*P*D}} zM4r&1oGPQJ%9w*4tiVFN3|2V`;t630v){s&W;D8=cj{q$wnV~du#b^ofIcjC&4^7+ z)F`vJnPiW_8#?MS%B?ylGMHWJ>`R?B4XU^=XO$DQkCe*iV6BT~Bx+|MEYDmQ+DY)H znQse1s|C&_ZJ8L`qS4 z;<-N|Ltrr-)@s%wwM7>TF2mV7S12LJg$NuwgXfLb^F=(byAULp|J7H-p>7L~5W2CG zCC3}O3sK?$Oam+tSP^B3ID@h>^@VdV^|4@yZ){H((hp4~_qZGhA6O}wVGQ$;1wFx~ z8Nj8KBxdfDs^DS*Oi#|WqE3GH^Ata_uPGe(q zNgAc#d;%d7qaiXePH!OzE0E%E6hXB>`$+eym$<=EQ9uHLCz1hk0rR zCal%Y=rvT1KSN(9W~Vu~mzJu)n>LAxE8eNCo`VVM^HM$YUEef4-lF4-!i`$JkFE}= zFg9w-GtfCVpO~9I@QM@%*K8M8-DoE^Yx9XC*Z10d^hp2B`(isb?SJs>9a_yM)&FMv zz}RXBRKF8#VbCif{p&!%11N^;Mgtp8(aPc$b3}&2!Kcmo~`Lv+M*oaajTJTgjDW1b{`fNn+M1T>GZ)_3#*s%oR^m+1D>iHTRc@2X35;%I> zP&g{y8zq*}QFp8B5=B;dzzA9WK~>|EYH)@rntCK8G<84mc~b}nWL5Dheoweb)*Wgr zbqUE78RUzdmIwf6GDZLnOAS%e=^u6CCV)xgW;`Ojx;{TT6KF8!|An#mv-aqt z^jf%ixn6owTfRb`!uLw-UsVpeKg@SA3s~Vvk?8A z@WuaCD+);>KG;80FjXW`loQR7yO^g_WemgF*HL6$A<#vyMaA)k;kw0#A?$(`w`PN? zv;U&U;h}nbvmX3%lK&&zpM`1}SXWOOz4T3AkzRO6;!*wr&l08$bG2a$P0x||)d61P zGg&VuDyA13FOOorZH|rUOx8=AAuFjev;?pFYb$c<`VH3OmDKQ_kAQ;)J-(4V{)18R zV@MSw$#SF@M$CJg85CmW)WoubKAXnokdb5&Hvq5vZCMU3*FzX1L-*$hr*o^t>yY{B zn*zBK4<2&`q>$L^OE>uPCH^z@`V8?rd`=Aez$@woj?m*wNalWNn?IUZpN?&}r~rN$ zEMf?#Y=#KAsnYdgwNWQ$pHG_Xq`zC#sa-`rWdCKGBP^&QrP}S*%zb z31bpWcT%Bgh>uE~D!&s@F&u@{&y{gco(Ub~<~8ohXL=NvJ0DSVK)5);pGq7mfqu&r z>R_pzqlFx>h(T~dRQx~RE&!i2W7)Dyk?>2EfCAK861wQ=1QZFb?0_OX zuM=U1xLaP)Ll1zW2fM6dy2NsX@7rd@I?Ax;Jqj{poy(&Z7}8mStMK$ymfb*d9HZNM z2=7Q`xgNTarEW})CnSM{m3TCkAfrh1^pZBk@+X&=3vU!h{S^{5Ck)AlF|ti34B2b= z^L1l7cBaZAJT(%k>?JExj@SNn5gp(xVytl23kAh<-@RCjiM}g@bcNkzzitsa*3w_{ zs)PPtvyyi~|0EN_Jq~wRY;-*LaUR{qW>4h9d{6Mzltew7?E=(u13A3LkwjI0?yRbW zH^4=gD|HcB1c1lXYbDn7m)C%NCdq!4j1{vap6gWUao+$H{0^C938$NxwoG?6$6Hs7 z%R%uAr}r)qBu@q(62kZpE1JFX-k`7#!oGdsja|25oE9VI_kW2s#vD1 znh`VK53fz1(ygwJX~`;(5&!^J0W~`>3yQGPWnTT)tAf*b?ud&dUwdL+C0nDv&e(%Z zQdx``^L+FEc86mnHX;>i-C@N366zc$RL<`qmP5N;vMXK&>r7V+!Ac>!R8~l7hXuC8H7x`ONq|2LE?0?q0XA72u5#|VCbs9O*Qk`%_*Lv7p6sAK)(7zDTRRH(jdBUAsUS#GS3~ zdW0gT#y<0G1ZHgTtq!eL@%+$a6H;$uh|A61pYbl_XM!B_4c0@`%gvvikSRUtBs&t> zcfe$3D@pBM|E;lm_0Z&UW1GDruCMG6 z>ABS+&7ZNUAeJUsn1Y{f=@`nTq_3qjj&(xt<{Ttxg$J02qsejK9e3T2XvoA|e{+gs zw)!_*8(c$HIC-EfA+h79_7Dl+{}_haDzyQu$$gukrLjB>o>|$Z(nFQ4yc0fQa@Bq}jkirtDxD|XdzXkv-CR+gXk52;n#x(Ro39_KJJiB#Byi&Zj z3<`otZ3BDR8ns%GVKo&SBK0jVfa59lsd5?9aT-+SQ^?_7?X0NtFT_eLdlE)KC9--O zl?|cvLBMsV9?BXzjeL42EuyUslyD0H`z{ob_-sh1v2uaa?7bDsNc!QHZuuKkb|>6@ zRVe-S?r-&AeStur)y`lZE7GL*l1 zBOW&=G*KKzJ(W(eAn`L8nQ;2S%Z0l#IN@}c;%1c-!O1kmIAmV%&uiQ{gS0t9Fp93f z_mEI=RgPJ-H6B-96ZYI8rlqfg388hQ`Yvv)Y;{tuoNCi=A`x*gA5eZl23FC#k*>Yr z%5o<6ID*g=2bBqHnakNYHC-g9SNu}Yf=9J&!^5jP9#XFqSOOwb^RJRyW?gzJ)PW^5 znaqczkoFO2lnvk63D54^6g>Yn*@9=Ca2E6N9%L3ZvazxZ-XLQw<(^RGz80Jv4|WB| zYb*iB>8H-6piwC<^_5dqjyCYvZsWGqs#lE!BUYKA`WUXluQ0jQyrfaeM$B=L;#wxD z=;e}Z6Ny%7NE`C z9-Q)xfHqLF15lYQpu0!`yPTF)0s#LzfOi1grpJ5xGoh6R^RLYci%S(`7B!r~J=Bm! z9I{Zuy0LWAJPV#4BO*CHUA#nJKm7l;s4Ko1{fy(MjMfLGt%|5fJ23`}~9X_rU}Hk@}zc$MUn9u@HB2ti7}el!vNzna{oP)gciP z#olyk?Pz4dyLBT}T-$=pFL=67^>A@aw=)>BW09Si!_G@V$S>fpuE~FPq5pvJp zp*xQk?^Q+Oxu=F$nb`kLyy%dKUig}8yG8Br=Dni)Sv7A=NK3E0;t_G{WasA6 zS2l>0K-0HSAK5&{cF7XTem3!9h4sR&LiVwmk3P$eZ%YJILSZ<4iD=pIdm@anPFUOr z*;P?wIQn)N1TdSi z{3o_{yNGP^eEO1Z?*;cPgWHrOBspRTiHXXoCb3iZZT3VE!Ro8@1Rue;SqvCme@=b$ac^1ZJvmEjgU`& zWz4|(gbE=>Rd8RTz{ym%0*m0k+$)11y%KRl)b(;m8lMg37VIJubp);1^~2ogu|;R0LkR)9s{k$%LRlybRFgh5?nq82iM zvUxVm6D}{9tsQCL*P;z}e5TEv*vg-2ZL_s|ch1({*uX=MB;?o$TdX|>O<(wTdYv1{ z(L7t57x{#n$ZTDExQX)#lGlsWhcbwd#5}dd|Cxf#cMd>n3wwIhcJ6fD8L!SPjw@JW zmkPC%(A@$pCq-lXy2kExtq{uA3ZZ_jQ07KB_bSv~ z#g4pT5=tv z$%$?tD>LvSF37&i;tnQl^`MB^Bdei~UfyDnZY(qil`GC7=%>I))f zR#vNIGTO=Sx+W{Y!^O!ZhO6-G*cB18ud8QPTOw~(B5!iR;mHLrS+8acM-GS?v~U+2 zh~}eXiV<|TXxl$bE|*zfF5)lj zf;mjFx7lege07LT?7Pi9gu4m(_v=WRLzOo9L+P!^gC=#f3Fg@3a=T}}+zBTWtvr_B zRep=?hrPef<|V9a`^+lg$EC-8H)GakFJ4b=%{F7>vI7q6Fw0IkcCp)lQH<|(I2K;3 zmmc+1YHRw0S2*&~%+|ClJK^}k;h0}cyms2qIQrhQ{qmRn4+CHuk&Q=2_^uW z=*L1YKTgt_o3=cauJo`N>!0*L-GKId!yREilb8 zHYH0HA}|mJsnn(fua&F#@2uHbt{Xdr@iluZFE8*j!(=JH+}*1 zQbfcSV9#vjZE7>L#TXv1rBydyPF((ZV&c z1L)j-RVM5D*@o9H6V6IwM_lnb>7iZ-PZF~l!a+V}x1RT~2pBE@tBm#pzAMfrOS(l* z9cLT`Ki1U&N3TjOL_PUxcKEKT3{@6Y7?I#UCn&2PO0Wn_ZQTMTiG~Wofanfrv zSIHR&|IVcKaLR~(DEo2=mKEcNCp_LMRjj|5a#F(o8e7()5Ae#%JPsZ_%}FVA`Ez~y zVZ2_3iMb4ExJPwwS=Eb=Mb}VH4}SQC^*CFbo1HO$G1KPe=5Ph;1Fx8^-M@FXHf#56 zZN}bDwRwBrU`3Zdd-t%a?;^{sSEP_me%yg$JEeZ2pG_aDf6 z`LlOFb+qz3pz`(j=6cApiQTD)3!A@SF9aM>q#uvhd0KZi6dxAI$M?4&pQ5gVp=-b& z{;l9|q&jwPnEp2bs9D66W)V|l%cHN*>x^F)zDP^rFU5NU*EHzisZQ-bP!@KB-B%&@L`in>4Z}*hyw&3@7%_LKawN4Dxtl6S3Y%clM`a0Y?h_FWG*yte z9Eng=p{;I>_4697`gW7zp6ws2WRI~)I$cGx?$Gp+ywPNQsgE;vjHAXLw^h?8^gG%U z;s6+KS*4g%TFvLOkR^eOXZ%ya)E7NOzSC;ol=liJ;+ZU`vOGzCnPe|fFSwV!)Q`Wb zH(?PZV_29%d)#+*N6SdibmOBK%n3{gW4CLN@DXY(avPN+g~MOsuB$9zg3gkfjkvco zUatM5DLR=);k2FcBjPW}pRFxj>_W0$I+9(ZZuT#>wu_cpxK%IRx$t5=cq(Jz`KWB3 z!d;k_Gv=S;4G-*JX++$?eLckkT}vzp>ZuA7kZi%$FOcWiH`=5}j-*JOhnVw;zGXO2 zqgziaCcs>uo11!;no5IutEkw}a zGut=IY&T`L$9NN!!om<>^1l8u<6k51hOclA_{kfac(a8!E!5H?waA~nTY2-U9^42o zXgICaexEUs{l31*+L|;0Z=1HJLV>r7t_&^7m9EU9;EWwqvqP$pKXWf0uHNir>c2tr zv)&UY1KU$68w~y>GycpIqeCLW=LeiI76~Q4T+Zugtx%HJNR<$ynfb@EK*k) zl5)l)t(XqC(&1tm50h^E6XQ`)EaTB2wa9p&vi&pTA-uWC9*<)itl<#oWH=ObYB>IQ z<~I$;Z`7NAcsRCrOLwxH*SQ`2w74Nq3V*%{{@kEDS*CY;OZQ4(1IuR!`t{-&VG^tUy1VQL|41Y<+0XIAxnbEOuol|155)3J-jn z$UzUG9L5&!kR7;dGrU8#OI_OkwD45KVubfut6fKbaW$eYXf^vGw<=?EmGM3s)EjuQ zPX2hb(c9d{8}4Nv$a>>8-eNy}Dw`mEKld63IM^*k9qh)|^3xsmRyii*K}Z>u|15rJ zJ;6rU)Le+}2#fn%Cis!)Y_0mup8R=3{_KRJV()UZLvEN_dm)3p+n80@59aNWC3lZ3 zx$o=F6U^yW|F5EXG$32UhfWBm9rUob+w%e`@^7$b2%DBpr7Y|tBG88DYT)KX1yec` z-{>{=>cInF5bpCX3CN~jHgKtzKXi5TQ0b*7oQ7GX0j7GSrT7?lhTR?6k?0>?n$_0E z-;3Jl%-?wR8P_klo}veDNq^o}J(IqC#M7H{bA%4T@7{QYD>LwoH$h68#0stAVLQFt zAidlmy=?H5?)8;n8M!Vzu+3BNwErdAm!s2y8?Xu;*N?vqlkTZUKIx%np7suD)SYd_ z*!bUtRj6S%8TM_ua~s2s?Z;%;o3(+d6Jk4M72S-z-5#E*Ux0tWxcgM^m33Bj}v*dH{UX{%n@VTJj?w< zcoYNZkSdhWvnX+XmwJsEZW<(EmYPXsv~O>J_&2v7`ls7pdoZc}&h?xP2-sSx#EkQX z$3O)j>?-8WXWe%CdLd0PEE%F6tGi*cI)wZM6T(>|(H7-&hyAgE@Hahtvr{i*wuAgV zV&D+rhOt70d`+vV;l^9KMXULU8*TLfZS|@0n)riT#JD$DTYa}P_#sx3`1=dWkg{$_ ziG0|Dvq`RTXC9Kx$R*n9hf;&BJtM7s-I4u0f(Ngu$b37qDbdJJ73ObFiv@roCDPg> z(=?jYK5U*-BduBP%niuE(c74%z~Gy$txlZRs`?BG@x;M-^t6@nGjz3BFqYbsPMtZf%qx@PG zcY9r->%Kfs@N|ClK(Emz{IBL`k{Uei^bZhTaWE?WUyK@Zz}geXm;eO2^L*UWk*1;} zZA0WF4$o+W3!)4rbfg{8jna37CpC?Bz!0)O$Wm)@?#FUXl5a&{>c^7zcl4!xvQ~eK zzSQqK>P!9LKHpRi90z>~UJ`8R(3g&hzO-L=9xHB;UYu+9Vwk$x`jUY5&ETUiHJ~pw zcGs6YrKd$-I(S{U-^e6<*!t3h4t?o$4DaYmg_{=)57S@!+#69=v)aain1tqh(DBIX&;rC773by$JA@tZl=8}xc|Ir8}lZ=HsrYDQ|O2LtY^ z%X_h9SRA@4R8UoWSgXmG=@S}9$Q|;7*b_VPVdCp2jkL4e*-_%wF-T)~r0 ze_!R%2bPv6BQbvX^t#}8)_Z)hR8!41&8$2h;Vt^%DY5+#>C?M`u!?3?s1IrEp zy`Ip$ql)y%hlBuSu^@gLpHz?`a6cvZ2{zLCCTw>bJ*CX2Ga-lAjH=N0LDmPp+dZMV z`JT{Hj#2s`jgxRK+`zBVCa=g4jN~R`q~8?^BmNsmybK;hOPA4-f3#{vTKJoPv-luA zSKXdBzgKLybyq!%@yJ0Y13-md38xPA%cGIJjQC`5>`Pnaq#QCXXPhR@l@9t)v2j8W z9%F~H1!}dsd3q{et0kxfaJyOc*tPamt6uh$vxWN!zxZeDc@K&`ZzP;@QyZj%t z=j}!#P1y4;7X{F^=V`$c^u)61U4cz6dRm5!gN(!0ma<9&a1{zfgj^>-h(yzjZ7!{> z*UMAM?$$=Pl{*^Ubq~Zn@fIXXmI00W7rwnVz-w>eZhhH_W!z~&L7P{szp8Yzx< zw4v~WJf5ik3JP?s-`Z%ARKH$$!mhu1iNwc4gqDSI%|)2v){25JK5pAM*PJzh-_r`Mt+adThsY zfNVW{E%vN^?MSn}%e|!$4;>bX?}EzW9i95_*pjTiTFtXG?1^viB079h6`sgJX4@BJ z#OBK_n!L99C{A|!pRWB+BiiC=>!1E|!2Xwr?+KOl(u>tmOULm~zpoE@U#~sx=cw4l zy)wS$1&-fHjx^T@hzqYqkmQh`yiWKkO0s%!OcMu4vY7*IXoDS@;4!wu(v^kRYwWPK zF^SKs!Nh@*B75que>`rk>GSG}dp)n|EY z2d4OQytTKcWQeD`fets%KSVv%&j!j;9E+Z&R!?bz|I_HXR4WU9$9g|nTYb@`*ja;L zrui-kewpfKqVKTg80go zo%$P({xRi&FMss#glJ20Mlokt8C!jmmYq5*8K;r)$;A&upX8xDuz4PjMf1;&{+P$W zMoo^8d$d-)`m$lkQawB^g(L8)=LO0JtY6A5f;p+cSAB=T_HbG3`{jWTTctkVkl2mk z>cQoK(^=ISB&>fpCpJ28;B?^BC;lsWZdk(Z#W_^9xq7ISU0NvHi3~HkT-dO|5$S~5 zvj!VZwFKX`5qUPNIR30N*5lcfSjw}hIsxdMT9i|FQy)blu{RRK{|fS-l4dMX%yOus z)27o26&k|(IU=jzSxkSD&{<)r=+x=nVvD`%BH2x3ln6RROXdouZH=3FcVs{H4qds# z%-Q`_0_^bbRdkmGUZAS4`uDtASN-Rokq7=o|6a=(o$|nU_3u5Q%6wP<-uV-f{CgLC zV0n-`{d>aGmVLYu{HSB_N5~=aaN!{+RqlHB3@}JX(I%e7AEVWt2mU?ssSjjAQ~(*8 z#Q4l$967?&5gSnzx|%%D(Ft)euWcUC{>PE`{=tw9W3#MpkR+EWpiIZoDn1p4JpI|; z7C{g`fP`-uNEm&|M}#YSfY5=8)c`t)UfO^yL;zeyI*qfu>O=xakzhkVx-){wC>9(= ze)EtvwzI!ZNO6&Ddygcl|*EHEJ)|aDpmPsdSMxRrA!ZCC#| zP&T-iKSU?+`Jp(+`iY|$_uq2_qJQo&j(bXv`L1yXkRE*JTKhMwK815*GO%{nBxHdt zOjU(#*M|FDQB`_etNk0S5WDO#b%~{{a**#?`?>tiX1GRM&cf-jMr2|%$wfOZ`Jwvdg$uVxFT)!_{^u${IVmX1{ov$eu?vB zcamJRifwFhXz&D^QLs7tX3iBSQ(pG%GJncL6Yva9EwN`2 zlSs^Xb&spLq~(mu(Kvx(1~>U~!^{(+qTED!cNr}SF;25UH%Boig~~R_O2pQiOQiGU zx4<{gwO{|YdQAuyy`T$HLQWNzv7IP`I*qb@R443SyD|0MPYzk13Ygw5AavOATUy;D zm&>ilEV_bZV;fQ|0%`R*iRdg>VE;9NEdlZkwE&6TDS|GsSp|vMESIsZGd62zNB&_J z8e^ArMrNs;iO8&>$;F0s6g%IhRU)ulW?>?#PMAQ)`uVNFY*!0f!CQe9%=QO(gqT?f zW|MfCT?8B^7Hq)>Y5yNX$yxM3I#hsu`yJ!^E@aP15=tJ&qGF>YfeLk z+!?;n%;uVgRZc(qlzSX2FG}NZ+K#BEw+AJr6>O&?b@io^DvJ|yr;hMmVzrWjdWBzf zuD4VlknrMsJ#_9WXC1358oP0B62Fk}N5w2KyAz*5WYxnN%tSbZ8_rp~7k0o>mP)(` zB>(O}E?UKQi%30%4V_%Vc7Oa@#ceU|r{u*}$}TkaKPP7W{`5qD)U5Yqu5z;dR(#Xl zroNiOO#R1P0lJx({mgMD^(-dIoW!IrPfU8Iy3BkgdJ&VpoQCASL?R4{OgNE0qhWL>{+PR z)5JIRl&)aMsTFxbd5MqeVPP)M$A7o(JUZYpHpjPNIXqLX(Os>E63tGuc+HutWfIJ8 z2{ZT{h5Tk90CDta-!xgY-$Z34Mu=(O#gPp98s za1C!#@jg|iME2o*YQ_81jC<%ZwD=Kdr;&p=pIUnaKf?Lcn0a~`Hazigv2_h8adH}Q zdf~|_k%K)lUpG$2`bG}ob!ul9ZHGP2i-cM(pOMq$dqjO4!PDodFsy~=t{0IMK_Z;} z29OVR;Wl>J)e2J=!3=$aKC$ ze%WbZMh>P_812T1oo29A3+q#j4Z$r%Wf-8gQ<85sDz@ofow`m;P9#OAGJmOSomivf zd7OGqQ-H|x6+BD-+<*yBt#C~m72sz6K=;c4<`^Xc$$V{;2p#U18B`pD?_uqh06iyL zLTu8uM%7ao@i?RURh%`V)t(PRWH0{|YhZ4afm#3XjrbU2Jy}6iN#_O$8Y_?m1E;vi zPd4)ZD8cOZJo75AP?!0o9Of}9p|Ec)oPD^{;$Hb!5~XU~#l%K$H{W_yG<7n3?)Yj3 zvtGTOz4AFgt^7N`%Xq&yDNiuz{_mCjuJ7H{DSO9%&h^jyKIAut=U;LC8@~$fNAjD* zPuUv^l~J)wvxb!TuJd~ zyVt%ArK}swi&)>vkauO8ClbxY7^(K`_X3*uU`OTm`mK~Yj_>d~xA^pm!tK~f$pgq^ zMIkaah;=MUAJmtfe2Z!q4#QAZmg07vbeC>fh`kW~ZM%OzfxR)TLEBuS9?9k; zD@b&0bcAYeL=;^okl`gU|C3`z)ZraNNnI<5StozYM^w5j0f=8Ed`m`sDx~KsXYj<* zYR;eERI-t1d=a>@T%}L2*T@2;s9)lwZsgbe!EzJM-2k&;M@#QiS&L+ei(M&YQykSC z)t`PF`+SA@TV*#S%5EYbWnIV{l=UmIL$+#|zvfT$2TV?!$`VA^fZu3t_FIRYTu&2b zHaB;Vbx%Cd-t_ml|5VG_AnE&6`Vpe<<_`Ku-OnZf`3U*uX4}uayN9#|>GJ#;mDgyE z8^5PD?sCo6-ro4fKmAF#<7ZIWvWPIWWhdI4U&c9cKYEBXYr(YSAuBe6XmSb67CqC; z=~{Ktr_kfB09WJH$l;6$d@4mE3Eqe0RXaVwjePO6Ur~9Kbc zGJBhJ>VH@2i-m%FC0Agr{9V6vc)d)^z*_mcerbL^t2|Hgcm2}7^yCbf;c5~=ydG;>{oc8Mi=lbw5b!BS2u|1Z>m1QPGxf z!T9jNOd{F5g}bEU^$!n_-5E1Ptjl+=6`H`XW6JrDz_oP8G@N3#iljbRr$>3x?ouMqX%at;tiMxKdz{>X z(T&fmWH(-EPl+mHoB2nD5JcK6nW@{Esb^4hq>4^Zo(Q+G(;Ym36zp?45o`k|-(PSg z*(;nWk&kQ?~gOaA$s2VeF3e z;y$Y~a~Io3m?2h+%T5G9()@wzm^TheWr*BkF@I}!XK)Xj(Flx9(3AaHK_-JWZq{Vz z_exj#8Yu;A`byA|MPG?}I_AVLpy5t3vq|$Qpka<3{$K0eD)rb&n)UKb3k_Db+fF5u zrkeBLFkj?dbOu6&&6T?8KLxUvjgU&&xutZTqskTDRp;p%sZ$Z+vmvL?UpVb0(I2dU zfpGR0p!3?9{hr})v-p1NjVKP-#oW)OhildH0(JIrBz(6oh`6l8Q&}}df0~(9N9am? zpIdZ|QdLK#as0a!ADmpg0=1-(@HM3M8sVlDwzAO>Y#Ycfs$d(+h5+RbKVnJoN`?Ab zusB#G0ak_ad3>MMFVSSaW4}1~FHu3Fv?xH|v9x$4Ivjk;HG^Gec7Y?d{Kb1W2i7~&svlioBMGf(N z_|W)L4`yRa|MEXWb&4P@<)8~8q8udjt4*++3srUM&@asOMvUM$QLmmuy?TdrPfRYs zs1w~P;zqeLj#Qvr9pQ6r;R#cXlPFh&eTsS|X8xw=<)S@3XK7PLEPB3rw3IC#CHmU^ zoTXdwn91YH%}7iu=<&DxvUxAEej?BJzC)Qr-hbtK2kpq$aDUHF^bFa{^9O#@_#NPP z9vvv=SI*D!Z(7Gmm`C==G~%o&F|+VtZFo!p=xR=Di*d}Gg)FtOc z$tm_>&U4r$^@za_QCs|i1d5?@u;=T$zVhz^S)~WEdd^f+W8aq^~)pCzJZpuM8>Snj9tqOv&(fD*a1v*RTl8Rgn3vt z%i)B2qJ9CS@J5z;9rr(8YykmFf zy^R)>iHON=>qSYG!4V!UBK|)~y)wmA)VfytBOWc|LUCLz?VMfgEj{AZ#(wJ6#vQ3D z{Zf1MK~_G@FJ9*%ZMk(X5U?~CcsT;~(S^L0@K|Dy*}!2gS>u$C^VQr0brEG+RrpC& z>F3CNpL@dN;!NM*zEp6Qss&qMcQV8GD=UK-E%MclE|>~0_~xK8mlq$lz>A&VMgI(E z6{ydfzQ)%F!qyrn0!;ICO;kX*8|+$C89=fBFK+I(-TRQ!M# z!{5`j%1>ld8&Q|WyRnzM^TV+2w9#^yZDq0lRQ8u*J|KG7f@-fqR%}=z{wAE2C&%ov zb*1nSXRL%r_E+$n)`EX!s`6pW3l&Tf3ZH;!~db}PTc^WiqCUoUg{&^{ z@=4H{;x#_LO?YgfAVNVnR~8{`I;1-ZV&XK0b}cwl62lLPAeffsE=kLp|DPP$kH1yW zX?nDZ29H*0V&Kx9?6ue&`;m>VnBivYj}mT+k6Dl&j^!(VZP{7C=P^!r3ZwFkl|$HC zfv<|)_V)qb2!U@_5`0G!@JUR(I6`drOaZ=?h}EpG4>-1 ztm0#_I=vKf7#FK5J#Bs7%)$nyu}-SgYGjju?ANI0+lE@j{4g?oMdao zYU@oK;P?1xqp|y#s>VqmrYlS7qoKRmO~*Dq~Ter>Q(&#+PH) z3p|EiRAtO7;kfj2(shza*C%?6Ik3u!#9MUpl%ptRvmC|F9 z7j1~N-+UtKR5W%z_kULRN6k}k8SA`P5@6M;@b9gcQh2W_+`+O%pBLdA{iMGaoj~rf zZ@JeNNuq29+U3$3qG`m-+VQMMRlMvxVk(;n(Q^k*X>Ma{N4)IzM9k!cqE8h_;AB3~ z9mLR?95eZ5gRtdgo1{4vefkcj?R5d3%nP~z_z%Em$MgPE@Qv#R-(}t5E9-!7#dCt- z1OgNA5iJtBw&++S$L~27$+wmdZM8!dLG2MSd&ojKl7;Z7w&rN$ldOAQ>sSa+L_3$a z$<{*H7JZWSngFTh+Gn{!I>W6^o69WRxxp2oK34%XaF@>aR%yp8~F5! z8C8n;u2w#8Daa0jNb-6&5F^~-`4cIs-1d1xM?yAyZ-7Hxyd0#-ZL7M? zVi$xG9OWD+FGpIa2p+&ZP_3Fco^_oEvJCna6({ivgfGwW{hjBr z7_S%dh4&^oj%AlRN+s6ko{h64jd-H5avaORTV@-#j>KQPrtAl7C7+D9APkWUQmL;TuDYbQm)*p7Yl*;Hn~*DfUuX6WcE_joxLnSDF7jbI+BBG z?#SuJUi1Va)Unj6e<3GF?rJr%p&G}TKtn$h;Rgl+gT?D?{`fUP5a9fy{nAPF9f?dS zHz&M9@5H(JkB353e=ZG*$fR7U$)OO3dnR?p4u#M;|2hG!wKZffE+IP#EXWBboDQir zh=~3T3mq!Hj&S;W=$zJf?>_j8#Rane5Mq)tcdMY^*HOc-{6b0|#xQg){S$s^a>Vvw- zmnB&ve8brR?BVkapAn2|hkKSpFQzd1f%#|U2zX24N_^)<&n8u2O`h3M%wt!1Rd;@_ zg4O48;@-R3^7}+WmR_ik#&G(Vf)LbHsByR1@MpnE*Ye5muOZ)efggRcJNyN>!Bo7e z8GBU>%@+K9V>fpI@1J1B!*;@Je1eLmEq^Eh@&nv>*dL7)D{(i&pHk{{?ad@hq))!i zN1t`$t{hdRoG3F_g7W1}j1%;@h_^^NUcV+TH6hP+qX#@SLIFn+f7Xn_*`pm^I zVbVVmKPX$jgHr*E)8(tR+A8j3&x3q_!&|yl3r>SC1j_o~uGNkgJvG>X?|E!abg1N$ zEwSb&R43=>yr4>|IMYwf-R!S|`xL<(7E?^-a&e+}m(-xbr_ZDBbaSFyh_TgH<6=yc zFxwpS3j}AozrU4At^N{h7X4{Qt3OBaxAO!@tl2Bav|HF4K()_c?y*>!`B!Qa)ZHXy zld`xWTZ>zmAv??ER`urAo@8aUsFhwuo5Ye1M|~bq&%;Nqjx{n%_zt4o|1&xvIVL&z zDvnn3nBWSgR)jgve1(D*GL&Q4%JD05$SFa@mD$<+;7sX(5@J|W&U;VflYEI8;?p|> z4tYbfh)}Yln`-|<+NwS5&s5(ZA1zN-=d_y3#?^=VJMu76;8D2GiQ3a#6w3 zs~bB;as=(#qqIk=vPQC^Zb3%0ndjmLcm37CrwDBY3EJv@{R1a&*J|Lbj_^a#w_J*J^H~ zHrk7>RJE!x&}x>+Lpb;Pj?CuEh}D?u#g(9z&}fYW6eVcmdMp2r)k+6;9Oe(nFY}{R z-U#v5j=>(^Q9w)(G|<*DTabwsB{RQh6&2V+fDoY7L6ihTn>Iw5W(zlJ>-&jS>P0U6+=`oVrfzwrdXv(HWT&%g1 z6_Dg|nDrFLO#gkW$-7mP{+p6BkM3yB3|X14lX`5fk8tF71Y=#622MDX;D{>u_sy=48l$|`+` z5%+9WK>$u}jY@|x@%>Z;{}>X+l>7>G+aj?s^|eTH$3Y9VvP27WNSA%)Y&EutWuPR7 zbNyndP%E7IW2%zDFOxTl9KV4v?1Rf$ZTuBcREmg;r=wcG=h>vCdRM^y{e>s*P9j^3*7({B+uSTuN~u=O8{sGFUE-me1f*WtC` zLvz0uu>;xWUe4-#n4c~syZ!ceOE2bmDwNSZ-`71$-WyI?4w=OHH@e!oWi9OD-*yv~ z6S^&P+E&{Y2jlKLiJi!vSBSK!O8$6bIa|1;lHf;DEo2{*ot8V*JG* z#Q3|EV6G;bh`AE89j#jn9ymlmKD!lf@XaPfO#&`%afim|#Usm3h@_$gpOo5y59hPO ziBHMRfyn$g5xI~xOdT^J9d#Sy^C}^cCy1XrYJP&>Su0@hv;s28X|4Rm=ftY$c5&Q> z%{PLTWI8hv@J4YV9$4o{>GHraCw=K$Q1;cs+^kcDvOU&n;%37SQdy!W8{rU`Z2yoD z#<(WN3lZBX5gVAkUzafC%ZAWW@!pjjhs;^R6EdoM@Zpt84(JQUobw5>o&amYU~I_| z_-M?Tv<_1_OZd|`#*llIx}MbOe<7Bm%<_EuNIVr2a>VZ522}QG%1Pi@=k;0pm*i6k zA{~~(1htxUkZgT&k5MR%X~7{RAj_2z!CZ~zz5rQn+;7STRQJLCwRZif7gM$Wj%drj zPp*VesU%75(Q`>c%x(%ctPi%IsXcm^@Y-G~p8Q^w$t95B@t97eywY4?AwGmem0tui(#0{lLfU0*KEsF{Pg!z4+ zbMKu@g4*_dfB(T`RyP${TCE;@rpnEvp zez3s88M#uwGaGkVWXf|YFYR@i+2l>Usb~}%W4&#qsZOT>q9g6yhcg<9H|v?Uc0;5e zgH%F=s_Tie8~qC@#hNXGt`m23RH10?*AUFww05{7*A<;|Vcgr1ON*W(A4b7tGG*ia zkt&>ahajeS#Tr*)2H*C3$oq%74^R;RGqXgDB^u8`yb|SqVXvI7WR?soLuIa+WgT zlAw$+T21M;476WIp#V~q@-S4DJ!~IH5aI-3oK9Bn9M(B!Qmz(}6R%iY(b0YqE}{pb zoPGxSI{l0dP@9xUc!Z{2tP1Ivw&$vrFp@5F`E%B^oh?b^nBE8G8j49wvcAn3xadus zm62a&n+vg&y=r{qx0`*Uoh1agWd#T$p-Gtdr{V06J+*gJEoHoln943A$8N(8iE)x~ zf>x59x<8$4>t6xtU##F-B-R!zWm+{!Oe`~ZHzDPWf^3ycEsKXq!}^SV_BBMrZOUL= z2#C{V1m7Zs>O|;=&VUK^a+1nT51@Zk?$AVL?1e#2FIun^B56OuTYFT?NMsc=SW4~9 zwrPE$Z->CjhQPucr2>~+ofdP`EhK!22CwC}#eNE(C*R-od!4SxuYh#&lX9LQpM!JN zRhH*mkXyrb?MTLR*x|5cyCC22tqdO+RMUIZy5#XnlZw-i{y7hi+B9li*&yxSMpTcu z($FU2WwWi&m$%jt1731zAbuV!_XJ_NYfi&SY9QVZcVk&Kp~8SM!(FA%fB<-{X6Qtm z23#E)0Mnd>;tm2J8R9Bt_9u`QbXS3Js;=G$%Oil15kXXdhuprbS zuMjyg8xjuV-|!);!n_1~&D}Me#p(`XafL$t#a*dBIiE}Hh^X$X+({!Y;&eat(2Zn_9z%^5ze<^B`r`!beE2 z)xRYS-MfiB3Utz@K4R*5b*Q_YG=Lw@CBnw}XdU;GYRj7Wkc2{pxf>SuO%COJG|6hV zCjLYew?#q8^mi&o=v3+boC><^z4o&O3Psa^4T3QjcvlF5hA%tqemW1ihj?G);jOUzJ+@1=*30 zx;K8*50h5Q_ur?M0~{^-31uVN&#@~D{VIvuwMOw+nD-T@w5=wDYGIusjoJ7Q%uC`+ z*KU~d2ds|a$ZgT$LO;G>05f89o1irj*2Af%-h*a^=$j+v3v)&AiXjt+cQ}5TdXMmF zWT;aa=#-U#(6M%!X_9M}N;sT^?gI`+S0!^pd;C~#Wuanq?hzS*-mK2B%Xnon+)V&i zS~^R-ZceK*dUZR`$dN;5)L2pETk*0QUxXRnm&8tBMc9tP`M@Sj9fUsKQi?vW&2tPxVz=J=Z$7gT?}y zw?PZfB{<;bR_-71g7>ufCu94fG~0oa?VMXEl3QA^RN4ODFjppvQ>z^fMoWy52<`s!Qz{-?wTo{;YkrB+43Gu+w!3lbRMWJIv)jIz}f9*jl+9G?Xc7k4U-vS^l zw|gwmqT<&^V6RppjxT6b5T9V1r8goHzznB{e8~r@wimQdAL%3i1!Q^ZZdM!+>AO|u zaSoo$mG}MoIVk1+0?wuO&|2EXtt9$Zu_qFXrR`$l)R^R8P;-&5_VA_bT*S`biT#F= z^AOE(A-;_?orEK%{a@fpYqOs#Wg^VSoE6MRH8nACIh3va@Sfy}4yNB$Thr-lK{n2x7v*8T6eeLqc4P+k>|7{QYiIh-Qr6;fo@$~)OZJhM9$x!#;=mG=?ud#g%d4UaC>^4~-& zX+>I7&JJ$>z5Th{egK$Qe?JA2WJ#QKU_3 z{#jy$!5m9CsAm{Wv}v`wp8~LdQ{3Lol4%QjSr4LhD{)rKV2~l9G=08`D;TacX{; zvr}K=2rA{)uI+QrXP1U%!nH|D%~MIQ(|#d(yQ{Xz)HQ!Xi@@U-NdM>3mXbS)lari+ zQY~E2muk^sKL^7&1qeZW*Waacisn9r0u+!tOAS9~U8Q@OG07ZxMOXP!752teY{OL? zI8juM5-ycH!lkEZE5aoOCcK<<^k2!B-qIqb0-2Jq|9kQ!k>7TfFKyStj}ZvWl!X0> z#C?3F3s(CLsb&BF3aX7%*W@+Bp$vgAt<%K8rZlCV3^;bP<8 zlPqP^A{w?6j`G_iOUwT2-$Opfh=@I0))QTtp#6;X zDjfAUt=C7bbrPc8_Iho5BE4QOl1#E*FDLrj)~ie86xMoG@^)FT#ZoF40DseZ4O6~a zuOjeEuUAXyQILE;V_=>2I+Yya+4aob#9H*Pkc#l>H9hc`U#UgUSDoGEt{&>Fk7F$t zEh1CXRE1hBE^~)Lz4IhXRCJqs<9h>t8P6nUP=bR2X5Xz~Ie;M6f6ub3wdj7*cEtfb z6b?}BvDUv9`G{l|4gd{Lg_;!{poKT6n$vjT(S(RqS+0q_5@Dk>k#c6Hf^6P!tj~G z-}d;=_}gNezqM+Bzdg>uJAamwcvdj1ZLY=ICc6n=JE}pkxXxZJ&-8l56~(Q{boT`> zw%pbOo`LVBr!)Xx@|Xjm(V~?qtBBVWV*_7vAt_hBD=7t#GBh(t$)11~NCwW;JP|FB zh0iVVBuutZ@a{Vqg@fwtsKj70b6#vv%>Zwt`5IB;a8nECEksDCVVjXWh0MRs%xs#E zs`?(1Ykx3C5px+5KXxBTYfW-lW^a#{chA5>zgnTA=)$Od$h)U7AE!9-?nwd>#NAT> zO($`8(5e%=s`pJ_Le-BSux4L8TV~@6$z$alK{DUEnA(ziPl5UnDqbC8aYWxrzL1u8 z`*m@Mq?pSOAe{Y>fw_JtYq)ld;TaAk@y2>#4jbD~it@dXhyzkhUR2ftaP=t5e%Vq$ z4*9z+(zG&iXpFYZ!jotIRfv;ohx`sAHnr1NdbsASa9PRCp^=sVhAQy%45>#iMS9om zEoXGhpFYC4TNW18w28wO9Jn$sM%!kG&Xkr$%8DTA&*#;f3KR`URI27@lo)wiN>nx* zV>(JKlw>VA!f?5~n0e*86FsF2L}+-qp_Kz^<@KRs(yf$B^kyIiO)jZ|!z+xUGlP(2 zeu+Ri_E%mLw=?Vf07gYll8pdU^DW$J!)qc)q?B=AE+6MW9;Z{@)RFS%l5znlrTG6+ z;fc5SR8mwV6XsIHpO#IG$X}%aiZe35hgefAg+e2gHcy#kh-9KbSo27u0&gAoA#2SqfJLB*=o~IX{%j)L-0X4*#JWn zx>s-Q!k$%!9a%w8xQ4grN@9?3h7KrO6`U0)(Ir~|a@vh`gyLNaQg6Cli8$?20)CWf zLRE$zvauf3jk&Y{g z3V6K+*={7zkSrnroZBV8dM!{q0)Y7SY zJ7rj4wPARtHq7m4!we}<=dH_ADJ`w@F@j7MyO|x`Y*^OROO{ zv$5B{Z)Fw?QoqLsnRq?CL=K7w*k@o4q zg&mb%^tD>$f7mFMs+By^YFxCGnB=KWO3S!IrJ@k}*hzz@k;k^5$WZJx*#Nr00uVdx z*77L#3oJlg@06=W#jJyFKi{E-Y5Wb{_H{~BX(f7GH2}QpO1|%u5EQDeRzk{%Bq(9-1RmKwgDdqiwIEk01d^_*X z^WluNPFQ9?oUlv^RkK#ia~IFuLs5QxPSL?+0#b%%rFFWWbH|_??;`LIjiz z^%wYOl15dD%t|3spZ%4cE_5lMw$odQZ{4t$$@$D{ZE+foi^wBRT3pP6wV z?Hpg+5_uq0VUlg|z9NFTfN{GVp%}IZDFA0C#b>5c8FAFq76Y8Yq=dE!p6#%wr(4W4 zO@4qTn;zOG%W01kt2lox`l!s~j-s?k8v#fauvE4dl_gz4lT>qzNLFCTY$>RH%bUmx zMb8vDgHmBZdkPDBr*WetFdXifmi7`A>v6U{R@%Z;2u$HY-?8~)|Nf7(aDqVi(04>3 z`9xw1Uv_`AsNk=csK+6SaHfkK-8>a8jgCc?U#kJqJ24Rwrky|$d0Ezyck`y?6$%mT zHLu^nl){-}Axy^~Bn|`BLNQRq(h8ruVs4djzYq6;7eZ|XPqooVcU+DOG#^*ZvG4(Y z{&twZyVr{NsZ^}2b7o?t0Y11LpvzHM;UmVep^e&-yxAElQI5XalHTYqrwr9mvYnSH zOW2a+Xsno9*b<0_Z1E5#o`dE%G!T@?7!PqGf6}vV-_sV+<&#`V5!Y0n7zg!t+ZlV z&V-qpVG!1EH(Tk7eNC9?i;j$&9gkEQqlb$1=@_wkFBbudPb)ri|HG8t7X_*)qvX2v zj{eCWw}4%EoapeX_l#3+JZ}*}1uG0K$~{uBrKNrG`@E;76)#;_HXD02ag=;forjhf{6gdrm?r(#g4AD(CGOygbE0D^xfZ3R1-Q6gB2;) zON|K_<9Y(c+QXK1QtTx-kcr3B!^7tvr_Ot#C%?A;uLACasU0hnm(zKX!!v_}^@O?f zFKYf>#OrI%q*5=~u5%{pX7a)nH&aq_%8F`vR5-91Zox*d&^3zq-D92rJ+H4(fY4$g zA%Oo0rqNC1`n1Cvk)B`%6t`3qi9bLW+{j4YM+@;TY}?n6OKNO_PphhK$>p}m>4k`` zw

uj#>2P%l0gS8dWtPP`B#$6eawFt}AeqtiY3z=Vmo59tP~LY_wq~D%86XGicZj z$4RNf32iJv1elsV~R zFcEok3xN65i!y9jh^zhDB81NrsR>*F;ybzSGSZ$E`lw1jnDhkT4>-7j(LyUx`SWrx zrs}mqT33ZTg}x}YEvEp^JrGsy{Z;P07(J{2peYP3M3cQU2jyutPb|^2D|sXT*EN5!+d$;4aLa-IW`* z?bcnG8}qg~ZgYP`-G1CrBn$bQC3jbTp>Inb!$&qA*}QqUN!VzeZ!^wsjU4EqEs^n$ zY%GklJRE81ftyl%-$qjT+LAKDnb)Du6qP;cRfo#HI4EOf?T5-cCHD5~f~Se^52aSd zxI{UQ5`1L|o|M5cBLU%d=1BlBIS}RXZUEV-|E~zP_$EB@%Yb{YgC`aVo*3?6mKP`k zL~-FC70xK*^;duDz!|54GtRSc#zYHeU=5(mi5z^Ys$@ay$?LxXIjG^u7$2q<)2oO& z1y?A%A-E!Ke+1503k{}lh88_dz$ew+oW>hl5m`H8iOfTxh1Ml#=_v3r_7H3_--$0| z-D~{*zs4CdKf2-=!6Qe}>-dav1@8!eH9#Of)W^a3f%sU|IRm&*7}q+lRq5|?AGQ{` z89Ff#{~=}>i}I%!qcMbrbahHT}@!sZRHwHIXPC2DV;?5{KiB?{`jfnWO<+j zOB`&(rWmJomqTUvEa55?HlF0d@S?GY}q@dnOyMZdVY<_gd#h6ca{ zj}hz}bKTe{b{~r#058tLt$}PPhk#z~v$ATj+kks=iM4)xg!}by{&_Bk&ls;({yK&`~fp54ewPrHLgS3M)9<+R_(F<_AsYM_I0Nar66UPqGf}6$u z3-_8df=)x#N+-l&{1`1JyJ}*$?1e-@_)M@H8pL7Qgc+(@3!QWAR4!_>{KPf_wU`)a z;p{ZjI|>uPB1Hbq9}G8p3HTBsgVP@)M8{3sP)s(1E(<3f-li?Rop6GF;!*~CAC1rZ zN;#;!m9Pp|w&kZCTRhyi=b3r!sI6u)?Svs5E{?JLcC2Se9;{aTVhL6AC1$nV8*bm_ z=aRH+?ANMF_cCDIVN*@Vy}1Fbc$rc_#g#lkJiPDM4<|i%c+We`iO@nT33n!2eLae| zu&`AquX(~)cjB7EDa$FAWf53CMA~qP%yiAb|Pi#OxF!QmpQHDzA6NZN|@s9fSO%~pkyOtxKL+yRq#1=M8 zE%E>fjhfa7a@an8 z!Elx#*j5&Lv25!j1^kxSS%}PXImcfl>Wb^&Uu0I6yMiN`>NLEawWQvV%weya;FGtu znyHo~fZH~)B~Cm?d7|ZEUUS`yEcUq~23rs?a{7v``Z4$!;=FLQVZq4(;{tbr3u~-W zxj--ReXGpf99>d6b?;xr*GuwRuiog5YFzDLZdTGAge98f$DHcN5uB2F8d=#sCcqfG1EnWa8jk9k6kP%92!t_iZ{P8Aq#I@*m$~S!cuEL ze&@N333kov3nQx@j;wNOUZrZ@#Z)jZXvNhBU0zohX?!@+*aKQoChD8q#HlTNgA~dr zTa3D;@2i$yTr@mUO_hve7UPHK+dUOHO~*X&f^5EfRpjCBcUABj>K{Zyburus9|BhR zK<0&6@H8)+yOPT>7176YQ5Kh*^SqpC(Z;lB>(}^kFNJbsZXBw}YqD;G*6wRkbVjIW z@`k^<)oXAT$Y^F4q7*{=?dRN<>sR}7$1j1icAEE z5`&4U%bcZROrf`AK_a!4RmmI6(4#!d@eW^YUj+lV>p9G{l@v2<$JA`^gkBnCvNuyr4oCE}8c5wBspZcW?reX1H8HrJpS6ydbrK_gw!B8n8#sl}g$p>M#ja&ud-pZE zgO@r@Y)vkjz4U!iA~$&ZNjpPal`RDYF)QTd!reFLyXVHO%hICjc*RgUZdrW^=A#E+{6&_mB?3Q{ zrUZ=Bn*N)mV4Hqvk)fpHc?uMKyr752iB1yJI$0v&uA$HzEM8_Kl{#T-7<=Y^52D#0RZ>05F zACl7%PXRE^VODj?LO`yI@F@BvT&J1(XOivb>4@#O5`OK&IXgR7IJU75WZyc{ETewkB zxH(#+-5;C~X`c}KzTfb($BE77`!zR(%e1RQKjI9yWt-iQm?i_JPDj4|0{w0&cUj&8 zEJacicrBoe6U9}+f0KfSJFZPEr6v6o&l1*TFgL<^FjeN4KcAI+-Z@*Y80575p|_6n zS+nU>nKR5T;Y(z84N-GQ zW*7FLuR+Q7OSQ2wkz4JMBF_h<+y3}wyg>{$_b3R>; zrzdVC6u{wb)}D}OvN68IDhK?(cxJ8WnFN5fLkWHX_5)lP;&;Q_u@{LDZ#TM2Len5 z(6W#7uNGzy#=)R{Q_0D>_q_{W=NQ?=X> zz>i8%7gA0CJW-C78PzDq;0%Ts)uKVS=S450dZqeOR+-$B(d{?w#&)bHi zoTkSr6E&ZuU=e$WC2A$-(>U{c?C8!BeqRg55HJ;!v7T(O8pq*8)ZogeH68H23v%ux z)ut?HtZXo+RoULj}16Sr6$!!jO+Dt|g_-8vM^A}6pqFX_cXj@YrpP61-}F$&xR zYmreh)5``Ax_qqjUsD zzz)+;E=au!```p5sw@0*(Nhwl#a2^Qb5wn~2^CtF`ShTIKmdq5!gnaok1QtoYm&+h zyV;D93@2VCw~NWf6lw!aUnc;R{6a_1l0BDO898C8XqYIx2SqU%LP+Te8s}e%vpw9;o`rtZtbRsflN#h z`EW`Ak|HFrHI>O#>XCD?|&{;}0Tz`6StUlsHKQ^0B4NF*S{m zTnGs^ypHe4%PN;E$Z^-Fi;!FZ&}dPEddju8?$>-JZ^&p*W6FodC7-nmE2S_Dfv$1$ zJ&QbyxA^gz5y}$}dQmYs^_6Uud1Pr6nLjCOKUc?s51I#REJ%xUFF6PIQjbPd8?MvkB> zJzsJGJ3(Aij9$1UT;~%><`m{vm2nAvG9D}n6DQ8i<_Jl@KzwZj(UG>C85vs}4of9y zjBTGGJ7TV1d;GPUN`L8>%mBYO`SmGASuy>w&WC+bGvz$v)Kip_dY`nwm?FD$&d{+r zsDfV;`7OTBs@!iRyRrXOxxX5ts)RCbRzX;qrefvr`Xc3ezQ1Lj)*`q}Q zD0j~@%1TheX_4O%A$4iqu{mRcZ%IvHR;>1u5BiLkt;r)|vJQEt+VS?9l_o%UrdI@& z*C)zM@F^fA5R|E(SSp=KgR=~9Zd8?iO{CwLngLEwy#l8IuGnwfs9ZMQ0}#(QijeK+ z9EX5e!MV!)Uh>3F{R18W948uOef*4t^ywjrmp+XF>P8mfXNy6UajTodDHF%$^z!R_f^R3E+(#SSTJ&*gF;@iWy8`ZAvN%Qt z^mlFfdE}pNZHd?+h*t9gxgyk;dc$X2of44x^ljGoF*E)d(BDX0Buj&dC=*id^%e}A zs1ZQnv!!%B4(LS~`wTe6yk~~22fp6q3$UBDDxk{!PN4Ktu4MR>sHH^V5Z#<@Md7h3 zc0;a|+_?sE$Qfzq793ht+Ez0F?P<4_cOC9~TG7+cNY6`ufNY z(MK+DBa#Xq*fy_}k2!L~X#}bJVI)}9qY3e4M|N~?+|f^D?%T)bCGy^^#ZpmDCEoE@=U#pFEOYX_a#YA8*-}0c*A#Oue|eY#%0?>_PAJ#Y~pfhUHFbH24S2u+-})R(z0YXtI8_( z7gg?0&Nq6UGpZp2{{GUIU_PXGHrTSNFzEKRD!GUQoafP7D_{{85d1$lnZPC_uB zzB$q+BX*HH!mZ|qw#}Q)#}xv35gxuJ>SGj6Q*tCpHsVE*E;{ZFR_SjgC)2#IRo9dl zE^^RQT}b0|X#8J1767IZO%YiLtMghp(cRzsvZPInvBI+(+j`|SMGkTNtQTFhWtA~G zcqn65eZB(u)fc*4!JEC|b1w`|m0)e@eH3Di5yU3VlQFuGG16j-R80M(>@r$0gjQ4; zcVzop*2oYfCs;!)C26r~c1pLToXrO*-J~QTe(Wf8QkKNrX~kp_BaBo&g9T{vgNXh36c3s!a~|`XZ&C)?$6AL@3~L zC24t-(nQYW)9NoSI=^v8zV`f2imLM7@il%_SV1bkKQAFf^E7|s_FR7+3LpHgMk!Xr z0B)K+sAeG-Qi^~T)dg)*m*9!#jAJD1+ZxlXPD*RBTB`48O#lD1F(MQFkj0?XryF@u zrd8Tq>t*2>o;l7k;B343*GmN*;!eU}vJS>mi#<;wv?a0&DPpHZF&hDW2AZ}7`2l^- zfPg-?Frd$3tM!4fePa?!m_d}_UBMpciZr2}!0*dbOah3y0vn&F8j&=hw$LIElL$YZ ztYCe!b~oMzTD5ytiLzimIXHFO6`tX98JDQ-R)pSNES zUTKD75b)rR@p%XH(vCp}QI6IiQ8$57CVd8f!`UN2_Wp2VT<|+374L( z#XbhSQ;q(4b*KARjOShBhgWnoWrH*D7w(U=dNaAev5(N3!gAB#7F*d-wr$>7qVEv5 zD*|5JuKYl&ztJ{radw%!t&Xk71_fOVTZW_Gu?<_cCI|5$hAny9^uw~7WYlj|Zi}=J ztUeLTj<#*eL*1b2V~xruBJC&6?*@g@P@NlD(;PY6y=Gv5t5Ksjsi`0$72PYN5s*>p zn4*HQ;Q@rK?aUe&p7VZ_M*7TbS5lN4lOWL42CWGF9Y|v!X3QtyyhOv|N&On7t;l+Q z>Ewm5mU1AF@5Plvz_1tq%kSiy7une!_*J?lX^&Ml<^|^YPQmX|aAMH`nr?WObfgCL zNNTw;vdBsbX=Qi@JStSkUb77XT5NhU|8=3~AD{$3Mm}+OAU;NILv5@@Uwj%Ok@9@{ zCs>gM3*D7GOVP(rTO2Z~!pyUhg)IhH$|J21@Q|vc6ZBRtr2GOcwwA)6NVMouDODVJ zzAhn33e<0hBqM8DBZslMl5JFM68ei@>?P+@5|rB6 zG`E6Rbu#Z#Uc8a7PYFVQ?!z99_tQeBTkjm|b%(y6Sc8>g#(bAhEm}vQ-047HHw^uw^ZH;%?4K1`h1L^Px{z#`)BIlNzUZPGMXFWW9NG%Eb9SKQItt+u zFysY;t4g-+gE=zE<$p?2zrCRi@e)roo4T<*%$mnmN6t6Q{ z>Eh{|Y?Hnz(jWPxY)kW|ToVUOIY&V1YDo~Np2mJoj z>#Dth65uULGyDe~snS1D;9e#W#Y0e}y@-Qyz&_G`dMM-<%1c=T+z6V|>b39z$}FW! zt-cZ0X3JlM-XNtGn=C}JjU&=+yu720&q{nGm0jJ_rlN1A5PzgO3*Ssf5@O|*)+e5C zhR z+$eOJLky_Us|ZE^#__ASWnPx<7fM0sDp8 z41(t*HsmlZVjD=E5sw!;6|cpfAsUFx>!kp3XU{|vDX|)p^zMEBlKoXWb|(81KVtiK z3b2J-_!IvxHQOzxV205ZHMj9utMKaut_( za~3-ySWsIUq%^vI33FP}#Gir+wI#o?LZ{}~oF>#L@_{$=*2=<=Ux0^^hU9!3|LF;3 zh&9(S9h^7TuOCF5k)}9hEkkn7$;JRn?5}LWM&26^$!RiP3Sr~Q4*|yZZh3q3H1{SNPO=9C4IBF32;wcTX40s2FgwSl>uYSwahtFU7)=E4q_Ftiz*SEOuiw-FXS`yvKKcR7bzpC zA!%zp%Lr=12l@xf@~fBvz~8Y#o?O;~HtvN)f8O=R+CAc;uC`I#I?UbPyHXenAqASi zg|PCxudzILgBJVbD-y%>a(CZ1Nsvpjf5t-TYHw28gyyv|1)h296rA zc;AYrnE@s$=n5iey;8$y<6McE-XmEZaMn}xYoCFD{hUATu zb}e7YOL%!qUaELW$2%=Z2!k^*qm9RrR1rlmJ8@20zaWrO7}@-6f(@t>om!ky`1}XaV7sQe3D~&WL@gC2^8yH(T`Z?el9WoiHrd;-rpD~VHVxfSdqI9 ziq5A)ss*#_+!*(i=R2lFQi^pCl*5Cm`=H`^OtB(Lby>a|xo{b?SF53kpAxLM;%fw& zsAh1Z*{xRt6R9YS@Na#Xbk3E_*NZz6x2vYO(d^Z$O_J!Xq;@W5PNzS~5?UtDY&827 zb?>#?N!iOunVIdRbT;S4BV1f$YlPWOm@IF3$ zobHkP96djk2d%blD4R}$X^!Y!S}Ns0SPIiZPvl^B$YYLVWVCuuBZp?lF!IVKq37Yo z@+DKD&$w}Q+R_0qK}MO+^s3E3lHx81p7_1HsPjmGm*o03n&J;xnYbVMo4UBRNb=U~oJ^)8p< zUSyuR%=?gz#69mGm8K;u+lc)0wS*`vm`cp5gK0RRo*wl*_zH&nGp@ z3w_2P!xA~hE0K^0?C;im>lAM_i-yujk=z78K&HCX6qc*fj>fx7(*Z+xs}8LfmnEUSacuVw&DcJEkcR=+k$bUOx!`7)slnQRNU^ z-O$$#`GQ(sPJeHt;Z>%&zc`~$o#h{$;Y0e!;@y~FkZrTsmYL07qW}Y~APp;Z1&4{u zoyFf$u5iy>_kG&JBi-!NF-YcKrm0l5sbinOlJf(T`q)O zAmE=1_+4sY=v5}~ZDhpQG6O|UZ`Dew8nlwiI@&Q25g@&23mBDk=2A#dUwnESMjiQJ z0jB+R60KFPN=|34ffuIgZ>FcSF+nCVlleTJ)0xZ@j-Je+^knX1E+3bk%zMn+_)zv$ z3z$E%@3!G9XbprnS6XDY7h1znER)+BuXMaxGF|587}lRp=H`gL9U1NMQ1HPL-9o?? zd@yU9jwD)djkIM)a31FK{ii*uFYD-$jLB73CLSk9jgNHesQG5Mf8;l=tpyB}YX2?Z zMkzx&?#w5QhvR+*WnB<2#Ndfwks?eIKcGG~%Vvg21^4E|iWx|ZlO6iQ-Aw6`h2$hj zo|R@3ws4m|*%ju#kG}K&;fFSOv|CV%d&x2Ns)rvs8*Q%;`t!x)^ zuWI9t9KTV^;)65i0tLe}neD)@SL9p%qVz&Z2Nb1czLdLD1BG^=6Ma-C5K8AIM8?>W zM3z&bvaN*{A>jZ8y*mcDf*jJEnr{(>&yNN6n3MzuQq0ucP(K@0D@VasB_1GJhOv;# zTG_lFpaKkVibUlnO=H=4uDnYYI~FF+F$T4TkYJ9TAoz?Vu#D^^jUam~iZqJ)p_IF@ z#F2M!jORqy1$nnrjtXl~oQxdxRai22OW~)g0t11KIJ<1Vs6<5*L`O5v9>8EV z!q#pjeZW|32I6O)dj+UGU)hIYQzas=1}Q9lDA04oH;e$iVr( zF4cSWlhdzc=REmgSjExu zRl8bXVjl!h8}~SdnT@*{I}eL>8*;YnS9;}_&=;5!0VTt;cBu@rB23;<79Eu$|x*d?XJ;2ecZ>~}fw=IXAUAP_Q7wdDDpz~RDNYT@jvdeN)jkDH%$xrE<63{S%}iM%7xg6;WH)`(ds9Mj-*?##v@>_Lhv*@cQdrra$;K zFFs-9llh^%CksQp6Az7-LC;IP6S1B*MY|m6aP0TH!fXBS1zu-i=aYt)>J;!Qu>v#U z_2K^i6}(nbnVMTi!|Urms;ExzvL@16^N}C?OL%QPs|&m?xm2D0FRsuz&)k9mznUF? zN1~W2@Lk%`+rxyY*>R2)sCqlpnjLaqJhOufGW2#YWHzW>WF~5EeB*rBrV3+49NoVm zNB3{Lq?Y;6fM2G6F&_@AwUmMHWWSh(d`*k4qOoz$KNy)#yC>Y3Yr^wq>Y;0>Ahjfr!-+I_t4Rl8dw7?Nc(x+32DUaLTBD?KmX3xkLbu1(RDApK$y zJdMnyOzwP>SKg=+XD>o(7Mio=Q`tZ(YYt3$#Xi!vLKX}E!aj~5o(3rpI^8@4Or`aV zdj=oX5nZu&KD|!$2g>`ZCi9h%M~%1K+f!)a0Z}dmaR2U)EX;QWPv8L+GRTj|U5t6} z`2kA=96OYIH(_k9%IBg>Y_*}DmFu~tGgNeNJEtRvzC&&-;&?GhTP8^Z>}#U+ZnDvW zk3c<}rMTAQGa199_+{Di0aBp=q|lW8ULi5Hl)qvA#NGQ`LNsa70Ptz9+|S128S)jA zB+{O(#lA;ic=3K@;lp_&YTcJ^SG;$U1Q`PllWpiSAU;m8!!5r}A`T)<2Q_*j^uhT{ zc)WNLDa>NEu85g0i3!h9_rjW6~lG1<-M)nw;|Yp871@~hgkzP)=9ruFrF z^#$dpJRkE&_0A=mG&3P{Y5oFBCCCHE{PFVH+XOA!jF|h z&QL=aerxR)syNeJ2?wi|5;<*sN@sqJ{PgO{jAV*Fv<@^1qL|}sgCdJt(PYY+^Tn@(mf9RRp*>LACPiA82UCbk-xm2K z+kb7vE7&ra_+P1?m|u2ca7scBR9f;Bf;580Bzh5~TU%T)0ETtY9$C|JkzVLL(7oo{=4Pf z?38!?|DZh9cN{So_RHVUl~do)*cWTYs)@lUBONKT^`r=`YC6{Q`K|pL>03M_qqX792pG`7HwDIJ>@-i zI)Cf>khKbGR=ew0D(YBTphd;@zV^U%gXMjVdJn62VQLd{9Ip1j4$y}7^9R)XPI*_3 zJ8KUdubOzb3cp6ZMJT~1To2~BlFo9g;D)fF|>4Q#5*Z>k&8RM)$yF1M*J zuc@v_Q(d>Fy6mR9?oD-BO?BUEsw-@&8`M;%HPsa(7d#D&B0q!^DSVHrr@R{(@AB>Z zGQ-7~Tf*07#5?K!H_yBOFPHbfY0J0ez4trXc&QD~Z_E2X4SzYK&VL1?*GT((X_wFJgWB4N`GhzI-Uwf) zznLodF?x6*9lkFLyGVN~bZ)9(*CfIk+X{V=wOPqyv`ZVfrM)`_aY_Lvq@Z7)oa=89 zlq=TOQc+qWg0RE7JK;%oq`|vSi>&nOAZs{?SqQRPa9{PORmY7H1 ze0pd%a)bt|-VONzVM)jea}a0|K0@^&F;&vxg%Og6yH5GbwU#K9AzPtM8gSC5^h?2c z0cb$weXf>@{9Hz}Ot7raSWKrBdwidyg>?N4{O#8p#1zQyZRSRFIH@OLt`fp^Vfx=t z=FuYADsL68-S(PSkkLvgnylbt(l3zoPmx|~4Pa-9Bvfwn*-gt64-ziN=ZxMrkW5ZH zBEQJ0z5++q^Dgl&?3>4cNFHG{`(AeZ!d!UHO*jVRfX z2`V^SQZpjv$Av;>kVx^JY=d;MG1;4dztbt+5l8?<^XDh1bT%Um2Wk*a33ccdI|A>v z0-t1{b74;#%>9an%+RNPgZwmJwZbG{2WiHY@o0H0-=wY5UJi^FdwTh3K}ki?v=z)0 z?iy|MV;tp+kM+gRT(^^`OB=+{Xbj>Ej=YHA2ig8KOS&OW6>Kr|yW?cYLj##1$~G%i z(7ys91xZyz@SIq~0<=NuSkOd{p(ls<;sv)-KJwX3&dT%Y6LS^(eR?*lOxh@N7E~Dl zQDyk}7Pv3@slYvIoxw#@v4UV3Nzqz7!c+c{>~YR3NU{d`Bx!U9^$G0HrSQ6?TG?(U<`PnMBtt~x*Y@%X$WoOc;RK-yfKV=$OU z?s1M!2Iu)P6|coM&E=YRv3PTBNFSlB_q1J2@sl(g8HOul44_*ZdYYlodc*h>=4!l^yvmUNMc0Gq)WO;^;Y6si}| zD~aH7G>8O&eyZSiXcxp71F4#e35>MHH_;=lWN8y zFb!hvD884Dsvupqnx3hG@v8VxH|%%H8L!e0wPg(yE56LDeeQkgSVOX}WzXn-Tg)^$ zxDdQtdp?jeMoB_uM-JRM?;qOp?<Rc_uFN8g<79=Ff%*#vv?1 z*oy*P)JkTzVa<&L!TFQ$hMN9cedCYDpXKJ^U zkC6}IUd7l(zgK|i`QdGj2y@QhG&o-uc#ag#IPG$*-(0cz6E!LJY)bTm#auC1v;^EI z0%_fEixE*5V^2c?UF;QTK3e(<{}=WO-G-zkIWlHqukgiSt5)9sQ+tKA+rX%Zg3Oh> ztTHm}6;%7dyczOMTYzKk0roPre#GVC(xI7YladSB(oKk^w*S}=<1~8VDBsOOK74UX ziA!n9`PcmovOJV{QW#_!(8->poTQK~U8FkZhj#zA{Zn2)Aa35t_=byACzmRwLMmVzM zUEc7W+KIuz$!mov5J~pspQTb|Cx$MH970}x6O{+BzmO9}EaKh%`1zRFPd?fn?8>w~ zSWk1oap^_-zWHN55+_kiCD($|;mGP4on)vu$O%^?2i^(F9nXs}Chrvp!tFwDcPJGg ziZRstrj^}ii@mZvNqfLM)djt?g}L09)(qnJGv2NH4$2iqM=}p)o}4S>>6i`#-xreI zy8?>ALGa3_q|m5^go)|TDA_Oe3Zsj&l|r?V9W?h&1ayYykP3!iBG~+pFd@qe#lzG{ zL6#RrxBS%|3Mi*7u*clUhZ~&Eg%xuaC;KhwSkv)K$&C^dg=d#U4~+`1Z(JzIciSS)FReX)4)=Z1P%!sK5|xU&el z%>T|iLzTV;e;Fur+7Jd<*0rgoV5tcAMJsYH8gVTaVcQ58WrNF?xA@$ghsEmWQV9|s z^`QA+e;_8L<@vI^=vRWL(O6*`BA^#%4S2n4W-I~qVv}>hs3Hbt+E35chb-XuqSz4W zug&JpBFMLWiC>c0(&to_erBBzW%cwg(&|6JmxjlmTAiH75y9<42@Mv>*&#U}%t4SOs0e?Mqcjn~?cP5t{vtX4Y&k$9w&e%^ZGg$g&}{2!%C^VRwbT zC((yowM~i#_@MFA9<}Qg6R=Y;0j~)Y;MG6W--kw1b+j@6lfRLjUR|TR-WN&bR!cgQu43EKDTspaP;g**&x=+6`LYICZRhxpKq6xb-3N8h2?a<`nNG21 z*%532zOMAq_bSl$X3-Yw}C!#vp2rc=yu#myyN7F-_ z=zf|IMRcEG1uCNZSn0FwdHyYl6WI*yMw0^NE&J^Y3y4k?JoN)H2PnsyLkLy7(hPkL z3&fT98sz50rQ0(@yf7$*fTGo~ZqCWWcM{%;S~fr3H*b|~N_?@4HHy!&Mw!;x8s!+V zMkz2KqPYTeDg9jfH3I>J>Fd)$Z;Sl~u(!xfWe*nj%zC?v^{G~`Ojtpm!QnEvq7E@H zd`C_wjP9D&vUYXHJ%hT&_#!J5Zx9M$SI9_oK6KF%mPo-gSo{d{z<`EM7`4v)pp|-h zziwr4tZLO63`qs%blovE-uvmJ;#0L!U@}V?OP7mQ)AJ0hgaEjZnRCpIinDugmOLC2 zi4A?##@JIf$4q(`Ai){lSRVIO9My1RxzuC6g+yDXE+!N9)KL$yWsmo@l;@3!e-7)Q z^sH1JD*-#NTICN@q4bTiaVP^%SrfkaEk2+4oth_@JAMljfn(xq?Q7^!r}eMSfPE@{ z<^Fq=HA_aKs~RTA9l^mEL9pdo{q03w%X}VpXdJ^bFQHwS%*2i%o!qw-?IRi7M|`DU zX-fw1?stFXEBPw)XaZ{*85u45ur!O!+=~yekvRg@V#IF`vFNmtXi=D0aWHqcN+ofz zU(2g*d!^*$$SX9D&qq`WD~FUaM<%~w9vQHC*I5HrE`g89u7^92iR&v<=8E}w$Qg7LA6{AN;l_-)fshSPXO)pEUG}P&(xYh6laiRI zI-XXPbe)J5c0@xuqF~wuR@b;R!79@{wpfn&R?Db%^;!z- zblegG?8RH9M+jp+HSg>x%ZG70!l(!+3R!3`@{S~UBAu{AC}&fMd7X`eI?*?^pDh4Y zcIZ4}yAc|(i83UHx;wljww4h2=WB_z@@kDxY_lDTPJj@L64GNPp$L;8j7M$p*S3ov zgPIY^A)VHX#%9m-wqyIEA$ZdJiWCA%k-Tj7-M9$#mu%zEdQFkfFr;>oI?XwG2+DjX6~iJC%X9g?J=agQUBgy#ul+CiFsbEfe5{nKJU84;CQrQK)i39{dOb zGS4=GP#$pXSd<4B5Nw-|X3#U=OfOhA)p2V;;`hr`Fugwg_Wy_gXa58067zIU*l+Z z?piHY2k-*%d30`ezTH$`{C-I-x4E5za$8KdvYE8JY(%+sUo(f@K}_6F8+(Loa4xqnv1@r{Vhaz>ajjV# z*P3ScO1VdBGjG6xm0=5$!0r))^$1_8M|jtrCiPz1f0@sH5XY=sXq4KK?h4M|#nFIY zt7z=#%F%%xUHL?HXZbbb|M9S+M=a6Ox5%0dYzlue^m}m)%(_uAHV73k0m9 zN>srBOGXD^8tGLL@md_@P>{Re7<>fo67UK7YEEFAtqpzI?H-V);#e_678I z`R2~v6k-@F$jNuB`4-4!P`{&`z4v_;^4Lz<|3bFHC5)*PBkav zi}8+mWuX_x?);|a%aF;?`Qwv@szI%%YOVezzg8bQ4~B5k>C0oXX?x?7PS#_EywPL~k>xXPoeD@hKCs@3nl(HwQl2vtYR%1V^xTf+6_KxN<2YW=MhzTYO#0vwQNgcHgjJzBXobrGtI-K*2{v}u!K(8iG77b6Jbe~VF+`Up3 zWCnlfce5F@Cm}^!Chbs{sSQ}kE7))0cCOy3ZVVs#Jan>30{KRg85D-Tih~KR@B;V4 z^D8JnTN$W|gzV4h+A5my4^g_iT)V%S8)#Jm@gDLs76j(s$x)Qh>2Sqo5p8O?i zwIy1je04rSRmb0!7@ufvKQKZkf z&5eNsE~66R18vpsgb#ckDp9e|sn{uGJRGv!8O!i#K)QfF#$8stwEvGH)J; z_OjXDJ%WSTjAZX{vES1ZridZa&K!_Us@-h(oB8Ur;jSqH!i3G#fQf)Hh^S*N@b;KQyZ7MA+>N18W(C`6*QrL@EzAW-HSg$QDJL|$8B zPx6q^fT41l$Fsf@91oPySa+3-#z8ZWK~17B^{h|fv8hKs7xV~+6Wo%#QwB&f9jx*M~_B{8i z?#M@Gf%ieYw`?~*fUA(i!TuF`GC8zMdSB^&E&3_NYtIkm3}KeOd6C(S2AYcVo0mE9(CY`F>zc3km!A}RMNqK8ej9Xjqf;(gZ+fsXqE zABd1)sh#v0=Kf927KxKcW}a3d_e&`fj@zuegr&(IJgF}e3ns2J1|#kBl|a7j3l?bg z-81PPc+1=*2a{k;dw9d`IqLS``I#*i`;W{9-)=Wps|N?286lST{*Lx`YU%m3G(}4{ zX0)_RSVl|d_buxtyGvyCG#NA7#^A@73+~u!{)x%R-O?f&69VIOe)5itK2^fc%Wo;a zyZ9kHQ16fPe4bw;KN$^mg0D~xbDgTyp8=6EV&zQfSH*t6Xe35*7bbJpx-b#PQCqlO zMpn?#!AREMQ~;UaE*qh77K14rD(IRn@n@Ch@~rTo5wBPkZ6#%P=$Ota6EUl-Y$*!| z=V3PeiCRF~27AtHzIWVM6luYtQemJ#spUv}iHYxc10+$?y}3#`E<% zVC?YN5y4}_i`*lE`Qb%bBZ57`i?T;(F)xYz#)2cs86?HEshpxF)mSYiS7~VVS^BE* zj&0#pE5iqIVZD+tW7@;vgM;wC)3C!ca#ge2vz&V?MI=fAZf)rm)FqZ0xK6ZMiBmNk zDK;D#^*Q=61aK#3MGlP!YLP=T@fMR><1aNsiR2AFV?JEZKZCOZM$XA32h${dC5J|( zI^$y>{`pxD9*(4p2o^-zXX0?e$T>%(^O5!==O;j_|l9TjQ#I(T9S|E<-ZniV+=%;ZYwo_K!4eQfAc z9Ka(wEm{ zA@S)`C(h)|ODI4)x@$}2`~cv>p+a-OD5wKp`6A8jT2zPw>g4VSSsmfq$L7woEct!L zsaQCzQQbe>9{MP;hLmuYI5?Q|wJ&lwyE>n19=i#^j}d_PNsgtaoZv) zRIJtK>u-g3ZVNYYTh_q&1B?j|modH#4Q-y0t1&rNgg^3DEg>YH{`Jx!*q+IcSk9-MP zY~MWDgY8xc;eOF2V*7rHfc)_}5k7sD`I-u)oF7VPF6AUQI-3p8PV#AVC0ynxD;sD0 zN3yl(xy*Ga5<-4h>J8&#K6)x4m192EmZ^JyjLG@L-I+gXofr3_+P!$W{+zdN6-L0{ zotR6<20%RlsaJ%IU=W_i0gAf?5jO{$J;gs0UX>gvr@NJx8asmdyRu0_Zw#Hi2Un3D{w;hz+ zt|_9~Mu2%WH5IR?VXN(Ips^U;z^G=;!vlEe6Fs!C>VznKxLJWe6=7lY1m@{u@P-Pk zy0k4`1qx@49I>KQSimahXF4sAK!%;^k2x*; zmlcItYl{1&2QX57^*E5F*Jz|?HNVzrl+{x{M<)0$`ux&SE9}6A%sW5`U5k13Yr_uP z;@L%^#j}djk9nE?XRgR^>``R@^iR9%`vV`susSuk+>kS0+jnG^T9CUNvj(gZ^UF>i zKg3jQ+QalvI`dX`(9>slSzQGrPp6#sf#ko*EN-m1D1GEU<0!>vT{ET|=FGAa2MheY zA*EfFVW+0jDxNshDn92(_%gPBYKDfL;;>^E)h(W1oO;OMo!|rxL)?#0;4<@lGH(VF zaFg(AHt-51c`f@Ew4w{Av2aMi&C=GDTrjVwK5Fx=RDb%4q7WevBt59c@b<~?7ZB#jgaT8 zaa(GtUa$;zYDECLsj%TpmV5x8+5IC1Ky07bLLRDKie3#0^XS3Bivicn8}*3^rIFJo zmPXG5l7@w`T$bKHdI6zDgce25q<6!K{oEZN7Qk&!pPoxTGMn6e)kH-PB&w2cDx=6i zUchP6r1Eq-GT(!k@e_ug#&Fyr;?Xm>Ascj}2Os8=ag_XL^+6AOy6`>iWQ>#53yZ)s zKPV1i&{RykP^Hd-MB^Zt%6Ny5v?^{ZYt(xMr&HKQh6)H(@lB{&GLa<5*=p@#&T-}b zaJE(Mr$*QvAc>THQ^!ThPp8Z?7N1vk6xZcQQqHR7=Lh&gHS~^g6Zb2CI@GwUueC`0 zuAcPAXybmj2Ys-g=!0WKAAAi%4tDjE(QWwWV?WAa)1jU_qVDu-$lDxQ4MOrI5S`k1 z<_Xbn*>dg!s@YHbyufDAPV`O$O-`pTv+1KJ*nto1igd%frUtqFI#!UUHN5PX`*F71 zhUdCFg^dM`TcmYq^!70UxF&UnY!4M}riJ~q)tDUmb1`*dA%d|<-#l8X8d`PFidA_Z z&str^-~>-gkap5$NjQoH&=1qHq%SMon<+&8BLf}MfEoZWop8SbtqtlJj zQXJHYs|d659p&J>ZXbuMKx@&b<_(8?ur6G!r+$_w>sGhTdpspx&FEK5vzoL8N&VJ* za9)`xtX59l%1n8+4wQs4-G6sX>8rpoH(+2Z+}Lbr7HpGK*>{|s{t4>DEo z;!MlqLP+VQz;;9RKE^{Mkj*c8VK%QhSIT>%@`=8Pv)bB7^;_4C>CQp?K7TyLlg)j9 zNbdVl+3zFO@6LNZ4aNMrWCW1`S-4(PzxP?{brGTAojPJ1{{fvMYV9l&_Okmm6EF{) zx{&KVEJ8IN8I@C-m&#O@y7ZV^4C!vhaj=Q z$BsG#TEx_^ML5(|40I}AV8n@OZyhS;S z;N0Xy9sh6!)LhgqtSfzh*HI&L`@F9Xo6b;wpX;|=dd7ZG<{qwW|8(<5O1;&lisRu% zL@}4H)qoTn(#*nCv;{SjMg;Q;L1E`G-)}w*1fZ93jE;L3{Y~H&&f=;OfhZomz47Ma zeys7W4ad=Sk9)L1Nz%+9U&Y!^%%)2H|I|Y@^fqh}IHSC?^#d{eVS3 zqCPoz5G72+3F;>H4Vlyesul_0nYGofz+Y?YS;@jfA*M!Sz`LvO++(}(Yh%?k?4?5{ zLTN$lML$f}34!PEC1$+4H$!>p-c282?|4mB5ela!+UbS^Pa4jCJ;=ObQ^Uv=(2dw* zKT@{YqsN7CQxhF29Kz=B_U2?^O{}?p`oI+f(?A+6Vhl0>Op^g7v6!~Nm>f!X6{TLJulF6_CnXx>agael>H(gB1xVKd zcRTzOGkHGKn`_=?F1e8ucL57O=B{TQpKr~B4Z(=VJ7YQBjA+Lw^36)6@5g#M%JfF+ zRzh*J*hH0cP238P_Y%I!8vlgS!4%ZlH%4VWSZUm-UTrRF!Df>*!oI3e4P>1kR>Sh zJE+L(@JkbR*6l>_L3Rdg@ik=do0CJX)jKm1 zqb-{U)9o$f4`=V4*5Jj&1U!4r*gKU;h$`{5k#no_#5)p4T&a&i&^CmFQMp4_&ixI1 zVhUMo^!vi^xB5J z$;h#{9^EWsbVqdEk1@I*OK;YBDN{0#I@``CsT+U@J2B({llewDX)bx?B1NHE^O|_Z z%IMD~BQ{0BG?`?cC67dj%OkXD6bGY2p3-x&k8LhQ>W0(4 zQ&yTUx0^42;(Zx2UtX2{QZQ#~8vGIQ=ot|qIp3Di&xH(oKl4Is@C2d)=>rXvqGX05 z)ZCW~3e2mKKz*6vsR9TPhI6LF@Ex-I5}2IWf25;4&8EKrFzM6$*bXmtXfE~%#UAd* zuJvMHnUwwZ9%A<)zu^sfIU;nG@VUEObQyCeD81z6JbRB2sTfIUq|tK+FCJ(%FUG?K zhXd@+=N>K3NhF;v7Unscs_dWO2qSKhQ77s$&YSBbtJAEvrxUJ(oYU1TAt z!=JGe^?5T_|J~pvXtc_lQAwsk@VA}28B4j)fxtnz*C<~OsYFdg&yx*xsi&z*G=mTB zCQnb+3>mUU3R2Zo@mbsfJPFuC{E5Dll$q#wlOkDiyS}89S?l9*ws|5C1(V*h2BUUWR9WN7s@mFc{zK2goPwEUXNYKXKG z2;E*^WIK11xoQ{l_JGElOQjeHBhDF)Z|M<|-APOwW;R6#C-G7wgU)%K5quTWLj&Q- z?2?8a3hvc{_mnkKA{H-kiwsXHH9GD!^ zjD)R?|Lzqg*C7i{QMY*#g2^H0eju2+t*lwG8Xm2Jh+uwuAcvZ4`zmdT9~6{w33O@@ zShS76;$#K;O^L8~G_sEKP1Py8QIUX*K*wv2FRbWzV3Yzc(Z+LFByP)A^xgNps+fxA zRpnRNs)QdWH&YcAxR0tNuarWLF~nS3=G7+|yd$hG@nAe#Ej3g^Tg!E)%1d;o%9?Ph zvcwE$yB~8vr=FIwgV)m{X}q4k%^UOX7Mv1PR=KX8X-ds?^}A5pd+X{Ci~O$sidOY* zkm!@WHmFxa)JU%(6@Ej+1jCaQOx8nqf;wa>!SrkrC}~lObD9mJxeV=>au4Rk1yGMz z<#(9bVU_>FOc{YkxwryV)==yiT0^ISSrXpd65PUh0_s5$rD`=3o$elT?q57;?9eNN zJlhO|kABRv!G5KR<|_>fuGlpuKwVSWG9L7ookJQU^@ABi;srZYRnw=j%Il(4dL^nk z7uDodhR@plxGjF1L}Qgzdc3Loo)h)oq$kvSqE5QN3#?ItY(6(#Uh1y=Qm}(*MfHR7 zswrYI--&Ruzt~R3S5L!1#s!H`RF8ceew~^LV`@Ky>WT5t)wG-^I=G#1Dz6^(Kyour zUS}WwBZJa&lYiZ_JtqJ26I9b2L{8bZcZi(c@4qnlwM;O=8VH*IPh)`WRO^P&Tfgw8 znl`1mF6)UW$C&!(i%!|`N?($*8ItmA-G_erUOUWH%ijnL*gEu zLu8{gGr3fg{qk%Ll0Mvm`DO#WBZKPANZI@>_z6qV_X4*E>-K~U1Dslq1s4KKm0jxo z@^i1DeY3^v31@Tn$Zc;eoZ@BFv;+~5)Wh&q|1Zp2GqaLIe*R1zAa&Pww%_^#PEqT7 zB3Lz8g7zi{;xw1Dqn_=I*{K&`1si^*IpOwn7pw};vo_WqSYSV~u9;I@o0wBs>vV>; zw-z=W!9%RQXr^u5gdX+IK*L}bET@oZWeuHJ9BVHOITN9bJSNoW=hjrJU}tOI{?%@A z3mv>m;_PnV;};NZjr**oU~|o4PEYZlo`DPX1qT)D=Do1GQ6E=4YcJfnkf*uhC3*oZ zb0e!vGB~q>LVan|o%j?w&)uE*BxKz@uei>cS80`d&47w)=UX$qeYt?~Y&ITM*6zQ) zO6k4nC$qP~-4vYih>IBk>wN5rbzXb35>+%E^OijeXg6YNcY{eNU? z|Lni*&S`k76jax%2r?y_jez{6NNL`u&iA6vJq!j4WP7J;J?M+3@S#sPn*E$dvvfErb}O|Wm~B;WiWF3onpmYrCgl_TbRaQqgBW^ZAE}=F!v6Eo;g;akI0i$InRe~ zt6RpvY0f`jpdLpKZFQ1>C&>6NY39A%!)tZR+!Y*dAaNx-KWM~gm1XZ@B<0-CUfG>V zeL+gqhy2b^?4^U3n3D(IKW3>g4!=@2DN!;gTiL4-@hnJPP4fb7%YAaT z7jQ)`;508_A^|BwE+&H&xzCUAA_pm8i1L$9a7ujdBm(wRz!%I}0$LSNqJV8)fE3Sps z>Z6tV`(D7M3aC=R`CdS^0uE6CcvMvws(^zPKm!%<>1YB>t&j2oURHq4EmQk@0Z%Ak zpaPH#)F;1GK(PWk`61vs1(-}<_W~jcFvXe`8N=!dFwI!wrLYuW`d2Jb^4P{sWHqvI z1*w1Y0-jYskpgr!MT+|ra2U_&8+mi|rX15cs=7jNs#1CeZxi(xUS_|#C1E@yBj26izSD38nls(@B~2Bo;n zNnDcgI)_Ur&&X%O&REsX;JIgRq=zcjcVoVI4Z&oQku`$N&eAcwO1^TzD&Jh0j7+&d z{!2l@W0Hp7(MI6T04+M^oe7b+T#I>Ov#O1)FR$#vX86A zwi91v-hyHJk+){^mNZG6WV_y+_!iyTOo>!Mljd9W0}(2fh+*73{Osk1xW5-s&W-x; zh2N&7sw3JPoQWBE2li7NCwG2L3X6n_oM->*2?Og@0B(Hfrs^CqL zq}Nif3TJs$Py_ucDAt%bwpOkix4n>^foHo({-Yw*FIr77&S*1kGcU=Y9?#_cAtvKq zNl!8IVsmzL^4+}P2t?*~jcHymoY*gs%lp#k!}r5|?tpfgeEZ?<*xyH_Egp8oIxL(v zqEtVi6~FJuvXa?=Pq4~_i^ztXlH+$E!#C%a*cYV@3m1OTNZE7FcDA;-MX|0CHy90cp`7p&WX^>4Hl^Y0v2<#_{i%^IG31UuP*YGTJ9s_p*wx+Ugz@GC>+Yn0be;(#YXz{L%w|Y6m{fyt%4@cVkfj(E+Ooor8?$61L$Y zC*{7)nJqi;19_|fAHxA2%3*>Pe--4qaF^+clW9eJz*^HB>pUFW4YAJtPAc^{@vE*@ z&i|>WIePj%Puc!mhy=0u`T$OD-J2jcO+Q-kOG(6Gq}+;15LnWY(~Ky-TX=qw{6 z;-oS!JQz^VZW64p8LzBQmYpg6p!3R3Kr-(SVol?8y6IZtIi|%^Bc4dsB6=8Gi~dT7 zjgvET0OyUjjiR`-mT;>{3j?B<0c+)<2;_I`2enE+=<6m)sY&u}@_EZ@I)E3DTtGJL zmXV$v8v^%_8}yG>(tWuCYqv-v=uM*$w8g|rO(;J4Ipw(&6LVXPdyv#@#=u&60H7{( z_~zKamY~!SW-=L02sW~@bN3bLzrdDgBY&MOh;1^PrH27RDH)Gr&9f_{Oaj_!Pp zQ^8VI3`wY&HC2H#yfwlw-o-&9XtrAI*x32OFbA!rzqnzQ^q#;&1X~q0TtbaZhl%}`S9eQ*Ui#--HhCMop$%5*~;T) zB{}2>v-ipE>^yc&vLpj>5R!Xxsi3n*>sVapw$`PfASbb01wYYObY3a7e~F~*=sH41 z#a{cmcXj}E$>4AVE3=7SHu?28qp?%gnp%{Gv<@^IYH=4a0CvjtD@Q;v;tk1tjWUb(jA-lUZD+`ka|p=`y|{ zeWp8orP5D%RV`7yp8brMz=`76Ex#EIHk7-=+QNf;8okoFF@>sU{R-JxJ_PHo$T@!|lx>>!I$LB?Y1(teN zpJs_kA(tpQc?wUTr~7=)ky-6Q%QvUpsnGc`Ayr*+w5}uDy zkQMZa=On9Sd8rlu7hWNg6nb&$y+e>iNgNV^->CqEOG0L$t#w{9_PttCg|*JsGHZ;- zEel!WI&1LfvH03dII}UFSsQ{~7IL1P_o88!MY`E#NT0FPdvs1R_zesmc)c6(#=Z_YphRB%EcSI!MCxr{M^@HO^PE9pYUj;h*mnkX zgb|Kt=?IDs3?DGdi-vgIhn=0-KV{thIXgS2Y(m1Wx1F7i)?SULJ^AvlEX?>w%ruM% zCoV2R*(Hb;+b~J%z^6WUq zd_mOIxkQ@-T0_}p`E|YM0|vMaJu%a*VR{}nQSYy!LpW$cYF@T<&$D(*Iik}KZ~S5z zGC%K!8gI0Ac`2g@k?Kk-6MJ(+?47ab3bwp6aKK+hMj`RP=pph7HJ&CbTpS6K1831fCOE%oBAMp*yt5vJ%O^l^7Z@OcqxdAnbTjP2wlyQJok=ohKz zl7;L=tiDvUwQp$QdT9)X7QWpK+=T|bS2y52g%to;&PSBy9`h;#$%i!0e7qHTd4pmn z6-5H*0k8&db>Cb^c7fN8_7tOHH7%x6;bh^N#HTrGTZ_@#4YpOct>^ea(N=G0xCGG# z5jyBVzb_r=EuwLbP)r>$`m$jRT1CH__jS=U2<@ja*6!z6`&YzPtJk96yTK0pRiw=C zu^No!4v{i{P2b%*VHkMUZ}yy{vo-E=D@~0#I`BM$^jPd^IrM_We17th<@7~2m!qzfc*<>-n3RXJF-hh|kXl=bFhxf5D*Oz@pxk$ddV@-y1&_c958K#yd( zB~AFnTMZ|ptsKVs73S_ZNv?7qf-@OONURw{R5kS|r5tUoJo`u-K7B2A0|xKDmiX4- z+Qj%_kpvp^4V5Cje@f%iKQx<%6w3r!@};CB_IOTT&-W8QdSGDx#`M+D)t5Kka8yCV zsPr6COyYf#3@_BAzn-6v;PDD>RNAQBctaUJR~l~^Sr8pFx*1{EiHdCYzo33cQNNMt z(Uf<1UN*pr7eh%UWI5E-(SA$>&jnoR8M?=g@yMNH!;+A(y2F*Quz;5;%0I%u_ybOYGC3q?7ZGkY()69%eB|-R*392AbFgp=8AxIU}LPMuQ90L4WU_Y zXCWHP)RuYY3QS$;r^Z;v);@=d1MnPeF*!0N(u@0K%>TN3&v2 z@@EciN&cv~^dZ9|&y*Z%zHGRGT}*jPM@dW9h@+6WGzrTXOsGvCk}28n7N56t(0T3fLNEl{)=y(dC(S}uk}m0?kH7FeO~))Av?>sTY^#&? zUQ9`{jLH&cXMF+`5;`eJvuNP+g)lc z`Yu(h=d?QWphjZ)av9HL?^Uvqj)r=|jT7S8fNJrW zK}>)XyXjMF<+rTH&j&R`I7V%ll*Vtj=TGhVrPxUs6b*_xrvtY}Yg5nD@+CZS#EQ+_ zK~~c`0*uEt$h6sBL95O2ouLq-13197nsjS(ZXX6c6$2}$$B;4;GuYM=1e)x2Ij~>CN8`>#mrIOU*c;SAQG|#7-@#n@t zMmOFs=*Ih#VDEcm5&M`BFR9zeCbX)QMjJyO+uVzuRF|I!XQbF0i_{G&DFH@DpC9j& zwl$%}@YVj4s$ibPy0wJLMx^0Bj0d}-T_cU!O4ylK969|ejEm~vA&lv~_;4No6Rgfe zmtn4_-DnHEY0gl_d&@*S;B>}OJV$T9!^#%WB7_G80S`x7F`cOy0~S+U36K48Q?9NU z3P#fRBUYVdiW$i`MgrSag_Qgy=i&cucpGkVWBvDP(4yMM7PJ>M#BtfT#d$6E_rAc= zBX*$6gQfKFnM=(09F=04^c{0}R z4Kw9lHolzdr~8v)87X8!V7`g;`C~bLbho|bD*v3!s*rW#z1dy<=|TgN1#a?R0jP2% z<0xN*eV(tpM(RS@S$_Av^JheQE&iu^`FFX;|8L|E4D`zXg+{;rPvhTeU-d7jfvt5O z*JWaFac$yyo9k1q=dqJ=C4k+))x>o&msTvFKV@f4n3}^kZD-hqhgO%;BW^W-1e|vm zf^53$W3-?-EWUK`DsyD-qs|KNELUUhEe7h+)KB=y;>};s0PyBK1a37 zj9%hKqUFKMrmqf!;Od5 zeexq}_6koEZZ_l5>7IezitHxQ_*;a91!o|W8OC)AmuR_#?`<(XApbh|cL3-j3Zy*I z)4$GlZbbI%zvN#BD>YgFI{%3<4J-cSGmw#Jjux4G`le6!-0h6T-42$F#A$WL(|*t; zs9`uF_q%S`)8`KHrhoSBa>qh)Kr&C_m`JWk>zVl^UrsR@tTbogO=Zn~vUcwJlW5d3 z-Qgk3fX!y3{HkY|q`_C&#Pk-Y0?$$m{9x{UtdI60R3$1IFqX@m@87`iAR2V@jeEG< z$x_$7{ef(PvxxumS15pOmAsEUP6(Oc|J)0V%3u5uzfjJ>l|NJ*!Z%U``AUBvz33k? zXtVSW$Fr5Z0x)E$3AfChgy|F3l4Weymh1fQVxVsYCNPWjnX`15xy4I$lWhGt_UdT` z(+b9N+12Qj|55!z(Ae&vUnuB|E`F`p@A<_JcAwLtms^jFE0mtX@a*0yC8vyJ+E>PL zy856;BCg;KGP50xV#w2cc9K4uN&g%%Q|2>@IBCFwM)Yv&kv@7qSmreLPml6ngNhr$ z3yG)siBH5@#3SQKJ*|Q06QhT6ifNkT9X6|-Y~fOiOhfOMSxTBMYS?xjHpVgAj8#$^ z0z??|rXFy_y9`W29sgO!+P)gNv1_UoCtgGr*iIOo2`~K8*fBjOeW0P2tk~z|l?}yy zX>?iWab*#4-b!CC`=wuoD|XdazY|G3piJf-V<(MOPtF+XoeuXGEhJ4mAUmHlrPEZ{ z6zk9L4NsVPZjIYzJ8M+Qh~q{alNC1Twh|DQxr#4j&~(;TWs+2L1z4S&^=3-Weu+jo zZS1BoZF)W_*ew{o4?R+@Rykkl^d~Msi%iKWBrq0E&r?kTub5h({)vTC9KyZox6@jL z)%r}Tl%M+Qzgvq!O5A1!LOqL>2gJ)BR_|YT5v@`&)+05H8g9TIR`sIiQ%?oYj9p(i zEowi;BfFI2uPa5M;eDPnC4YEPs;7Bd*;v=uhQkF^Sia6ysxa1dLc>VK6sOPdgJ;I(7mkgd*!|XTTJ%VH z05%^H%RAa}<_$jC`Y%&s=!sl()wF1tnk%pcK)rN@q}%;ik=M*SN1)t4fND;nn%Kro zjyW9GTBLf%br@g5C^>uc5oae0Pi2T!scRA5@OrqN8x1c=52CyEqCWs?8IKW+L2~oB z{_e@5C}L|Ap^jM2z2O`?v2+CwJuH=5@jF4i7zn(soQyPqMn5NZT;C@MO{_7KE3w8P zki^46AnC)6r;8ow18t16YET>#O(Xsp`5VSlFmsMmMJf)Em(1z!B6!841${c!qpi#d|s7xVE6Go8Us^BKew6V z+6r%?3Usaa2{lAEMLhrx!lzPF^UZA2-FyEemQ$udqizs|gZ@&D#tW=w+E<`B(_rt5 zIea05%^v}bCrF6&z_70GDK=@VyW8c-qS`0dHvIeae&hGv zr))aCIG^8mDu4%{ku7CC>86ys8T=Ko&-VC@%lF_n(h|dOgzxC{8+pp@Z5HJ1J70J( zN5AcKH}YcmjqTn1#&*><$8S8C6x1%qZ~WbVfMK-Sb@Lmkc-=61qLff(99|ldQ@Uti zvHR^01$PVJo7I(iE&1GC$!^(j3|N0%48;livid&*;fPBC`T-*s+GpjnCW{Lih8wmd zM44NnI$IkHf;q0-$=vhJNf8AYF6<`9Rgv)G=^?D38E4cR~2Ije{MSr2*#`3dbS zX4CJ6%)u-)=OzDu56kyp#5!6qbBRg6mPe130QshK-0ioKwZ~n?aZ!8LaZ&BvFa=!S zb5VME#rrc4YYVnFp*diMMk{YvZxxheWnOdI^@OxzSjf0XWT+!#ftVlueX z_f_Qf@Kwa#CN~qNyV+g-Aor>N$jyY8+$VT3);qz~ve%f=Hu;P|&~BN}9H) zY4|rmcp-m#3wBzlbPVbt575ClXlSI?1njxrWg)qIwgK?oKs0HN0V3C}Tht9i-S`K2 zyRXO(z1972lbwaV?$2a{;cYI?>5ZIe7PfTn0Z$9xo2p^Y;k7u8^#Kd^AuQJ?BZ+G{ zs0kHgEHHuqCDGW0p4@O53rI6BVJ9;l>u;4AtRO7K3POl8qfqJ7O#2yPH2nCFV9A8B z$&9+elo=e%tpzR67Y{e`wQo(S_N8MMp_Lzjb%k}0q+OUkJ+!i)v6laS>c^&* zlhEwmZeu?6)EBnRLS;eFA)u+yu6#)W1VU8E~F=A9fKS( zr`#|;J+#Baouj$;e`w2q$c+H}aoD0yPt&2FIrBI)j{3FLZ z-_SH!xL(lSV-{yW*YRo-pJ1M)?_B7sy2&hWu#F7Fi@RI9ILZx!o3u(dO2i3WkH|X#ozn7dO~ZWd)9*)#8qYwjDoh~%P2&DJc>dn@*Y`X^Z9&=QB4!z67nLYz2&a)A9ZX}KAJRou%!yV&$?2R(qc;W2gBTRl_hztYFsXNGN zbqDDUOVh3UceA2fO>c;IFY5sDB}B5AV!*3z2IgXCTg4`SYv}oir&u`Xk*8hQ6OVPd|Vyuk9BMO8D0?C$n|pS_kvJ$vLH0x zNVE^3XUq{uU3&7ba6J>j+)-j_Gm)9UhdoCEDWuvtkspTv{^!D;F9D=yTE*u#y~Hruf82@EbRq+#&Zg(yG7(F#vhIUfDOl8FnJ6QkgByIWVj6jNY?y&j2USf;gUS4fT^5GygL^ zXy{PdE_&<-3v$J&+w%?j@ z$TlX>z`TP))2HD{7(3Ah;K7s)W9iXs1u~mV9ah#B73aPyjQ(5tCw_gAq=>E2R|OKM z@M>UJFI}d9xcsE#s)M{AV3InFuzdL&zs%G9Pb8+C_sQ}41l2*jdEA9; zkYy`Iz$?CtM{T2ZV>fJW-Yn7KsWheN@@wrYnybKUlUX5~7-;O3iTPvJ^Yp<2Q1gh) zOI4&A-f|Q2@C6ePz8NjMarEC9-m&99+A8iNtbn_Q*=Hrr8(kT8E*LS66VlN``HhU0 z*}!u!rZlww@_=+tgg8t7g7&z3G3W!>KgfsjWIcPMk{i&Xa@W5i$_Y%9sGn8qY{Nhw zi1JMxmSYcX+?y;@>(9FMWL#$K=8G_2Fl(K5L_N|+cpL$OD+x~Tm(9CmH668t(z~$F z!l7K_mz*X_?S?^rYDEC^pI&pVIPPay+vAO<*Oo5fHoCdi`2emH9VHnNPzI+~u%TY} za0Px5(-46rQ5>jkD8dW<9)UQjc^^sQ)NeJ2F|&*oGz|2xxOFdBJQ1C0OU4Qn zLS(Gy;R1KZZ`kl;5wnN8Q-jgu-E%eXBgL#emiXLaYW!jtb4#1Z*B}edUz10iEAG;z zg)!NT^ogSm9|@-6s+m>SUx}Twi&Klb{_AB|Z&EJSq6zv|nVi7`RQ6nCs3OiL_fPjQ zLnI#jT>?aU{MrO$VITeh;cn6d<}G~hv6nEP9jEB4O>_@=w!PEqS$;t?-M};#;p;va z;920Y{JQL%)1gOsPm1Cz^Q&iBn^NO`pPcq2+w=CTd*@#?uF=v}&~N~dxQquXrkU#w z+iP|+m#qGjJpk9|TzYoYF~`<@VV*zWdXg){wLjr{euwJ?uJ7=E z6W5(wdcK_NCtNRZ-L?bUnvB*cKu*v5b3cY_EZ4nUKVeb2pG(gRxc`nzx^a(leatl& zP}lR{xcc*a4|!cr+TU{NIjK9>V_c_yiQXO8xm@G9^nN$jZ@D)6&yR7x`3vtnP6RT% ziY@-hZcFOPWj*Xi<;ioJ#fF0eBw+U;&uOnd>nCMl{`<&tV)Zj2j56C-yjay!q|+eH ze^R9L?kgFNe@>+HyHtD+krVzfbUC1VPx7VPnUZCyowx2dqJP-kYT} ztoe50j+sSqdRi{f2ex!$KX&F%`XdMhuqv0c)1C_>Bkfdj3u{4y;mqi7vg6JqJahs{8#AT;0o>unV znMV4j59>`?)#wa{N^J|8U5X8%CyHrL#DAT-x|BB}LH`jc+pDPLDRN!-V4p^m{xOo* z!m4h?OPEOLE4ULOCDA_W7yahwjfU^lEx&@*n(yFQ#)^yf8@*8$z5g8B6>NBf@~!wM zDwYiTk-_MV2x%=#oj{&>>$kP?VAIcG)Q%!jI|?H>FXO=z^ng53shz-h*&Kfh^s!_r zL{c8}RndLYMP`R*^z)jnm4C6Zei_&lu6}-=r3wcg45SWVUW=a~Fc333roB17laAxv zTEsa6{J%TO8fk!fi6#1}#?M*8UuHh0k2IYzL0R=pZ6--L_FlkR^d(Pb%pU58iAR~p z-DHhxmzB&|%Wljm^s#RJjq;<&a`*OshdjN5S4JPEZ!rb3qIl}};RH@MfH2sC-%bC_ z%lx5S=Ka(6C;@U*^bNjDd)eYJHdWOq|FsZ2ARZN_YLd^`cnWncEf z)3TbL^|`PE5=vW(Z~U9Th-_!n6>w@cV3!Ga10C9QySE3-Z^>oV}8X3&pAW$fp>?B)83&sgY=mT#;ahJ~khzN=_NDL_vHE&!nj{ zsh$=>oRntid9!}{b ze0y|K?kr@SFMAD`_*OdLTkj!{l`qS(>8@PweB`d-g)JFk!N@P6O0&f0-vfZ++6n;o zY~@|k{8s*$)!Wc<-S$Uhxdb%D9*YS4l1xVl^{4%|3R@F!G(EoE#KOz}jdDrO8^*!qF?PjHy8>P1x4PF;(PszQ8O+bj#gE2_ z%s@{s#HMz=;#UOckZPKJnltpUAID3lYaVfXnD_4%%yyq3yXs?0~x_pwSF z;Z*U%E+mBcAf5VNN3^yC+u8Op1v%cJg3qR z{}u$PWM~Gl0HnW#cqjSzM=jyR#L~ta`(?1a-9fu0zjvQeG;Gn*CQ;Dxlwiql$xI>F z{aS!xqKlRMx?J*x>kY_e0K1KHNf}N|;qkeKX}KA5F5H-{NC87C8Lfp5{}j>9og(s- zJ3Cp>kv%)H+$BqMGiRq8<0X{(A&zaU$;FYgq-Vt79X$67xB5;?8OLfa8{8z9FW78 zUQOfn0An2M<>rb-vG0fatQOiKIpmKYP(X4>)jPUneQaNDrVTUcU)l3gfY$d3w^P*$ zzF5N<4^fYAW0D=k5V#S=$Y z;Tr6sIINYuww2xRx$pehnZ8DT1|jv>ljhS8LTu2@Wvc7K&l~xjYb`#ntjyzr7)voZ zQ^JR3O$nFfto)Ajm?!D)`L=}1v$llG@~r}o%%wj%WRQ6Jl&sBCidM?3G2xN=<8(73s4wH_Xg2je^{eJx_Kq5R~F66`ut?b8e|sah{X{uqkPj0^sbN{{|x=vQv`& zmF~O;JTha}x~CvLQeDVyX8Xh-`ow_$&${GPTHtkw=?|70ApEPlWPC@DF1hUfT$g0K z!rU|A(uTk~yDcOdlEtP4&q5=A{HL4^R^(fezid=o?$|I(_aU6jx>qHOdx;@`I4@9u zT+x9erz&k?@FqLy6zP%WyBLI*fzQa~DsU#2$|3JL#hH>-6q5+f@je{ve<-EQM)RRF zaah|#ZB&?KSljXp0d0ls+(J+R&RUJH)?$t?DmW?d*~|dLLR7vWT(>agm$_H|K`gW> zTiBbS>-ZctG0sx7Rrhgv3jX@Ql-;xi){g!O3>nQ~$o<@_m*gPC6Bf z)OmfHDH+55Myt}o^^H{Cm_SYr*f(;q35P*LoivaXR{#? z@t**esvs(0$Ik^r&{4e6~qh1CiySwCG z&IgnOOInXVZ~wsri(Oypoe`9K+m62^fcO|bVh&+)O)84+?{h?9FR`IW{U_08J{v8w z?kvrGp0D}L@wGpnnIH8YrvW7FG9f#x;s8E@Sd70Z$R< zJjrSC=P@dAEh78TP%~7KIc~4%|CKwpV(OT!wcFRP@O);-9{h^ze4OpkRK2LKt&=Ww-vvh1q+AY z%Z!WeaxF|#D(N5kNlLH5X(UVxxrzSV*J4?g2!5YLndWFc1nKhz8wE>J*(Kt!A8Cmo zW}p(4G++%d5j?@mZ!c?TFpcdhb7t%Wi&aVd9uq?KXE2yExFbJNH<|XU*2UDi)GAkv zrxZ}@PHN4FmRzhBC!>>DQadEic91!knld@Js-E%b+oZ7SK4(>Bj}Y`%h!V05CTyte+qyt3sF9`=?Q%uafti;bZUPCK~KTt1YZUt3*~ z2~U<3E2%hQ7u=#T$m5roQw1{4uFsR*Pp`;ZFZYDkp6hEJJ<$th%OVD5Yuv+NKA4GW z7W#dx&okWG3-XiY?YF)4sAqZglidT6706}VJ^j&Vg50~yv_B{PT5MMJWblMHO;+&^f=XPY~aVN*F~%|zbP=;qgv zEEt|=24^`zMPv$A2_8gkd0fTg6(aRPK~%!^$7_ojFVP6uF-(?Rreq?N{=hzMkNujD zFTCNB(s>YWwYsCc_xd||H6kGePgi95DtYHex2l)+a(*=62|*}45y|v~ci>^x9V=vr zc^AB+Xe|_}Q7Mg_Dp9vc#OjeJYRsl4BCM|y&)K3A{<*`VQ*PPIVNu5-H9c=|dGzD~ zF(@ZN>B=6A+dvFA^EJ2>yd$RrgyrRzUcLMrMGo8#`fH+<3^Hq zkNILwx%^6(dVD(QuHp-Q7|zSWaHGSV5$G5Y3Y4RC^>J!Fb=T6_OCGYE(d|b3@nwLu z0d_e8JY@0RzvA6M(O-c{{A;?HLhK{;i78uh{FR$M$~3-MX^nvP(k156lhKZQFF8ba zx6);C-5_@q_43!am)sG&SSwAb$PXAXvv@}#gIHX*1p%imJSOT=PFxDviMsck_zgTP zzIz70R=J5LRSj=8Li{AQo%m$lY8F2{m0QC5E~$^4_=WmSPUF{@a>D?3(WFQg+@H6Qr^dY;mn$dgjQFhH-1HjGNv<5>O?##U=W$zs2!(1tgQEVoq|o zc^eWlWh7VdhCt!c_}!`r8eQD<*Bbw&p6sNbYuq)TnRuGj>AjhcoOqY_rZ0#qKqiLo zOgT=xh__5h@IDizzIWT^nrAaE$?01{tJJRmCx<%M-DIBJc9OWy-f1vZ!~TtHOkJ$F z+|8ywqet*fnkjkkULF#s{Jahnh@h&orplCm_iS#)la8&JWXU|Z?3t3Ilv=_z7mUZ}_>yIL@f$W+(4%Bx^*tG%AUkH=v=ny&cY0IJ|ef}fEeG9!T|jj!zi6(DASs+<3jIOC&`CS(TBS&1PVc;@sw zQ*!SzzR~paq(0i1ejAl88Qi344%`8Sm*qj>KLvgDK;Mf%pB4Wzd(=H4ko8UwIQJR~ z?*V} z{i*S{ucq@V@@-r%8S1ani9lmj0egbPg>cYMzs$Pzc3`hH{$t{~{flmP;##&{7ee?a zJB#&teJM+kQV_C2A*eZ++rwSnty#CE`Iu1GD?>5`J&G2rMy<3fUtMqQA-mTcX6?iwp@FJiLCe z>K;gAy_~6H)I+wW4T=;qB71VQQkY#EWWn}I1VzIASI(sdtV&DJU5uqo9$COZJ;(D!L zzC*aFA>(tAMCrLb4a=+6PFA=7blufWHMbGI52 zah^;cmq-7GjGIOBT?k8s3R~#|c(I+={Pu^8^F{nnCyw`?=C#pmv+fMFz%sSoU~ zcU<+CXTK)(8pN&j$de-_{JroEBLd#iaVXY|itPa{w)(-t*)lNwWJ*v69HhXKN%=Jn`elhThfBkqY;)dQ2HpVLKwAXCwK+%nUbrBKpHIe zVSxS8>pkFAfGQCn4{*gkVmUYVXG1>D{f?pA?k=L+YQGYGQA7BD0l$%CnStMM;5`q1 zM{h^xY$oynhvd%pvhX|1A5ZC%H+@WRhX7zCt#bhELofCKw&VlwIh={!#js`J*LtMg z3}-j|4hgLh{9>-_did8qyiN?Q0aO%tnE~kB`bqf>V<>yLDLW5Z+2i@ZIz0!h=_5mH z7z48Lp>t@uWc2oW=kML?Nob8INKCOsD$>?1hX{<=HvaTq1fL()2R=mAnhvqzp8{*C z%s&mDMod1ua5oa&7-l$7^WEU5VqgL6QOXTqs^q3W2os(}LfVNl-oze@$b(}b4}PoX zWXTh*fOH}KXn^!ova=14t|UHF(oRHMyvENgk~EK~n=4bnWn!strw^2}B3+A8wBp*< zvaW0CE$59PujMy(<2UK0y@@tSipL-S8J=eWG)ya+#m=9HV} zJanhuoP`D{XwGGb&y?&WqPsb)hF&|wbYOuuZDQVWl4Y7^W*1vW!evjvqjnssyVpi8 z$#0)&nrUQu`e<`BhV9&>huR&l6P<@`Q{df`xj7F{<+s*}Pcva_W|&)YmbopRty|Jt zvy)3zPxl;JmcBsT!96oxlb`XL9vMIU4>R5_I7MmEl>CwV!1uHb@D}v=iCyR&tuZtP zNkkY#uYtG|^#C=}a4B%7%IS~b0+U7DR3_oA{St`Fy8ZDMXiY^nJEi6uZw%O9qlTN* zGU(1i>vJ*M&t5K>8VH(rC&d{txlBRhnFTmGhMgvIsRvmH8F}M7kdZ>_5*xW@4*OU5 z$3U&uw&ai-w-J>Lmi#?SZC*s0^bZIc*Zt*A_Y{SGtNXJ$@3YsG<~TBR%ai~mxFN@R zG3#?N0~Irjn4w`k7cDo5WYH6rfTghxO2G(hFeuYd5p#=$MR zZExS_)1QXb7`v6u<#YfKP zYI=a>|90<;jJ&=Wz%y&Do9{P%G4*t>@sz2jUl`w+db*>mlRD|?wz3ZYsi{mrH`BTq zZ;#%C=Q1whw1%C(hMi|3cu|jSA_~4PDoy?ClXZwbMw}0!whE!@KMyA_0h&MSx&9-z zcPyF?R_u>_XuZM5DpPgb=wJZoH#)F;1Z)mMLN-Ljh2 z)MIz>)^oH&H+8Tq5WcbLg`xr^%&1h)1r8-Q+m7*76p(E6aRYG>>?k^q^%-&cw zgIW3@fMzG|FB82KK@*}Tf{eKxNl119Nqp3-cA*zMTU2Gj*F8v{JkZW;j#O{9VmGLS zjNh3u<_7qx_K1Sh3P(hoPa0me`!X#efsfLCv1d8@{c!T+ z!K9vZZ_2RMoL3mZZv0vOH!HSQO~^cHSO3L|T}hVcV+@LDP-pYvp-mQI*&uv|T1;Cb<8aGZIQ)}Z>zXTv7wa}{c&TpZ4X>OTvKpF+hM&mqjlp7m z8-gbGvY?6GIMaHiovOXqQU`QgGqcWWaP`$reqPz7h&J{+&CzQWvsE!4DdrQ!w3?U( z#ca{f8~nWTCO1EDV&*fNceJF7yT?BqXogJF@y)@J>b2Jm9@jV~(7a$!>h~g%xAK5XR z3j@|l7mqyihd3XGwzmYLvs1Sa<9rx<7lqB04S@h&oKT$Tm~#@Z(*k|)=M;Nu?0`?k zbj}$~NNu2R?A`t?@9y97RzG=y+B%@kWK)qEOExX<_8YLnydRR1&~3}Rg9p5>cdMzC zS3?gkWK%noxhcb}tLWi^g_%Zvtwkr%c4LN&6h#X78Uaepaklmxc5JKXy=U|*09ZKi zDqS9Gd?NrNntNSc)5hqvz}^V9V8eUt$MLawjjSpeCf30NZp3Vx12!`w2! zq8Jla#pK-ZSQ`6wJ`jxF=w1lJ1`A{9r&y`%<8WF4(AFN*sgfcm2>SL?fEB;Z2dC80 zJaprmG2k0eH*Sf?{ZNk}^*PD1VW#sUh$ssaolX1fOuL>d>jQto zRG|#vKIu);wV%L;yOkt)v6 zpF-^}34oEpix04%SN4`P;y) zcJhi0ISjL%wD6JV(>rwzgB^M#kRHsBAf}-&Rc1D39iv+1-m+IKPrO$@YY#~2Q9ok4c5ojb~3nl+Aw45#$a$Ts-dFSQ&U8(B!l-nqP1t6`?Hx; ze-Qfb>AsP`OPC%BTN5`D|1;v#r}MC!2WjZf_7b9JVFGqC3_D&YezEuJPR_>v*!w_g zuVYLYZ0T1XRVHi-a9rk|&qC*&g~F0KEGL1H7g)>ybf2PnwQ8;V68>cuF*HMBL53vVLd&XW>lhh1yku~5yJ9)w zXFmi2V8>jyj2oF5$&F^-+Zia{At^C6>^@x%E(N*mO|Z!I1&cZ*R$>H;%#+bWLiR6b z6lg#9k(s$e*N@!a^CDv*YJ`z3>VYqM$n*Jho&@%KvDl->Qm;h4>t1=9-#5M>f$zUS zBu8Ekc@h=!d-05sq2<6jaRGcsiAn^+=aZL_dZgP>??T&)ZFBHBI*3>LA=p2!+YBz; zOSggPDRIg&2rKu_WA@!^;2zpyoEiANMqP5J)zkBisuzAQ*K04i4ZKM3*IMbxJvAM| zIB9<}kpc}{`9G)to?8B(YU|)dppX>@v*O!SJs1o|B&=5*Quh!!y7`viIp9W)N1E0s zaWc59P2c35vUQ(W2PG}1eO4Cad}))rkM>%AQ?_KRIP9a;^a(KEchQP{!B?+ZGK2Da zDw^x5{oD&K%k|Vp?peGDz61j7&W-z??)BP-uU`HXlyff#J3x>x`(p#Eva?T_{nOw7 zMKGZ6H+|#3_5JPH?|<~)`~F+m@6YP_{lfPQ_DEo(j9ncFuiTC`<5FzMI7uk8A%vV3 zZJiwQ#;w8|X5Hn4SWlCEk3Bl8n@aZzNWMSl3GdMDQeDu-q@PWfpTXY^&2IjfE}P zSr#Z*Jbed9Y=;5YWbl^1sVz+9Wbmj}s$aK5TOf9YzRMvBH)Vz9?kh*>)7D0u zKw~pJ5qyly{c>Sp;=hRZOv&G3%C!@$J^Lb{!rgFHLBYuZCPYd!q`Q(=>}qUj4StK{ zfplAr|Lnt$8eNKD&1}gt--U3t@O!DU>alw@`WS2&LdeoLW7{)1WXx7|Ww2y@mJxK= z3@uf5;+w4KtX>)>_62g+0LfJ9>O3c3CW&&r{776W@v#bhWRk+F>5lDP3%eFWIn8BT~QBon|UQYN3`6UKo2R&F%5U71=M2HD6@+J6``%Y1)4| z6(`FJU-7yZCD=XWkvvJ3`^y?tP+MGkQMHlvd`$<>hREx*t(M?_ehn}H^qlvVay4o6 zce6AnrP_XLe<8AD@crkxIiBsb=-KJTJ-5jZu{_-F_{;Zl|1)EF)A3x?c;K8=rmRTI-J&BIH7yAe zI>{ntD~a^HCWAQGfm+^{ZhA9WqCC>)$=WQ67}d`OjJ9K5@AW|WKp)}lS)m^8@`>bb#(Gq z*BB15*OFW-JhkY-9w_eTj=DH+sTjdaPTmkkBUTGWQ5z0ty&436*`m7bdjN$GN`()u zYBZQg_Jr-&8M@J%6HBf5T%KCv*U%tzh(5|}Avcq^vs6%#2)<3y9vzFaogG_MLq`Ox zTYk;gNRwTn{_qXII&s<7rJW|5tfkzE!+I35y8Y0ue?R%k-#Hv8Tk*I^g5(6&ILJ=g z<3R=|0%Yrd=DvNlL3>oMo$Oah2c5?K{c&!bpJ5T@e5vy^3=|{J>~z@Q_;#gz==4C= zsap;Z8_+YNq+V@}C~4d@!=U2$3JTF*Yy4y?0jyN&#ve1543_HEt=z8B-`=o49LW8m z3}I4ra~*+m!ejwdH%unBD2ivmq%w6c+JgS)m>#{+qsuvk+B%$3N(5grx0>>7+T>Do zDMOST@>`-ZCCen8hCNK{))%Q+T@%cz%{+MN3*2x16)wduDU-aWD=*2T-@1VzV^fzL zvWR2;oCStl86|}edW{UN+K;^4!HA+jCage!(uzNBz-G$woUG#{U)Ir^k}^iS)B|r{ z*3o;(QI8KRT~^j{Key(hJXk-87o#g!vyv@J@ctvPe0>iRFs4~QGtGKWgOX>koOSdG zD!Z3yi;>FQeD%A7X=`wh5YC-wjb?+{0WCj*j)F{}qk6;X&9{w6&Utcdr-4&Zuyzue z-B>G>Nhd@=DohPq<9k-@0RiTJy<)o$>rpX_isj}%6EC#{y%G8+wU~Qu$cDIoFl+81 zD_L{PsiXTlh@aHG4Av?mE|DDa&|?JrhK~?k*5BqbH#Q=JkJ#O=;KaR2q$y$ilh+`NYnFJJl7j_xj@WDo$wMwg8xO&6m=5CF{ z+3&-uSiSQehN_ez?7+eJM3&z#{2{GL_)YvgY)V&W=g`u^K@+;rWC8! zS#czzumFD&p;&;A*6d;ZeXHLJYtgABOpW8GzPfep1Uni0X{)d_LKBG9G`mF?_$OXt ze{TkA2C?2i?LJ>5O{Qcs3_cE+?o$_^lkxjTppv?Jh^*IEYx$w-f3GKh!N%U> zb>v$E?!AB8NB*4O(zk9^ZTIYr^-}cy2ldm~?6DOr%B$a(761MEHLejIP|y%`X9o9f zpsWV0uvh)|uk62BhjjPfwEscIpoY0DYAzNjFI4--h7?B;_r zW$?ThWoEZ&OXCjv4i*MHie>F*vd)5=QM_4!;>|LIL9X~Uso8dLoVv`t_-b)&*vokC z9y^Rt^$9F)b)C|ut`KjfAK8=t1o3%wFE;PyBb_GKbtvOJGeR$)OP);kQpg4}rV>hn z$)RM)!K_f-oVC1fCQKF{>>i&U`Z!4(xts#Gz)ZV+@JrZGm1m^_q>&ZOOW$M1x;K5s z-aYGjEw1bId$!BGC$SxEj@k2j7B!5ls$yl!ukwmJS=ZplAcRd9LKCzukX6_ML3lgJh8sFvM=$Xcv#W$86 zZ)79WKk!a(<{q21Au-D7B7ND|!Hw*?kI(<&1S8d${zkVI36nN!MdJAU4^PPd@I)gn zX=AJ5sn9AoROw+hL28;Bek|qB^QZIjd zRdy@pz30nYqxrWSMA*%zHm*IyfACyiV5rwnzWFqHn_2GJT7W8Y9$D~%aURDfD5!|| zL|{()B;OOpMH1xzT>xAM;aRv(>gUWls5tF)Y;PAzP9l2r$D;@zp9C$?a>ZXy5_A}$ z(4RVH&XJ(y=@L&K$cnwi2R-S}HOdc>#2o#J2tu2oKMk#gyh*7mH#zTHiwsq)|tp)phEOv!xeAm*rmqK8~rreuiP*3tp_ir>!l1*VFz8<#yaLlh;kFIr%^B@yGvn_!1gTp( z8mZom1$t8a=pbw5Hpv+TIWJC6cT%x8Mp!Fv3dFQX?0WsW&$M*(8{meV4JjGxZRr>sBDL3} z)-87s6!Q9^)=9Ob1}@yFjtF@@5n5=v0{FPR*zl+8oDV|u_oaO0-W}F#@H=+$1kERZ zHhOZDnu@2%QFrWm`TyaD&B%l6#nK=05fTvc#f6vWR?ffEM`_P@m`BREmm!SF;5#=J zWl2emKQT4ADRU%Vz0XQYm!q~lYJRot?67ydXt#Vg)=*`iyVt2;Jh%ABS?%p>iZBiHC~VO| z?(q`^Z*u#tq5LK_Y)SoK9v^GJpwI!4z*;RjUjm&Th3uEUZwgov{d=Q;AD~|tnKEvB zp@7Ts$y3NhVSrc=%` zI?)}RYp-ZspQshN3Nip)_2D{8c~g(zWW+ zkeOEQKr*)_{Hec2?8!%i0t*&Xxo`VfoyLTbaL*ZM5CC67I8@WAAobW~6(GL_Hadz3@MGla-&g%!5aLx?yA-2@7*kBke%-Qltlc?>- zcRFvjYTNACm=maA?FsCL&O4zftSa*5RO(rK!mzNEur{co?$`V(65+zElV)j+WF=?J z$E6mMQMjaF;xb1O3csvuvc&o9^hsf7xMEA|oiSzJT9TkoFzu+_z=0=_^aN(ojVI7Q z6sJ6@Kwyp%KKms3L2ps#1w;zn&w`Nm}X~0j_t1D;2t%+k{TjdN>g2Rx;od{N!d@) zYrCjtKXoLXt?oA+Ngt}KcYA}~=Dv1ju11~Gucz1<*Y9sI&vANTP-{%@U=*px659g~ zJYy!|WK9OMp;l~v7Ci|-#ylpxpu6L#SsZU!2{CqZ`sH-+KIy9f&jX& znR16S1d-vzxQSBW47#xs3F6_yq{t4=H_%X}gFV1z^5VwwTH0q2jD*b+^s&1Ahah&v&sycPfsZ&+gCwf#W6&_&ifUX)FPPZl0bz_&ybgv{vqy*vP%IV33ULJ=Hy?>IZ zRn`$EQ^{JdifXHzjQ4P`#|;2U%M*#!MB>;WwgX$CE`puea_XsvP@F|5L^q7EP04#5 z8c`Xzq1EuH?XYu6aWJKv*YXn;IOH6*V35Ym=DqVmN z(9LKpe%kJY+9u9V^H;|f`93Q$6nt&*uS5Iz8Lc1Ob*Cx$8;fB9YW~Z>>Dj#qI|=T_ z;QNkH?mJ;=cN&4I1%$4}sI_ERy|5~#GRB6{t(fqK%@65*1%kwtB5>Sg^t3n@H*n8q zG!8e|<1=RO)kam3e_@H)U(9k0+PbhDT$d@io9Zq=S+VzJh1K0`bNfQdg9^XUx?^d_ zQ8wbPU|7Ex1YtiJbNWuXZ|cTbXgx$kS>^daPff6PuZp$!T6n!7k4daRanyq2j2H~C zk8A%53F*S+=(QFfw|9k16Ym67VT3&ug@pVG`efKJ(SSB9*u3vh%M__U&@MX4y$A+( z=-_qKQLFrIi5&=p`kI41;*C=NGZJj7jkKDou3b61&EB8W4A=gVVzxs=4&Uwc zjGf+*=ph#29tMCo#P&z|SKpVGcAh0m`%^3wEsNO=tu&t;5{Hs|--KlH0*6BUC5uIw zr_i^s&#NhoY z8o9v{)3AIud*=}qjKwa-Y-lAam|*tK$wKEU$bxrS78}aYy$Z50l=Tj-=i~rJA6+zT zI-j6RKTzKtv@<937Jx3_#&Cv*d(c1`oxhdI0_dGGHOOXk&oKOo(dR)8aN|9abxz(- zl`e{2r$9?m;KNorm4St=G>4BKjXnvGI9CWa^lZ=pN~s0=ID>%b%}@px`D+s>^9vCc zSv*EHh9KoMMEc&L&PDuE7Yia=d=H3&Jl?h#3XZcF)I0$gWV0>N1I)A0lkE80-I(4z z;k*pTDPnpw%%pjNea9-b06NH`ldzM8{S)kD4Ta%YXpDWP&B^=O0A#{B9FJsiM1v&v z;1uZ=11(g+002dV`vo)TDs@l`j&4G20{`aV>qS(@|3D5*-m5#1!a|JK7qc-ADJ;Yo z_XU!`!XBQ4cR$M46<8pWK@cL_Ezi=4p~ORg61AWTw8T;uy^Tt}cU}T{s^A%8pJVY2{`J#Bfb!$cbi1$<+Ne0U3BFQq9Q- z8)aD)BWN@7!dVC+?_hY|wIrW0YzV?Y$Bg>IjswK!Vz~YnNOQae9rzBwGk6bUuB)pU zH@5fAmx2FT1htAKaZcCUeSW8*S|fAcbue7$Dgi`BY`b-gb7q~<3s2T8kdrE4D|B%B zHAptKH&c~pb8xfdusX_Nb(DiTiUUMmKE^B&zw)aPUBMy~%s2)!t}I6eRYu3bKu3+c z4vXZlbPUW4#AbgTFl_>cV~x}?A&!LAA@>{Sg0JveZVg5(NSYh;p;KiGJm8pF?v|Rg+u(nSuK<*o> z7h#nsC6C^LIYE&;Ej#TrO~d-Q0qLJe?@b>IT=#XLKQT~ipTu%E-$&4;o04BOYUx&;N7=B8 z^OG>D;ovt5))mtXmO$geg#1nGG0Nji_d#m&;j~_CVh!Tkrrq81%+iNo0x|gg?A~r^ z7h}3#B!>R(L89F_XpMg}tWXj_liSgd3qhJi*SZ8Y62(TsU6i)P$&MKpE7>k~Ur zTGkthjl#n{iO2ZEt!#x6z+{m+>I9TyuV9;)FnG)(VuWt2JqI_-IgFT`1DX+AaSr2) zEpsTLIPDy|SIl94WpiY3H&_FA51_+z5n!&7h#&)$(eTV0lz@_3iWkd)#3CNO0SXbB zD5Mpqjl!)Wiag4qWB^(XK;XstWfFP$Rhi?^DUZmKVdF}ugeez{{Be9FDHsCJaEKuW zJOjtjpxHr@kQuc49SBSmjFc$orPM*u!b-42mUXV@_~$}&I*=4;ujq#cuMNWhbsFzn zFzF~fuLGNBQB?D zP^dt{VdQNQh6r$^{286UjnjN=h|e>E4|qU*PzVYM6e7L8aTyf)9aHkZ4A-*F26L}x zjC?Qi6{Cge+isx`6LZ5{`WB~9W*W<=?4g~a94W4JC zEfFU0-5a!@(fdobDM&N~Sfp0pY z^Jo_>G@`TU{LpTgDY^^%4>cwNcj6GWCzR%Fw(D25A*}{+EgC{$nTBv!p-DyYLs{rw z*?f3pf*HqP#sMag?k?Dm1^Y_O#4%A~TH^j@4{fHv87*3sjmoEng&T>-@docyV593T zgc?LD#zH`F5l{dO@~tZ?pT;BI9e+eKqy1jL4BR_|v?B2P34YxW^mOMwNFTrL{uIyG z!4vb5ffGuouUX9R0zb?Y(LN*()Dj!Mu?6FYETl4H65L~GyB+mKwolOC4?LCed8XGk zETBCdNzB~cm5asCc>Eg?X7B0-(K2ey#VWRDEInZh;M1RdLX@u`%++G$j{{$;r9- zIU|!V)DN%d5`aFbW-|z2qh6tDpcot|D7i`L>W-qLd5ZOcDx#v^t1&}3nlnW)Z+t_k z0jPWhW|qp4uPy9luL3hwQgnXfd%Z( z0epDQ0|B1TiG6@=LKv&@KRS||_LD}3IHg!BI)ZP3OAQt@ocR#rjRu)R4U(Ftk#GKD z&ILc1j3!QMk*)YfjT8H=i*(yLzh>U0g6IjVFF<-Ipf6zd{bMs$V!qmTkS5qWx+{IJ zc#mU(y^bc0J*SSY&N4jHLQ}<OD2tuu%hWMZOQg1!8A8Hj(oV&E-@OTTfFU6vxnZEhP#{ z2fYA0s8nd(n#m7j4e}^~$6TrJeY5Y&u8+YOcu%b}w7sV!^D?qANHYHlLqP0Ofh1^! z69_dJ5F*_qMS{|A2Uy1XZN&rif1_!3Sz@Z&Ko(u8feNxvCg0wqi+tUbryRCbAaqf_ zM6|62oUQ6>(6c8b0QvNMdT&3xizOzs9<<7g9|7BbHF$CQJo z!cIa!YjZIE6BDw~&bP}6M4}Or#TXIK{<`ccFiY_RbV~%eH>70N7fi*{i!6#tQ zxF;~uk56Q)&>03Qs_I^Za65a$E$I#SH5m`~g(Lg5{u$fV)h}ZgvW!vp{pJ+-pK_$$ zk8HnuE{%3bX5GC+Yjq8j8U}LgH+vrYg3^`u)dGsBvm!|MC~8e1AFA+y+LsJ)%Ne+s zd*N_!KTcJK)n%pLTpT)lHxj|DcR~>cxpfx*MX_ddpd#sT5v*WNd|<{DxY>UpG^`Gq z7ylMg|Ah&b+}-X)rq10c0yLzVAjLS>Wq$r<*D$I5>Rm=paE#&Ry&a$EjGKQODYe_- z^D}%pI*#d#`tObUlcwPAoV~7n0OO1{d-oWNFhl>lLfF={Unm#Rp;)>Z|AQhJ3U_zo ztX`dr7gl>$>@BtUrL+z+TS*2RQjm5mE^XU}Cs^)a5)ABsnWpM#5-cA@g2?T|M@Vau z_v8%MJnzYAZoN5pz_r>O+|2;RI_+r(!66u)85iN*d&v7^#F`3NE8Oh+0QXYl)Qkhe zK4#ok_X3)mMdVy?2eHmZ#vib#+6ZsQA=u9tu4Bu%BoqMtKv+n-ikDA+3VeX$a^ZXz%c)P=il2ta33k~ePBk1l>V0Qd-XSm zyAPyw zi7BAP`pvPhGdo_`_UYR7Cg0BmzK>zL2c;usw${Nct%XeZ@;yHtU40?+s!-TD;n~bg z`97Z)afVF!W}C1C%j7F1d$lT!u;=?6R%@TaYVDu-5wGR9{zN3Rl42mK-9 zRKD-EZao?Hy^-&Ol`Xa>+9$b2Vs6H@#+(~;HZ1VWxnS2dtGd2MgnaK$6^6EUj23vB zY-sxp{5aL826+|vxBlcoaSh(zvmu2Td<^h8ZfrCF>KRD=5a>t8HZtD7U{fMDb4ReQ z^AN$XaT6etWV7@G^gzKCV)HXP%_5KtVn%9FBeBclptyAnN7ZC}Miqz2)nI(b0;=SH z;TTB_PGA`@5VDDVWVZ2|Jj$|b6)VCM@htmZ@W5qVZHY*vIZ4B@E3!0^m88z&&`!GO{A zHRbPVsk@8yMuC3E1ZD?C6|PsQ99Yzan(w!tlUeHQ-hrl>iOAS{>zD}Gx2zyD--A}; zdCgO>sU~q2H>z4Cpg5-~>k2XvwQpg;tp0Jb`pl}Ldp02@7F4mSnl&I|R{x0E{mrT} zS*syKsm#EWFy8H=!|e!PXaaA!#*oSsBQmtStnjdO?4APS^p5WHsQI2l&38C!zE?Nm z=vrT}Tb~x}R=cYpCSWWtfkl?Z_Zx_qYhz8mda!pPljES!VURG{h=bV_nDjs*CPhH% zWHxfO#{}Q3NwYHeCO+HeOJX&oK))c2z?g|-9(Eg4*rgV z^8ir(jz|ISjl*Yw+sZ!mg0s-hOT-iw1C|+ESdtkQ^d~MX{D8(q#wd-%Jea!yO7htu z)`0@lfv?l-U5~YPL>dx4=A@w~82`t4$cFcYL=RGd;VUV!HsJ|@zCXs%47CIcL1p04 z-)CPKWvLY(2`M6a0v-|lDG|K@A<*c?gEh%Vk#~+jz`z--6o;8K7_WO~d%-)}L_ z!ivXyIAEHqMwJ-6cj8XS>p={{D^(XE@o!^;!Og`9SYzWkrFKs+`?QJqFrUCiQ>`yk z{SM&!Oqf<60N}i#)C4pWpg`bRsP``)?YnrifB87y#W36r+XRU1@qf9EY6C!6N=9ss zhuK){wy`jlMl5+aKHzA^+YpKd&D7iJ`*M88S=E58-|Q{m?W47 zdT3NLrvsC|p}& z3!w*mFjWN#h#gX3W$l9nYJ2<%Fezer{XgmStM-_SZ-P9UgE90Kh?s1dtto-YQP`#} z_Z3U~1Bo6K)zEf}4l!Tit@HXs;;KnZI7Tj5H*sh#vMKE1x)Pw!&0=HQNi-vumV-P&Qfv5~kDb}IP zaGgV(9L(+DeT0Pc;J)ri($E;%4$`alRGh&}%WA-DbzrB!copaE4A(tEpWC$_;_tKz zXuQZh?Ea8s`BcK#+`N4wFut}Slqe2FBvG8#Z$WYP{FAB)rg^8ZnZe~eEwG@a^n|_X z!IL1bZY}0iQQ6R; z;NwhwTGA!JJdF-WQXm1qDOCU*e=Zq13nHM*=^{;nK^KCYKS*|fc%=J4BFhJYQ$8pL zHOQ^xk~#O0K&8mRF@`)0+a$x4K$#lQVZw)I*(eB8HyJ_Iu zaVi(fDmPQl|B7-iyhKk%gV7njDxW;Ja@NHD%Jf{0ox!_LF+{-bx7qakxCU(`P6llNTCiXbOj2i zHh{ej3Jn8#A6R0X{uT&~JV#JPs-%9ZC((%zWjms5=6y-2AOD3B{-x0nLduc(%jUqY zkK@QSwd)V8_%HBoz=uiJLj5tXz0ZfqiX3Vr4gcy0b(@173KbH*!UUrJ(Tz{z+!e|Xj+W+Vn^kT>*!;9%qPImP8yE_ zPrRWFrX^QVJSc=f`e`W~;Xf8R55!Vtpg(B_N%s>_f6NG$C}p9p8wW};MQ6f(V}csX zT>66$MfeQqlQe-MII@)@P~)j1v+U-+!o0u&2IDOhN=bhU>39UBy9K0^!T2Nwi4V|% zdpOB+w^~366Cq3}1@b-y7J9^@lu&i}V@gD*W6()=ogoPph@y(6eU+=sJ~JuBxJruj zViJ7I3sBU{@_pvfg}(f8`I}%p4tR$U$L!0O&AyA0=)Hr#!-{Baj@efaBEWlUpz{kb z+W}A((7{XFQ3EOv3+OCqMaRu(bBxFR7$3ME;|63?=w)^R?_eE1V-BaMTt9Oeh61Jt zioBje=)7=trXmA@i#R}kgnxeOwphbP3A;0G!xl=HTz)3l^#E;W(K}ZHu0K<(q}lzs zF6{$MlDs;Ls%g$dobZKOo~Q@DP>3SH?j#?xVyr^MynM7-hCR1N5l?a|O7S zoBUVm4bM6ti9hQ|z~LPG@vveH5#cN7G?5EvH&>oeaOVfebqK~xhX{6i1ZQ916BG=DL0 z$rxJ{NNJKyjqfv9U}6^Lw3eSn1MB9~{Z<>ops$myHvA*N8aH*Cq~PNAar2N%6cLk| zrvtYS7jr0^5dwAtwAPSOe~T{{E7YK_$ftgUHb)juNVbYCIn4KcTj+b8(2zyTBwY#6 zXY$p5Mnl_HgfH-YiIr&GbE$+?t~itsI*2c^(5@wiiuX2Q17~+#3fL$|f$#W>h=f>Q zyhJW7BQ&~J*ZM}3i)Dl*-8W+a99z4%o-01E9uGT9KhsDW555kMmM_&XUGs`f= zvQI2w%%|y><5-7>MU45rce);?6gK|Dox`f6!1u!rN)xLI3ZU!oyIkrQ)dT!g0zx@7B!}I03X=O78@I*x)~@}INHq{ zCg}ie5bb*ts~!39ij|&QrVwU93xTdesCa0MzY(R%mM(#rgZ*L#-&-iyB1yA-f* zO=SLnrGSOLE})4DiJe|S6o|(FP-s5H(nuqQUs@VDaNUOQ1a_S^Jaf5(9j0;4CR-Ki zR@2VXP$hjts-)??DCL_@V>4>33jHqPwC~;S(F{S@ar&t5Ojq|$BKO#F`b;Y_0nA=Y zhj6#s!tFIa|&7hU)KNMo#$N?fMHs1wO|Htsn}#b>1066auw8AFT1o@HJTb zduZpis4R=&4||;0EujPwh%pwTli3@P9I>_Yy>sTPT;Bo5U4I^i9j$Qu*B{}Un7-m8 zs&_sJy7_#p>_0gWU#RGA6e9 z%Mz=Cd-Jh6nm{8B5O!KCjOGkxqAW3iq(tPFs2)4G0oyI>j{1m{BVj?xI~m5=Lh0an zOF@jG*JSsL*}a_I)7kw2b|02TVPe^RIJ=Ky_X+G?j{Cq>*R{R-Q(+c5*5VtHXn`Q0 zFUr6|Xynl{?GV=Hylu1i$DJ38A{5Pj`#a~T+Wgkzs-B|uyH$`J^Y?3*xBDB5AF(kA zLqvHw_B+~D!64!ZMPNGc!tiaVoT>V&shehLiMOdcU4n#uG3)sPI z`VRy$>3KOl9|@G>nT7Br1P&oK4zO?rnB8N0%-^9O39P~>7)ap}52G3|XTFrZ2O8nY zmc-(`a6AV7jIRMY{WEYb-F`qsB-Ty7!{7nPy1;Gp_ICnF=UoC8WMs-K+nFpd55Z7I zTKyOJ9)U7C@D)q_=$%C7!2Jv+d3N0iNEDWEOX5~69YdYU3riH7jD54<8qAzYpd4~TaccIuYE3omH#@hzxNuwa+K37kf&K>;Iopo^=zz*JFy zY#tzi0=$g?ow_*02If@VdTgu4(%SrLU>ke_%aL7^{|MU!8!CeXfzwC@co<%o(UIR? zqRzmgKus|IVkr_>X7noM> z%c+&3J|2gFwuM*C19SmLIHu3u<4`r zJQNbyH|e`|rr8HvBQiQ*YyU*du{S^}zE0;Ayo135DF0)MAV|X?n^9k7aR&xlCSWy`5v9w3OVgh+F&pzx*@Lw2h znZ4T)>7TFH?6%DAz&wn1Pg3{MjJL?Vh~de5RibJ=Jw*b~VQ|$9?FoB1P1*c@;7`bG z##^YdC`>zro34wW|2h?!Iw%k0nVStC)#mqIT38}S2dIOgvCo8~6u7tkus7&%1O{<` z9}9KWKsboG|7~FRr`LdzZjqLW%+LLpY-c2k_xKoIRg@|IrzDPQ%mez1V!)Yr}pS?L`T}_{r1Z=P!gHSr{9P{|X`e9i}vT zKE$8fi)8+;LP&q1jA*fJ)XBheMq#WeW4E&LEg+qXmyEuokmcQ|?LWRkiI9#5rhY!% z4fKjqT)sD>WAAG_fD+mkXNStjToijQiVYR^)yTnXuWgt1zP1Ms7@=mtpGGR~60L(I z0tQK+82Fq&s5uTYI&yWUkx;o_M#qsRCRzh)uGp5{MF}E@$L;E7&a+oiaGzQ zAY8%tMEU|ab4=cv#2kEW=Sm+I$l^ahTy&}hL>-1SZ>OUd*5!b#zgSO{I2P$=iw&cNdSl zBP4Fp5OKT6a9WJNu{8cB5TMwMj!s=;Cm#4u**+@}d81)K9c5Q+GeKZ<9n7e3fhwP0> z0KDLNJ@bNwxgBAW%9S*?3bT{0NpqXY8Kc{N>ZhRhq`W4PwhW*@l-%%#-OdpOe%;3h zph5E0yuzmcnZT4eH!arM$R~AF~CW%0X{C9qCg)=X9n(8P7Iq9=1%wP79)=L zfWA|ghX?A5K3xu;j6ZbBP>3IpJ9U%rik65E6Y)g#mV~Ou7){j|Tef2SF^ZJ zvF8Wb-NWuH*u9P2C$PJU-Ctq%Om@#=cO$#cXZKI1P)>eGWDSW5z^*?2Ie1L*<1QLl z3+UeZbW)~(#F#OeiS71~$Y>Er0zBf!q36L| zkPbnKLq+!6S*bU@q`-Axti%^KwONi$J`5YKL%k>lrqL=)J7Cw9!_ZRn?z#eQ}g&YU=@&MmY{)!$^2eB~Um*LHAN5O)oIQ%gTUU(<>xB-5(g{3n1`;UN=*pp@(v~C^fTm zB#TU%{(ukrjO4M1d{4mKzZl3r>SJVYeX#cv1K#TfW)8(7JuHnDj1=HO;956xH7|KN zeXT;etO9Z<(Z7Q16Yfx6BHe6nms{DgXgVrM1|&*FCfJym1qx`R4no0mqMSy8X`+Id zy;ouP%#9ufo^T^Dvb%>PtDMTuAXOp$^O)G65OsupAQq;5^h%9Bz(5NdJYZ@YAYyC< zBDonb9vLo6NN2uXeAoe(NK01-ZaBY3jo*c_xGj|p$y^6*Naj|9hGec9WnD+f<=8=U z89!t$1qkH8g9MT5NETE~IpN$P^jyl{R{Hv%6oQqs9dlra7?=ec5Z1I={V$p@usz(4 zSuWA=2RYB7U6<{mJg~!Xt|w7WISi*=_ac7{>@Vy=-irtCR9Yy4e9uli8d1@6%|XcMvqcf_K*M z&@b7kCe$d6)R2%8^~LrW%h%Mrh~I)QG-l{y@Lia|eXH=SuBMb>TRNaKA{TW?2iazQ zn%iS44NvK==y`hrcY){Z%OT-|Yv@oIIq>JK-kRy9ugHMzHbH}OsJKR@pJJq;6m{1* zC`(E&=AQAV7x3I91(N!u$nbxnxHpxM{U@n65M%5ix3e%6y&U#vMLR6mra#~JS7u0u z0cE|KSynJJwOJQkNkfD?se260w}zpbq3tHLMU(F&mLNVc%+R(F-HVQM2BY;QY^5a^ z&KnPE3~iUfUl_osUGZJ>5vg{I;(_BR@Y%2X>n_#6^AHTr;E-3w#=53rHg97af(7<> zK(g0u3k|!U1(p1|8$!R}NCk#G!^VH0a{Ri+vjTK_C;*BN;mpJ2a&W_wV6c1P3rH(w z4v#S6ymG>npQOxI3e|)Cf($p@N0&I%K3C7@90`A&O}YfQ;l%2=q{UlZbgZ^-h@Bf9=Wgo`E~b0c@AYlw8)uu zcPPdhkh#FE;OG6i)uCZ~F;)@!r4M7DA$NY=?6U&&$Jh>zv7kE<7)yAbdQYcc7k*Y8 z9}9@Gg&S_DWN<&h9Ke9=n}Vz+OuqGU&zU7jhPHOux+XOIOdnH+z^-VlH3`PQGnU#ucG++vJG$!#3Nr^|@o|I}mgE%5RCVqM zK#>CzL(T|1ddJ&0+bLfXw00xr?# zWd{(Y15kDZ!N2*7@VYE1@GL5q%)qHPb4Bmv{ES}g`Ak-54_!j}WZX?mR4H+aF}Iq@ zDLk+<7I6amG0Dn8uJ0G}f!9Xg23De!*2o;^Sy846=l~off{;F(LBOW_0290*#90HmcR#y11%w+hpfmK z71dk+zmy(R6q~`&|#7KKeh}`@qiDzKJMQKnEgscCJ9*e`24gyv%1DDXP%St_8N$gt6IB!<3lZmV zppjJ-A3E6`@G*U>yBA#nmS!3^ZH!kAsh*Adx`pJ;22OxQ78olo639V2yt<5kM`AIT zA}l(}0(i0HaJJqqkdN-i`lZIoL*un>F^@R-c>Nwm%(KSp;}^n{Sdl=>_tf4v*naX9 zvk4(fcI;xQ#V(fEgJsbn$WuDL=QbvN*~-k*m~Xy-t;}p6F|SDKph=^_L@Q!S1{JLW zExXYXf1O-4_g^@&854CRxb@0a0ob$*lK=nX@?ZzX#9x{3jsF}Z->LobtyHRCxAfW2 zKEjV&MPMS7 zj@%8yLJr!vGPuWrEj+Pk2CGsN%)TGZ-fpd71JUN9jUQZ7y}+M21_$oGaKP~F`zGma zi}dPtDL*%JtZPq^^gg4X1wKrQ@4x_;|27^bVJ1|B-whiy1eM$$(pk_@*L2@N^sAP_ z@}P#cH5jb>1-D2CE^N<48Q$y1|0K+w=zfJ(R)R&)nsR#_|L}(`n5`Y-etNsql|ujq z(B}d0N8vMc^tAsJ=8Ki2zBNhj7Gk+fT4CN8_nXAQ!ZZ_OH~cEnq{6&QCHHfD4dDBV zuNuB;_aBJQb>G{yhX#6*%U3*%2S&AlFsLU6?$i10x4cd64m|WVbs)Z?8FoAeOmqyf z;l%^qZ|9lCDx|luxd{|!*x`&;X$PU8dCR=}xX$c7?kU0!tDSp0P=K@_eD7hS(ql1c zCw-l}zllrhq&FRV`ywU{TV5QKeLwE*j!XN|cXam`y0oJv>7-@S0irF~hL*OQ$vtfF zQuVUE{IPH2K6=Iy@0VM#|HBx%TgJqaQ#M8>r(z6dyg{<*j>e691+DZ1B{$M$5n2`9 zEh@fcgog2A60wt?_P;^6=usMVVoy4e`X4duOG0qYQU-el8iFio-K ze&TGvZU(eRh(!t-pT^7;EmvQJet>CttiQr<5ta&E(*dpnR2lm;!j_jdP+fGSZv)l9 z=590qW$RSzk2MB{Er4w_+>A}qI*sPszq=poiU4Pm_78Yt4g=nm)T4yU^5tra}?))uQF1FIne zk7D3a3=Gc|;mj#xaN;=BI4w4vzh^ycLv;;fOf&m_l?@%B)J*w9^LfAe~7yzyh821Y1XJF^!rW2_IOCz$dT{ zwy0v?NV4KUzpDe?L|syi9>)HU$IL;@EMPmpAkO)QIqVrA56jm;^Y&oxtWV&mExF%0 zvFGS1LL<(Zru_{p(&@n7Z2<|)1+f%R|CfQz-1Y&UXBN*ET zq<2%0GQ zLsW4`F#b`+{Yd*(dEZ7f>Qml#ss7o1bZ7c{I7U^{Fdu%9SKQ}74yq~)zy2Hj{bv=2 z*nCevwmk{$75JZ2+*nlHSXOZ#S#rPN71sql4n`K(arIjBpffySrOxmzBXKXXXX+T; zqw(o?{FQyWp9cd65Lg8JShlOk74U=gZughG=aeu#{*Zj_V4JE3?3ff*#!a60+bM-#x0^{`-)<(l z{=1pzlxc~6WzYjG=bZ;B!`+w>_%&sa1}(#%FrRIRGLl{v0{xe?4~zdWj8|4-j}(%i zFn9hMzOpYF2U~M=P*`BARR3=H6F&ccevKow9GwE`o>46aEW@ zTQ1x>;a)A=8-#n0aCZv#ufh!o_XpvsH5~p3;U)<;Pq-%G)(Y1n+%3YrU${GjdqB9y zgnLrBdM!^ULAcX}J4d(|3-?Ol{#Lk~g}Y6-2ZZ}~;r<}pNF9eiS-5$^Efek)dj9>4 za9zSZF5EYSyHmIi2zRq^3xuoGONXd0<$aS#=W5~JEZlp9`xoK9AY3KhlyIJ}JA|wF z$A-M?!+1F5`#ABwP=s3{+#|yOYVm%(aNiLAS;9Ro+;ZV|2=_0-Jt^|}xp1!%{`Uy? zR^eVNTqR#wBE1Jf{8JaIncBW&F?W^l<@x;mP?W^JEB=bR#mK*#L^#D?aZ{rQkEeVO zbt_Z@|1zx^R%=E|v(;5ySDBicmb=25m7Zm-wPx}!yP(7Fe@Wt{*Odld`%OOwuSokj zIB~?!!Q;B0gHM0;Q?TUkKLz{4pyy-YJ1}^Wzo-8%4DZA6n;5O~9M`ElAa9DrbCJEq)=;t3)@*myyX=m& z6$`D6^$qnlCmvkRI;&$<{`{2XR!5z+vC-zpOvP0IPgi}7t=P7vvTjXdR&$fB*_zR` zGX2j5R+p_`?iN_9>uePz*6LN(m9`3VQ;n@f zd0U7RMvJYDkjpyUJY8sa+4cW=x&>C36_9gQ^K_Ppbj(dom$jk6Rznry0)kf}JIz+7 zQ(nyxF57Em1$%On-6hxB-Ay%4M|DM`wZ19E;hZtUWna0n0cD`;Jpn(8zp9eqj=^tT zB=^v-;pr>wSC%I`T{dguWV9K&Wbs{zziY!;;agVNTPi7=m6T5ru|&ih!Jxo?Ii$U< z7wK;bNk6pyAGwpKr?k^hI5nl$4p56kk9ISBY@(B3=He6cG5eNqmnG@3K6D*Q$J3 zfqCiWvMiU-5Fyu~+pMrU6?Cb#iFlLy#Z$_}V=faV92oPMh?m$e9`83wf@Q21aSY5< zK%x=`!b!M_t&@-{g{OY%W{13v~x{*E!|-Ci%kBCB-R`RU$s) z;}L&A*S5M{_C`*%;U1bMnnBE z?WD@Uk2gYouk0-%J=9w3L5|RVWNH#J!qSz#j+a{*#X|ef1o2(z|Di2PsrS%+*dpS^ z@nlr)CRn1(RbOu_`9rYilS7Z3YdIYZ-+bNvyS6`%;OruJM1e!$ z2_7gnH+RxtUpeHyKnzys&FQYLwmF@2P3^a1)mO`F9Q7`n((#8t zcBCp{J%=|_WT7wq3Jk37<{=_dGa9u12rZZ^$`r#nNFeY+2Yiw)nZn>d;m5uj}M*C_!fwD7#I!1QGiAVd(T77$lNDN!hlN2~uIBijD)JW)`Blcn(0)yP=Q#*Fm_Lny{pd7@ zn85jXg@07s)Kbo0ZV~Qw;i^P$;0;{tod-lq zy1~QoBoi=Pk9qjJA}@r}(}R!j?@IlIzBgN&>Z?}~wp=p7TYKlg@CUZ@_g?9DsCqxc z+mC|mYCD7ns&<|LYKvpMs+GeVPl@4Lhac4|3r2(*73^-RX6%lu4w5t~Gs`tga zg1>I@9xc9~5bsL)tAzi53dgu%AYPSrqqIAvU2R>>+fBz>?s~4`E@LLvMNW@Q5+mmj z{q@)#Jp5VpfHq&z(r0DN%A7T8R@SWSSvj+Evr@CtAoyow zWoFIF%F4>l%E`*jPR&lsPS4KB&di>bot2%Pos*rLlbVy3lb(~2lbJIsCo3mACnqO2 z7g@|j^0@$=i)gvC66QF ze-{41msi^=8f}f$bq)Zn6Yyk&rRz8N5p56k_rs64g;4*8;79$&VQpGzlOZ!Yh|M8t zqL35*h1f-wG*K8(m6fhK8PQaSMEXj*KQ7)U(??u9whf#VhnlpJm!CKxxZT%pzJotES%v~z&T_v6c{%ftu6J9?nVeS^;g+= zgsmd}`6B+=cy~EKg8*I*AL=dE8b6wcctD#(;=CZ=(AO>hN6d$VHo#k%QP@E zi3D06Sx%xNI*|unnzPGOavd+<6p`*~@vijStHpbm_#T>%LF>I=|2-n&EB!ZIQmJSR zIxC(V<014}70o2JSyw0w0Kckt+^pY_DV6`E^op0>Zd@Y1e#-s}0+ zyV3u^gD>sc*ZJn(J}}+!;_Lh0c+*_GWcejmRQhhX@rh@i-L-pv=X-G@M_pF&!-+FN zPvdVsIy!7(lYQLy%Jo-2_V_i=zc6ytgoz6m6)!2jwBm}ZuX%dkU*GwA&$s__IB#^h ze>Wv%=0lG^yQ}lf_m19Wy#0>U8z;W?*EfR2OD?@ETyKb-I`ivqn(Wzg=glwNa#QI_ z_kn|l4j*|R=E*9#a`J|w+6@KzvD&b>)<=hV{-m22(K=Q$S}$p5YBROr8Yw(1JT9`t zFg$!&xJElJGD4%*gljPWqKVe(G*MyFuy|c@_}K8};p(ta(Iwgini(2N8y9AX&eM)R zuTpN*UU{D9fNsNf&6uzaKWi=t9~qGlF(P_I^p#uNAYQ9T9L-{fPRj~pqUe%qo1bR5R6OE&y1U)nPiw`@LaFmaQo<}_)WL!X6j~# ztA`~-cwU<9iuSxaCR*nS>O4oIzq?nH9no4b!m~r~c~uvgFk2HDmZM*&j}CK1P0(Db zy(GfpO&AwBGQy(uY!3U=1JR?jY4>Sc|28E&TBq|o7~A@P2vQMyvQO8vOz3H1x= zH`RyLe~o@8;$8K7>c2_HbRVm~&<50B%RSnY>eHGZrRb@%FDPDes!M|kC}Yl1@pf6>YL!c{aX3l^QI(a`~(`aXCYBkc#6wik7nl!^W zZDPa(-85Z+cG&rzN5T@diP|K6W>oQl*6fIpk@|$$g}It)eMIU=-6YLeon*|>F4oP| zMuta(8|A6m=!h&$o^DLIHafh-oShMq5uT!tY@M=n@ihJUBgae`H)2#oF%l?<866%O zX3|fMa7WF*;QX-Jy2!AL!X#a+M(6Rbm|)UJdLF!Da$!_tSj;(jVUbzWwWB=G&8aDk zHbq1(EF5dnm&TaGBRxMXj2y37WX{$M(?^EohDWw$jSio!8F#T{NRRQ}UF(kWyu8^` z9n+Q?JMzXyHY~dDxedAD=W8p%rbI4`OwygRVf$sa#oFBPI3qQQ+fVA--aS9!{x4cH z4AS_pVOo9b^*-$?U5q9oJoe_QMG>w!o*yEe`sVnBS5a439x>W;UF#yv#`%W$wvvfq zVV-xU=`NTgHP6tD(W+aG6XWu9QtP4fH+=3nk!;aMYSrGj0?S;_o;hKXcA0K$rn+_5 zbZt%a#gU%Ja>mC@*G8a6gn91r9?`~WVl->Cm0{>#hG=aL3YVmxT-;g~Jsx?<(hoz> zh;YxV=S8-K4N21lZD;x|ko7_^8f!1LIqW4?htphywY8zX zrl1}Q;Zl2}t=QgVTjr|GVPYKnROxQ6blI82SqVmq#sFNOw)1f#iNC5!;L>xbe+B&5 zSPntMtCdwZ>8ezhpR-jpe3U#fTCSS-&GfC)&QF!6+aEkOUA?Vp#)KcMQdDQ;tltN# zvVN96&XOW0Whcgboc*U^*4&v1_vNOJvzor2aG%APX)Wow^1h3f*e5N$_l5hGs@}Ag z+791Ws`}fcGS#tg!8-q-!o!HLJo^Gcd2F#-C)0N{x?| zY@mv2wWK{)nlSdVsJw^>DM2emfQaa(Y3At9PmttnM9}I%{lX*FE|S#Y(fSpVIwC6EtR4$^l9WA6LW;U5DKSE-)kEB}GK5WfW4<h6%K?*OGhFrfP&d=f(k5} zC@D#W+RfDI=t&AQs4y_9qU?99PTvg@i;T;g{JG&J5Ug4(|SK)i3Z{YnlN4PVDJ6X7K!c}Mu(_d6t zSJ)jcruKk8V;oCv=5R<>q>K1ZmWLta_mW7iN7*7=kC>OA>2Nz;)icv;(lTpSWTdC9 zNX^Wst*xGwoomg`%u0i3U7eFtlWohWot0UelTo4Mt5*+WZ+2EFnib@1hsYPvBf1{M zuYbOnCvISq*`?_96uDY>_kK?m`BlCv@f6o{7nie@@1gNKKH=ZD+{^K`LYq@}GvE@%$-sE=XE1Jv9A$@r~vfLj9kDAIayT{!hb? zbHb)iWhvsiFyoL?pzY%^U+lGdx z`X=2?v%6_6$<6C#q*AD}!c9OwP8-BO4gS<#L*Zt^f6*ZR7z3D|1;RNrIxtyb3|Aga zrQ00P8o8S=0S8qC6`b;u2i=$>FRvCe;gxRiYe~uSTv=XYb;`Z-icl=oSuuB$##9*7 zna+wmI^{Kh$>&`U+|KJUm-2*5x`V$fqq#6`loVIZB}M& zMmp1Kxzo~f+*z4!yryNjbLvUcRS&=Vtjzk1^!l`{`WzI9bS*3F_67h5X-8y^%;v;G z{3oaf$Md=Hr~VQOClUTKyw^Ew)oy0#gQ>1YD0LWmZ8huH)pL$mcCLki~yvFmEa}k#nZY7sxVjpO?4JSy$g!-C%dR9kz2XyS!#?lPph@XUmOfsMU7NW6P*Y zr?u9GdZL<^Cr_4Zo08-i7s!;#S?T;0eDe%=Q|+#k<@GLy+g59BaN5?TwhR{j6;vFB zPibga-I(%!gDP;2VB}rA-CW5sh50PF)DA-Z>)=Q9pnw0CsTIprrDzfssta4q!bVqq zjTP!rM-^++HT9TWbJW;s&Vm<%c0u%F9W|(ss_X?=*ivW3=!odtxD4Ty_N}-;GrK}v zg4|yen>KJS82)f2eQ@yxpQdz9QWH!o5wndxd+4aFuX> z7VnP>_wJDI&x`kmg!`0mw+i`!(T^!iO;k((dfm7tUS5NhLKI?Ds+ri{F}UN8Xv33=3s@pwiZTSb)c;53)4BEKQ>-IG1A-WB?yxI@W#*&yJ=w~!HQEeyRHWC(h$ zdRVpz29G)gt_SYp`BK_#sNE0j1RU08SuqY+Q0l@w&dR*JCfk~%$pm~dF@Tt-6L6+5 zsD1N5PFD?+1s%ddB;o#n={XRd_}%oEhE`X<1>z1R~3`eJwEw zxDJVYF&+}{pQ6W|3m3*W^+>3iOnewK&Z<$iSF`kMFc%4{LgF1U9d2)`zlzYpljU|f zb${S^pG}G2>a&enX(Ra`6Q|Pj(|7y0iMsv;$3N% zefj0~bbUk`q?`~c!qV<{J4gr^V zT)K$163YPF8sKiF-2pZb2FPR|Wk=o|;t||88VsxyV8-9i^pYl#;a`h716S^DZibr6 zR%36bieQGxBz-yDG;a@$jGK(Hz+-}mciGA0xe0U89CZYt3HGYw4+YFxglbylq$LJU zsAN{dM1uKH4?!zMz#4QVw8B-k7RVuNK9=PI1D7U-DO3KsKy{_t8v|-LZ=269(fj?(UbJ2D`nPiU&PaJ^BYjAD66>Y$+>K z&?OoPC%~G$IhhE_40{t9|MnG#EILG9k+phvBL{>UV>9t&KCu>k+ltmp3BaFE(D%Of z)!Q1bE?@`-wS`HZV)$)qLG?LTIP9y?<(uo9(dl@pum+^bT2lkui;4f$un#6YDxT;j zpgWW#dQB5E-4zeiQkaI+R--`cC$87o(O&w!_ltzYY_0+CwMe1{&9AW@N)9s0wl&qG zdU+WrZDuUZ$nz>-8Vz5JSLg=pQe0IVAFmow9?Dd=+rA@Kf*)MKbIJZ8d|y0XeU?D;lQl0uJfc z=%S-Lh{p@{KMH^1YeW5aYPi3kSIp{x(PF!tCnJ2q!@Qgd-#AUY101_T;q?Gs1im1X z=rgzjg@cr#m(d8tLp9vgyYA+!7?2xJq`u8WEY3Gy@R{WLOvTR zd=Iv(r#98IQ^XT;S4v(eR@n@M(kxW3hj>WP!6|x$0P?Kk;%Xcv*x*ry+bw zN(F}y^8E-J0D~Rk|18{24D70{`|Q z{9EBK4+3Yy5b&QI!vCcq;O|rXDUE&?n(ME-hTyNakLB&)kZ?omlQ^mhdsDSATT&f* zl-H}$?r%iu>6C+^;HQapCfv~YuOKSXk)h(3i1w{eUtG4y7GYW|CieA>ZgqJr7 zap=->N7eB;_dnLmU5qKvCDY75E#<+GhXEG4VLQ}tV0xyLw89kaa@9aaaxSgKsA8*j zth9u0g&jP3Z?;k*a~|XQFBj=nR?_@bB{ScvggW1WXQdG08pXO z(eH_7&=opvQExsQtIkR+aBIYBPR>kMLIx8fjP`M!zhnlD>yR_>9O^#}e)K!rKD$52 zdS_)53@4zxsuFN%PKK^u$kQ6A?R)9k9U>m10f;vYbcNEbaIa%3=p*7gx*xkf5bp{; zLgUMbPv!Y$*NZHC=yy6Jgyn@WVnS!^fUT7C&b@$Cn}p6p44HOF@Xx za`rcmpKouOLyGYHrnMKKMj88ife>$|P6eCUXR1GS>U5c<_k@72wA<&zyMcXGsY31d zL*-@Ou7_;BX;siRt*(cN#(4VGR!2SKnW2S(uDQ9v3XZX_IRgc;Pw?^=i}XDrKMMV! zQ%;E9ADhhkl`=jl`QIw^gbIC$6>yaAs-WnX2e~VUeBW}K`}h3F-J#%Y75c{>;R-bO z;+V^)SU^c)J~2dmc;);n-o7~vZYem-4>BfWN!*w+-9$NdDezC z*0oM{j6lp>c<|DF40oDME4SSM?`!@J{+R_tjSSvLvxhy zBriAhJ-R~MHTVYn_mVEAQ-Nr+!rlPEv!9-+T)@fXU{tGtFoG$w3wliPT^To(eicgB zLitj8hRh&_h6@OI3Vj$Z(tTIZVd7os;^~V!EZLKu;^kG^W9T}X(0*kh%}K>l(DL~T z`NpCjTg$IPRLzQ&b+(3PB6_VN-4vcJ)l%SRGM+>I3*bL#5dV4bpAP?JePa6pWD4UL zi8jmnYXg>$9$<;vwWPL)$^XlGmtGdyYhfLS7)-*?7ZSb&5@MO%Zh?4j=CbiZA>G3l zi>23ENVZ=nzL2_*iPQ6$I9&p1TBKcoF*_f^Gyvu+ax}f0 zo0d6YV(40+NYLT|qHaE!7};tBfMtEt-6qJ-{a*WG0JAm)5&$fq>@=BzcrOSz)EDSlh~K&REh&`?nJ_OWar=iNF7PjhBD8nKprY0x1z%bgV|h*r zcnV#Rb_}F@zXXnTF&T;D3WZZ5;80(t>)+`Q`j?fKkSh$)HUWpqNY|zKQC>s+tKdf* zN~nJw{3tH%$}&&v%|h-n)W8C`P=umlof!Vmi`go0>;sZtVwQ#08(JBr87*FBA@r+K zpX2x;nnM?p72t-B5BYG z(#>P3s#GoFJyE=0377DK+*5mj1`SFSk~mNRC#edckpOFN$0C&`sE>q>Qzkti4WTrk z@D2>3VQFBjqahAvVke4Wos3b*LM7q&xTZ%Xew6TMjTg_<*69j;SK=%16!^n@n~L99{D$MF#_t67zk~R_h~K05-G$%v_^rn8O8hGETaI5b zehcu+!EX|N@%TmHcjCKX@EiO-#_t{c_T%>?e%tV?!|!hVw&3T-?`r%g!wuoQ9VhVY z_tw8>3AY0p>ibslT?v=7m&3VPz`IVk%_5xQuk355xC;Ii_k@Udj-JEQMR50QI@E|6 zdn|u;+8Sz8F!`L6{NI|&^e&~~OZlCU!xSzYM|i!=(=QhI*$+JzwT)2!FW^UQ;p}{h zy^1Eyl~d&@Q^5MNt&5OzwwNpw&fS1PZ7I}$iH6U!g!-4lpT^oye;J*b@)PQh)lh?m zCx7CbLc?dFyo15H82(cS34aCr!{JX`TVe33;`A;rkK6AgC0EfTy;ZKFb#_(UdPWHf zdDh(kqxXS9o8&48cp|7urL-F)!7id%bSdjtbTfH>B=L96}#>qgaQf5jy&rA3wR=lNUAv#knhIZf={hwx%+!)jXv+X)P09>yvq zkP21jEy!QGXi24MNm1c>6;*d$=|0lQ@j-2juF(AX z;m79Bo10-*QeO+hs>&2=b?q?`PodXeiFcrrU7>K=;Y;!YpAVTCQe5PVM^h>B#p_v4 zKjRq}v!M_QWvn!19Aap?k!YaAXN3Ce;h!;x|8V%vAH@G-_@&`TLTvBGNMM>m;eLTI zL=Qs!Ps48tjM)h=cH$lHU?p7O%?w8wI-}BIyjI% zo(4Udj&zK#a{n45cd;ewfD1uXm!wEF`29a|-4UxX{y*)VeSDO~x%X!`1VVUQ1wr?Is*Dk>`V ztoTON*46qFE4AnQyJv3LEQzh>^YoANJZC5duFa{X0Dn0-a9jM_f&LOwXLVQ z%3-eZo-*a8`6tBderN6JbXahhwLjX;&WQKy=c{iH<(Vq_fr#VfJO z@Pj&Dd<0Utewa0T`s}@_M~<2Iqx)vweccZ(c)u>(pmN!@p_Mw`j=qK7fo56>zPM^3 zr{OHEWbX4`bT6zaJ*ncP$;ZYQ>!qe%{_uYDYR)6GzZ(}WsbpuQmz!VYf&Ksndfd!KCEH$6a>0(nih>ni9~LjSGiO4B4-Wec1{g!Y{4V zi_c>!gWkMl)7G$&DrmZ>`ov4L&TO3bIuxJro|7kr!;3Fo%Cq-XLGSr%;%{@T>9Yw()UZo5+9FmrhnK z+q^jj*L1RLuf3LEY{;vA=ju_teDSz5r+9kf-DC?qy*1ggIW}EwHRtHs zfBfgLPn%QQ;QQcJfnkMaSm5(!V0b{ZFy22_(B8MRPdb;K5s-PLMXlOVT9}*kfKCrVeU6AQZ zn*Z0Yjm`f~v3`fQ&<=jk@9KAnx48!^+{^p;=^J{OAsmSZOSTDAu4D0D;TulA1XTFF z=l#a4bQlJ|e1t7aleo$_AJlKBNk`f;&INTB`m2lQA8p@nT*?4sXqcD5zTnKy+Vqv6 z%v=uYp7rn8D8JnaDsFx7*h757$`3z8UVQj~)$$kbN)P_h`2_wlroS06^-y0?TdGc7 zr1695VxA0|PABl&@`&lYH=AN@TrkyMtOPxq`}g6{hM~sueT|KVU^ozCuXu6c>f*ei zfw87A+&^9r>(_2QS$D`T9 z`I<;$NHT*Biien&^*mcbc|1v&2;_&TllkdnY;6zeQcjkbVQo1=KZXeZ0?M{HD0{B> zvaRnfFrWC)GHc67Q0YF)4@c=*11j7Hs_t{ovSTMm-IebI*9Lcj$|u`ZSJ^9+jjE^Y z5_)s=BsWKE-flo<{neYxiwE^xF(|KKZ=Y~pJQxc^Mt9V%d8N>1hS9c#VcJ8m2 zEu$DSle&)?-=6jydpd}h=GYUPM<36%`ddJSdtKbS&sAsL=hQ~4WS@>)8EOKd{wB04 z_wLB`(F4usD)!db1k8|01H#?zoys${hdf@HN7^zU>fyRKr;M_5ikFt7Tl(gkzTy1n zf%~fLTZI0>{mkINVdkLa%sqv9a|`qG*ZTL(&S!$Am<4}@llpr7s<_%sJ=g`W{wiQ$ zvgYP6h%0*GKtB>wpE>b?Yq~y$^pUI!>GN#6CqU(S6jXT1#dTfzY`YG#ZLp|^HW=F9 z45eQ9Le%rUc0GNNFF$DVCk0}jKUX_{9@x(eoLUg>7xkY~NXK_?3IZi25cBzwmRJ62 zm;WA@U*lkvd#16xH-~$DzPAV4^h~oV>zQV7ff+ojyQLf%WczPOff+I@$1l8Rpz{v7 zp9Kp0s*euzvKOC~6<5BhzGm^-yiP#vD`VTZ`^nG^AuS-#Kod9J(9 zDDL~r1bFq~si1sj2{;~9_(yyuO8SVG+Wv_R40vOeZ{m19GZR!e-^F!ZlH$Yqu0AZU z5U)5mNA0An;$eNvz)5z7(!3>EAWJ5@HAlTHIk+FxTs-dips52Dvh7y9bFj5Vyn?RF zlZ7GjML<8KNUJdu=db|z{6-xbKsXz@xOJdHiz`#DMQl_uIdPG=5O~?<6k$3eqGSGJ4@mXa_@(WZwlPZE)A_bMoQ=eO3&}>$~^ByuSw< zhUA$6Q|+9e&HFZlN#h6E5FwsB|3`;fy>T!Rviz?=g(76)M!PiFa3VG=@@=Tiu|ay3 zzBx;;`ZPs63@Yfl>KW!GU&@vHI;ilbug5(zj?xI|;hAyC@2Pt)&y4?Ux$3BU`xTou7m8&pkj75YKfJguaU_(Cu}%Fb7{pz{12RCwIQbzKtX)NVGGQ%<&xiu*R! z!YfY#l#OlRr?Ih#^bzv;A;f6W^G~YC+W0o85I`=QbzPD`LB1)N%2Qv|-v4rK9+>WK zv*NzZli^jybWk=g13!(;<)n|0&kq@AbHds9IH>TVi|e{)v+m(*1F>9N+hvcQb7u98 z=AvwtpY0=!A2im?i-+2rIKt{pJ8TEpH{`Y3b7QOxuYzjFa;~YwT2OK3u^G%`tAg$s zdJg4h&!5@6-;OIu;|I0l8sZTrkR>X&W*3yffT{(L85RZ5z*?CR%Yd?XyL*lOj6;`-7el!<9YUs(2CU9UuZ~o%f zbB+!*a6$E4NIdG|(j=XUlLIDvlGQ&RUg2|~uAzSh`d9U&zvUm$zbi-o9^w&C!@>HS zQ_=qgtFPr0tH0%oPJdzE4DK=2{yir23Uzk#i(ki5WPVT`Bg8fDD)@CweTnCv(`=bx zc!iTeua53{s4Lfsec$j!x6A8h$Av~Suz^PW}PEer)S+)&d?8Szn zSO2~Z5z_cU?NCO%)2caOLvm)o)HE-v;eD`a6i{`kFZp zUq8p{dk|E(?OYr8e*dpM_$%7}e)66YyS)$RJ;{l@Cs~yJo+SJl_Vx0fq?~JhP`kv4 z=Z^JECAKWFcGQU{q(QYyU{s+QRW-m22^4GgJMarPcdkjvhOB3Hzg>3c=uh{cKXE1P z0!iOoP+`7{7Z2;VdRX3oz%b+2*#1r#w-0^8dww@GlWTrZo#zlwIDtx+p7aXtsZK|8 zwbl6pP}ho&=)d}iyb+x1d+2jzQhGJtFY5WZvLi=#C-L@>RqpGKUmGy(S6f{tz$=^$ zN_RlN<=1xqeNCTN(&rEPEuj18xIgsX=QFwH2e1CbV@}}P(7B4=C||caGqu)+BfjD4 z@7jDsPMfz$XHJ{{y}0~9Hl(v`(r@1`;>jGFBHyxhq#VY<)Hkh73+n8>c?YQQG$`&A zm-ckfYd^g|dB{CW9yw_BkyZV@|LKhAWo}8mPMwY#V2-Nl<$pT=&1-(WcT>I}RPPM& z+~54|-?p|JhcPg<#>!^hYU?=%RQ+DR&9=kTgl&g1P;z`^20pSX+egaY=-o%+T=Rqa zr-pdM2~@JmjD06yqHAsWec=@bf}TEi-}r7X?fR&6|DF5ByID5qo$-F+shs;p=bg00 z?N;Xv-?cVOyu-$C0u}0AywkZ7R{CHH3AJ@M5-}mEY^7=vj*-kv=1b(}v@3MA9 z@3t5MOV(SPehsRGBVC#^K&9H{;?IJf?IQ|T@a&Oz zKei@QrXTb_hMB~p)=bYv*%fWE_9Z~?5}0u|&);Yhe+Y^z;BYSGDQvd!&0B0=joxbI z9owvY7pQdh9_`I6-S5#k&uty5`(XS2TeeGACd+n>zb@jr&ut|?#P{!Uy5JQ;pmgWH zFP-q#p*rRu^MiCOBtG5=RGP|3P+r89d6Cnx*w^t1&)+T5*NYGOb=;Mse-H6!j{cJS zxEDHoZTH$bJ^*@k%=tYtJcG_2o-s~E?g!O#I`MFhp3wa~`=LwaJ^?E1cJWX8ozWtl zz0|Wexj)u+W!WYFe3^J|{ZkKEo1!3R#(4HEZgYM8W9x5=K!rFc`!~4w4j1oFUWGGU zS;dFt>pk^5+3$6$-liT|@1%R<{jrrGy&p6sHxsXM0%?;SB@bG?>7Q9lf#IK88(#!f z!X#w6IvG?Ro*U=#exf1h-cP(MyR!N;oA=vs%sbs}nLs?Z9V6|w{20htJ)W*_I{O|2 zWkaWvV?($1&C4h+SFh5q%hD^qT2DLN<#$KBRD(&YVwec{hc;CltpI-$k$4XH0I?7h7wK*NdM-5ngR9w_REOTA0y2tc8icPknk=r{eo{J=gu9=cOd^4kwTn>5sgCU+=ND zw15gfcJc53-o}3dDsAAR(dHt4PnXvWH0RX~GBfH2n=dvGF~_%s%rPB9%@Ml~V;$Tv z^1yD-6oEWGa^tOVV~qIkc*Ay`#Dov1gPlXe*=r=`AoqjD!$RT_C-7|#zsCLdRcqTv zph7=nveh5|tS&k@{-vv@@t>osgLti1+Ce`K?_+LuW&Z(Gc*ED#%kTVCyU_P3e{<%h zS6^!WTtZoXkpJCCJa>-jeBIVP;V|@ujYq*a_N!fv{DZaqC{Xc6P$h44@nZ6-y$^R~ zoCzw&*)GoiU03jb*C*)D1MQkoJ?;Np!}_nM?|(%f9<*6ymJqK36;!75hyIBFy=iUP z3M$<1;yfpITZ=J={JHNCSLW!rk$9O`%0V6RKjA0nk-pjR3QIx7{dx2Iz08}ne?ez2 zzXkuN$kiVO)VZ0u_(A_0-A6nl=OXz1k@>*3OZY>JnJ()eQBZxDaG2q`N=rD5{Dt?l zu1$BoW!v~?pxXHIjP;>`Z`(Y_zY{RJO?(|x84La#@YWQ(4l2I>y?|Zcg)K5~w6m$F zF8i$=*7l_}#o)REys=C1@I^2EBa_ z!l$8qS#g&?+P@F}HP#sAQ_CMSJ!pOa&8W1<*TGCMioQX8%%I8r%^=o*1xDnX5tAP< zBLZzk_p3l*A5%ElTYD4_c=lk^y?0#6J^15;ela14XT(|0 znI((o=b13FWLDl?$aa%AL>Yca_RBM6)s&Sbdk9%6vgRz=USw%xnXK}HtnnI;Ym`hlt%T=X*{?hN*x_Jz-OoOzzfFINlb_+R++mf&>mA~NTN*yXUGkJVG;aDu~A9nN$3Zw^;FyxC#1 z!w!dg9KP?ckiJzI>+lqZa~yt^eo^??;c|z29X{sp0f$WvZ+1A{p;rihwErSI)HyxZ zJ6!8F(*IC;a-R3 z4%;2xU&B_}6_pa1Z%D>j3!a4dR}`PdSBP|ch`Cm=85}+2 zN|-Wz9w#07<TG6*u(yUZ(cjnk?Ao^Q@o9+I70hkNjG@zVqb~X`FHJ zFwlAKEYSI8zO|LFvH8aM^_XwYo-ym(88iJcbgIr#T^y&yP7`+-&d;uL$u7eW2b+qD zX*D%x=rn!X4%O`H`I4?*Rmldu0$s1TDqgwxl8Y}{Tv6p6+E;DNylEUFGv~~6W*k2` zZ0CZAsko}Lip}oJIJK~XZwHvEk}R)a19!#pYvPscC8&^q(=yfc9DGPQMTVndF2DGa zrLjy9MIGsoI`F-JQw9{ONuyrM#uEY_Zh12OCxiN!YW2soSjG*eOSeGrqH+rxq? zm;x?Zre>&M<2@QGh%VFFoL-ySI@_dP%1V{1*3#hyo)H17)!P&=nfjYG`3clrACc$$>l_8`-{DqHT(F%>gC6}kHF~s!Q8xo$Y<2LZ?+uxk__^II}G8;E_MGb;>5w^b+%;#_+sLs+X_0XxZG$ zwUybJRe5J}q~@X(OP5wwM|D6Ir*7@cpL@9-8VBzo6fN`2b-IW2dih(F=B&P=`V0;| zV;uOU`uFAk>8RfU*`fZCU?f}SoxbfI%4<{Ucb7usRC_K+#k%-Ow zzkfe{3xrsE0BH}ka6Oy&((pe5Pa00QzzdU5L1)lB0xn>0^B8#HPavgv4E*@U3tp&wufrb?dZ<0G zVR)hT!OnyiUIA6X3)82N|B0Z7^;6IXFVr5;b@0L^P#e7PL#XJXG}v~l7lxx>bnHk2&w!O_zo0(n)Gvc2RoPZ3gETx?)-Vw9bWtJ z)1bfIE2mj#sjF6YK zla==p^g~{K!J11k3wbU0z-6q(g>MJHaJelf2IgHsI^=rMHS|i`h9%&rt61l)GQrcJ z1bhTs3#qn)!I&;L64NfQG@H`=-j7p}H_9RqK^$<`$S z{^FbXF6leL3)b+SQF+1Igl$jZN05BOe4GENL+bYoc=K9Y#{~G@@7g*h!P0v6D9KK6 zx8u{`HFsD!-#9m)zmxx2N*CA#DZN=wJs|N(u=Flh4!G)W#sYG_hiGp3K4Tv~0j^B) zP8hxxyc?2jNwA^8+Mfc~Y~_DnN)Mjc!o2n@wt){r@~ckaJ@_B;1i0XbjAi&3c*=cT zQ+n|92be>|gWrIpD*-;^_%!(HPJEs;8Su8BSzQV6x}RHlEjalhTYdx_`wP~ClRgYS z2+5vy@ZsNBdpg06-#Q*#@jF{)EjaQC+rK5?CP;Zx;FKpV9|iY6&A6j{e#n_dNMkbv zPJEW;mR>OYyt4s(^#xmI27K@(nS*h-{-~@n10>bBmCPN*2m17#zfx6 zmndHt{HvW;L*RtMfayS<09OnMcx_kv__96W^eJn-5NcRw`edD<4-3oV1!p1I;sz|_E(fHy!I zeztk zf=`3BVbZhrLHHXeqj?n^dA#cja4qDuAvk)Ht#1kVRcH)(wHM~-K7rRd^9E=Q zd5NR0r>^;fAk=*ShX!pl0|qc*iZakCI^P8pbJWqqXk&FA2(o z*Lvs+poQ>S6a8bT2EHAfxt6|x*ZSny-=WP}JFNA}DX3KIh{02Cw_}jsex?EP#uxbW zJ84hSXl?Nk>jUOqc&*QU?k@DQo>%L4``u07!E5cV*73&Rg*QTRc&&>)Gl|bg4&DWY zSWBz5voAyA;kC~6-EE9Vt+{QX9Oz+q;VsY}c&$s_(@Hv}*&Z;zgw)QRVADO+k#(V3 z13L7F%u(=K6Z$2n9bW4;zXgTfq8#w2&^UOlr#$>V#wonkS8CnkI(Xrb`_Toj^^j*m z1sUuC_d(;CDtKR}yH4sM24!fT!4%g{Rb4A}Y;>%%GVElBrDvlF`@`MEIoQ!Ce+ zz^0!C%!Ico2Yd>e4zG2CpLvip@LEea16l~L^?RqaGv~rHQe}LDz zxfRf(@LEgvGiW!w*4MoPy$r84bt8U-55Q|(+Xv9{Khq8!0dq9820jd)0;S<2;5(2v zjvk>+pnb@-CM^vWXf4>U@$*jRD|oH@8u>Uj!)xu=0w@Wu^v^x6<#A zw6oHK$3ri}N5GpP@A(J(f#bCXrRqF_`gsGcyDb6e3jCGYd+%n;UjqP zhfoY&>mOc%mceU1!pi-$3%u4RB%zn#Q()m=?OYTBkNw!*Q^H_1BwytlSLQWH_fxGY z_+Egu;aYP5{t;RTp8=N!`JXpqEe@`NH0~1MosJi-%Ezaku~^U-8<|^#iy_U2!hZdN z-g^=u=Rte&+rd{L*}$32hVz=e@`aq)?8${^L9$tR10)-S&qJC|gf9&UdcS#v#|`Fx z_s?3~Gz5R4F2dW7WPJg=@PZO+kFew@%L~5+$!6hekjfX19&35w{~6D_nI|kB_ph`) zbr&xC9D9#BZ%_CZq;iCuC+yiEyaG~vg~y+QUeeD5>mj8Ve&I{DE*rpYrw7es(u5*G za}iVq&v~fkHYf_80MD3W`7*E^Qu_$6bo_EKGZh)>g~4ey%|LL5Z3&Daj@CWiv2P+`SZv-Fva?m`Ad^h;HGAln7 zT<&;b3fe;&;hT5>*?II8eH{m{gOt7&yv6Z459n#Ag)}+?=sl<% z-b90D0HpLG@M1`1#=(N~;qkE$_*qD4!r--#-lf!nw>Y`Zx_Dq7zP*QX!Iz*n;dL&? zG4nZ};P>POmqBCTBtZV8a5- zcY+hYV%snbrXcyPuxlZ8{lMbqFSKYb`kYkU%<5nc$j!T-}jJZRQXcvpjI3^f)th8s&8qm7+S`l-QKn z6xtl#9NAp8xn^@>b8>TPbNlA>=B~{J%|*@8=47+kQo1F&rDjY0md-5&TccYOTlZ}> z+sd|;Z>!pt*p}SZ)l$$})|zbXXx-gfw7qS6$M)UZGuw-Hgm;wgi0mla5!+F}qj`s^ zWgKElC>c(cC8NozWIWlP>`bPUnWWiJvLU=7vY~uK?FQ2jYA9(4HJ{H!x1EXK?8m0@=qqiApfi3zUqNFDtrcl3 zZ;Un8G}boOHzpg$H$|Gto8nG$XHy34C1{UstU`ZsV;dIi+Zf+ew<(GC(B|=8tJb19 zh0e0(^5&}Mn&!^teY9!Ome`h#EnQn`x7O31UE54cK})EmsHLQ3d`q~cv?bC~))H+g zZ;7>3wZvO$T54PBS`sbwEyqpcy@*xZ<*y`xQure^uf=1{Y(MV+fdcuNG&NNj1|(oRi6TgPt=Z!N_` zsj`QPT)DY^E+9)TH1*En=-z zt?|~*R8cq(X`0Fx|9#_%QIlVTg<)FQc|nVNKLNN?D; zAwzva4bg@e-qeO46*QJLCK^+X9gVxa`W4|#W%O&RsiP^~RFCh6n5tu5Uf0^zx)0y4+TQH7br`?zkd5u!SAha-j3>hzH2TUL Date: Wed, 22 Feb 2023 09:43:52 +0100 Subject: [PATCH 20/34] chore: Import base mod from DMF Co-authored-by: Aussiemon --- .gitignore | 1 + scripts/mods/dml/class.lua | 22 +++ scripts/mods/dml/hook.lua | 274 +++++++++++++++++++++++++++++++++++ scripts/mods/dml/init.lua | 16 ++ scripts/mods/dml/require.lua | 35 +++++ 5 files changed, 348 insertions(+) create mode 100644 .gitignore create mode 100644 scripts/mods/dml/class.lua create mode 100644 scripts/mods/dml/hook.lua create mode 100644 scripts/mods/dml/init.lua create mode 100644 scripts/mods/dml/require.lua 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 new file mode 100644 index 0000000..d0d22d4 --- /dev/null +++ b/scripts/mods/dml/init.lua @@ -0,0 +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 From 891efb7219af5763af7247b0c98281cfd70cb922 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:45:12 +0100 Subject: [PATCH 21/34] chore: Add ModManager class from VT2 --- scripts/mods/dml/mod_loader.lua | 628 ++++++++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 scripts/mods/dml/mod_loader.lua diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua new file mode 100644 index 0000000..406f707 --- /dev/null +++ b/scripts/mods/dml/mod_loader.lua @@ -0,0 +1,628 @@ +-- Copyright on this file is owned by Fatshark. +-- It is extracted, used and modified with permission only for +-- the purpose of loading mods within Warhammer 40,000: Darktide. +require("scripts/managers/mod/mod_shim") + +ModManager = class(ModManager) + +ModManager.init = function (self, boot_gui) + self._mods = {} + self._num_mods = nil + self._state = "not_loaded" + self._settings = Application.user_setting("mod_settings") or { + toposort = false, + log_level = 1, + developer_mode = false + } + self._chat_print_buffer = {} + self._reload_data = {} + self._gui = boot_gui + self._ui_time = 0 + self._network_callbacks = {} + local in_modded_realm = script_data["eac-untrusted"] + + Crashify.print_property("realm", (in_modded_realm and "modded") or "official") + + if rawget(_G, "Presence") then + Presence.set_presence("status", (in_modded_realm and "Modded Realm") or "Official Realm") + end + + ModShim.start() + + local has_enabled_mods = self:_has_enabled_mods() + local is_bundled = Application.bundled() + + printf("[ModManager] Mods enabled: %s // Bundled: %s", has_enabled_mods, is_bundled) + + if has_enabled_mods and is_bundled then + print("[ModManager] Fetching mod metadata ...") + + if in_modded_realm then + self._mod_metadata = {} + self._state = "fetching_metadata" + else + self:_fetch_mod_metadata() + end + else + self._state = "done" + self._num_mods = 0 + end +end + +ModManager.developer_mode_enabled = function (self) + return self._settings.developer_mode +end + +ModManager._draw_state_to_gui = function (self, gui, dt) + local state = self._state + local t = self._ui_time + dt + self._ui_time = t + local status_str = "Loading mods" + + if state == "scanning" then + status_str = "Scanning for mods" + elseif state == "loading" then + local mod = self._mods[self._mod_load_index] + status_str = string.format("Loading mod %q", mod.name) + elseif state == "fetching_metadata" then + status_str = "Fetching mod metadata" + end + + Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) +end + +ModManager.remove_gui = function (self) + assert(self._gui, "Trying to remove gui without setting gui first.") + + self._gui = nil +end + +ModManager._has_enabled_mods = function (self, in_modded_realm) + local mod_settings = Application.user_setting("mods") + + if not mod_settings then + return false + end + + for i = 1, #mod_settings, 1 do + if mod_settings[i].enabled then + return true + end + end + + return false +end + +local Keyboard = Keyboard +local BUTTON_INDEX_R = Keyboard.button_index("r") +local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") +local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") + +ModManager._check_reload = function (self) + return Keyboard.pressed(BUTTON_INDEX_R) and Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +end + +ModManager.update = function (self, dt) + local chat_print_buffer = self._chat_print_buffer + local num_delayed_prints = #chat_print_buffer + + if num_delayed_prints > 0 and Managers.chat then + for i = 1, num_delayed_prints, 1 do + Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) + + chat_print_buffer[i] = nil + end + end + + local old_state = self._state + + if self._settings.developer_mode and self:_check_reload() then + self._reload_requested = true + end + + if self._reload_requested and self._state == "done" then + self:_reload_mods() + end + + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.enabled and not mod.callbacks_disabled then + self:_run_callback(mod, "update", dt) + end + end + elseif self._state == "fetching_metadata" then + if self._mod_metadata then + self:_start_scan() + end + elseif self._state == "scanning" and not Mod.is_scanning() then + local mod_handles = Mod.mods() + + self:_build_mod_table(mod_handles) + + self._state = self:_load_mod(1) + self._ui_time = 0 + elseif self._state == "loading" then + local handle = self._loading_resource_handle + + if ResourcePackage.has_loaded(handle) then + ResourcePackage.flush(handle) + + local mod = self._mods[self._mod_load_index] + local next_index = mod.package_index + 1 + local mod_data = mod.data + + if next_index > #mod_data.packages then + mod.state = "running" + local ok, object = pcall(mod_data.run) + + if not ok then + self:print("error", "%s", object) + end + + local name = mod.name + mod.object = object or {} + + self:_run_callback(mod, "init", self._reload_data[mod.id]) + self:print("info", "%s loaded.", name) + + self._state = self:_load_mod(self._mod_load_index + 1) + else + self:_load_package(mod, next_index) + end + end + end + + local gui = self._gui + + if gui then + self:_draw_state_to_gui(gui, dt) + end + + if old_state ~= self._state then + self:print("info", "%s -> %s", old_state, self._state) + end +end + +ModManager.all_mods_loaded = function (self) + return self._state == "done" +end + +ModManager.destroy = function (self) + self:unload_all_mods() +end + +ModManager._run_callback = function (self, mod, callback_name, ...) + local object = mod.object + local cb = object[callback_name] + + if not cb then + return + end + + local success, val = pcall(cb, object, ...) + + if success then + return val + else + self:print("error", "%s", val or "[unknown error]") + self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + + mod.callbacks_disabled = true + end +end + +ModManager._fetch_mod_metadata = function (self) + local url = "http://cdn.fatsharkgames.se/mod_metadata.txt" + local headers = { + ["User-Agent"] = "Warhammer: Vermintide 2" + } + + Managers.curl:get(url, headers, callback(self, "_cb_mod_metadata")) + + self._state = "fetching_metadata" +end + +ModManager._cb_mod_metadata = function (self, success, return_code, headers, data, userdata) + printf("[ModManager] Metadata request completed. success=%s code=%s", success, return_code) + + local mod_metadata = {} + + if success and return_code >= 200 and return_code < 300 then + local line_number = 0 + + for line in string.gmatch(data, "[^\n\r]+") do + line_number = line_number + 1 + line = string.gsub(line, "#(.*)$", "") + + if line ~= "" then + local key, value = string.match(line, "(%d+)%s*=%s*(%w+)") + + if key then + printf("[ModManager] Metadata set: [%s] = %s", key, value) + + mod_metadata[key] = value + else + printf("[ModManager] Malformed metadata entry near line %d", line_number) + end + end + end + end + + self._mod_metadata = mod_metadata +end + +ModManager._start_scan = function (self) + self:print("info", "Starting mod scan") + + self._state = "scanning" + + Mod.start_scan(not script_data["eac-untrusted"]) +end + +ModManager._build_mod_table = function (self, mod_handles) + fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") + + local user_settings_mod_list = Application.user_setting("mods") or {} + + if self._settings.toposort then + user_settings_mod_list = self:_topologically_sorted(user_settings_mod_list) + end + + table.dump(mod_handles, "mod_handles", 3) + + local mod_metadata = self._mod_metadata + + print("user_setting.mods =>") + + for i, mod_data in ipairs(user_settings_mod_list) do + local id = mod_data.id or -9999 + local handle = mod_handles[id] + local enabled = mod_data.enabled + + if not handle then + self:print("warning", "Mod %q with id %d was not found in the workshop folder.", mod_data.name, id) + self:print("warning", "Did you try loading an unsanctioned mod in Official?") + + enabled = false + end + + local metadata = mod_metadata[id] + + if enabled and metadata then + local last_updated_string = mod_data.last_updated + local month, day, year, hour, minute, second, am_pm = string.match(last_updated_string, "(%d+)/(%d+)/(%d+) (%d+):(%d+):(%d+) ([AP]M)") + + if month then + if am_pm == "PM" then + hour = tonumber(hour) + 12 + end + + local last_updated = string.format("%04d%02d%02dT%02d%02d%02dZ", year, month, day, hour, minute, second) + + printf("[ModManager] id=%s last_updated=%s metadata=%s", id, last_updated, metadata) + + if last_updated < metadata then + enabled = false + end + else + printf("[ModManager] Could not parse date for %s", id) + + enabled = false + end + end + + self._mods[i] = { + state = "not_loaded", + callbacks_disabled = false, + id = id, + name = mod_data.name, + enabled = enabled, + handle = handle, + loaded_packages = {} + } + end + + for i, mod_data in ipairs(user_settings_mod_list) do + printf("[ModManager] mods[%d] = (id=%d, name=%q, enabled=%q, last_updated=%q)", i, mod_data.id, mod_data.name, mod_data.enabled, mod_data.last_updated) + end + + self._num_mods = #self._mods + + self:print("info", "Found %i mods", #self._mods) +end + +ModManager._load_mod = function (self, index) + self._ui_time = 0 + local mods = self._mods + local mod = mods[index] + + while mod and not mod.enabled do + index = index + 1 + mod = mods[index] + end + + if not mod then + table.clear(self._reload_data) + + return "done" + end + + local id = mod.id + local handle = mod.handle + + self:print("info", "loading mod %s", id) + + local info = Mod.info(handle) + + self:print("spew", "\n%s\n", info) + Crashify.print_property("modded", true) + + local chunk, err_msg = loadstring(info) + + if not chunk then + self:print("error", "Syntax error in .mod file. Mod %q with id %d skipped.", mod.name, mod.id) + self:print("info", err_msg) + + mod.enabled = false + + return self:_load_mod(index + 1) + end + + local ok, data_or_error = pcall(chunk) + + if not ok then + self:print("error", "Error in .mod file return table. Mod %q with id %d skipped.", mod.name, mod.id) + self:print("info", data_or_error) + + mod.enabled = false + + return self:_load_mod(index + 1) + end + + mod.data = data_or_error + mod.name = mod.name or data_or_error.NAME or "Mod " .. id + mod.state = "loading" + + Crashify.print_property(string.format("Mod:%s:%s", id, mod.name), true) + + self._mod_load_index = index + + self:_load_package(mod, 1) + + return "loading" +end + +ModManager._load_package = function (self, mod, index) + mod.package_index = index + local package_name = mod.data.packages[index] + + if not package_name then + return + end + + self:print("info", "loading package %q", package_name) + + local resource_handle = Mod.resource_package(mod.handle, package_name) + self._loading_resource_handle = resource_handle + + ResourcePackage.load(resource_handle) + + mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle +end + +ModManager.unload_all_mods = function (self) + if self._state ~= "done" then + self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + + return + end + + self:print("info", "Unload all mod packages") + + for i = self._num_mods, 1, -1 do + local mod = self._mods[i] + + if mod and mod.enabled then + self:unload_mod(i) + end + + self._mods[i] = nil + end + + self._num_mods = nil + self._state = "unloaded" +end + +ModManager.unload_mod = function (self, index) + local mod = self._mods[index] + + if mod then + self:print("info", "Unloading %q.", mod.name) + self:_run_callback(mod, "on_unload") + + for _, handle in ipairs(mod.loaded_packages) do + Mod.release_resource_package(handle) + end + + mod.state = "not_loaded" + else + self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + end +end + +ModManager._reload_mods = function (self) + self:print("info", "reloading mods") + + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.state == "running" then + self:print("info", "reloading %s", mod.name) + + self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") + else + self:print("info", "not reloading mod, state: %s", mod.state) + end + end + + self:unload_all_mods() + self:_start_scan() + + self._reload_requested = false +end + +ModManager.on_game_state_changed = function (self, status, state_name, state_object) + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.enabled and not mod.callbacks_disabled then + self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) + end + end + else + self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + end +end + +ModManager._topologically_sorted = function (self, mod_list) + local visited = {} + local sorted = {} + + for _, mod_data in ipairs(mod_list) do + if not visited[mod_data] then + self:_visit(mod_list, visited, sorted, mod_data) + end + end + + return sorted +end + +ModManager._visit = function (self, mod_list, visited, sorted, mod_data) + self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id) + + if visited[mod_data] then + return mod_data.enabled + end + + if visited[mod_data] ~= nil then + self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id) + + return false + end + + visited[mod_data] = false + local enabled = mod_data.enabled or false + slot6 = 1 + slot7 = mod_data.num_children or 0 + + for i = slot6, slot7, 1 do + local child_id = mod_data.children[j] + local child_index = table.find_by_key(mod_list, "id", child_id) + local child_mod_data = mod_list[child_index] + + if not child_mod_data then + self:print("warning", "Mod with id %d not found", id) + elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then + self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.", mod_data.name, mod_data.id, child_id) + + enabled = false + end + end + + mod_data.enabled = enabled + visited[mod_data] = true + sorted[#sorted + 1] = mod_data + + return enabled +end + +local LOG_LEVELS = { + spew = 4, + info = 3, + warning = 2, + error = 1 +} + +ModManager.print = function (self, level, str, ...) + local message = string.format("[ModManager][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 + + if log_level <= 2 then + print(message) + end + + if log_level <= self._settings.log_level then + self._chat_print_buffer[#self._chat_print_buffer + 1] = message + end +end + +ModManager.network_bind = function (self, port, callback) + local ncbs = self._network_callbacks + + fassert(not ncbs[port], "Port %d already in use", port) + + ncbs[port] = callback +end + +ModManager.network_unbind = function (self, port) + local ncbs = self._network_callbacks + + fassert(ncbs[port], "Port %d not in use", port) + + ncbs[port] = nil +end + +ModManager.network_is_occupied = function (self, port) + return self._network_callbacks[port] ~= nil +end + +ModManager.network_send = function (self, destination_peer_id, port, payload) + if destination_peer_id == self._my_peer_id then + Managers.state.network.network_transmit:queue_local_rpc("rpc_mod_user_data", port, payload) + end + + local channel_id = PEER_ID_TO_CHANNEL[(self._is_server and destination_peer_id) or self._host_peer_id] + + if channel_id then + RPC.rpc_mod_user_data(channel_id, self._my_peer_id, destination_peer_id, port, payload) + end +end + +ModManager.rpc_mod_user_data = function (self, relay_channel_id, source_peer_id, destination_peer_id, port, payload) + if destination_peer_id == self._my_peer_id then + local cb = self._network_callbacks[port] + + if cb then + cb(source_peer_id, payload) + end + elseif self._is_server then + local channel_id = PEER_ID_TO_CHANNEL[destination_peer_id] + + if channel_id then + RPC.rpc_mod_user_data(channel_id, source_peer_id, destination_peer_id, port, payload) + end + end +end + +ModManager.register_network_event_delegate = function (self, network_event_delegate) + network_event_delegate:register(self, "rpc_mod_user_data") + + self._network_event_delegate = network_event_delegate +end + +ModManager.unregister_network_event_delegate = function (self) + self._network_event_delegate:unregister(self) + + self._network_event_delegate = nil +end + +ModManager.network_context_created = function (self, host_peer_id, my_peer_id, is_server) + self._host_peer_id = host_peer_id + self._my_peer_id = my_peer_id + self._is_server = is_server +end + +return From a36e6f4adbd537be882b511ec2e95b10944ece58 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:55:27 +0100 Subject: [PATCH 22/34] feat: Implement mod loader Co-authored-by: Aussiemon --- scripts/mods/dml/init.lua | 16 + scripts/mods/dml/mod_loader.lua | 906 +++++++++++--------------------- 2 files changed, 324 insertions(+), 598 deletions(-) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index d0d22d4..34583d1 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -13,4 +13,20 @@ dofile("scripts/mods/dml/require") dofile("scripts/mods/dml/class") dofile("scripts/mods/dml/hook") +function loader:init(boot_gui, mod_data) + local ModLoader = dofile("scripts/mods/dml/mod_loader") + local mod_loader = ModLoader:init(boot_gui, mod_data) + self._mod_loader = mod_loader + + Mods.hook.set(StateGame, "update", function(func, dt, ...) + mod_loader:update(dt) + return func(dt, ...) + end) + +end + +function loader:update(dt) + self._mod_loader:update(dt) +end + return loader diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 406f707..b7440f5 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -1,628 +1,338 @@ -- Copyright on this file is owned by Fatshark. -- It is extracted, used and modified with permission only for -- the purpose of loading mods within Warhammer 40,000: Darktide. -require("scripts/managers/mod/mod_shim") +local ModLoader = class("ModLoader") -ModManager = class(ModManager) - -ModManager.init = function (self, boot_gui) - self._mods = {} - self._num_mods = nil - self._state = "not_loaded" - self._settings = Application.user_setting("mod_settings") or { - toposort = false, - log_level = 1, - developer_mode = false - } - self._chat_print_buffer = {} - self._reload_data = {} - self._gui = boot_gui - self._ui_time = 0 - self._network_callbacks = {} - local in_modded_realm = script_data["eac-untrusted"] - - Crashify.print_property("realm", (in_modded_realm and "modded") or "official") - - if rawget(_G, "Presence") then - Presence.set_presence("status", (in_modded_realm and "Modded Realm") or "Official Realm") - end - - ModShim.start() - - local has_enabled_mods = self:_has_enabled_mods() - local is_bundled = Application.bundled() - - printf("[ModManager] Mods enabled: %s // Bundled: %s", has_enabled_mods, is_bundled) - - if has_enabled_mods and is_bundled then - print("[ModManager] Fetching mod metadata ...") - - if in_modded_realm then - self._mod_metadata = {} - self._state = "fetching_metadata" - else - self:_fetch_mod_metadata() - end - else - self._state = "done" - self._num_mods = 0 - end -end - -ModManager.developer_mode_enabled = function (self) - return self._settings.developer_mode -end - -ModManager._draw_state_to_gui = function (self, gui, dt) - local state = self._state - local t = self._ui_time + dt - self._ui_time = t - local status_str = "Loading mods" - - if state == "scanning" then - status_str = "Scanning for mods" - elseif state == "loading" then - local mod = self._mods[self._mod_load_index] - status_str = string.format("Loading mod %q", mod.name) - elseif state == "fetching_metadata" then - status_str = "Fetching mod metadata" - end - - Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) -end - -ModManager.remove_gui = function (self) - assert(self._gui, "Trying to remove gui without setting gui first.") - - self._gui = nil -end - -ModManager._has_enabled_mods = function (self, in_modded_realm) - local mod_settings = Application.user_setting("mods") - - if not mod_settings then - return false - end - - for i = 1, #mod_settings, 1 do - if mod_settings[i].enabled then - return true - end - end - - return false -end +local LOG_LEVELS = { + spew = 4, + info = 3, + warning = 2, + error = 1 +} +local DEFAULT_SETTINGS = { + log_level = LOG_LEVELS.error, + developer_mode = false +} local Keyboard = Keyboard local BUTTON_INDEX_R = Keyboard.button_index("r") local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") -ModManager._check_reload = function (self) - return Keyboard.pressed(BUTTON_INDEX_R) and Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +ModLoader.init = function(self, boot_gui, mod_data) + self._mod_data = mod_data + self._gui = boot_gui + + self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS + + self._mods = {} + self._num_mods = nil + self._chat_print_buffer = {} + self._reload_data = {} + self._ui_time = 0 + + if Crashify then + Crashify.print_property("modded", true) + end + + self._state = "scanning" end -ModManager.update = function (self, dt) - local chat_print_buffer = self._chat_print_buffer - local num_delayed_prints = #chat_print_buffer +ModLoader.developer_mode_enabled = function(self) + return self._settings.developer_mode +end - if num_delayed_prints > 0 and Managers.chat then - for i = 1, num_delayed_prints, 1 do - Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) +ModLoader._draw_state_to_gui = function(self, gui, dt) + local state = self._state + local t = self._ui_time + dt + self._ui_time = t + local status_str = "Loading mods" - chat_print_buffer[i] = nil - end - end + if state == "scanning" then + status_str = "Scanning for mods" + elseif state == "loading" then + local mod = self._mods[self._mod_load_index] + status_str = string.format("Loading mod %q", mod.name) + end - local old_state = self._state + Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) +end - if self._settings.developer_mode and self:_check_reload() then - self._reload_requested = true - end +ModLoader.remove_gui = function(self) + self._gui = nil +end - if self._reload_requested and self._state == "done" then - self:_reload_mods() - end +ModLoader._check_reload = function() + return Keyboard.pressed(BUTTON_INDEX_R) and + Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +end - if self._state == "done" then +ModLoader.update = function(self, dt) + local chat_print_buffer = self._chat_print_buffer + local num_delayed_prints = #chat_print_buffer + + if num_delayed_prints > 0 and Managers.chat then + for i = 1, num_delayed_prints, 1 do + -- TODO: Use new chat system + -- Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) + + chat_print_buffer[i] = nil + end + end + + local old_state = self._state + + if self._settings.developer_mode and self:_check_reload() then + self._reload_requested = true + end + + if self._reload_requested and self._state == "done" then + self:_reload_mods() + end + + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "update", dt) + end + end + elseif self._state == "scanning" then + self:_build_mod_table() + + self._state = self:_load_mod(1) + self._ui_time = 0 + elseif self._state == "loading" then + local handle = self._loading_resource_handle + + if ResourcePackage.has_loaded(handle) then + ResourcePackage.flush(handle) + + local mod = self._mods[self._mod_load_index] + local next_index = mod.package_index + 1 + local mod_data = mod.data + + if next_index > #mod_data.packages then + mod.state = "running" + local ok, object = pcall(mod_data.run) + + if not ok then self:print("error", "%s", object) end + + local name = mod.name + mod.object = object or {} + + self:print("info", "%s loaded.", name) + + self._state = self:_load_mod(self._mod_load_index + 1) + else + self:_load_package(mod, next_index) + end + end + end + + local gui = self._gui + + if gui then + self:_draw_state_to_gui(gui, dt) + end + + if old_state ~= self._state then + self:print("info", "%s -> %s", old_state, self._state) + end +end + +ModLoader.all_mods_loaded = function(self) + return self._state == "done" +end + +ModLoader.destroy = function(self) + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "on_destroy") + end + end + + self:unload_all_mods() +end + +ModLoader._run_callback = function (self, mod, callback_name, ...) + local object = mod.object + local cb = object[callback_name] + + if not cb then + return + end + + local success, val = pcall(cb, object, ...) + + if success then + return val + else + self:print("error", "%s", val or "[unknown error]") + self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + + mod.callbacks_disabled = true + end +end + +ModLoader._start_scan = function(self) + self:print("info", "Starting mod scan") + self._state = "scanning" +end + +ModLoader._build_mod_table = function(self) + fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") + + for i, mod_data in ipairs(self._mod_data) do + printf("[ModLoader] mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) + + self._mods[i] = { + id = mod_data.id, + state = "not_loaded", + callbacks_disabled = false, + name = mod_data.name, + loaded_packages = {}, + packages = mod_data.packages, + } + end + + self._num_mods = #self._mods + + self:print("info", "Found %i mods", #self._mods) +end + +ModLoader._load_mod = function(self, index) + self._ui_time = 0 + local mods = self._mods + local mod = mods[index] + + if not mod then + table.clear(self._reload_data) + + return "done" + end + + self:print("info", "loading mod %i", mod.id) + + mod.state = "loading" + + Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true) + + self._mod_load_index = index + + self:_load_package(mod, 1) + + return "loading" +end + +ModLoader._load_package = function(self, mod, index) + mod.package_index = index + local package_name = mod.packages[index] + + if not package_name then + return + end + + self:print("info", "loading package %q", package_name) + + local resource_handle = Application.resource_package(package_name) + self._loading_resource_handle = resource_handle + + ResourcePackage.load(resource_handle) + + mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle +end + +ModLoader.unload_all_mods = function(self) + if self._state ~= "done" then + self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + + return + end + + self:print("info", "Unload all mod packages") + + for i = self._num_mods, 1, -1 do + local mod = self._mods[i] + + if mod then + self:unload_mod(i) + end + + self._mods[i] = nil + end + + self._num_mods = nil + self._state = "unloaded" +end + +ModLoader.unload_mod = function(self, index) + local mod = self._mods[index] + + if mod then + self:print("info", "Unloading %q.", mod.name) + + for _, handle in ipairs(mod.loaded_packages) do + ResourcePackage.unload(handle) + Application.release_resource_package(handle) + end + + mod.state = "not_loaded" + else + self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + end +end + +ModLoader._reload_mods = function(self) + self:print("info", "reloading mods") + + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.state == "running" then + self:print("info", "reloading %s", mod.name) + + self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") + else + self:print("info", "not reloading mod, state: %s", mod.state) + end + end + + self:unload_all_mods() + self:_start_scan() + + self._reload_requested = false +end + +ModLoader.on_game_state_changed = function(self, status, state_name, state_object) + if self._state == "done" then for i = 1, self._num_mods, 1 do local mod = self._mods[i] - if mod and mod.enabled and not mod.callbacks_disabled then - self:_run_callback(mod, "update", dt) - end - end - elseif self._state == "fetching_metadata" then - if self._mod_metadata then - self:_start_scan() - end - elseif self._state == "scanning" and not Mod.is_scanning() then - local mod_handles = Mod.mods() - - self:_build_mod_table(mod_handles) - - self._state = self:_load_mod(1) - self._ui_time = 0 - elseif self._state == "loading" then - local handle = self._loading_resource_handle - - if ResourcePackage.has_loaded(handle) then - ResourcePackage.flush(handle) - - local mod = self._mods[self._mod_load_index] - local next_index = mod.package_index + 1 - local mod_data = mod.data - - if next_index > #mod_data.packages then - mod.state = "running" - local ok, object = pcall(mod_data.run) - - if not ok then - self:print("error", "%s", object) - end - - local name = mod.name - mod.object = object or {} - - self:_run_callback(mod, "init", self._reload_data[mod.id]) - self:print("info", "%s loaded.", name) - - self._state = self:_load_mod(self._mod_load_index + 1) - else - self:_load_package(mod, next_index) - end - end - end - - local gui = self._gui - - if gui then - self:_draw_state_to_gui(gui, dt) - end - - if old_state ~= self._state then - self:print("info", "%s -> %s", old_state, self._state) - end -end - -ModManager.all_mods_loaded = function (self) - return self._state == "done" -end - -ModManager.destroy = function (self) - self:unload_all_mods() -end - -ModManager._run_callback = function (self, mod, callback_name, ...) - local object = mod.object - local cb = object[callback_name] - - if not cb then - return - end - - local success, val = pcall(cb, object, ...) - - if success then - return val - else - self:print("error", "%s", val or "[unknown error]") - self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) - - mod.callbacks_disabled = true - end -end - -ModManager._fetch_mod_metadata = function (self) - local url = "http://cdn.fatsharkgames.se/mod_metadata.txt" - local headers = { - ["User-Agent"] = "Warhammer: Vermintide 2" - } - - Managers.curl:get(url, headers, callback(self, "_cb_mod_metadata")) - - self._state = "fetching_metadata" -end - -ModManager._cb_mod_metadata = function (self, success, return_code, headers, data, userdata) - printf("[ModManager] Metadata request completed. success=%s code=%s", success, return_code) - - local mod_metadata = {} - - if success and return_code >= 200 and return_code < 300 then - local line_number = 0 - - for line in string.gmatch(data, "[^\n\r]+") do - line_number = line_number + 1 - line = string.gsub(line, "#(.*)$", "") - - if line ~= "" then - local key, value = string.match(line, "(%d+)%s*=%s*(%w+)") - - if key then - printf("[ModManager] Metadata set: [%s] = %s", key, value) - - mod_metadata[key] = value - else - printf("[ModManager] Malformed metadata entry near line %d", line_number) - end - end - end - end - - self._mod_metadata = mod_metadata -end - -ModManager._start_scan = function (self) - self:print("info", "Starting mod scan") - - self._state = "scanning" - - Mod.start_scan(not script_data["eac-untrusted"]) -end - -ModManager._build_mod_table = function (self, mod_handles) - fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") - - local user_settings_mod_list = Application.user_setting("mods") or {} - - if self._settings.toposort then - user_settings_mod_list = self:_topologically_sorted(user_settings_mod_list) - end - - table.dump(mod_handles, "mod_handles", 3) - - local mod_metadata = self._mod_metadata - - print("user_setting.mods =>") - - for i, mod_data in ipairs(user_settings_mod_list) do - local id = mod_data.id or -9999 - local handle = mod_handles[id] - local enabled = mod_data.enabled - - if not handle then - self:print("warning", "Mod %q with id %d was not found in the workshop folder.", mod_data.name, id) - self:print("warning", "Did you try loading an unsanctioned mod in Official?") - - enabled = false - end - - local metadata = mod_metadata[id] - - if enabled and metadata then - local last_updated_string = mod_data.last_updated - local month, day, year, hour, minute, second, am_pm = string.match(last_updated_string, "(%d+)/(%d+)/(%d+) (%d+):(%d+):(%d+) ([AP]M)") - - if month then - if am_pm == "PM" then - hour = tonumber(hour) + 12 - end - - local last_updated = string.format("%04d%02d%02dT%02d%02d%02dZ", year, month, day, hour, minute, second) - - printf("[ModManager] id=%s last_updated=%s metadata=%s", id, last_updated, metadata) - - if last_updated < metadata then - enabled = false - end - else - printf("[ModManager] Could not parse date for %s", id) - - enabled = false - end - end - - self._mods[i] = { - state = "not_loaded", - callbacks_disabled = false, - id = id, - name = mod_data.name, - enabled = enabled, - handle = handle, - loaded_packages = {} - } - end - - for i, mod_data in ipairs(user_settings_mod_list) do - printf("[ModManager] mods[%d] = (id=%d, name=%q, enabled=%q, last_updated=%q)", i, mod_data.id, mod_data.name, mod_data.enabled, mod_data.last_updated) - end - - self._num_mods = #self._mods - - self:print("info", "Found %i mods", #self._mods) -end - -ModManager._load_mod = function (self, index) - self._ui_time = 0 - local mods = self._mods - local mod = mods[index] - - while mod and not mod.enabled do - index = index + 1 - mod = mods[index] - end - - if not mod then - table.clear(self._reload_data) - - return "done" - end - - local id = mod.id - local handle = mod.handle - - self:print("info", "loading mod %s", id) - - local info = Mod.info(handle) - - self:print("spew", "\n%s\n", info) - Crashify.print_property("modded", true) - - local chunk, err_msg = loadstring(info) - - if not chunk then - self:print("error", "Syntax error in .mod file. Mod %q with id %d skipped.", mod.name, mod.id) - self:print("info", err_msg) - - mod.enabled = false - - return self:_load_mod(index + 1) - end - - local ok, data_or_error = pcall(chunk) - - if not ok then - self:print("error", "Error in .mod file return table. Mod %q with id %d skipped.", mod.name, mod.id) - self:print("info", data_or_error) - - mod.enabled = false - - return self:_load_mod(index + 1) - end - - mod.data = data_or_error - mod.name = mod.name or data_or_error.NAME or "Mod " .. id - mod.state = "loading" - - Crashify.print_property(string.format("Mod:%s:%s", id, mod.name), true) - - self._mod_load_index = index - - self:_load_package(mod, 1) - - return "loading" -end - -ModManager._load_package = function (self, mod, index) - mod.package_index = index - local package_name = mod.data.packages[index] - - if not package_name then - return - end - - self:print("info", "loading package %q", package_name) - - local resource_handle = Mod.resource_package(mod.handle, package_name) - self._loading_resource_handle = resource_handle - - ResourcePackage.load(resource_handle) - - mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle -end - -ModManager.unload_all_mods = function (self) - if self._state ~= "done" then - self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) - - return - end - - self:print("info", "Unload all mod packages") - - for i = self._num_mods, 1, -1 do - local mod = self._mods[i] - - if mod and mod.enabled then - self:unload_mod(i) - end - - self._mods[i] = nil - end - - self._num_mods = nil - self._state = "unloaded" -end - -ModManager.unload_mod = function (self, index) - local mod = self._mods[index] - - if mod then - self:print("info", "Unloading %q.", mod.name) - self:_run_callback(mod, "on_unload") - - for _, handle in ipairs(mod.loaded_packages) do - Mod.release_resource_package(handle) - end - - mod.state = "not_loaded" - else - self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) - end -end - -ModManager._reload_mods = function (self) - self:print("info", "reloading mods") - - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.state == "running" then - self:print("info", "reloading %s", mod.name) - - self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") - else - self:print("info", "not reloading mod, state: %s", mod.state) - end - end - - self:unload_all_mods() - self:_start_scan() - - self._reload_requested = false -end - -ModManager.on_game_state_changed = function (self, status, state_name, state_object) - if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.enabled and not mod.callbacks_disabled then + if mod and not mod.callbacks_disabled then self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) end end - else - self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) - end + else + self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + end end -ModManager._topologically_sorted = function (self, mod_list) - local visited = {} - local sorted = {} +ModLoader.print = function(self, level, str, ...) + local message = string.format("[ModLoader][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 - for _, mod_data in ipairs(mod_list) do - if not visited[mod_data] then - self:_visit(mod_list, visited, sorted, mod_data) - end - end + if log_level <= 2 then + print(message) + end - return sorted + if log_level <= self._settings.log_level then + self._chat_print_buffer[#self._chat_print_buffer + 1] = message + end end -ModManager._visit = function (self, mod_list, visited, sorted, mod_data) - self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id) - - if visited[mod_data] then - return mod_data.enabled - end - - if visited[mod_data] ~= nil then - self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id) - - return false - end - - visited[mod_data] = false - local enabled = mod_data.enabled or false - slot6 = 1 - slot7 = mod_data.num_children or 0 - - for i = slot6, slot7, 1 do - local child_id = mod_data.children[j] - local child_index = table.find_by_key(mod_list, "id", child_id) - local child_mod_data = mod_list[child_index] - - if not child_mod_data then - self:print("warning", "Mod with id %d not found", id) - elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then - self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.", mod_data.name, mod_data.id, child_id) - - enabled = false - end - end - - mod_data.enabled = enabled - visited[mod_data] = true - sorted[#sorted + 1] = mod_data - - return enabled -end - -local LOG_LEVELS = { - spew = 4, - info = 3, - warning = 2, - error = 1 -} - -ModManager.print = function (self, level, str, ...) - local message = string.format("[ModManager][" .. level .. "] " .. str, ...) - local log_level = LOG_LEVELS[level] or 99 - - if log_level <= 2 then - print(message) - end - - if log_level <= self._settings.log_level then - self._chat_print_buffer[#self._chat_print_buffer + 1] = message - end -end - -ModManager.network_bind = function (self, port, callback) - local ncbs = self._network_callbacks - - fassert(not ncbs[port], "Port %d already in use", port) - - ncbs[port] = callback -end - -ModManager.network_unbind = function (self, port) - local ncbs = self._network_callbacks - - fassert(ncbs[port], "Port %d not in use", port) - - ncbs[port] = nil -end - -ModManager.network_is_occupied = function (self, port) - return self._network_callbacks[port] ~= nil -end - -ModManager.network_send = function (self, destination_peer_id, port, payload) - if destination_peer_id == self._my_peer_id then - Managers.state.network.network_transmit:queue_local_rpc("rpc_mod_user_data", port, payload) - end - - local channel_id = PEER_ID_TO_CHANNEL[(self._is_server and destination_peer_id) or self._host_peer_id] - - if channel_id then - RPC.rpc_mod_user_data(channel_id, self._my_peer_id, destination_peer_id, port, payload) - end -end - -ModManager.rpc_mod_user_data = function (self, relay_channel_id, source_peer_id, destination_peer_id, port, payload) - if destination_peer_id == self._my_peer_id then - local cb = self._network_callbacks[port] - - if cb then - cb(source_peer_id, payload) - end - elseif self._is_server then - local channel_id = PEER_ID_TO_CHANNEL[destination_peer_id] - - if channel_id then - RPC.rpc_mod_user_data(channel_id, source_peer_id, destination_peer_id, port, payload) - end - end -end - -ModManager.register_network_event_delegate = function (self, network_event_delegate) - network_event_delegate:register(self, "rpc_mod_user_data") - - self._network_event_delegate = network_event_delegate -end - -ModManager.unregister_network_event_delegate = function (self) - self._network_event_delegate:unregister(self) - - self._network_event_delegate = nil -end - -ModManager.network_context_created = function (self, host_peer_id, my_peer_id, is_server) - self._host_peer_id = host_peer_id - self._my_peer_id = my_peer_id - self._is_server = is_server -end - -return +return ModLoader From 15dc40b9ab1a262986b6da74b3900a343dcf9fca Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 10:23:06 +0100 Subject: [PATCH 23/34] refactor: Move indexed objects to local and change whitespace --- .luacheckrc | 31 ++- scripts/mods/dml/class.lua | 24 +- scripts/mods/dml/hook.lua | 515 ++++++++++++++++++----------------- scripts/mods/dml/init.lua | 3 +- scripts/mods/dml/require.lua | 56 ++-- 5 files changed, 327 insertions(+), 302 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 552b17e..f213500 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -10,7 +10,30 @@ ignore = { "212/self", -- Disable unused self warnings. } -std = "+DT" +std = "+DT+DML" + +stds["DML"] = { + read_globals = { + "MODS_HOOKS", "MODS_HOOKS_BY_FILE", "Log", + Mods = { fields = { + lua = { fields = { "debug", "io", "ffi", "os" }}, + hook = { fields = { + "set", + "set_on_file", + "enable", + "enable_by_file", + "remove", + "front", + "_get_item", + "_get_item_hook", + "_patch", + }}, + "original_require", + "require_store", + "original_class", + }}, + }, +} stds["DT"] = { read_globals = { @@ -32,13 +55,9 @@ stds["DT"] = { Managers = { fields = { "mod", "event", "chat" }}, - Mods = { fields = { - lua = { fields = { "debug", "io", "ffi", "os" }}, - "original_require", - "require_store", - }}, "Crashify","Keyboard","Mouse","Application","Color","Quarternion","Vector3","Vector2","RESOLUTION_LOOKUP", "ModManager", "Utf8", "StateGame", "ResourcePackage", "class", "Gui", "fassert", "printf", "__print", "ffi", + "class", }, } diff --git a/scripts/mods/dml/class.lua b/scripts/mods/dml/class.lua index 19b0c91..53896de 100644 --- a/scripts/mods/dml/class.lua +++ b/scripts/mods/dml/class.lua @@ -1,9 +1,13 @@ -Mods.original_class = Mods.original_class or class +local original_class = Mods.original_class or class +Mods.original_class = original_class local _G = _G local rawget = rawget local rawset = rawset +-- The `__index` metamethod maps a proper identifier `CLASS.MyClassName` to the +-- stringified version of the key: `"MyClassName"`. +-- This allows using LuaCheck for the stringified class names in hook parameters. _G.CLASS = _G.CLASS or setmetatable({}, { __index = function(_, key) return key @@ -11,12 +15,12 @@ _G.CLASS = _G.CLASS or setmetatable({}, { }) 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 + local result = 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 diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua index f74822f..7f17888 100644 --- a/scripts/mods/dml/hook.lua +++ b/scripts/mods/dml/hook.lua @@ -1,274 +1,275 @@ ---[[ - 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 function NOOP() end + local item_template = { - name = "", - func = EMPTY_FUNC, - hooks = {}, + name = "", + func = NOOP, + hooks = {}, } local item_hook_template = { - name = "", - func = EMPTY_FUNC, - enable = false, - exec = EMPTY_FUNC, + name = "", + func = NOOP, + enable = false, + exec = NOOP, } 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 +local function print_log_info(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 +-- +-- Get function by function name +-- +local function get_func(func_name) + return assert(loadstring("return " .. func_name))() +end + +-- +-- Get item by function name +-- +local function get_item(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 = get_func(func_name) + + -- Save + table.insert(MODS_HOOKS, item) + + return item +end + +-- +-- Get item hook by mod name +-- +local function get_item_hook(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 +-- +local function patch() + 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 + +-- +-- Set hook +-- +local function set(mod_name, func_name, hook_func) + local item = get_item(func_name) + local item_hook = get_item_hook(item, mod_name) + + print_log_info(mod_name, "Hooking " .. func_name) + + item_hook.enable = true + item_hook.func = hook_func + + patch() +end + +-- +-- Set hook on every instance of the given file +-- +local function set_on_file(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 = string.format( + "Mods.require_store[\"%s\"][%i].%s", + this_filepath, this_index, func_name + ) + 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 +-- +local function enable(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 + patch() + end + end + end + end + + return +end + +-- +-- Enable all hooks on a stored file +-- +local function enable_by_file(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 _, hook_create_func in ipairs(all_file_hooks) do + hook_create_func(filepath, store_index) + end + end +end + +-- +-- Remove hook from chain +-- +local function remove(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) + + 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 +-- +local function front(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) + + patch() + end + end + end + end + + return 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, + set = set, + set_on_file = set_on_file, + enable = enable, + enable_by_file = enable_by_file, + remove = remove, + front = front, + _get_item = get_item, + _get_item_hook = get_item_hook, + _patch = patch, } diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 34583d1..71f299d 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -3,7 +3,6 @@ local loader = {} Mods = { - hook = {}, lua = setmetatable({}, { __index = { debug = debug, io = io, ffi = ffi, os = os }, }), @@ -11,7 +10,7 @@ Mods = { dofile("scripts/mods/dml/require") dofile("scripts/mods/dml/class") -dofile("scripts/mods/dml/hook") +Mods.hook = dofile("scripts/mods/dml/hook") function loader:init(boot_gui, mod_data) local ModLoader = dofile("scripts/mods/dml/mod_loader") diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua index 6e876f7..d311b56 100644 --- a/scripts/mods/dml/require.lua +++ b/scripts/mods/dml/require.lua @@ -1,35 +1,37 @@ -Mods.require_store = Mods.require_store or {} -Mods.original_require = Mods.original_require or require +local require_store = Mods.require_store or {} +Mods.require_store = require_store + +local original_require = Mods.original_require or require +Mods.original_require = original_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 + local store = require_store[filepath] + local num_store = #store + if not store or num_store == 0 then + return true + end + + if store[num_store] ~= new_result then + return true + end end require = function(filepath, ...) - local Mods = Mods + local result = original_require(filepath, ...) + if result and type(result) == "table" then + if can_insert(filepath, result) then + require_store[filepath] = require_store[filepath] or {} + local store = require_store[filepath] - local result = Mods.original_require(filepath, ...) - if result and type(result) == "table" then + table.insert(store, result) - if can_insert(filepath, result) then - Mods.require_store[filepath] = Mods.require_store[filepath] or {} - local store = Mods.require_store[filepath] + --print("[Require] #" .. tostring(#store) .. " of " .. filepath) + local Mods = Mods + if Mods.hook then + Mods.hook.enable_by_file(filepath, #store) + end + end + end - 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 + return result +end From 09c0d3a5ae1459c007e2e279278674e952688186 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 11:55:34 +0100 Subject: [PATCH 24/34] fix: Add missing game state hooks Co-authored-by: Aussiemon --- .luacheckrc | 2 + scripts/mods/dml/init.lua | 81 ++++++++++++++++++++++++++++++------ scripts/mods/dml/message.lua | 47 +++++++++++++++++++++ 3 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 scripts/mods/dml/message.lua diff --git a/.luacheckrc b/.luacheckrc index f213500..e04e4da 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -8,6 +8,7 @@ ignore = { "12.", -- ignore "Setting a read-only global variable/Setting a read-only field of a global variable." "542", -- disable warnings for empty if branches. These are useful sometime and easy to notice otherwise. "212/self", -- Disable unused self warnings. + "432/self", -- Allow shadowing `self`, often happens when creating hooks in functions } std = "+DT+DML" @@ -28,6 +29,7 @@ stds["DML"] = { "_get_item_hook", "_patch", }}, + message = { fields = { "echo", "notify" }}, "original_require", "require_store", "original_class", diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 71f299d..dcfa892 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -2,30 +2,87 @@ -- to initialize the modding environment. local loader = {} -Mods = { - lua = setmetatable({}, { - __index = { debug = debug, io = io, ffi = ffi, os = os }, - }), -} +Mods = {} -dofile("scripts/mods/dml/require") -dofile("scripts/mods/dml/class") -Mods.hook = dofile("scripts/mods/dml/hook") +function loader:init(libs, mod_data, boot_gui) + -- The metatable prevents overwriting these + self._libs = setmetatable({}, { __index = libs }) + Mods.lua = self._libs + + dofile("scripts/mods/dml/message") + dofile("scripts/mods/dml/require") + dofile("scripts/mods/dml/class") + dofile("scripts/mods/dml/hook") -function loader:init(boot_gui, mod_data) local ModLoader = dofile("scripts/mods/dml/mod_loader") - local mod_loader = ModLoader:init(boot_gui, mod_data) + local mod_loader = ModLoader:new(boot_gui, mod_data) self._mod_loader = mod_loader - Mods.hook.set(StateGame, "update", function(func, dt, ...) + -- The mod loader needs to remain active during game play, to + -- enable reloads + Mods.hook.set("DML", "StateGame.update", function(func, dt, ...) mod_loader:update(dt) return func(dt, ...) end) + -- Skip splash view + Mods.hook.set("Base", "StateSplash.on_enter", function(func, self, ...) + local result = func(self, ...) + + self._should_skip = true + self._continue = true + + return result + end) + + -- Trigger state change events + Mods.hook.set("Base", "GameStateMachine._change_state", function(func, self, ...) + local old_state = self._state + local old_state_name = old_state and self:current_state_name() + + if old_state_name then + mod_loader:on_game_state_changed("exit", old_state_name, old_state) + end + + local result = func(self, ...) + + local new_state = self._state + local new_state_name = new_state and self:current_state_name() + + if new_state_name then + mod_loader:on_game_state_changed("enter", new_state_name, new_state) + end + + return result + end) + + -- Trigger ending state change event + Mods.hook.set("Base", "GameStateMachine.destroy", function(func, self, ...) + local old_state = self._state + local old_state_name = old_state and self:current_state_name() + + if old_state_name then + mod_loader:on_game_state_changed("exit", old_state_name) + end + + return func(self, ...) + end) end function loader:update(dt) - self._mod_loader:update(dt) + local mod_loader = self._mod_loader + mod_loader:update(dt) + + local done = mod_loader:all_mods_loaded() + if done then + mod_loader:_remove_gui() + end + + return done +end + +function loader:done() + return self._mod_loader:all_mods_loaded() end return loader diff --git a/scripts/mods/dml/message.lua b/scripts/mods/dml/message.lua new file mode 100644 index 0000000..3ea27fe --- /dev/null +++ b/scripts/mods/dml/message.lua @@ -0,0 +1,47 @@ +local chat_sound = "wwise/events/ui/play_ui_click" + +local notify = function(message) + local event_manager = Managers and Managers.event + + if event_manager then + event_manager:trigger("event_add_notification_message", "default", message, nil, chat_sound) + end + + print(message) +end + +local echo = function(message, sender) + local chat_manager = Managers and Managers.chat + local event_manager = Managers and Managers.event + + if chat_manager and event_manager then + local message_obj = { + message_body = message, + is_current_user = false, + } + + local participant = { + displayname = sender or "SYSTEM", + } + + local message_sent = false + + local channel_handle, channel = next(chat_manager:connected_chat_channels()) + if channel then + event_manager:trigger("chat_manager_message_recieved", channel_handle, participant, message_obj) + message_sent = true + end + + if not message_sent then + notify(message) + return + end + end + + print(message) +end + +Mods.message = { + echo = echo, + notify = notify, +} From 36afe1466c8d87447de0d20790b5fcc8528b10b6 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 14:48:47 +0100 Subject: [PATCH 25/34] refactor: Drop loadstring for hooks --- scripts/mods/dml/hook.lua | 65 ++++++++++++++------------------------- scripts/mods/dml/init.lua | 8 ++--- 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua index 7f17888..deed632 100644 --- a/scripts/mods/dml/hook.lua +++ b/scripts/mods/dml/hook.lua @@ -30,23 +30,24 @@ end -- -- Get function by function name -- -local function get_func(func_name) - return assert(loadstring("return " .. func_name))() +local function get_func(obj, func_name) + return obj[func_name] end -- -- Get item by function name -- -local function get_item(func_name) +local function get_item(obj, func_name) -- Find existing item for _, item in ipairs(MODS_HOOKS) do - if item.name == func_name then + if item.obj == obj and item.name == func_name then return item end end -- Create new item local item = table.clone(item_template) + item.obj = obj item.name = func_name item.func = get_func(func_name) @@ -82,48 +83,30 @@ end -- local function patch() 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 + local is_first_hook = j == 1 + if is_first_hook then if hook.enable then - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. hook_name .. ".func(" .. item_name .. ".func, ...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + local mod_hook = MODS_HOOKS[i] + return mod_hook.hooks[j].func(mod_hook.func, ...) + end else - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. item_name .. ".func(...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + return MODS_HOOKS[i].func(...) + end end else if hook.enable then - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. hook_name .. ".func(" .. before_hook_name .. ".exec, ...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + local mod_hook = MODS_HOOKS[i] + return mod_hook.hooks[j].func(mod_hook.hooks[j - 1].exec, ...) + end else - assert( - loadstring( - hook_name .. ".exec = function(...)" .. - " return " .. before_hook_name .. ".exec(...)" .. - "end" - ) - )() + MODS_HOOKS[i].hooks[j].exec = function(...) + return MODS_HOOKS[i].hooks[j - 1].exec(...) + end end end @@ -131,7 +114,7 @@ local function patch() end -- Patch orginal function call - assert(loadstring(item.name .. " = " .. item_name .. ".hooks[" .. last_j .. "].exec"))() + item.obj[item.name] = MODS_HOOKS[i].hooks[last_j].exec end end @@ -225,10 +208,8 @@ local function remove(func_name, mod_name) end end else - local item_name = "MODS_HOOKS[" .. tostring(i) .. "]" - -- Restore orginal function - assert(loadstring(item.name .. " = " .. item_name .. ".func"))() + item.obj[item.name] = MODS_HOOKS[i].func -- Remove hook function table.remove(MODS_HOOKS, i) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index dcfa892..9a716f5 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -20,13 +20,13 @@ function loader:init(libs, mod_data, boot_gui) -- The mod loader needs to remain active during game play, to -- enable reloads - Mods.hook.set("DML", "StateGame.update", function(func, dt, ...) + Mods.hook.set("DML", GameState, "update", function(func, dt, ...) mod_loader:update(dt) return func(dt, ...) end) -- Skip splash view - Mods.hook.set("Base", "StateSplash.on_enter", function(func, self, ...) + Mods.hook.set("Base", StateSplash, "on_enter", function(func, self, ...) local result = func(self, ...) self._should_skip = true @@ -36,7 +36,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger state change events - Mods.hook.set("Base", "GameStateMachine._change_state", function(func, self, ...) + Mods.hook.set("Base", GameStateMachine, "_change_state", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() @@ -57,7 +57,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger ending state change event - Mods.hook.set("Base", "GameStateMachine.destroy", function(func, self, ...) + Mods.hook.set("Base", GameStateMachine, "destroy", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() From dcd08db47ad1bb90ed39339188813fb510897cba Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 18:30:14 +0100 Subject: [PATCH 26/34] fix: Fix initial loading --- scripts/mods/dml/hook.lua | 6 +++--- scripts/mods/dml/init.lua | 19 ++++++++++++------- scripts/mods/dml/mod_loader.lua | 18 +++++++++++------- scripts/mods/dml/require.lua | 5 ++--- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/scripts/mods/dml/hook.lua b/scripts/mods/dml/hook.lua index deed632..f64a158 100644 --- a/scripts/mods/dml/hook.lua +++ b/scripts/mods/dml/hook.lua @@ -49,7 +49,7 @@ local function get_item(obj, func_name) local item = table.clone(item_template) item.obj = obj item.name = func_name - item.func = get_func(func_name) + item.func = get_func(obj, func_name) -- Save table.insert(MODS_HOOKS, item) @@ -121,8 +121,8 @@ end -- -- Set hook -- -local function set(mod_name, func_name, hook_func) - local item = get_item(func_name) +local function set(mod_name, obj, func_name, hook_func) + local item = get_item(obj, func_name) local item_hook = get_item_hook(item, mod_name) print_log_info(mod_name, "Hooking " .. func_name) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 9a716f5..f455539 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -1,10 +1,14 @@ +local StateGame = require("scripts/game_states/state_game") +local StateSplash = require("scripts/game_states/game/state_splash") +local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") + -- The loader object that is used during game boot -- to initialize the modding environment. local loader = {} Mods = {} -function loader:init(libs, mod_data, boot_gui) +function loader:init(mod_data, libs, boot_gui) -- The metatable prevents overwriting these self._libs = setmetatable({}, { __index = libs }) Mods.lua = self._libs @@ -15,18 +19,19 @@ function loader:init(libs, mod_data, boot_gui) dofile("scripts/mods/dml/hook") local ModLoader = dofile("scripts/mods/dml/mod_loader") - local mod_loader = ModLoader:new(boot_gui, mod_data) + local mod_loader = ModLoader:new(mod_data, libs, boot_gui) self._mod_loader = mod_loader + Managers.mod = mod_loader -- The mod loader needs to remain active during game play, to -- enable reloads - Mods.hook.set("DML", GameState, "update", function(func, dt, ...) + Mods.hook.set("DML", StateGame, "update", function(func, dt, ...) mod_loader:update(dt) return func(dt, ...) end) -- Skip splash view - Mods.hook.set("Base", StateSplash, "on_enter", function(func, self, ...) + Mods.hook.set("DML", StateSplash, "on_enter", function(func, self, ...) local result = func(self, ...) self._should_skip = true @@ -36,7 +41,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger state change events - Mods.hook.set("Base", GameStateMachine, "_change_state", function(func, self, ...) + Mods.hook.set("DML", GameStateMachine, "_change_state", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() @@ -57,7 +62,7 @@ function loader:init(libs, mod_data, boot_gui) end) -- Trigger ending state change event - Mods.hook.set("Base", GameStateMachine, "destroy", function(func, self, ...) + Mods.hook.set("DML", GameStateMachine, "destroy", function(func, self, ...) local old_state = self._state local old_state_name = old_state and self:current_state_name() @@ -75,7 +80,7 @@ function loader:update(dt) local done = mod_loader:all_mods_loaded() if done then - mod_loader:_remove_gui() + mod_loader:remove_gui() end return done diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index b7440f5..1331473 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -3,6 +3,10 @@ -- the purpose of loading mods within Warhammer 40,000: Darktide. local ModLoader = class("ModLoader") +local ScriptGui = require("scripts/foundation/utilities/script_gui") + +local FONT_MATERIAL = "content/ui/fonts/arial" + local LOG_LEVELS = { spew = 4, info = 3, @@ -19,8 +23,9 @@ local BUTTON_INDEX_R = Keyboard.button_index("r") local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") -ModLoader.init = function(self, boot_gui, mod_data) +ModLoader.init = function(self, mod_data, libs, boot_gui) self._mod_data = mod_data + self._libs = libs self._gui = boot_gui self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS @@ -31,10 +36,6 @@ ModLoader.init = function(self, boot_gui, mod_data) self._reload_data = {} self._ui_time = 0 - if Crashify then - Crashify.print_property("modded", true) - end - self._state = "scanning" end @@ -55,7 +56,9 @@ ModLoader._draw_state_to_gui = function(self, gui, dt) status_str = string.format("Loading mod %q", mod.name) end - Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) + local msg = status_str .. string.rep(".", (2 * t) % 4) + Log.info("ModLoader", msg) + ScriptGui.text(gui, msg, FONT_MATERIAL, 48, Vector3(20, 30, 1), Color.white()) end ModLoader.remove_gui = function(self) @@ -120,9 +123,9 @@ ModLoader.update = function(self, dt) if not ok then self:print("error", "%s", object) end - local name = mod.name mod.object = object or {} + self:_run_callback(mod, "init", self._reload_data[mod.id]) self:print("info", "%s loaded.", name) self._state = self:_load_mod(self._mod_load_index + 1) @@ -197,6 +200,7 @@ ModLoader._build_mod_table = function(self) name = mod_data.name, loaded_packages = {}, packages = mod_data.packages, + data = mod_data, } end diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua index d311b56..bc808a9 100644 --- a/scripts/mods/dml/require.lua +++ b/scripts/mods/dml/require.lua @@ -6,12 +6,11 @@ Mods.original_require = original_require local can_insert = function(filepath, new_result) local store = require_store[filepath] - local num_store = #store - if not store or num_store == 0 then + if not store or #store then return true end - if store[num_store] ~= new_result then + if store[#store] ~= new_result then return true end end From 70b7529fc4bde72f980801c979e607142962f496 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 10:05:20 +0100 Subject: [PATCH 27/34] feat: Improve mod loader logging --- scripts/mods/dml/mod_loader.lua | 73 +++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 1331473..48e941f 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -3,6 +3,9 @@ -- the purpose of loading mods within Warhammer 40,000: Darktide. local ModLoader = class("ModLoader") +local table_unpack = table.unpack or unpack +local table_pack = table.pack or pack + local ScriptGui = require("scripts/foundation/utilities/script_gui") local FONT_MATERIAL = "content/ui/fonts/arial" @@ -58,7 +61,7 @@ ModLoader._draw_state_to_gui = function(self, gui, dt) local msg = status_str .. string.rep(".", (2 * t) % 4) Log.info("ModLoader", msg) - ScriptGui.text(gui, msg, FONT_MATERIAL, 48, Vector3(20, 30, 1), Color.white()) + ScriptGui.text(gui, msg, FONT_MATERIAL, 25, Vector3(20, 30, 1), Color.white()) end ModLoader.remove_gui = function(self) @@ -119,14 +122,17 @@ ModLoader.update = function(self, dt) if next_index > #mod_data.packages then mod.state = "running" - local ok, object = pcall(mod_data.run) + local ok, object = xpcall(mod_data.run, self._libs.debug.traceback) - if not ok then self:print("error", "%s", object) end + if not ok then + Log.error("ModLoader", "Failed 'run' for %q: %s", mod.name, object) + end mod.object = object or {} self:_run_callback(mod, "init", self._reload_data[mod.id]) - self:print("info", "%s loaded.", name) + + Log.info("ModLoader", "Finished loading %q", mod.name) self._state = self:_load_mod(self._mod_load_index + 1) else @@ -142,7 +148,7 @@ ModLoader.update = function(self, dt) end if old_state ~= self._state then - self:print("info", "%s -> %s", old_state, self._state) + Log.info("ModLoader", "%s -> %s", old_state, self._state) end end @@ -170,20 +176,26 @@ ModLoader._run_callback = function (self, mod, callback_name, ...) return end - local success, val = pcall(cb, object, ...) + local args = table_pack(...) + + local success, val = xpcall(function() return cb(object, table_unpack(args)) end, self._libs.debug.traceback) if success then return val else - self:print("error", "%s", val or "[unknown error]") - self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + Log.error("ModLoader", "Failed to run callback %q for mod %q with id %q. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + if type(val) == "table" then + Log.error("ModLoader", "<>\n<>\n%s\n<>\n<>\n%s\n<>\n<>\n%s\n<>", val.error, val.traceback, val.locals, val.self) + else + Log.error("ModLoader", "Error: %s", val or "[unknown error]") + end mod.callbacks_disabled = true end end ModLoader._start_scan = function(self) - self:print("info", "Starting mod scan") + Log.info("ModLoader", "Starting mod scan") self._state = "scanning" end @@ -191,7 +203,7 @@ ModLoader._build_mod_table = function(self) fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") for i, mod_data in ipairs(self._mod_data) do - printf("[ModLoader] mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) + Log.info("ModLoader", "mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) self._mods[i] = { id = mod_data.id, @@ -206,7 +218,7 @@ ModLoader._build_mod_table = function(self) self._num_mods = #self._mods - self:print("info", "Found %i mods", #self._mods) + Log.info("ModLoader", "Found %i mods", #self._mods) end ModLoader._load_mod = function(self, index) @@ -220,11 +232,11 @@ ModLoader._load_mod = function(self, index) return "done" end - self:print("info", "loading mod %i", mod.id) + Log.info("ModLoader", "Loading mod %q", mod.id) mod.state = "loading" - Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true) + Crashify.print_property(string.format("Mod:%s:%s", mod.id, mod.name), true) self._mod_load_index = index @@ -241,7 +253,7 @@ ModLoader._load_package = function(self, mod, index) return end - self:print("info", "loading package %q", package_name) + Log.info("ModLoader", "Loading package %q", package_name) local resource_handle = Application.resource_package(package_name) self._loading_resource_handle = resource_handle @@ -253,12 +265,12 @@ end ModLoader.unload_all_mods = function(self) if self._state ~= "done" then - self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + Log.error("ModLoader", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) return end - self:print("info", "Unload all mod packages") + Log.info("ModLoader", "Unload all mod packages") for i = self._num_mods, 1, -1 do local mod = self._mods[i] @@ -278,7 +290,7 @@ ModLoader.unload_mod = function(self, index) local mod = self._mods[index] if mod then - self:print("info", "Unloading %q.", mod.name) + Log.info("ModLoader", "Unloading %q.", mod.name) for _, handle in ipairs(mod.loaded_packages) do ResourcePackage.unload(handle) @@ -287,22 +299,22 @@ ModLoader.unload_mod = function(self, index) mod.state = "not_loaded" else - self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + Log.error("ModLoader", "Mod index %i can't be unloaded, has not been loaded", index) end end ModLoader._reload_mods = function(self) - self:print("info", "reloading mods") + Log.info("ModLoader", "reloading mods") for i = 1, self._num_mods, 1 do local mod = self._mods[i] if mod and mod.state == "running" then - self:print("info", "reloading %s", mod.name) + Log.info("ModLoader", "reloading %s", mod.name) self._reload_data[mod.id] = self:_run_callback(mod, "on_reload") else - self:print("info", "not reloading mod, state: %s", mod.state) + Log.info("ModLoader", "not reloading mod, state: %s", mod.state) end end @@ -322,20 +334,21 @@ ModLoader.on_game_state_changed = function(self, status, state_name, state_objec end end else - self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + Log.warning("ModLoader", "Ignored on_game_state_changed call due to being in state %q", self._state) end end ModLoader.print = function(self, level, str, ...) - local message = string.format("[ModLoader][" .. level .. "] " .. str, ...) - local log_level = LOG_LEVELS[level] or 99 + local f = Log[level] + if f then + f("ModLoader", str, ...) + else + local message = string.format("[ModLoader][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 - if log_level <= 2 then - print(message) - end - - if log_level <= self._settings.log_level then - self._chat_print_buffer[#self._chat_print_buffer + 1] = message + if log_level <= 2 then + print(message) + end end end From 23a15cc69281aeac1bc079365487425e93e63bce Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 10:06:49 +0100 Subject: [PATCH 28/34] feat: Add API for DMF to get mod data --- scripts/mods/dml/mod_loader.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 48e941f..3dc2734 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -68,6 +68,25 @@ ModLoader.remove_gui = function(self) self._gui = nil end +ModLoader.mod_data = function(self, id) + -- Since this primarily exists for DMF, + -- we can optimize the search for its use case of looking for the + -- mod currently being loaded + local mod_data = self._mods[self._mod_load_index] + + if mod_data.id ~= id then + mod_data = nil + + for _, v in ipairs(self._mods) do + if v.id == id then + mod_data = v + end + end + end + + return mod_data +end + ModLoader._check_reload = function() return Keyboard.pressed(BUTTON_INDEX_R) and Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + From 33756cd6d5f018668f4b4f392ec38bfa48378299 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 10:07:25 +0100 Subject: [PATCH 29/34] Dump mod data for issue debugging --- scripts/mods/dml/mod_loader.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 3dc2734..bb35d77 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -27,6 +27,7 @@ local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") ModLoader.init = function(self, mod_data, libs, boot_gui) + table.dump(mod_data, nil, 5, function(...) Log.info("ModLoader", ...) end) self._mod_data = mod_data self._libs = libs self._gui = boot_gui From 2d15647fb5f57da36cdd40096a49daaba8ce58a9 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 00:23:41 +0100 Subject: [PATCH 30/34] feat: Move class and require hooks into early loading --- scripts/mods/dml/class.lua | 26 ------------------------ scripts/mods/dml/init.lua | 18 +++++------------ scripts/mods/dml/mod_loader.lua | 8 ++++---- scripts/mods/dml/require.lua | 36 --------------------------------- 4 files changed, 9 insertions(+), 79 deletions(-) delete mode 100644 scripts/mods/dml/class.lua delete mode 100644 scripts/mods/dml/require.lua diff --git a/scripts/mods/dml/class.lua b/scripts/mods/dml/class.lua deleted file mode 100644 index 53896de..0000000 --- a/scripts/mods/dml/class.lua +++ /dev/null @@ -1,26 +0,0 @@ -local original_class = Mods.original_class or class -Mods.original_class = original_class - -local _G = _G -local rawget = rawget -local rawset = rawset - --- The `__index` metamethod maps a proper identifier `CLASS.MyClassName` to the --- stringified version of the key: `"MyClassName"`. --- This allows using LuaCheck for the stringified class names in hook parameters. -_G.CLASS = _G.CLASS or setmetatable({}, { - __index = function(_, key) - return key - end -}) - -class = function(class_name, super_name, ...) - local result = 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 diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index f455539..21a16a6 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -1,3 +1,6 @@ +dofile("scripts/mods/dml/message") +dofile("scripts/mods/dml/hook") + local StateGame = require("scripts/game_states/state_game") local StateSplash = require("scripts/game_states/game/state_splash") local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") @@ -6,20 +9,9 @@ local GameStateMachine = require("scripts/foundation/utilities/game_state_machin -- to initialize the modding environment. local loader = {} -Mods = {} - -function loader:init(mod_data, libs, boot_gui) - -- The metatable prevents overwriting these - self._libs = setmetatable({}, { __index = libs }) - Mods.lua = self._libs - - dofile("scripts/mods/dml/message") - dofile("scripts/mods/dml/require") - dofile("scripts/mods/dml/class") - dofile("scripts/mods/dml/hook") - +function loader:init(mod_data, boot_gui) local ModLoader = dofile("scripts/mods/dml/mod_loader") - local mod_loader = ModLoader:new(mod_data, libs, boot_gui) + local mod_loader = ModLoader:new(mod_data, boot_gui) self._mod_loader = mod_loader Managers.mod = mod_loader diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index bb35d77..d2e5b16 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -26,10 +26,10 @@ local BUTTON_INDEX_R = Keyboard.button_index("r") local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") -ModLoader.init = function(self, mod_data, libs, boot_gui) +ModLoader.init = function(self, mod_data, boot_gui) table.dump(mod_data, nil, 5, function(...) Log.info("ModLoader", ...) end) + self._mod_data = mod_data - self._libs = libs self._gui = boot_gui self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS @@ -142,7 +142,7 @@ ModLoader.update = function(self, dt) if next_index > #mod_data.packages then mod.state = "running" - local ok, object = xpcall(mod_data.run, self._libs.debug.traceback) + local ok, object = xpcall(mod_data.run, Script.callstack) if not ok then Log.error("ModLoader", "Failed 'run' for %q: %s", mod.name, object) @@ -198,7 +198,7 @@ ModLoader._run_callback = function (self, mod, callback_name, ...) local args = table_pack(...) - local success, val = xpcall(function() return cb(object, table_unpack(args)) end, self._libs.debug.traceback) + local success, val = xpcall(function() return cb(object, table_unpack(args)) end, Script.callstack) if success then return val diff --git a/scripts/mods/dml/require.lua b/scripts/mods/dml/require.lua deleted file mode 100644 index bc808a9..0000000 --- a/scripts/mods/dml/require.lua +++ /dev/null @@ -1,36 +0,0 @@ -local require_store = Mods.require_store or {} -Mods.require_store = require_store - -local original_require = Mods.original_require or require -Mods.original_require = original_require - -local can_insert = function(filepath, new_result) - local store = require_store[filepath] - if not store or #store then - return true - end - - if store[#store] ~= new_result then - return true - end -end - -require = function(filepath, ...) - local result = original_require(filepath, ...) - if result and type(result) == "table" then - if can_insert(filepath, result) then - require_store[filepath] = require_store[filepath] or {} - local store = require_store[filepath] - - table.insert(store, result) - - --print("[Require] #" .. tostring(#store) .. " of " .. filepath) - local Mods = Mods - if Mods.hook then - Mods.hook.enable_by_file(filepath, #store) - end - end - end - - return result -end From 3cb511d0f760d379d022dbe6f537b068dac04f84 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 9 Mar 2023 17:40:22 +0100 Subject: [PATCH 31/34] chore: Update config with new fields --- dtmt.cfg | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dtmt.cfg b/dtmt.cfg index 3831232..209d523 100644 --- a/dtmt.cfg +++ b/dtmt.cfg @@ -1,7 +1,12 @@ id = "dml" name = "Darktide Mod Loader" -description = "This is my new mod 'Darktide Mod Loader'!" -version = "0.1.0" +summary = "The low-level facilities that enable loading mods from specially prepared bundles." +version = "23.4.05" +author = "Darktide Modders" + +categories = [ + Tools +] resources = { init = "scripts/mods/dml/init" @@ -10,7 +15,3 @@ resources = { packages = [ "packages/dml" ] - -depends = [ - "dmf" -] From d294f85b88aef45e10f3dff1492182fe10f2322b Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 22 Jul 2023 16:33:07 +0200 Subject: [PATCH 32/34] feat: Allow setting developer mode Since this setting is now partially goverened by DMF's mod options, there needs be some way for DMF to communicate this setting. --- .gitignore | 1 + scripts/mods/dml/mod_loader.lua | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 373df2e..6b2af9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ dml.zip +out/ diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index d2e5b16..eb31db7 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -47,6 +47,10 @@ ModLoader.developer_mode_enabled = function(self) return self._settings.developer_mode end +ModLoader.set_developer_mode = function(self, enabled) + self._settings.developer_mode = enabled +end + ModLoader._draw_state_to_gui = function(self, gui, dt) local state = self._state local t = self._ui_time + dt From 7b8a8d1094a1ceba56d931e66681881036868b54 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 22 Jul 2023 16:38:50 +0200 Subject: [PATCH 33/34] refactor: Format code --- scripts/mods/dml/mod_loader.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index eb31db7..812d7fb 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -168,7 +168,7 @@ ModLoader.update = function(self, dt) local gui = self._gui if gui then - self:_draw_state_to_gui(gui, dt) + self:_draw_state_to_gui(gui, dt) end if old_state ~= self._state then @@ -192,7 +192,7 @@ ModLoader.destroy = function(self) self:unload_all_mods() end -ModLoader._run_callback = function (self, mod, callback_name, ...) +ModLoader._run_callback = function(self, mod, callback_name, ...) local object = mod.object local cb = object[callback_name] @@ -207,9 +207,12 @@ ModLoader._run_callback = function (self, mod, callback_name, ...) if success then return val else - Log.error("ModLoader", "Failed to run callback %q for mod %q with id %q. Disabling callbacks until reload.", callback_name, mod.name, mod.id) + Log.error("ModLoader", "Failed to run callback %q for mod %q with id %q. Disabling callbacks until reload.", + callback_name, mod.name, mod.id) if type(val) == "table" then - Log.error("ModLoader", "<>\n<>\n%s\n<>\n<>\n%s\n<>\n<>\n%s\n<>", val.error, val.traceback, val.locals, val.self) + Log.error("ModLoader", + "<>\n<>\n%s\n<>\n<>\n%s\n<>\n<>\n%s\n<>", + val.error, val.traceback, val.locals, val.self) else Log.error("ModLoader", "Error: %s", val or "[unknown error]") end @@ -350,13 +353,13 @@ end ModLoader.on_game_state_changed = function(self, status, state_name, state_object) if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] - if mod and not mod.callbacks_disabled then - self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) - end - end + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) + end + end else Log.warning("ModLoader", "Ignored on_game_state_changed call due to being in state %q", self._state) end From 6c5604cb1cb656effaaa19cf09446dddd18ee96e Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 10 Nov 2023 08:20:41 +0100 Subject: [PATCH 34/34] Implement loading non-bundled mods --- scripts/mods/dml/init.lua | 17 +++-- scripts/mods/dml/mod_loader.lua | 121 +++++++++++++++++++------------- 2 files changed, 80 insertions(+), 58 deletions(-) diff --git a/scripts/mods/dml/init.lua b/scripts/mods/dml/init.lua index 21a16a6..2c729b4 100644 --- a/scripts/mods/dml/init.lua +++ b/scripts/mods/dml/init.lua @@ -7,13 +7,11 @@ local GameStateMachine = require("scripts/foundation/utilities/game_state_machin -- The loader object that is used during game boot -- to initialize the modding environment. -local loader = {} +local DML = {} -function loader:init(mod_data, boot_gui) +function DML.create_loader(mod_data, boot_gui) local ModLoader = dofile("scripts/mods/dml/mod_loader") local mod_loader = ModLoader:new(mod_data, boot_gui) - self._mod_loader = mod_loader - Managers.mod = mod_loader -- The mod loader needs to remain active during game play, to -- enable reloads @@ -64,10 +62,11 @@ function loader:init(mod_data, boot_gui) return func(self, ...) end) + + return mod_loader end -function loader:update(dt) - local mod_loader = self._mod_loader +function DML.update(mod_loader, dt) mod_loader:update(dt) local done = mod_loader:all_mods_loaded() @@ -78,8 +77,8 @@ function loader:update(dt) return done end -function loader:done() - return self._mod_loader:all_mods_loaded() +function DML.done(mod_loader) + return mod_loader:all_mods_loaded() end -return loader +return DML diff --git a/scripts/mods/dml/mod_loader.lua b/scripts/mods/dml/mod_loader.lua index 812d7fb..4da08a4 100644 --- a/scripts/mods/dml/mod_loader.lua +++ b/scripts/mods/dml/mod_loader.lua @@ -59,13 +59,12 @@ ModLoader._draw_state_to_gui = function(self, gui, dt) if state == "scanning" then status_str = "Scanning for mods" - elseif state == "loading" then + elseif state == "loading" or state == "initializing" then local mod = self._mods[self._mod_load_index] status_str = string.format("Loading mod %q", mod.name) end local msg = status_str .. string.rep(".", (2 * t) % 4) - Log.info("ModLoader", msg) ScriptGui.text(gui, msg, FONT_MATERIAL, 25, Vector3(20, 30, 1), Color.white()) end @@ -117,24 +116,19 @@ ModLoader.update = function(self, dt) self._reload_requested = true end - if self._reload_requested and self._state == "done" then + if self._reload_requested and old_state == "done" then self:_reload_mods() end - if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and not mod.callbacks_disabled then - self:_run_callback(mod, "update", dt) - end - end - elseif self._state == "scanning" then + if old_state == "done" then + self:_run_callbacks("update", dt) + elseif old_state == "scanning" then + Log.info("ModLoader", "Scanning for mods") self:_build_mod_table() self._state = self:_load_mod(1) self._ui_time = 0 - elseif self._state == "loading" then + elseif old_state == "loading" then local handle = self._loading_resource_handle if ResourcePackage.has_loaded(handle) then @@ -144,29 +138,47 @@ ModLoader.update = function(self, dt) local next_index = mod.package_index + 1 local mod_data = mod.data - if next_index > #mod_data.packages then - mod.state = "running" - local ok, object = xpcall(mod_data.run, Script.callstack) - - if not ok then - Log.error("ModLoader", "Failed 'run' for %q: %s", mod.name, object) - end - - mod.object = object or {} - - self:_run_callback(mod, "init", self._reload_data[mod.id]) - - Log.info("ModLoader", "Finished loading %q", mod.name) - - self._state = self:_load_mod(self._mod_load_index + 1) - else + if next_index <= #mod_data.packages then self:_load_package(mod, next_index) + else + self._state = "initializing" end end + elseif old_state == "initializing" then + local mod = self._mods[self._mod_load_index] + local mod_data = mod.data + + Log.info("ModLoader", "Initializing mod %q", mod.name) + + mod.state = "running" + local ok, object = xpcall(mod_data.run, function(err) + if type(err) == "string" then + return err .. "\n" .. Script.callstack() + else + return err + end + end) + + if not ok then + if object.error then + object = string.format( + "%s\n<>\n%s\n<>\n<>\n%s\n<>\n<>\n%s\n<>", + object.error, object.traceback, object.locals, object.self) + end + + Log.error("ModLoader", "Failed 'run' for %q: %s", mod.name, object) + end + + mod.object = object or {} + + self:_run_callback(mod, "init", self._reload_data[mod.id]) + + Log.info("ModLoader", "Finished loading %q", mod.name) + + self._state = self:_load_mod(self._mod_load_index + 1) end local gui = self._gui - if gui then self:_draw_state_to_gui(gui, dt) end @@ -181,15 +193,18 @@ ModLoader.all_mods_loaded = function(self) end ModLoader.destroy = function(self) + self:_run_callbacks("on_destroy") + self:unload_all_mods() +end + +ModLoader._run_callbacks = function(self, callback_name, ...) for i = 1, self._num_mods, 1 do local mod = self._mods[i] if mod and not mod.callbacks_disabled then - self:_run_callback(mod, "on_destroy") + self:_run_callback(mod, callback_name, ...) end end - - self:unload_all_mods() end ModLoader._run_callback = function(self, mod, callback_name, ...) @@ -202,16 +217,25 @@ ModLoader._run_callback = function(self, mod, callback_name, ...) local args = table_pack(...) - local success, val = xpcall(function() return cb(object, table_unpack(args)) end, Script.callstack) + local success, val = xpcall( + function() return cb(object, table_unpack(args)) end, + function(err) + if type(err) == "string" then + return err .. "\n" .. Script.callstack() + else + return err + end + end + ) if success then return val else Log.error("ModLoader", "Failed to run callback %q for mod %q with id %q. Disabling callbacks until reload.", callback_name, mod.name, mod.id) - if type(val) == "table" then + if val.error then Log.error("ModLoader", - "<>\n<>\n%s\n<>\n<>\n%s\n<>\n<>\n%s\n<>", + "Error: %s\n<>\n%s<>\n<>\n%s<>\n<>\n%s<>", val.error, val.traceback, val.locals, val.self) else Log.error("ModLoader", "Error: %s", val or "[unknown error]") @@ -230,7 +254,8 @@ ModLoader._build_mod_table = function(self) fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") for i, mod_data in ipairs(self._mod_data) do - Log.info("ModLoader", "mods[%d] = id=%q | name=%q", i, mod_data.id, mod_data.name) + Log.info("ModLoader", "mods[%d] = id=%q | name=%q | bundled=%s", i, mod_data.id, mod_data.name, + tostring(mod_data.bundled)) self._mods[i] = { id = mod_data.id, @@ -240,12 +265,13 @@ ModLoader._build_mod_table = function(self) loaded_packages = {}, packages = mod_data.packages, data = mod_data, + bundled = mod_data.bundled or false, } end self._num_mods = #self._mods - Log.info("ModLoader", "Found %i mods", #self._mods) + Log.info("ModLoader", "Found %i mods", self._num_mods) end ModLoader._load_mod = function(self, index) @@ -267,9 +293,12 @@ ModLoader._load_mod = function(self, index) self._mod_load_index = index - self:_load_package(mod, 1) - - return "loading" + if mod.bundled and mod.packages[1] then + self:_load_package(mod, 1) + return "loading" + else + return "initializing" + end end ModLoader._load_package = function(self, mod, index) @@ -287,7 +316,7 @@ ModLoader._load_package = function(self, mod, index) ResourcePackage.load(resource_handle) - mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle + table.insert(mod.loaded_packages, resource_handle) end ModLoader.unload_all_mods = function(self) @@ -353,13 +382,7 @@ end ModLoader.on_game_state_changed = function(self, status, state_name, state_object) if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and not mod.callbacks_disabled then - self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) - end - end + self:_run_callbacks("on_game_state_changed", status, state_name, state_object) else Log.warning("ModLoader", "Ignored on_game_state_changed call due to being in state %q", self._state) end