Modding Improvements #18
| @@ -10,6 +10,7 @@ | ||||
|  | ||||
| #include <utils/hook.hpp> | ||||
| #include <utils/memory.hpp> | ||||
| #include <utils/string.hpp> | ||||
| #include <utils/io.hpp> | ||||
|  | ||||
| namespace fastfiles | ||||
| @@ -19,6 +20,25 @@ namespace fastfiles | ||||
| 		utils::hook::detour db_try_load_x_file_internal_hook; | ||||
| 		utils::hook::detour db_find_x_asset_header_hook; | ||||
|  | ||||
| 		template <typename T> inline void merge(std::vector<T>* target, T* source, size_t length) | ||||
| 		{ | ||||
| 			if (source) | ||||
| 			{ | ||||
| 				for (size_t i = 0; i < length; ++i) | ||||
| 				{ | ||||
| 					target->push_back(source[i]); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		template <typename T> inline void merge(std::vector<T>* target, std::vector<T> source) | ||||
| 		{ | ||||
| 			for (auto& entry : source) | ||||
| 			{ | ||||
| 				target->push_back(entry); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void db_try_load_x_file_internal(const char* zone_name, const int zone_flags, const int is_base_map) | ||||
| 		{ | ||||
| 			console::info("Loading fastfile %s\n", zone_name); | ||||
| @@ -90,6 +110,42 @@ namespace fastfiles | ||||
| 			console::info("Unloaded fastfile %s\n", name); | ||||
| 			game::PMem_Free(name, alloc_dir); | ||||
| 		} | ||||
|  | ||||
| 		const auto skip_extra_zones_stub = utils::hook::assemble([](utils::hook::assembler& a) | ||||
| 		{ | ||||
| 			const auto skip = a.newLabel(); | ||||
| 			const auto original = a.newLabel(); | ||||
|  | ||||
| 			a.pushad64(); | ||||
| 			a.test(ebp, game::DB_ZONE_CUSTOM); // allocFlags | ||||
| 			a.jnz(skip); | ||||
|  | ||||
| 			a.bind(original); | ||||
| 			a.popad64(); | ||||
| 			a.mov(rdx, 0x140835F28); | ||||
| 			a.mov(rcx, rsi); | ||||
| 			a.call_aligned(strcmp); | ||||
| 			a.jmp(0x1403217C0); | ||||
|  | ||||
| 			a.bind(skip); | ||||
| 			a.popad64(); | ||||
| 			a.mov(r15d, 0x80); | ||||
| 			a.not_(r15d); | ||||
| 			a.and_(ebp, r15d); | ||||
| 			a.jmp(0x1403217F6); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	bool exists(const std::string& zone_name) | ||||
| 	{ | ||||
| 		auto is_localized = game::DB_IsLocalized(zone_name.data()); | ||||
| 		auto handle = game::Sys_CreateFile(game::Sys_Folder(is_localized), utils::string::va("%s.ff", zone_name.data())); | ||||
| 		if (handle != (HANDLE)-1) | ||||
| 		{ | ||||
| 			CloseHandle(handle); | ||||
| 			return true; | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	void enum_assets(const game::XAssetType type, const std::function<void(game::XAssetHeader)>& callback, const bool include_override) | ||||
| @@ -101,6 +157,57 @@ namespace fastfiles | ||||
| 		}), &callback, include_override); | ||||
| 	} | ||||
|  | ||||
| 	void Load_CommonZones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) | ||||
| 	{ | ||||
| 		std::vector<game::XZoneInfo> data; | ||||
| 		merge(&data, zoneInfo, zoneCount); | ||||
|  | ||||
| 		if (fastfiles::exists("mod")) | ||||
| 		{ | ||||
| 			data.push_back({ "mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0 }); | ||||
| 		} | ||||
|  | ||||
| 		game::DB_LoadXAssets(data.data(), static_cast<std::uint32_t>(data.size()), syncMode); | ||||
| 	} | ||||
|  | ||||
| 	void Load_LevelZones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) | ||||
| 	{ | ||||
| 		std::vector<game::XZoneInfo> data; | ||||
| 		merge(&data, zoneInfo, zoneCount); | ||||
|  | ||||
| 		if (fastfiles::exists("mod")) | ||||
| 		{ | ||||
| 			data.push_back({ "mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0 }); | ||||
| 		} | ||||
|  | ||||
| 		game::DB_LoadXAssets(data.data(), static_cast<std::uint32_t>(data.size()), syncMode); | ||||
| 	} | ||||
|  | ||||
| 	utils::hook::detour sys_createfile_hook; | ||||
| 	HANDLE sys_create_file_stub(game::Sys_Folder folder, const char* base_filename) | ||||
| 	{ | ||||
| 		auto* fs_basepath = game::Dvar_FindVar("fs_basepath"); | ||||
| 		auto* fs_game = game::Dvar_FindVar("fs_game"); | ||||
|  | ||||
| 		std::string dir = fs_basepath ? fs_basepath->current.string : ""; | ||||
| 		std::string mod_dir = fs_game ? fs_game->current.string : ""; | ||||
|  | ||||
| 		if (base_filename == "mod.ff"s) | ||||
| 		{ | ||||
| 			if (!mod_dir.empty()) | ||||
| 			{ | ||||
| 				auto path = utils::string::va("%s\\%s\\%s", dir.data(), mod_dir.data(), base_filename); | ||||
| 				if (utils::io::file_exists(path)) | ||||
| 				{ | ||||
| 					return CreateFileA(path, 0x80000000, 1u, 0, 3u, 0x60000000u, 0); | ||||
| 				} | ||||
| 			} | ||||
| 			return (HANDLE)-1; | ||||
| 		} | ||||
|  | ||||
| 		return sys_createfile_hook.invoke<HANDLE>(folder, base_filename); | ||||
| 	} | ||||
|  | ||||
| 	class component final : public component_interface | ||||
| 	{ | ||||
| 	public: | ||||
| @@ -118,6 +225,19 @@ namespace fastfiles | ||||
| 			utils::hook::set<uint8_t>(0x1402FBF23, 0xEB); // DB_LoadXFile | ||||
| 			utils::hook::nop(0x1402FC445, 2); // DB_SetFileLoadCompressor | ||||
|  | ||||
| 			// Don't load eng_ + patch_ with loadzone | ||||
| 			utils::hook::nop(0x1403217B1, 15); | ||||
| 			utils::hook::jump(0x1403217B1, skip_extra_zones_stub, true); | ||||
|  | ||||
| 			// Add custom zone paths | ||||
| 			sys_createfile_hook.create(game::Sys_CreateFile, sys_create_file_stub); | ||||
|  | ||||
| 			// Load our custom fastfiles (Mod) | ||||
| 			utils::hook::call(0x1405E7113, Load_CommonZones); | ||||
|  | ||||
| 			// Reload mod after level is loaded to allow overriding map stuff | ||||
| 			utils::hook::call(0x140320ED1, Load_LevelZones); | ||||
|  | ||||
| 			command::add("materiallist", [](const command::params& params) | ||||
| 			{ | ||||
| 				game::DB_EnumXAssets_FastFile(game::ASSET_TYPE_MATERIAL, [](const game::XAssetHeader header, void*) | ||||
|   | ||||
| @@ -3,16 +3,27 @@ | ||||
| #include "game/game.hpp" | ||||
| #include "game/dvars.hpp" | ||||
|  | ||||
| #include "command.hpp" | ||||
| #include "console.hpp" | ||||
| #include "filesystem.hpp" | ||||
| #include "scheduler.hpp" | ||||
|  | ||||
| #include "mods.hpp" | ||||
|  | ||||
| #include <utils/hook.hpp> | ||||
| #include <utils/io.hpp> | ||||
|  | ||||
| namespace mods | ||||
| { | ||||
|  | ||||
| 	std::optional<std::string> mod_path; | ||||
|  | ||||
| 	namespace | ||||
| 	{ | ||||
| 		utils::hook::detour sys_create_file_hook; | ||||
|  | ||||
| 		bool release_assets = false; | ||||
|  | ||||
| 		void db_build_os_path_from_source(const char* zone_name, game::FF_DIR source, unsigned int size, char* filename) | ||||
| 		{ | ||||
| 			char user_map[MAX_PATH]{}; | ||||
| @@ -38,66 +49,74 @@ namespace mods | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		game::Sys_File sys_create_file_stub(const char* dir, const char* filename) | ||||
| 		void restart() | ||||
| 		{ | ||||
| 			auto result = sys_create_file_hook.invoke<game::Sys_File>(dir, filename); | ||||
|  | ||||
| 			if (result.handle != INVALID_HANDLE_VALUE) | ||||
| 			scheduler::once([]() | ||||
| 			{ | ||||
| 				return result; | ||||
| 			} | ||||
|  | ||||
| 			if (!is_using_mods()) | ||||
| 				release_assets = true; | ||||
| 				const auto _0 = gsl::finally([]() | ||||
| 					{ | ||||
| 				return result; | ||||
| 			} | ||||
|  | ||||
| 			// .ff extension was added previously | ||||
| 			if (!std::strcmp(filename, "mod.ff") && mods::db_mod_file_exists()) | ||||
| 			{ | ||||
| 				char file_path[MAX_PATH]{}; | ||||
| 				db_build_os_path_from_source("mod", game::FFD_MOD_DIR, sizeof(file_path), file_path); | ||||
| 				result.handle = game::Sys_OpenFileReliable(file_path); | ||||
| 			} | ||||
|  | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		void db_load_x_assets_stub(game::XZoneInfo* zone_info, unsigned int zone_count, game::DBSyncMode sync_mode) | ||||
| 		{ | ||||
| 			std::vector<game::XZoneInfo> zones(zone_info, zone_info + zone_count); | ||||
|  | ||||
| 			if (db_mod_file_exists()) | ||||
| 			{ | ||||
| 				zones.emplace_back("mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0); | ||||
| 			} | ||||
|  | ||||
| 			game::DB_LoadXAssets(zones.data(), static_cast<unsigned int>(zones.size()), sync_mode); | ||||
| 		} | ||||
|  | ||||
| 		const auto skip_extra_zones_stub = utils::hook::assemble([](utils::hook::assembler& a) | ||||
| 		{ | ||||
| 			const auto skip = a.newLabel(); | ||||
| 			const auto original = a.newLabel(); | ||||
|  | ||||
| 			a.pushad64(); | ||||
| 			a.test(ebp, game::DB_ZONE_CUSTOM); // allocFlags | ||||
| 			a.jnz(skip); | ||||
|  | ||||
| 			a.bind(original); | ||||
| 			a.popad64(); | ||||
| 			a.mov(rdx, 0x140835F28); | ||||
| 			a.mov(rcx, rsi); | ||||
| 			a.call_aligned(strcmp); | ||||
| 			a.jmp(0x1403217C0); | ||||
|  | ||||
| 			a.bind(skip); | ||||
| 			a.popad64(); | ||||
| 			a.mov(r15d, 0x80); | ||||
| 			a.not_(r15d); | ||||
| 			a.and_(ebp, r15d); | ||||
| 			a.jmp(0x1403217F6); | ||||
| 						release_assets = false; | ||||
| 					}); | ||||
|  | ||||
| 				game::Com_Shutdown(""); | ||||
| 			}, scheduler::pipeline::main); | ||||
| 		} | ||||
|  | ||||
| 		void full_restart(const std::string& arg) | ||||
| 		{ | ||||
| 			if (game::environment::is_mp()) | ||||
| 			{ | ||||
| 				command::execute("vid_restart"); | ||||
| 				scheduler::once([] | ||||
| 					{ | ||||
| 						//mods::read_stats(); | ||||
| 					}, scheduler::main); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			auto mode = game::environment::is_mp() ? " -multiplayer "s : " -singleplayer "s; | ||||
|  | ||||
| 			utils::nt::relaunch_self(); | ||||
| 			utils::nt::terminate(); | ||||
| 		} | ||||
|  | ||||
| 		bool mod_requires_restart(const std::string& path) | ||||
| 		{ | ||||
| 			return utils::io::file_exists(path + "/mod.ff") || utils::io::file_exists(path + "/zone/mod.ff"); | ||||
| 		} | ||||
|  | ||||
| 		void set_filesystem_data(const std::string& path, bool change_fs_game) | ||||
| 		{ | ||||
| 			if (mod_path.has_value()) | ||||
| 			{ | ||||
| 				filesystem::unregister_path(mod_path.value()); | ||||
| 			} | ||||
|  | ||||
| 			if (change_fs_game) | ||||
| 			{ | ||||
| 				game::Dvar_SetFromStringByNameFromSource("fs_game", path.data(), game::DVAR_SOURCE_INTERNAL); | ||||
| 			} | ||||
|  | ||||
| 			if (path != "") | ||||
| 			{ | ||||
| 				filesystem::register_path(path); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	void set_mod(const std::string& path, bool change_fs_game) | ||||
| 	{ | ||||
| 		set_filesystem_data(path, change_fs_game); | ||||
|  | ||||
| 		if (path != "") | ||||
| 		{ | ||||
| 			mod_path = path; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			mod_path.reset(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	bool is_using_mods() | ||||
| @@ -141,15 +160,72 @@ namespace mods | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// Don't load eng_ + patch_ with loadzone | ||||
| 			utils::hook::nop(0x1403217B1, 15); | ||||
| 			utils::hook::jump(0x1403217B1, skip_extra_zones_stub, true); | ||||
| 			command::add("loadmod", [](const command::params& params) | ||||
| 			{ | ||||
| 				if (params.size() < 2) | ||||
| 				{ | ||||
| 					console::info("Usage: loadmod mods/<modname>"); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 			// Add custom zone paths | ||||
| 			sys_create_file_hook.create(game::Sys_CreateFile, sys_create_file_stub); | ||||
| 				/*if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) | ||||
| 				{ | ||||
| 					console::info("Cannot load mod while in-game!\n"); | ||||
| 					game::CG_GameMessage(0, "^1Cannot load mod while in-game!"); | ||||
| 					return; | ||||
| 				}*/ | ||||
|  | ||||
| 			// Load mod.ff | ||||
| 			utils::hook::call(0x1405E7113, db_load_x_assets_stub); // R_LoadGraphicsAssets According to myself but I don't remember where I got it from | ||||
| 				const auto path = params.get(1); | ||||
| 				if (!utils::io::directory_exists(path)) | ||||
| 				{ | ||||
| 					console::info("Mod %s not found!\n", path); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				console::info("Loading mod %s\n", path); | ||||
| 				set_mod(path, true); | ||||
|  | ||||
| 				if ((mod_path.has_value() && mod_requires_restart(mod_path.value())) || | ||||
| 					mod_requires_restart(path)) | ||||
| 				{ | ||||
| 					console::info("Restarting...\n"); | ||||
| 					full_restart("-mod \""s + path + "\""); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					restart(); | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
| 			command::add("unloadmod", [](const command::params& params) | ||||
| 			{ | ||||
| 				if (!mod_path.has_value()) | ||||
| 				{ | ||||
| 					console::info("No mod loaded\n"); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				/*if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) | ||||
| 				{ | ||||
| 					console::info("Cannot unload mod while in-game!\n"); | ||||
| 					game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); | ||||
| 					return; | ||||
| 				}*/ | ||||
|  | ||||
| 				console::info("Unloading mod %s\n", mod_path.value().data()); | ||||
|  | ||||
| 				if (mod_requires_restart(mod_path.value())) | ||||
| 				{ | ||||
| 					console::info("Restarting...\n"); | ||||
| 					set_mod("", true); | ||||
| 					full_restart(""); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					set_mod("", true); | ||||
| 					restart(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
|   | ||||
| @@ -195,6 +195,27 @@ namespace ui_scripting | ||||
| 			setup_functions(); | ||||
|  | ||||
| 			lua["print"] = function(reinterpret_cast<game::hks::lua_function>(0x14017B120)); // hks::base_print | ||||
|  | ||||
| 			lua["directoryexists"] = [](const std::string& string) | ||||
| 			{ | ||||
| 				return utils::io::directory_exists(string); | ||||
| 			}; | ||||
|  | ||||
| 			lua["listfiles"] = [](const std::string& string) | ||||
| 			{ | ||||
| 				return utils::io::list_files(string); | ||||
| 			}; | ||||
|  | ||||
| 			lua["directoryisempty"] = [](const std::string& string) | ||||
| 			{ | ||||
| 				return utils::io::directory_is_empty(string); | ||||
| 			}; | ||||
|  | ||||
| 			lua["fileexists"] = [](const std::string& string) | ||||
| 			{ | ||||
| 				return utils::io::file_exists(string); | ||||
| 			}; | ||||
|  | ||||
| 			lua["table"]["unpack"] = lua["unpack"]; | ||||
| 			lua["luiglobals"] = lua; | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,8 @@ namespace game | ||||
| 	 * Functions | ||||
| 	 **************************************************************/ | ||||
|  | ||||
| 	WEAK symbol<void(char const* finalMessage)> Com_Shutdown{ 0x0, 0x140415B30 }; | ||||
|  | ||||
| 	WEAK symbol<void(unsigned int id)> AddRefToObject{0x1403D7A10, 0x1404326D0}; | ||||
| 	WEAK symbol<void(int type, VariableUnion u)> AddRefToValue{0x1403D7740, 0x1404326E0}; | ||||
| 	WEAK symbol<unsigned int(unsigned int id)> AllocThread{0, 0x1404329B0}; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user