From 41564b65787ef3a3e35b89fd88151766b753df09 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:55:27 +0100 Subject: [PATCH] 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