diff --git a/vmf/scripts/mods/vmf/modules/vmf_mod_manager.lua b/vmf/scripts/mods/vmf/modules/vmf_mod_manager.lua index a64a6fe..9b286c4 100644 --- a/vmf/scripts/mods/vmf/modules/vmf_mod_manager.lua +++ b/vmf/scripts/mods/vmf/modules/vmf_mod_manager.lua @@ -116,6 +116,14 @@ function vmf.initialize_mod_data(mod, mod_data) vmf.set_internal_data(mod, "is_mutator", mod_data.is_mutator) vmf.set_internal_data(mod, "allow_rehooking", mod_data.allow_rehooking) + local mod_manager = Managers.mod + local current_mod_load_index = mod_manager._mod_load_index + if current_mod_load_index then + vmf.set_internal_data(mod, "mod_handle", mod_manager._mods[current_mod_load_index].handle) + else + mod:warning("Could not determine current mod load index. Package management won't be available for this mod.") + end + -- Register mod as mutator @TODO: calling this after options initialization would be better, I guess? if mod_data.is_mutator then vmf.register_mod_as_mutator(mod, mod_data.mutator_settings) @@ -163,4 +171,4 @@ end -- VARIABLES vmf.mods = _mods -vmf.mods_unloading_order = _mods_unloading_order \ No newline at end of file +vmf.mods_unloading_order = _mods_unloading_order diff --git a/vmf/scripts/mods/vmf/modules/vmf_package_manager.lua b/vmf/scripts/mods/vmf/modules/vmf_package_manager.lua new file mode 100644 index 0000000..704364c --- /dev/null +++ b/vmf/scripts/mods/vmf/modules/vmf_package_manager.lua @@ -0,0 +1,185 @@ +local vmf = get_mod("VMF") + +local NOOP = function() end + +local _queued_packages = {} +local _loading_package = nil +local _loaded_packages = {} + +-- ##################################################################################################################### +-- ##### VMFMod ######################################################################################################## +-- ##################################################################################################################### + +--[[ + Loads a mod package. + * package_name [string] : package name. needs to be the full path to the `.package` file without the extension + * callback [function]: (optional) callback for asynchronous loading + * sync [boolean] : (optional) load the packages synchronously, freezing the game until it is loaded +--]] +function VMFMod:load_package(package_name, callback, sync) + if vmf.check_wrong_argument_type(self, "load_package", "package_name", package_name, "string") or + vmf.check_wrong_argument_type(self, "load_package", "callback", callback, "function", "nil") or + vmf.check_wrong_argument_type(self, "load_package", "sync", sync, "boolean", "nil") + then + return + end + + if self:has_package_loaded(package_name) then + self:error("Package '%s' has already been loaded", package_name) + return + end + + local mod_handle = self:get_internal_data("mod_handle") + if not mod_handle then + self:error("Failed to get mod handle. Package management is not available.") + return + end + + if not _loaded_packages[self] then + _loaded_packages[self] = {} + end + + local resource_package = Mod.resource_package(mod_handle, package_name) + if not resource_package then + self:error("Could not find package '%s'.", package_name) + return + end + + local is_loading = self:is_package_loading(package_name) + + if sync then + if not is_loading then + resource_package:load() + end + + resource_package:flush() + + _loaded_packages[self][package_name] = resource_package + else + if is_loading then + self:error("Package '%s' is currently loading", package_name) + return + end + + table.insert(_queued_packages, { + mod = self, + package_name = package_name, + resource_package = resource_package, + callback = callback or NOOP, + }) + end +end + + +--[[ + Unlaods a loaded mod package. + * package_name [string]: package name. needs to be the full path to the `.package` file without the extension +--]] +function VMFMod:unload_package(package_name) + if vmf.check_wrong_argument_type(self, "unload_package", "package_name", package_name, "string") then + return + end + + if not self:has_package_loaded(package_name) then + self:error("Package '%s' has not been loaded", package_name) + return + end + + local resource_package = _loaded_packages[self][package_name] + + resource_package:unload() + Mod.release_resource_package(resource_package) + _loaded_packages[self][package_name] = nil +end + + +--[[ + Returns whether the mod package is currently being loaded. + * package_name [string]: package name. needs to be the full path to the `.package` file without the extension +--]] +function VMFMod:is_package_loading(package_name) + if vmf.check_wrong_argument_type(self, "is_package_loading", "package_name", package_name, "string") then + return + end + + if _loading_package and _loading_package.mod == self and _loading_package.package_name == package_name then + return true + end + + for _, queued_package in ipairs(_queued_packages) do + if queued_package.mod == self and queued_package.package_name == package_name then + return true + end + end + + return false +end + + +--[[ + Returns whether the mod package has been fully loaded. + * package_name [string]: package name. needs to be the full path to the `.package` file without the extension +--]] +function VMFMod:has_package_loaded(package_name) + if vmf.check_wrong_argument_type(self, "has_package_loaded", "package_name", package_name, "string") then + return + end + + local loaded_packages = _loaded_packages[self] + return loaded_packages and loaded_packages[package_name] ~= nil +end + +-- ##################################################################################################################### +-- ##### VMF internal functions and variables ########################################################################## +-- ##################################################################################################################### + +-- Loads queued packages one at a time +function vmf.update_package_manager() + local loading_package = _loading_package + if loading_package and loading_package.resource_package:has_loaded() then + loading_package.resource_package:flush() + + _loaded_packages[loading_package.mod][loading_package.package_name] = loading_package.resource_package + _loading_package = nil + + -- The callback has to be called last, so that any calls to `has_package_loaded` or `is_package_loading` + -- return the correct value + vmf.safe_call_nr(loading_package.mod, {"'%s' package loaded callback", loading_package.package_name}, + loading_package.callback, loading_package.package_name) + end + + local queued_package = _queued_packages[1] + if queued_package and not _loading_package then + _loading_package = queued_package + table.remove(_queued_packages, 1) + + _loading_package.resource_package:load() + end +end + + +-- Forcefully unloads all mods and cleans the queue. +function vmf.unload_all_resource_packages() + for mod, packages in pairs(_loaded_packages) do + for package_name in pairs(packages) do + mod:warning( + "Force-unloading package '%s'. Please make sure to properly release packages when the mod is unloaded", + package_name + ) + mod:unload_package(package_name) + end + end + + _queued_packages = {} + + if _loading_package then + _loading_package.mod:warning( + "Still loading package '%s'. Memory leaks may occur when unloading while a package is loading.", + _loading_package.package_name + ) + + _loading_package.callback = function(package_name) + _loading_package.mod:unload_package(package_name) + end + end +end \ No newline at end of file diff --git a/vmf/scripts/mods/vmf/vmf_loader.lua b/vmf/scripts/mods/vmf/vmf_loader.lua index 8c998a7..018b7d9 100644 --- a/vmf/scripts/mods/vmf/vmf_loader.lua +++ b/vmf/scripts/mods/vmf/vmf_loader.lua @@ -13,6 +13,7 @@ local vmf_mod_object = {} function vmf_mod_object:init() dofile("scripts/mods/vmf/modules/vmf_mod_data") dofile("scripts/mods/vmf/modules/vmf_mod_manager") + dofile("scripts/mods/vmf/modules/vmf_package_manager") dofile("scripts/mods/vmf/modules/core/safe_calls") dofile("scripts/mods/vmf/modules/core/events") dofile("scripts/mods/vmf/modules/core/settings") @@ -55,6 +56,7 @@ end -- ##################################################################################################################### function vmf_mod_object:update(dt) + vmf.update_package_manager() vmf.mods_update_event(dt) vmf.check_keybinds() vmf.execute_queued_chat_command() @@ -90,6 +92,7 @@ function vmf_mod_object:on_reload() if VT1 then vmf.reset_map_view() end vmf.mods_unload_event(false) vmf.remove_custom_views() + vmf.unload_all_resource_packages() vmf.hooks_unload() vmf.reset_guis() end