allow loading of custom zones (basic mod support) (#172)

* allow loading of custom zones

* remove game::

* remove unnecessary hook

* mod support

- fs_game support
- precache all weapon files loaded in database
- parse startup variables correctly

Co-authored-by: quaK <38787176+Joelrau@users.noreply.github.com>
This commit is contained in:
m 2022-08-16 15:23:36 -07:00 committed by GitHub
parent fd7d136bf3
commit 690310fd76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 421 additions and 8 deletions

View File

@ -11,6 +11,7 @@
#include "fastfiles.hpp"
#include "scheduler.hpp"
#include "logfile.hpp"
#include "dvars.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
@ -22,7 +23,6 @@ namespace command
namespace
{
utils::hook::detour client_command_hook;
utils::hook::detour parse_commandline_hook;
std::unordered_map<std::string, std::function<void(params&)>> handlers;
std::unordered_map<std::string, std::function<void(int, params_sv&)>> handlers_sv;
@ -105,10 +105,44 @@ namespace command
parsed = true;
}
void parse_commandline_stub()
void parse_startup_variables()
{
auto& com_num_console_lines = *reinterpret_cast<int*>(0x35634B8_b);
auto* com_console_lines = reinterpret_cast<char**>(0x35634C0_b);
for (int i = 0; i < com_num_console_lines; i++)
{
game::Cmd_TokenizeString(com_console_lines[i]);
// only +set dvar value
if (game::Cmd_Argc() >= 3 && game::Cmd_Argv(0) == "set"s)
{
const std::string& dvar_name = game::Cmd_Argv(1);
const std::string& value = game::Cmd_Argv(2);
const auto* dvar = game::Dvar_FindVar(dvar_name.data());
if (dvar)
{
game::Dvar_SetCommand(dvar->hash, "", value.data());
}
else
{
dvars::on_register(dvar_name, [dvar_name, value]()
{
game::Dvar_SetCommand(game::generateHashValue(dvar_name.data()), "", value.data());
});
}
}
game::Cmd_EndTokenizeString();
}
}
void parse_commandline_stub(char* commandline)
{
//utils::hook::invoke<void>(0x17CB60_b, commandline); // Com_ParseCommandLine
parse_command_line();
parse_commandline_hook.invoke<void>();
parse_startup_variables();
}
game::dvar_t* dvar_command_stub()
@ -531,7 +565,7 @@ namespace command
}
else
{
parse_commandline_hook.create(0x157D50_b, parse_commandline_stub);
utils::hook::call(0x15C44B_b, parse_commandline_stub);
add_commands_mp();
}

View File

@ -263,6 +263,8 @@ namespace dvars
utils::hook::detour dvar_register_vector3_hook;
utils::hook::detour dvar_register_enum_hook;
utils::hook::detour dvar_register_new_hook;
utils::hook::detour dvar_set_bool_hook;
utils::hook::detour dvar_set_float_hook;
utils::hook::detour dvar_set_int_hook;
@ -407,6 +409,26 @@ namespace dvars
return dvar_register_enum_hook.invoke<game::dvar_t*>(hash, name, value_list, default_index, flags);
}
std::unordered_map<int, std::function<void()>> dvar_on_register_function_map;
void on_register(const std::string& name, const std::function<void()>& callback)
{
dvar_on_register_function_map[game::generateHashValue(name.data())] = callback;
}
game::dvar_t* dvar_register_new(const int hash, const char* name, game::dvar_type type, unsigned int flags,
game::dvar_value* value, game::dvar_limits* domain, const char* description)
{
auto* dvar = dvar_register_new_hook.invoke<game::dvar_t*>(hash, name, type, flags, value, domain, description);
if (dvar && dvar_on_register_function_map.find(hash) != dvar_on_register_function_map.end())
{
dvar_on_register_function_map[hash]();
dvar_on_register_function_map.erase(hash);
}
return dvar;
}
void dvar_set_bool(game::dvar_t* dvar, bool boolean)
{
const auto disabled = find_dvar(disable::set_bool_disables, dvar->hash);
@ -505,6 +527,8 @@ namespace dvars
dvar_register_vector3_hook.create(SELECT_VALUE(0x419A00_b, 0x182DB0_b), &dvar_register_vector3);
dvar_register_enum_hook.create(SELECT_VALUE(0x419500_b, 0x182700_b), &dvar_register_enum);
dvar_register_new_hook.create(SELECT_VALUE(0x41B1D0_b, 0x184DF0_b), &dvar_register_new);
if (!game::environment::is_sp())
{
dvar_register_bool_hashed_hook.create(SELECT_VALUE(0x0, 0x182420_b), &dvar_register_bool_hashed);

View File

@ -26,4 +26,6 @@ namespace dvars
void set_string(const std::string& name, const std::string& string);
void set_from_string(const std::string& name, const std::string& value);
}
void on_register(const std::string& name, const std::function<void()>& callback);
}

View File

@ -82,6 +82,131 @@ namespace fastfiles
return result;
}
utils::hook::detour db_read_stream_file_hook;
void db_read_stream_file_stub(int a1, int a2)
{
// always use lz4 compressor type when reading stream files
*game::g_compressor = 4;
return db_read_stream_file_hook.invoke<void>(a1, a2);
}
void skip_extra_zones_stub(utils::hook::assembler& a)
{
const auto skip = a.newLabel();
const auto original = a.newLabel();
a.pushad64();
a.test(esi, game::DB_ZONE_CUSTOM); // allocFlags
a.jnz(skip);
a.bind(original);
a.popad64();
a.mov(rdx, 0x8E2F80_b);
a.mov(rcx, rbp);
a.call(0x840A20_b);
a.jmp(0x398070_b);
a.bind(skip);
a.popad64();
a.mov(r14d, game::DB_ZONE_CUSTOM);
a.not_(r14d);
a.and_(esi, r14d);
a.jmp(0x39814F_b);
}
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);
}
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 load_pre_gfx_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode)
{
std::vector<game::XZoneInfo> data;
merge(&data, zoneInfo, zoneCount);
// code_pre_gfx_mp
game::DB_LoadXAssets(data.data(), static_cast<std::uint32_t>(data.size()), syncMode);
}
void load_post_gfx_and_ui_and_common_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode)
{
std::vector<game::XZoneInfo> data;
merge(&data, zoneInfo, zoneCount);
// code_post_gfx_mp
// ui_mp
// common_mp
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_ui_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode)
{
std::vector<game::XZoneInfo> data;
merge(&data, zoneInfo, zoneCount);
// ui_mp
game::DB_LoadXAssets(data.data(), static_cast<std::uint32_t>(data.size()), syncMode);
}
}
bool exists(const std::string& zone)
{
auto is_localized = game::DB_IsLocalized(zone.data());
auto handle = game::Sys_CreateFile((is_localized ? game::SF_ZONE_LOC : game::SF_ZONE), utils::string::va("%s.ff", zone.data()));
if (handle != (HANDLE)-1)
{
CloseHandle(handle);
return true;
}
return false;
}
std::string get_current_fastfile()
@ -113,6 +238,68 @@ namespace fastfiles
db_find_xasset_header_hook.create(game::DB_FindXAssetHeader, db_find_xasset_header_stub);
g_dump_scripts = dvars::register_bool("g_dumpScripts", false, game::DVAR_FLAG_NONE, "Dump GSC scripts");
// Allow loading of unsigned fastfiles
if (!game::environment::is_sp())
{
utils::hook::nop(0x368153_b, 2); // DB_InflateInit
}
// Allow loading of mixed compressor types
utils::hook::nop(SELECT_VALUE(0x1C4BE7_b, 0x3687A7_b), 2);
// Fix compressor type on streamed file load
db_read_stream_file_hook.create(SELECT_VALUE(0x1FB9D0_b, 0x3A1BF0_b), db_read_stream_file_stub);
// Don't load extra zones with loadzone
if (!game::environment::is_sp())
{
// TODO: SP?
utils::hook::nop(0x398061_b, 15);
utils::hook::jump(0x398061_b, utils::hook::assemble(skip_extra_zones_stub), true);
}
if (!game::environment::is_sp())
{
// Add custom zone paths
sys_createfile_hook.create(game::Sys_CreateFile, sys_create_file_stub);
// load our custom pre_gfx zones
utils::hook::call(0x15C3FD_b, load_pre_gfx_zones);
utils::hook::call(0x15C75D_b, load_pre_gfx_zones);
// load our custom ui and common zones
utils::hook::call(0x686421_b, load_post_gfx_and_ui_and_common_zones);
// load our custom ui zones
utils::hook::call(0x17C6D2_b, load_ui_zones);
}
command::add("loadzone", [](const command::params& params)
{
if (params.size() < 2)
{
console::info("usage: loadzone <zone>\n");
return;
}
const char* name = params.get(1);
if (!fastfiles::exists(name))
{
console::warn("loadzone: zone \"%s\" could not be found!\n", name);
return;
}
game::XZoneInfo info;
info.name = name;
info.allocFlags = game::DB_ZONE_GAME;
info.freeFlags = 0;
info.allocFlags |= game::DB_ZONE_CUSTOM; // skip extra zones with this flag!
game::DB_LoadXAssets(&info, 1, game::DBSyncMode::DB_LOAD_ASYNC);
});
}
};
}

View File

@ -4,6 +4,8 @@
namespace fastfiles
{
bool exists(const std::string& zone);
std::string get_current_fastfile();
void enum_assets(const game::XAssetType type,

View File

@ -83,6 +83,9 @@ namespace filesystem
get_search_paths().insert(".");
get_search_paths().insert("h1-mod");
get_search_paths().insert("data");
// fs_game flags
utils::hook::set<uint32_t>(0x189275_b, 0);
}
};
}

View File

@ -247,13 +247,13 @@ namespace party
return connect_state.challenge;
}
void start_map(const std::string& mapname)
void start_map(const std::string& mapname, bool dev)
{
if (game::Live_SyncOnlineDataFlags(0) > 32)
{
scheduler::once([=]()
{
command::execute("map " + mapname, false);
start_map(mapname, dev);
}, scheduler::pipeline::main, 1s);
}
else
@ -299,6 +299,8 @@ namespace party
command::execute(utils::string::va("party_maxplayers %i", maxclients->current.integer), true);
}*/
command::execute((dev ? "sv_cheats 1" : "sv_cheats 0"), true);
const auto* args = "StartServer";
game::UI_RunMenuScript(0, &args);
}
@ -349,7 +351,17 @@ namespace party
return;
}
start_map(argument[1]);
start_map(argument[1], false);
});
command::add("devmap", [](const command::params& argument)
{
if (argument.size() != 2)
{
return;
}
party::start_map(argument[1], true);
});
command::add("map_restart", []()

View File

@ -6,7 +6,7 @@ namespace party
void reset_connect_state();
void connect(const game::netadr_s& target);
void start_map(const std::string& mapname);
void start_map(const std::string& mapname, bool dev = false);
void clear_sv_motd();
game::netadr_s get_state_host();

View File

@ -0,0 +1,124 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "console.hpp"
#include "command.hpp"
#include "fastfiles.hpp"
#include "utils/hook.hpp"
namespace weapon
{
namespace
{
utils::hook::detour g_setup_level_weapon_def_hook;
void g_setup_level_weapon_def_stub()
{
// precache level weapons first
g_setup_level_weapon_def_hook.invoke<void>();
std::vector<game::WeaponDef*> weapons;
// find all weapons in asset pools
fastfiles::enum_assets(game::ASSET_TYPE_WEAPON, [&weapons](game::XAssetHeader header)
{
weapons.push_back(header.weapon);
}, false);
// sort weapons
std::sort(weapons.begin(), weapons.end(), [](game::WeaponDef* weapon1, game::WeaponDef* weapon2)
{
return std::string_view(weapon1->name) <
std::string_view(weapon2->name);
});
// precache items
for (std::size_t i = 0; i < weapons.size(); i++)
{
console::debug("precaching weapon \"%s\"\n", weapons[i]->name);
game::G_GetWeaponForName(weapons[i]->name);
}
}
template <typename T>
void set_weapon_field(const std::string& weapon_name, unsigned int field, T value)
{
auto weapon = game::DB_FindXAssetHeader(game::ASSET_TYPE_WEAPON, weapon_name.data(), false).data;
if (weapon)
{
if (field && field < (0xE20 + sizeof(T)))
{
*reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(weapon) + field) = value;
}
else
{
console::warn("weapon field: %d is higher than the size of weapon struct!\n", field);
}
}
else
{
console::warn("weapon %s not found!\n", weapon_name.data());
}
}
void set_weapon_field_float(const std::string& weapon_name, unsigned int field, float value)
{
set_weapon_field<float>(weapon_name, field, value);
}
void set_weapon_field_int(const std::string& weapon_name, unsigned int field, int value)
{
set_weapon_field<int>(weapon_name, field, value);
}
void set_weapon_field_bool(const std::string& weapon_name, unsigned int field, bool value)
{
set_weapon_field<bool>(weapon_name, field, value);
}
}
class component final : public component_interface
{
public:
void post_unpack() override
{
g_setup_level_weapon_def_hook.create(0x462630_b, g_setup_level_weapon_def_stub);
#ifdef DEBUG
command::add("setWeaponFieldFloat", [](const command::params& params)
{
if (params.size() <= 3)
{
console::info("usage: setWeaponFieldInt <weapon> <field> <value>\n");
return;
}
set_weapon_field_float(params.get(1), atoi(params.get(2)), static_cast<float>(atof(params.get(3))));
});
command::add("setWeaponFieldInt", [](const command::params& params)
{
if (params.size() <= 3)
{
console::info("usage: setWeaponFieldInt <weapon> <field> <value>\n");
return;
}
set_weapon_field_int(params.get(1), atoi(params.get(2)), static_cast<int>(atoi(params.get(3))));
});
command::add("setWeaponFieldBool", [](const command::params& params)
{
if (params.size() <= 3)
{
console::info("usage: setWeaponFieldBool <weapon> <field> <value>\n");
return;
}
set_weapon_field_bool(params.get(1), atoi(params.get(2)), static_cast<bool>(atoi(params.get(3))));
});
#endif
}
};
}
REGISTER_COMPONENT(weapon::component)

View File

@ -1010,6 +1010,20 @@ namespace game
DB_LOAD_SYNC_SKIP_ALWAYS_LOADED = 0x5,
};
enum DBAllocFlags : std::int32_t
{
DB_ZONE_NONE = 0x0,
DB_ZONE_COMMON = 0x1,
DB_ZONE_UI = 0x2,
DB_ZONE_GAME = 0x4,
DB_ZONE_LOAD = 0x8,
DB_ZONE_DEV = 0x10,
DB_ZONE_BASEMAP = 0x20,
DB_ZONE_TRANSIENT_POOL = 0x40,
DB_ZONE_TRANSIENT_MASK = 0x40,
DB_ZONE_CUSTOM = 0x200 // added for custom zone loading
};
struct XZoneInfo
{
const char* name;
@ -1397,6 +1411,11 @@ namespace game
const char* name;
};
struct WeaponDef
{
const char* name;
};
union XAssetHeader
{
void* data;
@ -1408,6 +1427,7 @@ namespace game
LuaFile* luaFile;
GfxImage* image;
TTF* ttf;
WeaponDef* weapon;
};
struct XAsset

View File

@ -164,6 +164,9 @@ namespace game
WEAK symbol<int(XAssetType type)> DB_GetXAssetTypeSize{0x0, 0x0};
WEAK symbol<XAssetHeader(XAssetType type, const char* name,
int createDefault)> DB_FindXAssetHeader{0x1F1120, 0x3950C0};
WEAK symbol<bool(const char* zone, int source)> DB_FileExists{0x1F0D50, 0x394DC0};
WEAK symbol<void(XZoneInfo* zoneInfo, unsigned int zoneCount, DBSyncMode syncMode)> DB_LoadXAssets{0x1F31E0, 0x397500};
WEAK symbol<bool(const char* zoneName)> DB_IsLocalized{0x0, 0x396790};
WEAK symbol<void(int clientNum, const char* menu,
int a3, int a4, unsigned int a5)> LUI_OpenMenu{0x3F20A0, 0x1E1210};
@ -207,6 +210,7 @@ namespace game
WEAK symbol<bool()> Sys_IsDatabaseReady2{0x3AB100, 0x4F79C0};
WEAK symbol<bool(int, void const*, const netadr_s*)> Sys_SendPacket{0x0, 0x5BDA90};
WEAK symbol<bool(const char* path)> Sys_FileExists{0x0, 0x0};
WEAK symbol<HANDLE(Sys_Folder, const char* baseFilename)> Sys_CreateFile{0x0, 0x5B2860};
WEAK symbol<const char*(const char*)> UI_GetMapDisplayName{0x0, 0x4DDEE0};
WEAK symbol<const char*(const char*)> UI_GetGameTypeDisplayName{0x0, 0x4DD8C0};
@ -238,6 +242,7 @@ namespace game
WEAK symbol<int> connectionState{0x0, 0x2EC82C8};
WEAK symbol<int> g_poolSize{0x0, 0x0};
WEAK symbol<int> g_compressor{0x2574804, 0x3962804};
WEAK symbol<scrVarGlob_t> scr_VarGlob{0xBD80E00, 0xB138180};
WEAK symbol<scrVmPub_t> scr_VmPub{0xC3F4E20, 0xB7AE3C0};