diff --git a/README.md b/README.md index 229b8703..bddb8f83 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ | `-nosteam` | Disable friends feature and do not update Steam about the game's current status just like an invisible mode. | | `-unprotect-dvars` | Allow the server to modify saved/archive dvars. | | `-zonebuilder` | Start the interactive zonebuilder tool console instead of starting the game. | +| `-disable-notifies` | Disable "Anti-CFG" checks | +| `-disable-mongoose` | Disable Mongoose HTTP server | +| `-disable-rate-limit-check` | Disable RCon rate limit checks | ## Disclaimer diff --git a/deps/GSL b/deps/GSL index afaaa71b..4300304e 160000 --- a/deps/GSL +++ b/deps/GSL @@ -1 +1 @@ -Subproject commit afaaa71bcee45d9c90c21f8bd3ba2b12902242e9 +Subproject commit 4300304ef24c247b3db0255763f46b9f95c3a83d diff --git a/deps/iw4-open-formats b/deps/iw4-open-formats index 6af596a0..700a2ae8 160000 --- a/deps/iw4-open-formats +++ b/deps/iw4-open-formats @@ -1 +1 @@ -Subproject commit 6af596a010eebf727e5d914bf9a01903c14ae128 +Subproject commit 700a2ae858c27568d95e21ce8c5f941d36c4a6c4 diff --git a/deps/libtomcrypt b/deps/libtomcrypt index 93f5348c..b96e96cf 160000 --- a/deps/libtomcrypt +++ b/deps/libtomcrypt @@ -1 +1 @@ -Subproject commit 93f5348c47d3578091a4ee5b90f4add216b46d1b +Subproject commit b96e96cf8b22a931e8e91098ac37bc72f9e2f033 diff --git a/deps/libtommath b/deps/libtommath index c6a00c26..3746c58f 160000 --- a/deps/libtommath +++ b/deps/libtommath @@ -1 +1 @@ -Subproject commit c6a00c26ca2192c713a36227fdd84d126cdc95b9 +Subproject commit 3746c58f29a1ebea15046932bbc9dacc35b4b214 diff --git a/deps/pdcurses b/deps/pdcurses index 0eb254ae..5c62af03 160000 --- a/deps/pdcurses +++ b/deps/pdcurses @@ -1 +1 @@ -Subproject commit 0eb254ae43cdd1f532d515accac59377087883f6 +Subproject commit 5c62af03e9a05e3a3ae8c8354c1234b772dcf4b0 diff --git a/deps/rapidjson b/deps/rapidjson index 973dc9c0..476ffa2f 160000 --- a/deps/rapidjson +++ b/deps/rapidjson @@ -1 +1 @@ -Subproject commit 973dc9c06dcd3d035ebd039cfb9ea457721ec213 +Subproject commit 476ffa2fd272243275a74c36952f210267dc3088 diff --git a/deps/zlib b/deps/zlib index 48c37410..79a0e447 160000 --- a/deps/zlib +++ b/deps/zlib @@ -1 +1 @@ -Subproject commit 48c3741002aca9dae84e9f2288ca149af14c9128 +Subproject commit 79a0e447a0dfa32979420cb21cfb96d684b2c9d5 diff --git a/scripts/convert_private_key.bat b/scripts/convert_private_key.bat new file mode 100644 index 00000000..226fb35a --- /dev/null +++ b/scripts/convert_private_key.bat @@ -0,0 +1,3 @@ +@echo off +echo Exporting DER rsa-private.key to PEM... +openssl rsa -in rsa-private.key -inform DER -outform PEM -out exported-rsa-private.key diff --git a/scripts/convert_private_key.sh b/scripts/convert_private_key.sh new file mode 100644 index 00000000..e2f6c9e5 --- /dev/null +++ b/scripts/convert_private_key.sh @@ -0,0 +1,4 @@ + #!/bin/bash + +echo "Exporting DER rsa-private.key to PEM..." +openssl rsa -in rsa-private.key -inform DER -outform PEM -out exported-rsa-private.key diff --git a/src/Components/Modules/AssetHandler.cpp b/src/Components/Modules/AssetHandler.cpp index 6d1d7967..7b308f2d 100644 --- a/src/Components/Modules/AssetHandler.cpp +++ b/src/Components/Modules/AssetHandler.cpp @@ -355,9 +355,13 @@ namespace Components AssetHandler::TypeCallbacks[type] = callback; } - void AssetHandler::OnLoad(Utils::Slot callback) + std::function AssetHandler::OnLoad(Utils::Slot callback) { AssetHandler::RestrictSignal.connect(callback); + + return [callback](){ + AssetHandler::RestrictSignal.disconnect(callback); + }; } void AssetHandler::ClearRelocations() diff --git a/src/Components/Modules/AssetHandler.hpp b/src/Components/Modules/AssetHandler.hpp index c965e58b..34a21a43 100644 --- a/src/Components/Modules/AssetHandler.hpp +++ b/src/Components/Modules/AssetHandler.hpp @@ -23,7 +23,7 @@ namespace Components ~AssetHandler(); static void OnFind(Game::XAssetType type, Utils::Slot callback); - static void OnLoad(Utils::Slot callback); + static std::function OnLoad(Utils::Slot callback); static void ClearRelocations(); static void Relocate(void* start, void* to, DWORD size = 4); diff --git a/src/Components/Modules/AssetInterfaces/ITracerDef.cpp b/src/Components/Modules/AssetInterfaces/ITracerDef.cpp index 98962180..7500b23b 100644 --- a/src/Components/Modules/AssetInterfaces/ITracerDef.cpp +++ b/src/Components/Modules/AssetInterfaces/ITracerDef.cpp @@ -3,9 +3,9 @@ namespace Assets { - void ITracerDef::load(Game::XAssetHeader* /*header*/, const std::string& /*name*/, Components::ZoneBuilder::Zone* /*builder*/) + void ITracerDef::load(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder) { - // don't load from filesystem right now + header->tracerDef = builder->getIW4OfApi()->read(Game::XAssetType::ASSET_TYPE_TRACER, name); } void ITracerDef::mark(Game::XAssetHeader header, Components::ZoneBuilder::Zone* builder) diff --git a/src/Components/Modules/AssetInterfaces/IWeapon.cpp b/src/Components/Modules/AssetInterfaces/IWeapon.cpp index 40e8fdd5..e256ebcd 100644 --- a/src/Components/Modules/AssetInterfaces/IWeapon.cpp +++ b/src/Components/Modules/AssetInterfaces/IWeapon.cpp @@ -3,8 +3,16 @@ namespace Assets { - void IWeapon::load(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* /*builder*/) + void IWeapon::load(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder) { + header->weapon = builder->getIW4OfApi()->read(Game::XAssetType::ASSET_TYPE_WEAPON, name); + + if (header->weapon) + { + return; + } + + // Try loading raw weapon if (Components::FileSystem::File(std::format("weapons/mp/{}", name))) { @@ -121,7 +129,10 @@ namespace Assets if (asset->weapDef->projIgnitionEffect) builder->loadAsset(Game::XAssetType::ASSET_TYPE_FX, asset->weapDef->projIgnitionEffect); if (asset->weapDef->turretOverheatEffect) builder->loadAsset(Game::XAssetType::ASSET_TYPE_FX, asset->weapDef->turretOverheatEffect); -#define LoadWeapSound(sound) if (asset->weapDef->##sound##) builder->loadAsset(Game::XAssetType::ASSET_TYPE_SOUND, asset->weapDef->##sound##) + + + // They are not subassets, because they don't get loaded automatically +#define LoadWeapSound(sound) if (asset->weapDef->##sound##) builder->loadAsset(Game::XAssetType::ASSET_TYPE_SOUND, asset->weapDef->##sound##, false) LoadWeapSound(pickupSound); LoadWeapSound(pickupSoundPlayer); @@ -193,6 +204,33 @@ namespace Assets LoadWeapSound(missileConeSoundAlias); LoadWeapSound(missileConeSoundAliasAtBase); + + for (size_t i = 0; i < 37; i++) + { + { + const auto anim = asset->weapDef->szXAnimsLeftHanded[i]; + if (anim && strnlen(anim, 1) > 0) { + builder->loadAssetByName(Game::XAssetType::ASSET_TYPE_XANIMPARTS, anim, false); + } + } + { + const auto anim = asset->weapDef->szXAnimsRightHanded[i]; + if (anim && strnlen(anim, 1) > 0) { + builder->loadAssetByName(Game::XAssetType::ASSET_TYPE_XANIMPARTS, anim, false); + } + } + { + const auto anim = asset->szXAnims[i]; + if (anim && strnlen(anim, 1) > 0) { + builder->loadAssetByName(Game::XAssetType::ASSET_TYPE_XANIMPARTS, anim, false); + } + } + } + + if (asset->szAltWeaponName && *asset->szAltWeaponName != 0 && asset->weapDef->ammoCounterClip != Game::AMMO_COUNTER_CLIP_ALTWEAPON) // A very bad way to check if this is already an alt + { + builder->loadAssetByName(Game::XAssetType::ASSET_TYPE_WEAPON, asset->szAltWeaponName, false); + } } void IWeapon::writeWeaponDef(Game::WeaponDef* def, Components::ZoneBuilder::Zone* builder, Utils::Stream* buffer) @@ -782,4 +820,26 @@ namespace Assets buffer->popBlock(); } + + IWeapon::IWeapon() + { + Components::Command::Add("dumpweapon", [](const Components::Command::Params* params) + { + if (params->size() < 2) return; + + std::string weapon = params->get(1); + + const auto header = Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_WEAPON, weapon.data()); + if (header.data) + { + Components::ZoneBuilder::RefreshExporterWorkDirectory(); + Components::ZoneBuilder::GetExporter()->write(Game::XAssetType::ASSET_TYPE_WEAPON, header.data); + } + else + { + Components::Logger::Print("Could not find weapon {}!\n", weapon); + } + } + ); + } } diff --git a/src/Components/Modules/AssetInterfaces/IWeapon.hpp b/src/Components/Modules/AssetInterfaces/IWeapon.hpp index e9f05d5b..55d5ae5b 100644 --- a/src/Components/Modules/AssetInterfaces/IWeapon.hpp +++ b/src/Components/Modules/AssetInterfaces/IWeapon.hpp @@ -6,6 +6,7 @@ namespace Assets { public: Game::XAssetType getType() override { return Game::XAssetType::ASSET_TYPE_WEAPON; } + IWeapon(); void save(Game::XAssetHeader header, Components::ZoneBuilder::Zone* builder) override; void mark(Game::XAssetHeader header, Components::ZoneBuilder::Zone* builder) override; diff --git a/src/Components/Modules/AssetInterfaces/IXAnimParts.cpp b/src/Components/Modules/AssetInterfaces/IXAnimParts.cpp index e8b9860c..0a9bd565 100644 --- a/src/Components/Modules/AssetInterfaces/IXAnimParts.cpp +++ b/src/Components/Modules/AssetInterfaces/IXAnimParts.cpp @@ -14,7 +14,7 @@ namespace Assets if (asset->names) { - for (char i = 0; i < asset->boneCount[Game::PART_TYPE_ALL]; ++i) + for (unsigned char i = 0; i < asset->boneCount[Game::PART_TYPE_ALL]; ++i) { builder->addScriptString(asset->names[i]); } @@ -22,7 +22,7 @@ namespace Assets if (asset->notify) { - for (char i = 0; i < asset->notifyCount; ++i) + for (unsigned char i = 0; i < asset->notifyCount; ++i) { builder->addScriptString(asset->notify[i].name); } @@ -165,7 +165,7 @@ namespace Assets unsigned short* destTagnames = buffer->dest(); buffer->saveArray(asset->names, asset->boneCount[Game::PART_TYPE_ALL]); - for (char i = 0; i < asset->boneCount[Game::PART_TYPE_ALL]; ++i) + for (unsigned char i = 0; i < asset->boneCount[Game::PART_TYPE_ALL]; ++i) { builder->mapScriptString(destTagnames[i]); } @@ -181,7 +181,7 @@ namespace Assets Game::XAnimNotifyInfo* destNotetracks = buffer->dest(); buffer->saveArray(asset->notify, asset->notifyCount); - for (char i = 0; i < asset->notifyCount; ++i) + for (unsigned char i = 0; i < asset->notifyCount; ++i) { builder->mapScriptString(destNotetracks[i].name); } diff --git a/src/Components/Modules/AssetInterfaces/IclipMap_t.cpp b/src/Components/Modules/AssetInterfaces/IclipMap_t.cpp index 82a1c3fd..9ac93e68 100644 --- a/src/Components/Modules/AssetInterfaces/IclipMap_t.cpp +++ b/src/Components/Modules/AssetInterfaces/IclipMap_t.cpp @@ -306,7 +306,7 @@ namespace Assets Game::CollisionPartition* destPartitions = buffer->dest(); buffer->saveArray(asset->partitions, asset->partitionCount); - for (int i = 0; i < asset->partitionCount; ++i) + for (size_t i = 0; i < asset->partitionCount; ++i) { Game::CollisionPartition* destPartition = &destPartitions[i]; Game::CollisionPartition* partition = &asset->partitions[i]; diff --git a/src/Components/Modules/Auth.cpp b/src/Components/Modules/Auth.cpp index ef93603f..42269ba9 100644 --- a/src/Components/Modules/Auth.cpp +++ b/src/Components/Modules/Auth.cpp @@ -21,7 +21,8 @@ namespace Components { 0xf4d2c30b712ac6e3, 0xf7e33c4081337fa3, - 0x6f5597f103cc50e9 + 0x6f5597f103cc50e9, + 0xecd542eee54ffccf, }; bool Auth::HasAccessToReservedSlot; diff --git a/src/Components/Modules/Bots.cpp b/src/Components/Modules/Bots.cpp index 099948ab..576bcdc8 100644 --- a/src/Components/Modules/Bots.cpp +++ b/src/Components/Modules/Bots.cpp @@ -19,12 +19,17 @@ namespace Components std::size_t Bots::BotDataIndex; + std::vector Bots::RemoteBotNames; + struct BotMovementInfo { std::int32_t buttons; // Actions std::int8_t forward; std::int8_t right; std::uint16_t weapon; + std::uint16_t lastAltWeapon; + std::uint8_t meleeDist; + float meleeYaw; bool active; }; @@ -55,6 +60,17 @@ namespace Components { "activate", Game::CMD_BUTTON_ACTIVATE }, }; + void Bots::UpdateBotNames() + { + const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto* masterServerName = (*Game::com_masterServerName)->current.string; + + Network::Address master(Utils::String::VA("%s:%u", masterServerName, masterPort)); + + Logger::Print("Getting bots...\n"); + Network::Send(master, "getbots"); + } + std::vector Bots::LoadBotNames() { std::vector result; @@ -101,7 +117,7 @@ namespace Components return result; } - int Bots::BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int statStuff, int port) + int Bots::BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int stats, int port) { std::string botName; std::string clanName; @@ -109,6 +125,11 @@ namespace Components static const auto botNames = []() -> std::vector { auto names = LoadBotNames(); + if (names.empty()) + { + Logger::Print("bots.txt was empty. Using the names from the master server\n"); + names = RemoteBotNames; + } if (sv_randomBotNames->current.enabled) { @@ -133,7 +154,7 @@ namespace Components clanName = "BOT"s; } - return _snprintf_s(buffer, 0x400, _TRUNCATE, connectString, num, botName.data(), clanName.data(), protocol, checksum, statVer, statStuff, port); + return _snprintf_s(buffer, 0x400, _TRUNCATE, connectString, num, botName.data(), clanName.data(), protocol, checksum, statVer, stats, port); } void Bots::Spawn(unsigned int count) @@ -273,6 +294,23 @@ namespace Components g_botai[entref.entnum].right = static_cast(rightInt); g_botai[entref.entnum].active = true; }); + + GSC::Script::AddMethod("BotMeleeParams", [](const Game::scr_entref_t entref) // Usage: BotMeleeParams(, ); + { + const auto* ent = GSC::Script::Scr_GetPlayerEntity(entref); + if (!Game::SV_IsTestClient(ent->s.number)) + { + Game::Scr_Error("BotMeleeParams: Can only call on a bot!"); + return; + } + + const auto yaw = Game::Scr_GetFloat(0); + const auto dist = std::clamp(static_cast(Game::Scr_GetFloat(1)), std::numeric_limits::min(), std::numeric_limits::max()); + + g_botai[entref.entnum].meleeYaw = yaw; + g_botai[entref.entnum].meleeDist = static_cast(dist); + g_botai[entref.entnum].active = true; + }); } void Bots::BotAiAction(Game::client_s* cl) @@ -282,8 +320,10 @@ namespace Components return; } + auto clientNum = cl - Game::svs_clients; + // Keep test client functionality - if (!g_botai[cl - Game::svs_clients].active) + if (!g_botai[clientNum].active) { Game::SV_BotUserMove(cl); return; @@ -294,10 +334,13 @@ namespace Components userCmd.serverTime = *Game::svs_time; - userCmd.buttons = g_botai[cl - Game::svs_clients].buttons; - userCmd.forwardmove = g_botai[cl - Game::svs_clients].forward; - userCmd.rightmove = g_botai[cl - Game::svs_clients].right; - userCmd.weapon = g_botai[cl - Game::svs_clients].weapon; + userCmd.buttons = g_botai[clientNum].buttons; + userCmd.forwardmove = g_botai[clientNum].forward; + userCmd.rightmove = g_botai[clientNum].right; + userCmd.weapon = g_botai[clientNum].weapon; + userCmd.primaryWeaponForAltMode = g_botai[clientNum].lastAltWeapon; + userCmd.meleeChargeYaw = g_botai[clientNum].meleeYaw; + userCmd.meleeChargeDist = g_botai[clientNum].meleeDist; userCmd.angles[0] = ANGLE2SHORT((cl->gentity->client->ps.viewangles[0] - cl->gentity->client->ps.delta_angles[0])); userCmd.angles[1] = ANGLE2SHORT((cl->gentity->client->ps.viewangles[1] - cl->gentity->client->ps.delta_angles[1])); @@ -321,11 +364,38 @@ namespace Components } } - void Bots::G_SelectWeaponIndex(int clientNum, int iWeaponIndex) + void Bots::G_SelectWeaponIndex(int clientNum, unsigned int iWeaponIndex) { if (g_botai[clientNum].active) { g_botai[clientNum].weapon = static_cast(iWeaponIndex); + g_botai[clientNum].lastAltWeapon = 0; + + auto* def = Game::BG_GetWeaponCompleteDef(iWeaponIndex); + + if (def && def->weapDef->inventoryType == Game::WEAPINVENTORY_ALTMODE) + { + auto* ps = &Game::g_entities[clientNum].client->ps; + auto numWeaps = Game::BG_GetNumWeapons(); + + for (auto i = 1u; i < numWeaps; i++) + { + if (!Game::BG_PlayerHasWeapon(ps, i)) + { + continue; + } + + auto* thisDef = Game::BG_GetWeaponCompleteDef(i); + + if (!thisDef || thisDef->altWeaponIndex != iWeaponIndex) + { + continue; + } + + g_botai[clientNum].lastAltWeapon = static_cast(i); + break; + } + } } } @@ -446,14 +516,19 @@ namespace Components } } - count = std::min(Game::MAX_CLIENTS, count); + count = std::clamp(count, 1, Game::MAX_CLIENTS); - Logger::Print("Spawning {} {}", count, (count == 1 ? "bot" : "bots")); + Logger::Print("Spawning {} {}\n", count, (count == 1 ? "bot" : "bots")); Spawn(count); }); } + bool Bots::Player_UpdateActivate_stub(int) + { + return false; + } + Bots::Bots() { AssertOffset(Game::client_s, bIsTestClient, 0x41AF0); @@ -471,11 +546,34 @@ namespace Components Utils::Hook(0x441B80, G_SelectWeaponIndex_Hk, HOOK_JUMP).install()->quick(); + // fix bots using objects (SV_IsClientBot) + Utils::Hook(0x4D79C5, Player_UpdateActivate_stub, HOOK_CALL).install()->quick(); + Utils::Hook(0x459654, SV_GetClientPing_Hk, HOOK_CALL).install()->quick(); sv_randomBotNames = Game::Dvar_RegisterBool("sv_randomBotNames", false, Game::DVAR_NONE, "Randomize the bots' names"); sv_replaceBots = Game::Dvar_RegisterBool("sv_replaceBots", false, Game::DVAR_NONE, "Test clients will be replaced by connecting players when the server is full."); + Scheduler::OnGameInitialized(UpdateBotNames, Scheduler::Pipeline::MAIN); + + Network::OnClientPacket("getbotsResponse", [](const Network::Address& address, const std::string& data) + { + const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto* masterServerName = (*Game::com_masterServerName)->current.string; + + Network::Address master(Utils::String::VA("%s:%u", masterServerName, masterPort)); + if (master == address) + { + auto botNames = Utils::String::Split(data, '\n'); + Logger::Print("Got {} names from the master server\n", botNames.size()); + + for (const auto& entry : botNames) + { + RemoteBotNames.emplace_back(entry, "BOT"); + } + } + }); + // Reset BotMovementInfo.active when client is dropped Events::OnClientDisconnect([](const int clientNum) -> void { diff --git a/src/Components/Modules/Bots.hpp b/src/Components/Modules/Bots.hpp index 30925db3..342007da 100644 --- a/src/Components/Modules/Bots.hpp +++ b/src/Components/Modules/Bots.hpp @@ -10,13 +10,17 @@ namespace Components static void SV_DirectConnect_Full_Check(); private: - using botData = std::pair< std::string, std::string>; + using botData = std::pair; static const Game::dvar_t* sv_randomBotNames; static const Game::dvar_t* sv_replaceBots; static std::size_t BotDataIndex; + static std::vector RemoteBotNames; + + static void UpdateBotNames(); + static std::vector LoadBotNames(); static int BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int statStuff, int port); @@ -28,9 +32,11 @@ namespace Components static void BotAiAction(Game::client_s* cl); static void SV_BotUserMove_Hk(); - static void G_SelectWeaponIndex(int clientNum, int iWeaponIndex); + static void G_SelectWeaponIndex(int clientNum, unsigned int iWeaponIndex); static void G_SelectWeaponIndex_Hk(); + static bool Player_UpdateActivate_stub(int); + static int SV_GetClientPing_Hk(int clientNum); static bool IsFull(); diff --git a/src/Components/Modules/ClientCommand.cpp b/src/Components/Modules/ClientCommand.cpp index f80f9eb8..f7c8f5ca 100644 --- a/src/Components/Modules/ClientCommand.cpp +++ b/src/Components/Modules/ClientCommand.cpp @@ -1,6 +1,8 @@ #include #include "ClientCommand.hpp" +#include "Weapon.hpp" + #include "GSC/Script.hpp" using namespace Utils::String; @@ -382,7 +384,14 @@ namespace Components Game::XModel* model = nullptr; if (ent->model) { - model = Game::G_GetModel(ent->model); + if (Components::Weapon::GModelIndexHasBeenReallocated) + { + model = Components::Weapon::G_ModelIndexReallocated[ent->model]; + } + else + { + model = Game::G_GetModel(ent->model); + } } Game::vec3_t point, angles; diff --git a/src/Components/Modules/Dedicated.cpp b/src/Components/Modules/Dedicated.cpp index 30fe4245..a5748d8a 100644 --- a/src/Components/Modules/Dedicated.cpp +++ b/src/Components/Modules/Dedicated.cpp @@ -168,7 +168,7 @@ namespace Components return; } - const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto masterPort = (*Game::com_masterPort)->current.unsignedInt; const auto* masterServerName = (*Game::com_masterServerName)->current.string; Network::Address master(Utils::String::VA("%s:%u", masterServerName, masterPort)); diff --git a/src/Components/Modules/Download.cpp b/src/Components/Modules/Download.cpp index 282f6e86..43433425 100644 --- a/src/Components/Modules/Download.cpp +++ b/src/Components/Modules/Download.cpp @@ -435,8 +435,41 @@ namespace Components MongooseLogBuffer.push_back(c); } - static std::optional InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) + void Download::ReplyError(mg_connection* connection, int code) { + std::string msg{}; + switch(code) + { + case 400: + msg = "Bad request"; + break; + + case 403: + msg = "Forbidden"; + break; + + case 404: + msg = "Not found"; + break; + } + + mg_http_reply(connection, code, "Content-Type: text/plain\r\n", "%s", msg.c_str()); + } + + void Download::Reply(mg_connection* connection, const std::string& contentType, const std::string& data) + { + const auto formatted = std::format("Content-Type: {}\r\n", contentType); + mg_http_reply(connection, 200, formatted.c_str(), "%s", data.c_str()); + } + + std::optional Download::InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) + { + if (!(*Game::com_sv_running)->current.enabled) + { + // Game is not running ,cannot return info + return std::nullopt; + } + const auto status = ServerInfo::GetInfo(); const auto host = ServerInfo::GetHostInfo(); @@ -486,7 +519,7 @@ namespace Components return { out }; } - static std::optional ListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) + std::optional Download::ListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) { static nlohmann::json jsonList; static std::filesystem::path fsGamePre; @@ -534,7 +567,7 @@ namespace Components return { out }; } - static std::optional MapHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) + std::optional Download::MapHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) { static std::string mapNamePre; static nlohmann::json jsonList; @@ -580,12 +613,18 @@ namespace Components return { out }; } - static std::optional FileHandler(mg_connection* c, const mg_http_message* hm) + std::optional Download::FileHandler(mg_connection* c, const mg_http_message* hm) { std::string url(hm->uri.ptr, hm->uri.len); Utils::String::Replace(url, "\\", "/"); + if (url.size() <= 5) + { + ReplyError(c, 400); + return {}; + } + url = url.substr(6); // Strip /file Utils::String::Replace(url, "%20", " "); @@ -608,7 +647,7 @@ namespace Components if ((!Maps::GetUserMap()->isValid() && !Party::IsInUserMapLobby()) || !isValidFile) { - mg_http_reply(c, 403, "Content-Type: text/html\r\n", "%s", "403 - Forbidden"); + ReplyError(c, 403); return {}; } @@ -618,7 +657,7 @@ namespace Components { if ((!url.ends_with(".iwd") && url != "mod.ff") || url.find("_svr_") != std::string::npos) { - mg_http_reply(c, 403, "Content-Type: text/html\r\n", "%s", "403 - Forbidden"); + ReplyError(c, 403); return {}; } } @@ -629,7 +668,7 @@ namespace Components std::string file; if ((!isMap && fsGame.empty()) || !Utils::IO::ReadFile(path, &file)) { - mg_http_reply(c, 404, "Content-Type: text/html\r\n", "404 - Not Found %s", path.data()); + ReplyError(c, 404); } else { @@ -644,7 +683,7 @@ namespace Components return {}; } - static std::optional ServerListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) + std::optional Download::ServerListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) { std::vector servers; @@ -661,7 +700,7 @@ namespace Components return { out }; } - static void EventHandler(mg_connection* c, const int ev, void* ev_data, [[maybe_unused]] void* fn_data) + void Download::EventHandler(mg_connection* c, const int ev, void* ev_data, [[maybe_unused]] void* fn_data) { using callback = std::function(mg_connection*, const mg_http_message*)>; @@ -693,7 +732,7 @@ namespace Components { if (const auto reply = i->second(c, hm)) { - mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.value().data()); + Reply(c, "application/json", reply.value()); } handled = true; diff --git a/src/Components/Modules/Download.hpp b/src/Components/Modules/Download.hpp index 84ee7584..b4e4dde1 100644 --- a/src/Components/Modules/Download.hpp +++ b/src/Components/Modules/Download.hpp @@ -1,5 +1,9 @@ #pragma once + +struct mg_connection; +struct mg_http_message; + namespace Components { class Download : public Component @@ -71,6 +75,7 @@ namespace Components this->valid_ = false; } } + }; class FileDownload @@ -100,5 +105,14 @@ namespace Components static bool DownloadFile(ClientDownload* download, unsigned int index); static void LogFn(char c, void* param); + static void ReplyError(mg_connection* connection, int code); + static void Reply(mg_connection* connection, const std::string& contentType, const std::string& data); + + static std::optional FileHandler(mg_connection* c, const mg_http_message* hm); + static void EventHandler(mg_connection* c, const int ev, void* ev_data, void* fn_data); + static std::optional ListHandler(mg_connection* c, const mg_http_message* hm); + static std::optional InfoHandler(mg_connection* c, const mg_http_message* hm); + static std::optional ServerListHandler(mg_connection* c, const mg_http_message* hm); + static std::optional MapHandler(mg_connection* c, const mg_http_message* hm); }; } diff --git a/src/Components/Modules/GSC/IO.cpp b/src/Components/Modules/GSC/IO.cpp index be8c2a53..7d866434 100644 --- a/src/Components/Modules/GSC/IO.cpp +++ b/src/Components/Modules/GSC/IO.cpp @@ -30,12 +30,20 @@ namespace Components::GSC std::filesystem::path IO::BuildPath(const char* path) { const std::filesystem::path fsGame = (*Game::fs_gameDirVar)->current.string; - if (!fsGame.empty()) + + // check if we need to append scriptdata folder, for backwards compat + std::string spath = path; + if (!spath.starts_with(Game::SCRIPTDATA_DIR + "/"s) && !spath.starts_with(Game::SCRIPTDATA_DIR + "\\"s)) { - return fsGame / "scriptdata"s / path; + spath = Game::SCRIPTDATA_DIR + "/"s + spath; } - return DefaultDestPath / "scriptdata"s / path; + if (!fsGame.empty()) + { + return fsGame / spath; + } + + return DefaultDestPath / spath; } void IO::GScr_OpenFile() @@ -154,7 +162,7 @@ namespace Components::GSC return; } - file = file.substr(0, 1024 - 1); // 1024 is the max string size for the SL system + file = file.substr(0, (1 << 16) - 1); // 65535 is the max string size for the SL system Game::Scr_AddString(file.data()); }); diff --git a/src/Components/Modules/GSC/ScriptError.cpp b/src/Components/Modules/GSC/ScriptError.cpp index 94943f5a..edf1795a 100644 --- a/src/Components/Modules/GSC/ScriptError.cpp +++ b/src/Components/Modules/GSC/ScriptError.cpp @@ -194,7 +194,16 @@ namespace Components::GSC unsigned int ScriptError::Scr_GetPrevSourcePos(const char* codePos, unsigned int index) { - return ScrParserGlob.sourcePosLookup[index + Scr_GetPrevSourcePosOpcodeLookup(codePos)->sourcePosIndex].sourcePos; + const auto prevIndex = Scr_GetPrevSourcePosOpcodeLookup(codePos)->sourcePosIndex; + const auto sPos = ScrParserGlob.sourcePosLookup[index + prevIndex].sourcePos; + + // I'm not sure why this is necessary - when given a valid codePos, sometimes + // the sourcePos ends up being a negative number (got a case where it was -2) + // which will output a giantic unsigned number and crash + // So we make sure it's not gonna happen here by clamping the number + const auto uPos = static_cast(std::max(0, static_cast(sPos))); + + return uPos; } Game::OpcodeLookup* ScriptError::Scr_GetPrevSourcePosOpcodeLookup(const char* codePos) @@ -326,7 +335,9 @@ namespace Components::GSC if (Game::scrVarPub->programBuffer && Scr_IsInOpcodeMemory(codePos)) { auto bufferIndex = Scr_GetSourceBuffer(codePos - 1); - Scr_PrintSourcePos(channel, ScrParserPub.sourceBufferLookup[bufferIndex].buf, ScrParserPub.sourceBufferLookup[bufferIndex].sourceBuf, Scr_GetPrevSourcePos(codePos - 1, index)); + const auto prevSourcePos = Scr_GetPrevSourcePos(codePos - 1, index); + + Scr_PrintSourcePos(channel, ScrParserPub.sourceBufferLookup[bufferIndex].buf, ScrParserPub.sourceBufferLookup[bufferIndex].sourceBuf, prevSourcePos); return; } } diff --git a/src/Components/Modules/GSC/ScriptStorage.cpp b/src/Components/Modules/GSC/ScriptStorage.cpp index 672a599c..326b1ce5 100644 --- a/src/Components/Modules/GSC/ScriptStorage.cpp +++ b/src/Components/Modules/GSC/ScriptStorage.cpp @@ -84,12 +84,12 @@ namespace Components::GSC const nlohmann::json json = Data; - FileSystem::FileWriter("scriptdata/scriptstorage.json").write(json.dump()); + FileSystem::FileWriter(Game::SCRIPTDATA_DIR + "/scriptstorage.json"s).write(json.dump()); }); Script::AddFunction("StorageLoad", [] // gsc: StorageLoad(); { - FileSystem::File storageFile("scriptdata/scriptstorage.json"); + FileSystem::File storageFile(Game::SCRIPTDATA_DIR + "/scriptstorage.json"s); if (!storageFile.exists()) { return; diff --git a/src/Components/Modules/MapRotation.cpp b/src/Components/Modules/MapRotation.cpp index 0648a28f..98148978 100644 --- a/src/Components/Modules/MapRotation.cpp +++ b/src/Components/Modules/MapRotation.cpp @@ -208,7 +208,6 @@ namespace Components nlohmann::json MapRotation::to_json() { - assert(!DedicatedRotation.empty()); return DedicatedRotation.to_json(); } diff --git a/src/Components/Modules/Maps.cpp b/src/Components/Modules/Maps.cpp index aeb62f5e..dd647b41 100644 --- a/src/Components/Modules/Maps.cpp +++ b/src/Components/Modules/Maps.cpp @@ -105,14 +105,15 @@ namespace Components { std::string data = RawFiles::ReadRawFile(name, buffer, size); - if (Maps::UserMap.isValid()) + if (Maps::UserMap.isValid()) { const auto mapname = Maps::UserMap.getName(); const auto arena = GetArenaPath(mapname); if (Utils::IO::FileExists(arena)) { - data.append(Utils::IO::ReadFile(arena)); + // Replace all arenas with just this one + data = Utils::IO::ReadFile(arena); } } @@ -850,6 +851,14 @@ namespace Components // Load usermap arena file Utils::Hook(0x630A88, Maps::LoadArenaFileStub, HOOK_CALL).install()->quick(); + // Always refresh arena when loading or unloading a zone + Utils::Hook::Nop(0x485017, 2); + Utils::Hook::Nop(0x4FD8C7, 2); // Gametypes + Utils::Hook::Nop(0x4BDFB7, 2); // Unknown + Utils::Hook::Nop(0x45ED6F, 2); // loadGameInfo + Utils::Hook::Nop(0x4A5888, 2); // UI_InitOnceForAllClients + + // Allow hiding specific smodels Utils::Hook(0x50E67C, Maps::HideModelStub, HOOK_CALL).install()->quick(); diff --git a/src/Components/Modules/Node.cpp b/src/Components/Modules/Node.cpp index 630b389e..4f170bb4 100644 --- a/src/Components/Modules/Node.cpp +++ b/src/Components/Modules/Node.cpp @@ -184,6 +184,8 @@ namespace Components if (!Dedicated::IsEnabled()) { + if (ServerList::UseMasterServer) return; // don't run node frame if master server is active + if (Game::CL_GetLocalClientConnectionState(0) != Game::CA_DISCONNECTED) { WasIngame = true; @@ -264,7 +266,7 @@ namespace Components if (list.isnode() && (!list.port() || list.port() == address.getPort())) { - if (!Dedicated::IsEnabled() && ServerList::IsOnlineList() && list.protocol() == PROTOCOL) + if (!Dedicated::IsEnabled() && ServerList::IsOnlineList() && !ServerList::UseMasterServer && list.protocol() == PROTOCOL) { #ifdef NODE_SYSTEM_DEBUG Logger::Debug("Inserting {} into the serverlist", address.getString()); diff --git a/src/Components/Modules/RCon.cpp b/src/Components/Modules/RCon.cpp index 52a5cc25..369b903d 100644 --- a/src/Components/Modules/RCon.cpp +++ b/src/Components/Modules/RCon.cpp @@ -9,16 +9,18 @@ namespace Components { std::unordered_map RCon::RateLimit; - std::vector RCon::RconAddresses; + std::vector RCon::RConAddresses; - RCon::Container RCon::RconContainer; - Utils::Cryptography::ECC::Key RCon::RconKey; + RCon::Container RCon::RConContainer; + Utils::Cryptography::ECC::Key RCon::RConKey; std::string RCon::Password; - Dvar::Var RCon::RconPassword; - Dvar::Var RCon::RconLogRequests; - Dvar::Var RCon::RconTimeout; + Dvar::Var RCon::RConPassword; + Dvar::Var RCon::RConLogRequests; + Dvar::Var RCon::RConTimeout; + + std::string RCon::RConOutputBuffer; void RCon::AddCommands() { @@ -61,11 +63,44 @@ namespace Components Logger::Print("You are connected to an invalid server\n"); }); + Command::Add("rconSafe", [](const Command::Params* params) + { + if (params->size() < 2) + { + Logger::Print("Usage: {} \n", params->get(0)); + return; + } + + const auto command = params->join(1); + + auto* addr = reinterpret_cast(0xA5EA44); + Network::Address target(addr); + if (!target.isValid() || target.getIP().full == 0) + { + target = Party::Target(); + } + + if (!target.isValid()) + { + Logger::Print("You are connected to an invalid server\n"); + return; + } + + const auto& key = CryptoKeyRSA::GetPrivateKey(); + const auto signature = Utils::Cryptography::RSA::SignMessage(key, command); + + Proto::RCon::Command directive; + directive.set_command(command); + directive.set_signature(signature); + + Network::SendCommand(target, "rconSafe", directive.SerializeAsString()); + }); + Command::Add("remoteCommand", [](const Command::Params* params) { if (params->size() < 2) return; - RconContainer.command = params->get(1); + RConContainer.command = params->get(1); auto* addr = reinterpret_cast(0xA5EA44); Network::Address target(addr); @@ -91,9 +126,9 @@ namespace Components Network::Address address(params->get(1)); const auto hash = std::hash()(*reinterpret_cast(&address.getIP().bytes[0])); - if (address.isValid() && std::ranges::find(RconAddresses, hash) == RconAddresses.end()) + if (address.isValid() && std::ranges::find(RConAddresses, hash) == RConAddresses.end()) { - RconAddresses.push_back(hash); + RConAddresses.push_back(hash); } }); } @@ -113,7 +148,7 @@ namespace Components const auto ip = address.getIP(); const auto lastTime = RateLimit[ip.full]; - if (lastTime && (time - lastTime) < RconTimeout.get()) + if (lastTime && (time - lastTime) < RConTimeout.get()) { return false; // Flooding } @@ -127,7 +162,7 @@ namespace Components for (auto i = RateLimit.begin(); i != RateLimit.end();) { // No longer at risk of flooding, remove - if ((time - i->second) > RconTimeout.get()) + if ((time - i->second) > RConTimeout.get()) { i = RateLimit.erase(i); } @@ -138,7 +173,7 @@ namespace Components } } - void RCon::RconExecuter(const Network::Address& address, std::string data) + void RCon::RConExecutor(const Network::Address& address, std::string data) { Utils::String::Trim(data); @@ -159,7 +194,7 @@ namespace Components password.erase(password.begin()); } - const auto svPassword = RconPassword.get(); + const auto svPassword = RConPassword.get(); if (svPassword.empty()) { Logger::Print(Game::CON_CHANNEL_NETWORK, "RCon request from {} dropped. No password set!\n", address.getString()); @@ -172,11 +207,10 @@ namespace Components return; } - static std::string outputBuffer; - outputBuffer.clear(); + RConOutputBuffer.clear(); #ifndef _DEBUG - if (RconLogRequests.get()) + if (RConLogRequests.get()) #endif { Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing RCon request from {}: {}\n", address.getString(), command); @@ -184,15 +218,39 @@ namespace Components Logger::PipeOutput([](const std::string& output) { - outputBuffer.append(output); + RConOutputBuffer.append(output); }); Command::Execute(command, true); Logger::PipeOutput(nullptr); - Network::SendCommand(address, "print", outputBuffer); - outputBuffer.clear(); + Network::SendCommand(address, "print", RConOutputBuffer); + RConOutputBuffer.clear(); + } + + void RCon::RConSafeExecutor(const Network::Address& address, std::string command) + { + RConOutputBuffer.clear(); + +#ifndef _DEBUG + if (RConLogRequests.get()) +#endif + { + Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing Safe RCon request from {}: {}\n", address.getString(), command); + } + + Logger::PipeOutput([](const std::string& output) + { + RConOutputBuffer.append(output); + }); + + Command::Execute(command, true); + + Logger::PipeOutput(nullptr); + + Network::SendCommand(address, "print", RConOutputBuffer); + RConOutputBuffer.clear(); } RCon::RCon() @@ -203,16 +261,16 @@ namespace Components { Network::OnClientPacket("rconAuthorization", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { - if (RconContainer.command.empty()) + if (RConContainer.command.empty()) { return; } - const auto& key = CryptoKey::Get(); + const auto& key = CryptoKeyECC::Get(); const auto signedMsg = Utils::Cryptography::ECC::SignMessage(key, data); Proto::RCon::Command rconExec; - rconExec.set_command(RconContainer.command); + rconExec.set_command(RConContainer.command); rconExec.set_signature(signedMsg); Network::SendCommand(address, "rconExecute", rconExec.SerializeAsString()); @@ -238,21 +296,21 @@ namespace Components 0x08 }; - RconKey.set(std::string(reinterpret_cast(publicKey), sizeof(publicKey))); + RConKey.set(std::string(reinterpret_cast(publicKey), sizeof(publicKey))); - RconContainer.timestamp = 0; + RConContainer.timestamp = 0; Events::OnDvarInit([] { - RconPassword = Dvar::Register("rcon_password", "", Game::DVAR_NONE, "The password for rcon"); - RconLogRequests = Dvar::Register("rcon_log_requests", false, Game::DVAR_NONE, "Print remote commands in log"); - RconTimeout = Dvar::Register("rcon_timeout", 500, 100, 10000, Game::DVAR_NONE, ""); + RConPassword = Dvar::Register("rcon_password", "", Game::DVAR_NONE, "The password for rcon"); + RConLogRequests = Dvar::Register("rcon_log_requests", false, Game::DVAR_NONE, "Print remote commands in log"); + RConTimeout = Dvar::Register("rcon_timeout", 500, 100, 10000, Game::DVAR_NONE, ""); }); Network::OnClientPacket("rcon", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { const auto hash = std::hash()(*reinterpret_cast(&address.getIP().bytes[0])); - if (!RconAddresses.empty() && std::ranges::find(RconAddresses, hash) == RconAddresses.end()) + if (!RConAddresses.empty() && std::ranges::find(RConAddresses, hash) == RConAddresses.end()) { return; } @@ -268,50 +326,97 @@ namespace Components auto rconData = data; Scheduler::Once([address, s = std::move(rconData)] { - RconExecuter(address, s); + RConExecutor(address, s); + }, Scheduler::Pipeline::MAIN); + }); + + Network::OnClientPacket("rconSafe", [](const Network::Address& address, [[maybe_unused]] const std::string& data) -> void + { + const auto hash = std::hash()(*reinterpret_cast(&address.getIP().bytes[0])); + if (!RConAddresses.empty() && std::ranges::find(RConAddresses, hash) == RConAddresses.end()) + { + return; + } + + const auto time = Game::Sys_Milliseconds(); + if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time)) + { + return; + } + + RateLimitCleanup(time); + + if (!CryptoKeyRSA::HasPublicKey()) + { + return; + } + + auto& key = CryptoKeyRSA::GetPublicKey(); + if (!key.isValid()) + { + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "RSA public key is invalid\n"); + } + + Proto::RCon::Command directive; + if (!directive.ParseFromString(data)) + { + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "Unable to parse secure command from {}\n", address.getString()); + return; + } + + if (!Utils::Cryptography::RSA::VerifyMessage(key, directive.command(), directive.signature())) + { + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "RSA signature verification failed for message from {}\n", address.getString()); + return; + } + + std::string rconData = directive.command(); + Scheduler::Once([address, s = std::move(rconData)] + { + RConSafeExecutor(address, s); }, Scheduler::Pipeline::MAIN); }); Network::OnClientPacket("rconRequest", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { - RconContainer.address = address; - RconContainer.challenge = Utils::Cryptography::Rand::GenerateChallenge(); - RconContainer.timestamp = Game::Sys_Milliseconds(); + RConContainer.address = address; + RConContainer.challenge = Utils::Cryptography::Rand::GenerateChallenge(); + RConContainer.timestamp = Game::Sys_Milliseconds(); - Network::SendCommand(address, "rconAuthorization", RconContainer.challenge); + Network::SendCommand(address, "rconAuthorization", RConContainer.challenge); }); Network::OnClientPacket("rconExecute", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { - if (address != RconContainer.address) return; // Invalid IP - if (!RconContainer.timestamp || (Game::Sys_Milliseconds() - RconContainer.timestamp) > (1000 * 10)) return; // Timeout + if (address != RConContainer.address) return; // Invalid IP + if (!RConContainer.timestamp || (Game::Sys_Milliseconds() - RConContainer.timestamp) > (1000 * 10)) return; // Timeout - RconContainer.timestamp = 0; + RConContainer.timestamp = 0; Proto::RCon::Command rconExec; rconExec.ParseFromString(data); - if (!Utils::Cryptography::ECC::VerifyMessage(RconKey, RconContainer.challenge, rconExec.signature())) + if (!Utils::Cryptography::ECC::VerifyMessage(RConKey, RConContainer.challenge, rconExec.signature())) { return; } - RconContainer.output.clear(); + RConContainer.output.clear(); Logger::PipeOutput([](const std::string& output) { - RconContainer.output.append(output); + RConContainer.output.append(output); }); Command::Execute(rconExec.command(), true); Logger::PipeOutput(nullptr); - Network::SendCommand(address, "print", RconContainer.output); - RconContainer.output.clear(); + Network::SendCommand(address, "print", RConContainer.output); + RConContainer.output.clear(); }); } - bool RCon::CryptoKey::LoadKey(Utils::Cryptography::ECC::Key& key) + bool RCon::CryptoKeyECC::LoadKey(Utils::Cryptography::ECC::Key& key) { std::string data; if (!Utils::IO::ReadFile("./private.key", &data)) @@ -323,7 +428,7 @@ namespace Components return key.isValid(); } - Utils::Cryptography::ECC::Key RCon::CryptoKey::GenerateKey() + Utils::Cryptography::ECC::Key RCon::CryptoKeyECC::GenerateKey() { auto key = Utils::Cryptography::ECC::GenerateKey(512); if (!key.isValid()) @@ -339,7 +444,7 @@ namespace Components return key; } - Utils::Cryptography::ECC::Key RCon::CryptoKey::LoadOrGenerateKey() + Utils::Cryptography::ECC::Key RCon::CryptoKeyECC::LoadOrGenerateKey() { Utils::Cryptography::ECC::Key key; if (LoadKey(key)) @@ -350,16 +455,103 @@ namespace Components return GenerateKey(); } - Utils::Cryptography::ECC::Key RCon::CryptoKey::GetKeyInternal() + Utils::Cryptography::ECC::Key RCon::CryptoKeyECC::GetKeyInternal() { auto key = LoadOrGenerateKey(); Utils::IO::WriteFile("./public.key", key.getPublicKey()); return key; } - const Utils::Cryptography::ECC::Key& RCon::CryptoKey::Get() + Utils::Cryptography::ECC::Key& RCon::CryptoKeyECC::Get() { static auto key = GetKeyInternal(); return key; } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::LoadPublicKey() + { + Utils::Cryptography::RSA::Key key; + std::string data; + + if (!Utils::IO::ReadFile("./rsa-public.key", &data)) + { + return key; + } + + key.set(data); + return key; + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::GetPublicKeyInternal() + { + auto key = LoadPublicKey(); + return key; + } + + Utils::Cryptography::RSA::Key& RCon::CryptoKeyRSA::GetPublicKey() + { + static auto key = GetPublicKeyInternal(); + return key; + } + + bool RCon::CryptoKeyRSA::LoadPrivateKey(Utils::Cryptography::RSA::Key& key) + { + std::string data; + if (!Utils::IO::ReadFile("./rsa-private.key", &data)) + { + return false; + } + + key.set(data); + return key.isValid(); + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::GenerateKeyPair() + { + auto key = Utils::Cryptography::RSA::GenerateKey(4096); + if (!key.isValid()) + { + throw std::runtime_error("Failed to generate RSA key!"); + } + + if (!Utils::IO::WriteFile("./rsa-private.key", key.serialize(PK_PRIVATE))) + { + throw std::runtime_error("Failed to write RSA private key!"); + } + + if (!Utils::IO::WriteFile("./rsa-public.key", key.serialize(PK_PUBLIC))) + { + throw std::runtime_error("Failed to write RSA public key!"); + } + + return key; + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::LoadOrGeneratePrivateKey() + { + Utils::Cryptography::RSA::Key key; + if (LoadPrivateKey(key)) + { + return key; + } + + return GenerateKeyPair(); + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::GetPrivateKeyInternal() + { + auto key = LoadOrGeneratePrivateKey(); + return key; + } + + Utils::Cryptography::RSA::Key& RCon::CryptoKeyRSA::GetPrivateKey() + { + static auto key = GetPrivateKeyInternal(); + return key; + } + + bool RCon::CryptoKeyRSA::HasPublicKey() + { + return Utils::IO::FileExists("./rsa-public.key"); + } } diff --git a/src/Components/Modules/RCon.hpp b/src/Components/Modules/RCon.hpp index eef8d404..ef691ce1 100644 --- a/src/Components/Modules/RCon.hpp +++ b/src/Components/Modules/RCon.hpp @@ -11,17 +11,17 @@ namespace Components class Container { public: - int timestamp; - std::string output; - std::string command; - std::string challenge; - Network::Address address; + int timestamp{}; + std::string output{}; + std::string command{}; + std::string challenge{}; + Network::Address address{}; }; - class CryptoKey + class CryptoKeyECC { public: - static const Utils::Cryptography::ECC::Key& Get(); + static Utils::Cryptography::ECC::Key& Get(); private: static bool LoadKey(Utils::Cryptography::ECC::Key& key); static Utils::Cryptography::ECC::Key GenerateKey(); @@ -29,18 +29,39 @@ namespace Components static Utils::Cryptography::ECC::Key GetKeyInternal(); }; + class CryptoKeyRSA + { + public: + static bool HasPublicKey(); + + static Utils::Cryptography::RSA::Key& GetPublicKey(); + static Utils::Cryptography::RSA::Key& GetPrivateKey(); + + private: + static Utils::Cryptography::RSA::Key GenerateKeyPair(); + + static Utils::Cryptography::RSA::Key LoadPublicKey(); + static Utils::Cryptography::RSA::Key GetPublicKeyInternal(); + + static bool LoadPrivateKey(Utils::Cryptography::RSA::Key& key); + static Utils::Cryptography::RSA::Key LoadOrGeneratePrivateKey(); + static Utils::Cryptography::RSA::Key GetPrivateKeyInternal(); + }; + static std::unordered_map RateLimit; - static std::vector RconAddresses; + static std::vector RConAddresses; - static Container RconContainer; - static Utils::Cryptography::ECC::Key RconKey; + static Container RConContainer; + static Utils::Cryptography::ECC::Key RConKey; static std::string Password; - static Dvar::Var RconPassword; - static Dvar::Var RconLogRequests; - static Dvar::Var RconTimeout; + static std::string RConOutputBuffer; + + static Dvar::Var RConPassword; + static Dvar::Var RConLogRequests; + static Dvar::Var RConTimeout; static void AddCommands(); @@ -48,6 +69,7 @@ namespace Components static bool RateLimitCheck(const Network::Address& address, int time); static void RateLimitCleanup(int time); - static void RconExecuter(const Network::Address& address, std::string data); + static void RConExecutor(const Network::Address& address, std::string data); + static void RConSafeExecutor(const Network::Address& address, std::string command); }; } diff --git a/src/Components/Modules/ServerList.cpp b/src/Components/Modules/ServerList.cpp index 89dabe28..fd496edb 100644 --- a/src/Components/Modules/ServerList.cpp +++ b/src/Components/Modules/ServerList.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "Discovery.hpp" #include "Events.hpp" @@ -10,10 +11,14 @@ #include "Toast.hpp" #include "UIFeeder.hpp" +#include +#include +#include + namespace Components { - bool ServerList::SortAsc = true; - int ServerList::SortKey = static_cast>(Column::Ping); + bool ServerList::SortAsc = false; + int ServerList::SortKey = static_cast>(Column::Players); unsigned int ServerList::CurrentServer = 0; ServerList::Container ServerList::RefreshContainer; @@ -24,6 +29,8 @@ namespace Components std::vector ServerList::VisibleList; + bool ServerList::UseMasterServer = false; + Dvar::Var ServerList::UIServerSelected; Dvar::Var ServerList::UIServerSelectedMap; Dvar::Var ServerList::NETServerQueryLimit; @@ -271,6 +278,69 @@ namespace Components SortList(); } + void ServerList::ParseNewMasterServerResponse(const std::string& servers) + { + std::lock_guard _(RefreshContainer.mutex); + + rapidjson::Document doc{}; + const rapidjson::ParseResult result = doc.Parse(servers); + if (!result || !doc.IsObject()) + { + UseMasterServer = false; + Logger::Print("Unable to parse JSON response. Using the Node System\n"); + return; + } + + if (!doc.HasMember("servers")) + { + UseMasterServer = false; + Logger::Print("Unable to parse JSON response: we were unable to find any server. Using the Node System\n"); + return; + } + + const rapidjson::Value& list = doc["servers"]; + if (!list.IsArray() || list.Empty()) + { + UseMasterServer = false; + Logger::Print("Unable to parse JSON response: we were unable to find any server. Using the Node System\n"); + return; + } + + Logger::Print("Response from the master server contains {} servers\n", list.Size()); + + std::size_t count = 0; + + for (const auto& entry : list.GetArray()) + { + if (!entry.HasMember("ip") || !entry.HasMember("port")) + { + continue; + } + + if (!entry["ip"].IsString() || !entry["port"].IsInt()) + { + continue; + } + + // Using VA because it's faster + Network::Address server(Utils::String::VA("%s:%u", entry["ip"].GetString(), entry["port"].GetInt())); + server.setType(Game::NA_IP); // Just making sure... + + InsertRequest(server); + ++count; + } + + if (!count) + { + UseMasterServer = false; + Logger::Print("Despite receiving what looked like a valid response from the master server, we got {} servers. Using the Node System\n", count); + return; + } + + UseMasterServer = true; + Logger::Print("Response from the master server was successfully parsed. We got {} servers\n", count); + } + void ServerList::Refresh([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info) { Dvar::Var("ui_serverSelected").set(false); @@ -291,34 +361,33 @@ namespace Components { Discovery::Perform(); } -#ifdef IW4_USE_MASTER_SERVER else if (IsOnlineList()) { - const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto masterPort = (*Game::com_masterPort)->current.unsignedInt; const auto* masterServerName = (*Game::com_masterServerName)->current.string; - // Check if our dvars can properly convert to a address - Game::netadr_t masterServerAddr; - if (!GetMasterServer(masterServerName, masterPort, masterServerAddr)) + RefreshContainer.awatingList = true; + RefreshContainer.awaitTime = Game::Sys_Milliseconds(); + + Toast::Show("cardicon_headshot", "Server Browser", "Fetching servers...", 3000); + + const auto* url = "http://iw4x.plutools.pw/v1/servers/iw4x"; + const auto reply = Utils::WebIO("IW4x", url).setTimeout(5000)->get(); + if (reply.empty()) { - Logger::Print("Could not resolve address for {}:{}", masterServerName, masterPort); - Toast::Show("cardicon_headshot", "^1Error", std::format("Could not resolve address for {}:{}", masterServerName, masterPort), 5000); + Logger::Print("Response from {} was empty or the request timed out. Using the Node System\n", url); + Toast::Show("cardicon_headshot", "^1Error", std::format("Could not get a response from {}. Using the Node System", url), 5000); UseMasterServer = false; return; } - Toast::Show("cardicon_headshot", "Server Browser", "Fetching servers...", 3000); + RefreshContainer.awatingList = false; - UseMasterServer = true; + ParseNewMasterServerResponse(reply); - RefreshContainer.awatingList = true; - RefreshContainer.awaitTime = Game::Sys_Milliseconds(); + // TODO: Figure out what to do with this. Leave it to avoid breaking other code RefreshContainer.host = Network::Address(std::format("{}:{}", masterServerName, masterPort)); - - Logger::Print("Sending server list request to master\n"); - Network::SendCommand(RefreshContainer.host, "getservers", std::format("IW4 {} full empty", PROTOCOL)); } -#endif else if (IsFavouriteList()) { LoadFavourties(); @@ -729,7 +798,9 @@ namespace Components if (Game::Sys_Milliseconds() - RefreshContainer.awaitTime > 5000) { RefreshContainer.awatingList = false; + Logger::Print("We haven't received a response from the master within {} seconds!\n", (Game::Sys_Milliseconds() - RefreshContainer.awaitTime) / 1000); + UseMasterServer = false; Node::Synchronize(); } } diff --git a/src/Components/Modules/ServerList.hpp b/src/Components/Modules/ServerList.hpp index 6526104a..abe61896 100644 --- a/src/Components/Modules/ServerList.hpp +++ b/src/Components/Modules/ServerList.hpp @@ -51,6 +51,8 @@ namespace Components static void UpdateVisibleInfo(); + static bool UseMasterServer; + static bool GetMasterServer(const char* ip, int port, Game::netadr_t& address); static Dvar::Var UIServerSelected; @@ -123,6 +125,8 @@ namespace Components std::recursive_mutex mutex; }; + static void ParseNewMasterServerResponse(const std::string& servers); + static unsigned int GetServerCount(); static const char* GetServerText(unsigned int index, int column); static const char* GetServerInfoText(ServerInfo* server, int column, bool sorting = false); diff --git a/src/Components/Modules/Weapon.cpp b/src/Components/Modules/Weapon.cpp index 2451420c..5f3fd9f0 100644 --- a/src/Components/Modules/Weapon.cpp +++ b/src/Components/Modules/Weapon.cpp @@ -6,6 +6,8 @@ namespace Components { const Game::dvar_t* Weapon::BGWeaponOffHandFix; + Game::XModel* Weapon::G_ModelIndexReallocated[G_MODELINDEX_LIMIT]; + bool Weapon::GModelIndexHasBeenReallocated; Game::WeaponCompleteDef* Weapon::LoadWeaponCompleteDef(const char* name) { @@ -433,6 +435,21 @@ namespace Components Utils::Hook::Set(0x4F76FB, 0x12EC + (sizeof(bg_sharedAmmoCaps) - (1200 * 4))); // Move arg4 pointers Utils::Hook::Set(0x4F7630, 0x12DC + (sizeof(bg_sharedAmmoCaps) - (1200 * 4))); + + + // Reallocate G_ModelIndex + Utils::Hook::Set(0x420654 + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x43BCE4 + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x44F27B + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x479087 + 1, G_ModelIndexReallocated); + Utils::Hook::Set(0x48069D + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x48F088 + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x4F457C + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x5FC762 + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x5FC7BE + 3, G_ModelIndexReallocated); + Utils::Hook::Set(0x44F256 + 2, G_MODELINDEX_LIMIT); + + GModelIndexHasBeenReallocated = true; } void* Weapon::LoadNoneWeaponHook() @@ -636,6 +653,10 @@ namespace Components Utils::Hook::Nop(0x408230, 5); // is asset default Utils::Hook::Nop(0x40823A, 2); // jump + // Automatically register weapons, even if the level is not loading + Utils::Hook::Nop(0x49E547, 2); + Utils::Hook::Set(0x44F240, 0xEB); + // Skip double loading for fs_game Utils::Hook::Set(0x4081FD, 0xEB); diff --git a/src/Components/Modules/Weapon.hpp b/src/Components/Modules/Weapon.hpp index 30af745b..60c17c60 100644 --- a/src/Components/Modules/Weapon.hpp +++ b/src/Components/Modules/Weapon.hpp @@ -5,12 +5,20 @@ #define WEAPON_LIMIT 2400 #define MAX_CONFIGSTRINGS (4139 - 1200 + WEAPON_LIMIT) +// Double the limit to allow loading of some heavy-duty MW3 maps +#define ADDITIONAL_GMODELS 512 + +#define G_MODELINDEX_LIMIT (512 + WEAPON_LIMIT - 1200 + ADDITIONAL_GMODELS) + namespace Components { class Weapon : public Component { public: Weapon(); + static Game::XModel* G_ModelIndexReallocated[G_MODELINDEX_LIMIT]; + + static bool GModelIndexHasBeenReallocated; private: static const Game::dvar_t* BGWeaponOffHandFix; diff --git a/src/Components/Modules/ZoneBuilder.cpp b/src/Components/Modules/ZoneBuilder.cpp index c0592631..96272646 100644 --- a/src/Components/Modules/ZoneBuilder.cpp +++ b/src/Components/Modules/ZoneBuilder.cpp @@ -30,7 +30,7 @@ namespace Components buffer(0xC800000), zoneName(name), dataMap("zone_source/" + name + ".csv"), - branding{nullptr}, + branding{ nullptr }, assetDepth(0), iw4ofApi(getIW4OfApiParams()) { @@ -212,7 +212,7 @@ namespace Components { return false; } - + } return true; @@ -359,8 +359,8 @@ namespace Components Game::XAssetHeader ZoneBuilder::Zone::saveSubAsset(Game::XAssetType type, void* ptr) { - Game::XAssetHeader header { ptr }; - Game::XAsset asset { type, header }; + Game::XAssetHeader header{ ptr }; + Game::XAsset asset{ type, header }; std::string name = Game::DB_GetXAssetName(&asset); int assetIndex = this->findAsset(type, name); @@ -433,7 +433,7 @@ namespace Components zoneBuffer.insert(zoneBuffer.begin(), static_cast(Utils::Cryptography::Rand::GenerateInt())); char lastByte = 0; - for(unsigned int i = 0; i < zoneBuffer.size(); ++i ) + for (unsigned int i = 0; i < zoneBuffer.size(); ++i) { char oldLastByte = lastByte; lastByte = zoneBuffer[i]; @@ -447,9 +447,9 @@ namespace Components Utils::IO::WriteFile("uncompressed", zoneBuffer); const auto _0 = gsl::finally([] - { - Utils::IO::RemoveFile("uncompressed"); - }); + { + Utils::IO::RemoveFile("uncompressed"); + }); zoneBuffer = Utils::Compression::ZLib::Compress(zoneBuffer); outBuffer.append(zoneBuffer); @@ -541,11 +541,11 @@ namespace Components void ZoneBuilder::Zone::addBranding() { const auto now = std::chrono::system_clock::now(); - + auto zoneBranding = std::format("Built using the IW4x ZoneBuilder! {:%d-%m-%Y %H:%M:%OS}", now); auto brandingLen = zoneBranding.size(); // + 1 is added by the save code - this->branding = {this->zoneName.data(), 0, static_cast(brandingLen), getAllocator()->duplicateString(zoneBranding)}; + this->branding = { this->zoneName.data(), 0, static_cast(brandingLen), getAllocator()->duplicateString(zoneBranding) }; if (this->findAsset(Game::ASSET_TYPE_RAWFILE, this->branding.name) != -1) { @@ -652,7 +652,7 @@ namespace Components void ZoneBuilder::Zone::addRawAsset(Game::XAssetType type, void* ptr) { - this->loadedAssets.push_back({type, {ptr}}); + this->loadedAssets.push_back({ type, {ptr} }); } // Remap a scriptString to it's corresponding value in the local scriptString table. @@ -788,38 +788,46 @@ namespace Components params.write_only_once = true; params.find_other_asset = [this](int type, const std::string& name) -> void* - { - return AssetHandler::FindAssetForZone(static_cast(type), name, this).data; - }; + { + return AssetHandler::FindAssetForZone(static_cast(type), name, this).data; + }; + + params.request_mark_asset = [this](int type, void* data) -> void + { + Game::XAsset asset {static_cast(type), {data}}; + + AssetHandler::ZoneMark(asset, this); + this->addRawAsset(static_cast(type), data); + }; params.fs_read_file = [](const std::string& filename) -> std::string - { - auto file = FileSystem::File(filename); - if (file.exists()) { - return file.getBuffer(); - } + auto file = FileSystem::File(filename); + if (file.exists()) + { + return file.getBuffer(); + } - return {}; - }; + return {}; + }; params.store_in_string_table = [](const std::string& text) -> unsigned int - { - return Game::SL_GetString(text.data(), 0); - }; + { + return Game::SL_GetString(text.data(), 0); + }; params.print = [](iw4of::params_t::print_type t, const std::string& message) -> void - { - switch (t) { - case iw4of::params_t::P_ERR: - Logger::Error(Game::ERR_FATAL, "{}", message); - break; - case iw4of::params_t::P_WARN: - Logger::Print("{}", message); - break; - } - }; + switch (t) + { + case iw4of::params_t::P_ERR: + Logger::Error(Game::ERR_FATAL, "{}", message); + break; + case iw4of::params_t::P_WARN: + Logger::Print("{}", message); + break; + } + }; if (*Game::fs_basepath && *Game::fs_gameDirVar) { @@ -833,7 +841,7 @@ namespace Components return params; } - int ZoneBuilder::StoreTexture(Game::GfxImageLoadDef **loadDef, Game::GfxImage *image) + int ZoneBuilder::StoreTexture(Game::GfxImageLoadDef** loadDef, Game::GfxImage* image) { size_t size = 16 + (*loadDef)->resourceSize; void* data = Utils::Memory::GetAllocator()->allocate(size); @@ -874,7 +882,7 @@ namespace Components // block the main thread from doing anything "main thread" specific while // the other thread is interrupting - + //while (ZoneBuilder::mainThreadInterrupted) std::this_thread::sleep_for(100ms); // normal functionality @@ -933,7 +941,7 @@ namespace Components Console::ShowAsyncConsole(); Utils::Hook::Call(0x43D140)(); // Com_EventLoop } - + Utils::Hook::Call(0x502580)(static_cast(__rdtsc())); // Netchan_Init Utils::Hook::Call(0x429080)(); // FS_InitFileSystem @@ -966,7 +974,7 @@ namespace Components Utils::Hook::Call(0x4454C0)(); // Item_SetupKeywordHash (for loading menus) Utils::Hook::Call(0x501BC0)(); // Menu_SetupKeywordHash (for loading menus) Utils::Hook::Call(0x4A1280)(); // something related to uiInfoArray - + Utils::Hook::Call(0x464A90)(GetCommandLineA()); // Com_ParseCommandLine Utils::Hook::Call(0x60C3D0)(); // Com_AddStartupCommands @@ -1033,7 +1041,7 @@ namespace Components if (code == Game::ERR_FATAL) { ExitProcess(1); - } + } } __declspec(naked) void ZoneBuilder::SoftErrorAssetOverflow() @@ -1060,19 +1068,19 @@ namespace Components const char* ret = "default"; Game::DB_EnumXAssetEntries(Game::XAssetType::ASSET_TYPE_MATERIAL, [techniqueName, &ret](Game::XAssetEntry* entry) - { - if (!replacementFound) { - Game::XAssetHeader header = entry->asset.header; - std::string name = techniqueName; - if (name[0] == ',') name = name.substr(1); - if (name == header.material->techniqueSet->name) + if (!replacementFound) { - ret = header.material->info.name; - replacementFound = true; + Game::XAssetHeader header = entry->asset.header; + std::string name = techniqueName; + if (name[0] == ',') name = name.substr(1); + if (name == header.material->techniqueSet->name) + { + ret = header.material->info.name; + replacementFound = true; + } } - } - }, false); + }, false); if (replacementFound) return ret; return ""; @@ -1099,7 +1107,7 @@ namespace Components { file = Game::Sys_CreateFile("zone\\zonebuilder\\", filename); } - + return file; } @@ -1110,26 +1118,21 @@ namespace Components params.write_only_once = true; params.find_other_asset = [](int type, const std::string& name) -> void* - { - if (ZoneBuilder::DumpingZone.empty()) { + // Do not deadlock the DB return Game::DB_FindXAssetHeader(static_cast(type), name.data()).data; - } - - // Do not deadlock the DB - return nullptr; - }; + }; params.fs_read_file = [](const std::string& filename) -> std::string - { - auto file = FileSystem::File(filename); - if (file.exists()) { - return file.getBuffer(); - } + auto file = FileSystem::File(filename); + if (file.exists()) + { + return file.getBuffer(); + } - return {}; - }; + return {}; + }; params.get_from_string_table = [](const unsigned int& id) -> std::string { @@ -1144,17 +1147,17 @@ namespace Components }; params.print = [](iw4of::params_t::print_type t, const std::string& message) -> void - { - switch (t) { - case iw4of::params_t::P_ERR: - Logger::Error(Game::ERR_FATAL, "{}", message); - break; - case iw4of::params_t::P_WARN: - Logger::Print("{}", message); - break; - } - }; + switch (t) + { + case iw4of::params_t::P_ERR: + Logger::Error(Game::ERR_FATAL, "{}", message); + break; + case iw4of::params_t::P_WARN: + Logger::Print("{}", message); + break; + } + }; return params; } @@ -1229,22 +1232,22 @@ namespace Components // The domain func of fs_game should NOT be used to set the value itself! // Hook should be moved further!! Utils::Hook::Set(0x643203, [](Game::dvar_t* dvar, Game::DvarValue value) - { - // Call original FS_GameDirDomainFunc - int result = Utils::Hook::Call(0x642FC0)(dvar, value); - - if (result) { - if (std::strcmp(value.string, dvar->current.string) != 0) - { - // CopyStringInternal - dvar->current.string = Game::CopyStringInternal(value.string); - Game::FS_Restart(0, 0); - } - } + // Call original FS_GameDirDomainFunc + int result = Utils::Hook::Call(0x642FC0)(dvar, value); - return result; - }); + if (result) + { + if (std::strcmp(value.string, dvar->current.string) != 0) + { + // CopyStringInternal + dvar->current.string = Game::CopyStringInternal(value.string); + Game::FS_Restart(0, 0); + } + } + + return result; + }); // set new entry point Utils::Hook(0x4513DA, ZoneBuilder::EntryPoint, HOOK_JUMP).install()->quick(); @@ -1260,7 +1263,7 @@ namespace Components // Don't exec startup config in fs_restart Utils::Hook::Set(0x461B48, 0xEB); - + // remove overriding asset messages Utils::Hook::Nop(0x5BC74E, 5); @@ -1272,17 +1275,18 @@ namespace Components auto jmp = 0x5B9841 - 0x5B97B6 + 1 + 0xFD + 0x100 - 515; Utils::Hook::Set(0x5B97B6 +1, static_cast(jmp)); - AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader asset, const std::string& name, bool* /*restrict*/) - { - if (!ZoneBuilder::TraceZone.empty() && ZoneBuilder::TraceZone == FastFiles::Current()) + AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader /* asset*/, const std::string& name, bool* /*restrict*/) { - ZoneBuilder::TraceAssets.emplace_back(std::make_pair(type, name)); + if (!ZoneBuilder::TraceZone.empty() && ZoneBuilder::TraceZone == FastFiles::Current()) + { + ZoneBuilder::TraceAssets.emplace_back(std::make_pair(type, name)); #ifdef _DEBUG - OutputDebugStringA(Utils::String::Format("%s\n", name)); + OutputDebugStringA(Utils::String::Format("%s\n", name)); #endif - } + } + }); - if (!ZoneBuilder::DumpingZone.empty()) + Command::Add("dumpzone", [](const Command::Params* params) { Utils::Memory::Allocator assetReallocator{}; // Needed to translate some assets from CODO for instance @@ -1298,7 +1302,19 @@ namespace Components asset.gameWorldMp = newWorldMp; } - if (ExporterAPI.is_type_supported(type) && name[0] != ',') + if (params->size() < 2) return; + + std::string zone = params->get(1); + ZoneBuilder::DumpingZone = zone; + ZoneBuilder::RefreshExporterWorkDirectory(); + + Game::XZoneInfo info; + info.name = zone.data(); + info.allocFlags = Game::DB_ZONE_MOD; + info.freeFlags = 0; + + Logger::Print("Loading zone '{}'...\n", zone); + { if (type == Game::XAssetType::ASSET_TYPE_IMAGE) { @@ -1314,287 +1330,351 @@ namespace Components asset.techniqueSet->remappedTechniqueSet = nullptr; } - ExporterAPI.write(type, asset.data); - Components::Logger::Print("."); + struct asset_t + { + Game::XAssetType type; + char name[128]; + }; + + std::vector assets{}; + const auto handle = AssetHandler::OnLoad([&](Game::XAssetType type, Game::XAssetHeader asset, const std::string& name, bool* /*restrict*/) + { + if (ExporterAPI.is_type_supported(type) && name[0] != ',') + { + Game::XAsset xasset = { type, asset }; + asset_t assetIdentifier{}; + + // Fetch name + const auto assetName = Game::DB_GetXAssetName(&xasset); + std::memcpy(assetIdentifier.name, assetName, strnlen(assetName, ARRAYSIZE(assetIdentifier.name) - 1)); + assetIdentifier.name[ARRAYSIZE(assetIdentifier.name) - 1] = '\x00'; + + assetIdentifier.type = type; + + assets.push_back(assetIdentifier); + } + }); + + Game::DB_LoadXAssets(&info, 1, true); + AssetHandler::FindOriginalAsset(Game::ASSET_TYPE_RAWFILE, zone.data()); // Lock until zone is loaded + + Logger::Print("Dumping zone '{}'...\n", zone); + handle(); // Release + for (const auto& asset : assets) + { + const auto assetHeader = Game::DB_FindXAssetHeader(asset.type, asset.name); + if (assetHeader.data) + { + ExporterAPI.write(asset.type, assetHeader.data); + } + else + { + Logger::Warning(Game::conChannel_t::CON_CHANNEL_ERROR, "Asset {} has disappeared while dumping!", asset.name); + } + } } - } - }); - Command::Add("dumpzone", [](const Command::Params* params) - { - if (params->size() < 2) return; + Logger::Print("Unloading zone '{}'...\n", zone); + info.freeFlags = Game::DB_ZONE_MOD; + info.allocFlags = 0; + info.name = nullptr; - std::string zone = params->get(1); - ZoneBuilder::DumpingZone = zone; - ZoneBuilder::RefreshExporterWorkDirectory(); - - Game::XZoneInfo info; - info.name = zone.data(); - info.allocFlags = Game::DB_ZONE_MOD; - info.freeFlags = 0; - - Logger::Print("Dumping zone '{}'...\n", zone); - - Game::DB_LoadXAssets(&info, 1, true); - AssetHandler::FindOriginalAsset(Game::ASSET_TYPE_RAWFILE, zone.data()); // Lock until zone is loaded - - Logger::Print("Unloading zone '{}'...\n", zone); - info.freeFlags = Game::DB_ZONE_MOD; - info.allocFlags = 0; - info.name = nullptr; - - Game::DB_LoadXAssets(&info, 1, true); - AssetHandler::FindOriginalAsset(Game::ASSET_TYPE_RAWFILE, "default"); // Lock until zone is unloaded - Logger::Print("Zone '{}' dumped", ZoneBuilder::DumpingZone); - ZoneBuilder::DumpingZone = std::string(); - }); + Game::DB_LoadXAssets(&info, 1, true); + AssetHandler::FindOriginalAsset(Game::ASSET_TYPE_RAWFILE, "default"); // Lock until zone is unloaded + Logger::Print("Zone '{}' dumped", ZoneBuilder::DumpingZone); + ZoneBuilder::DumpingZone = std::string(); + }); Command::Add("verifyzone", [](const Command::Params* params) - { - if (params->size() < 2) return; - - std::string zone = params->get(1); - - ZoneBuilder::BeginAssetTrace(zone); - - Game::XZoneInfo info; - info.name = zone.data(); - info.allocFlags = Game::DB_ZONE_MOD; - info.freeFlags = 0; - - Logger::Print("Loading zone '{}'...\n", zone); - - Game::DB_LoadXAssets(&info, 1, true); - AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_RAWFILE, zone.data()); // Lock until zone is loaded - - auto assets = ZoneBuilder::EndAssetTrace(); - - Logger::Print("Unloading zone '{}'...\n", zone); - info.freeFlags = Game::DB_ZONE_MOD; - info.allocFlags = 0; - info.name = nullptr; - - Game::DB_LoadXAssets(&info, 1, true); - AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_RAWFILE, "default"); // Lock until zone is unloaded - - Logger::Print("Zone '{}' loaded with {} assets:\n", zone, assets.size()); - - int count = 0; - for (auto i = assets.begin(); i != assets.end(); ++i, ++count) { - Logger::Print(" {}: {}: {}\n", count, Game::DB_GetXAssetTypeName(i->first), i->second); - } + if (params->size() < 2) return; - Logger::Print("\n"); - }); + std::string zone = params->get(1); - Command::Add("buildzone", [](const Command::Params* params) - { - if (params->size() < 2) return; + ZoneBuilder::BeginAssetTrace(zone); - std::string zoneName = params->get(1); - Logger::Print("Building zone '{}'...\n", zoneName); + Game::XZoneInfo info; + info.name = zone.data(); + info.allocFlags = Game::DB_ZONE_MOD; + info.freeFlags = 0; - Zone(zoneName).build(); - }); + Logger::Print("Loading zone '{}'...\n", zone); - Command::Add("buildall", []() - { - auto path = std::format("{}\\zone_source", (*Game::fs_basepath)->current.string); - auto zoneSources = FileSystem::GetSysFileList(path, "csv", false); + Game::DB_LoadXAssets(&info, 1, true); + AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_RAWFILE, zone.data()); // Lock until zone is loaded - for (auto source : zoneSources) - { - if (Utils::String::EndsWith(source, ".csv")) + auto assets = ZoneBuilder::EndAssetTrace(); + + Logger::Print("Unloading zone '{}'...\n", zone); + info.freeFlags = Game::DB_ZONE_MOD; + info.allocFlags = 0; + info.name = nullptr; + + Game::DB_LoadXAssets(&info, 1, true); + AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_RAWFILE, "default"); // Lock until zone is unloaded + + Logger::Print("Zone '{}' loaded with {} assets:\n", zone, assets.size()); + + int count = 0; + for (auto i = assets.begin(); i != assets.end(); ++i, ++count) { - source = source.substr(0, source.find(".csv")); + Logger::Print(" {}: {}: {}\n", count, Game::DB_GetXAssetTypeName(i->first), i->second); } - Command::Execute(std::format("buildzone {}", source), true); - } - }); + Logger::Print("\n"); + }); + + Command::Add("buildzone", [](const Command::Params* params) + { + if (params->size() < 2) return; + + std::string zoneName = params->get(1); + Logger::Print("Building zone '{}'...\n", zoneName); + + Zone(zoneName).build(); + }); + + Command::Add("buildall", []() + { + auto path = std::format("{}\\zone_source", (*Game::fs_basepath)->current.string); + auto zoneSources = FileSystem::GetSysFileList(path, "csv", false); + + for (auto source : zoneSources) + { + if (Utils::String::EndsWith(source, ".csv")) + { + source = source.substr(0, source.find(".csv")); + } + + Command::Execute(std::format("buildzone {}", source), true); + } + }); static std::set curTechsets_list; static std::set techsets_list; AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader, const std::string& name, bool*) - { - if (type == Game::ASSET_TYPE_TECHNIQUE_SET) { - if (name[0] == ',') return; // skip techsets from common_mp - if (techsets_list.find(name) == techsets_list.end()) + if (type == Game::ASSET_TYPE_TECHNIQUE_SET) { - curTechsets_list.emplace(name); - techsets_list.emplace(name); + if (name[0] == ',') return; // skip techsets from common_mp + if (techsets_list.find(name) == techsets_list.end()) + { + curTechsets_list.emplace(name); + techsets_list.emplace(name); + } } - } - }); + }); AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader asset, [[maybe_unused]] const std::string& name, [[maybe_unused]] bool* restrict) - { - if (type != Game::ASSET_TYPE_SOUND) { - return; - } - - auto sound = asset.sound; - - for (size_t i = 0; i < sound->count; i++) - { - auto thisSound = sound->head[i]; - - if (thisSound.soundFile->type == Game::SAT_LOADED) + if (type != Game::ASSET_TYPE_SOUND) { - if (thisSound.soundFile->u.loadSnd->sound.data == nullptr) + return; + } + + auto sound = asset.sound; + + for (size_t i = 0; i < sound->count; i++) + { + auto thisSound = sound->head[i]; + + if (thisSound.soundFile->type == Game::SAT_LOADED) { - // ouch - // This should never happen and will cause a memory leak - // Let's change it to a streamed sound instead - thisSound.soundFile->type = Game::SAT_STREAMED; + if (thisSound.soundFile->u.loadSnd->sound.data == nullptr) + { + // ouch + // This should never happen and will cause a memory leak + // Let's change it to a streamed sound instead + thisSound.soundFile->type = Game::SAT_STREAMED; - auto virtualPath = std::filesystem::path(thisSound.soundFile->u.loadSnd->name); + auto virtualPath = std::filesystem::path(thisSound.soundFile->u.loadSnd->name); - thisSound.soundFile->u.streamSnd.filename.info.raw.name = Utils::Memory::DuplicateString(virtualPath.filename().string()); + thisSound.soundFile->u.streamSnd.filename.info.raw.name = Utils::Memory::DuplicateString(virtualPath.filename().string()); - auto dir = virtualPath.remove_filename().string(); - dir = dir.substr(0, dir.size() - 1); // remove / - thisSound.soundFile->u.streamSnd.filename.info.raw.dir = Utils::Memory::DuplicateString(dir); + auto dir = virtualPath.remove_filename().string(); + dir = dir.substr(0, dir.size() - 1); // remove / + thisSound.soundFile->u.streamSnd.filename.info.raw.dir = Utils::Memory::DuplicateString(dir); + } } } - } - }); + }); Command::Add("buildtechsets", [](const Command::Params*) - { - Utils::IO::CreateDir("zone_source/techsets"); - Utils::IO::CreateDir("zone/techsets"); - - std::string csvStr; - - const auto dir = std::format("zone/{}", Game::Win_GetLanguage()); - auto fileList = Utils::IO::ListFiles(dir, false); - for (const auto& entry : fileList) { - auto zone = entry.path().string(); - Utils::String::Replace(zone, Utils::String::VA("zone/%s/", Game::Win_GetLanguage()), ""); - Utils::String::Replace(zone, ".ff", ""); + Utils::IO::CreateDir("zone_source/techsets"); + Utils::IO::CreateDir("zone/techsets"); - if (Utils::IO::FileExists("zone/techsets/" + zone + "_techsets.ff")) + std::string csvStr; + + const auto dir = std::format("zone/{}", Game::Win_GetLanguage()); + auto fileList = Utils::IO::ListFiles(dir, false); + for (const auto& entry : fileList) { - Logger::Print("Skipping previously generated zone {}\n", zone); - continue; - } + auto zone = entry.path().string(); + Utils::String::Replace(zone, Utils::String::VA("zone/%s/", Game::Win_GetLanguage()), ""); + Utils::String::Replace(zone, ".ff", ""); - if (zone.find("_load") != std::string::npos) - { - Logger::Print("Skipping loadscreen zone {}\n", zone); - continue; - } + if (Utils::IO::FileExists("zone/techsets/" + zone + "_techsets.ff")) + { + Logger::Print("Skipping previously generated zone {}\n", zone); + continue; + } - if (Game::DB_IsZoneLoaded(zone.c_str()) || !FastFiles::Exists(zone)) - { - continue; - } + if (zone.find("_load") != std::string::npos) + { + Logger::Print("Skipping loadscreen zone {}\n", zone); + continue; + } - if (zone[0] == '.') continue; // fucking mac dotfiles + if (Game::DB_IsZoneLoaded(zone.c_str()) || !FastFiles::Exists(zone)) + { + continue; + } - curTechsets_list.clear(); // clear from last run + if (zone[0] == '.') continue; // fucking mac dotfiles - // load the zone - Game::XZoneInfo info; - info.name = zone.c_str(); - info.allocFlags = Game::DB_ZONE_MOD; - info.freeFlags = 0x0; - Game::DB_LoadXAssets(&info, 1, 0); + curTechsets_list.clear(); // clear from last run - while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(100ms); // wait till its fully loaded + // load the zone + Game::XZoneInfo info; + info.name = zone.c_str(); + info.allocFlags = Game::DB_ZONE_MOD; + info.freeFlags = 0x0; + Game::DB_LoadXAssets(&info, 1, 0); - if (curTechsets_list.empty()) - { - Logger::Print("Skipping empty zone {}\n", zone); - // unload zone + while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(100ms); // wait till its fully loaded + + if (curTechsets_list.empty()) + { + Logger::Print("Skipping empty zone {}\n", zone); + // unload zone + info.name = nullptr; + info.allocFlags = 0x0; + info.freeFlags = Game::DB_ZONE_MOD; + Game::DB_LoadXAssets(&info, 1, true); + continue; + } + + // ok so we're just gonna use the materials because they will use the techsets + csvStr.clear(); + for (auto tech : curTechsets_list) + { + std::string mat = ZoneBuilder::FindMaterialByTechnique(tech); + if (mat.length() == 0) + { + csvStr.append("techset," + tech + "\n"); + } + else + { + csvStr.append("material," + mat + "\n"); + } + } + + // save csv + Utils::IO::WriteFile("zone_source/techsets/" + zone + "_techsets.csv", csvStr); + + // build the techset zone + std::string zoneName = "techsets/" + zone + "_techsets"; + Logger::Print("Building zone '{}'...\n", zoneName); + Zone(zoneName).build(); + + // unload original zone info.name = nullptr; info.allocFlags = 0x0; info.freeFlags = Game::DB_ZONE_MOD; Game::DB_LoadXAssets(&info, 1, true); - continue; + + while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(10ms); // wait till its fully loaded } - // ok so we're just gonna use the materials because they will use the techsets - csvStr.clear(); - for (auto tech : curTechsets_list) - { - std::string mat = ZoneBuilder::FindMaterialByTechnique(tech); - if (mat.length() == 0) + curTechsets_list.clear(); + techsets_list.clear(); + + Game::DB_EnumXAssets(Game::ASSET_TYPE_TECHNIQUE_SET, [](Game::XAssetHeader header, void*) { - csvStr.append("techset," + tech + "\n"); + curTechsets_list.emplace(header.techniqueSet->name); + techsets_list.emplace(header.techniqueSet->name); + }, nullptr, false); + + // HACK: set language to 'techsets' to load from that dir + const char* language = Utils::Hook::Get(0x649E740); + Utils::Hook::Set(0x649E740, "techsets"); + + // load generated techset fastfiles + auto list = Utils::IO::ListFiles("zone/techsets", false); + int i = 0; + int subCount = 0; + for (const auto& entry : list) + { + auto it = entry.path().string(); + + Utils::String::Replace(it, "zone/techsets/", ""); + Utils::String::Replace(it, ".ff", ""); + + if (it.find("_techsets") == std::string::npos) continue; // skip files we didn't generate for this + + if (!Game::DB_IsZoneLoaded(it.data())) + { + Game::XZoneInfo info; + info.name = it.data(); + info.allocFlags = Game::DB_ZONE_MOD; + info.freeFlags = 0; + + Game::DB_LoadXAssets(&info, 1, 0); + while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(10ms); // wait till its fully loaded } else { - csvStr.append("material," + mat + "\n"); + Logger::Print("Zone '{}' already loaded\n", it); } + + if (i == 20) // cap at 20 just to be safe + { + // create csv with the techsets in it + csvStr.clear(); + for (auto tech : curTechsets_list) + { + std::string mat = ZoneBuilder::FindMaterialByTechnique(tech); + if (mat.length() == 0) + { + csvStr.append("techset," + tech + "\n"); + } + else + { + csvStr.append("material," + mat + "\n"); + } + } + + std::string tempZoneFile = Utils::String::VA("zone_source/techsets/techsets%d.csv", subCount); + std::string tempZone = Utils::String::VA("techsets/techsets%d", subCount); + + Utils::IO::WriteFile(tempZoneFile, csvStr); + + Logger::Print("Building zone '{}'...\n", tempZone); + Zone(tempZone).build(); + + // unload all zones + Game::XZoneInfo info; + info.name = nullptr; + info.allocFlags = 0x0; + info.freeFlags = Game::DB_ZONE_MOD; + Game::DB_LoadXAssets(&info, 1, true); + + Utils::Hook::Set(0x649E740, "techsets"); + + i = 0; + subCount++; + curTechsets_list.clear(); + techsets_list.clear(); + } + + i++; } - // save csv - Utils::IO::WriteFile("zone_source/techsets/" + zone + "_techsets.csv", csvStr); - - // build the techset zone - std::string zoneName = "techsets/" + zone + "_techsets"; - Logger::Print("Building zone '{}'...\n", zoneName); - Zone(zoneName).build(); - - // unload original zone - info.name = nullptr; - info.allocFlags = 0x0; - info.freeFlags = Game::DB_ZONE_MOD; - Game::DB_LoadXAssets(&info, 1, true); - - while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(10ms); // wait till its fully loaded - } - - curTechsets_list.clear(); - techsets_list.clear(); - - Game::DB_EnumXAssets(Game::ASSET_TYPE_TECHNIQUE_SET, [](Game::XAssetHeader header, void*) - { - curTechsets_list.emplace(header.techniqueSet->name); - techsets_list.emplace(header.techniqueSet->name); - }, nullptr, false); - - // HACK: set language to 'techsets' to load from that dir - const char* language = Utils::Hook::Get(0x649E740); - Utils::Hook::Set(0x649E740, "techsets"); - - // load generated techset fastfiles - auto list = Utils::IO::ListFiles("zone/techsets", false); - int i = 0; - int subCount = 0; - for (const auto& entry : list) - { - auto it = entry.path().string(); - - Utils::String::Replace(it, "zone/techsets/", ""); - Utils::String::Replace(it, ".ff", ""); - - if (it.find("_techsets") == std::string::npos) continue; // skip files we didn't generate for this - - if (!Game::DB_IsZoneLoaded(it.data())) - { - Game::XZoneInfo info; - info.name = it.data(); - info.allocFlags = Game::DB_ZONE_MOD; - info.freeFlags = 0; - - Game::DB_LoadXAssets(&info, 1, 0); - while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(10ms); // wait till its fully loaded - } - else - { - Logger::Print("Zone '{}' already loaded\n", it); - } - - if (i == 20) // cap at 20 just to be safe + // last iteration + if (i != 0) { // create csv with the techsets in it csvStr.clear(); @@ -1603,6 +1683,7 @@ namespace Components std::string mat = ZoneBuilder::FindMaterialByTechnique(tech); if (mat.length() == 0) { + Logger::Print("Couldn't find a material for techset {}. Sort Keys will be incorrect.\n", tech); csvStr.append("techset," + tech + "\n"); } else @@ -1626,28 +1707,36 @@ namespace Components info.freeFlags = Game::DB_ZONE_MOD; Game::DB_LoadXAssets(&info, 1, true); - Utils::Hook::Set(0x649E740, "techsets"); - - i = 0; subCount++; - curTechsets_list.clear(); - techsets_list.clear(); } - i++; - } + // build final techsets fastfile + if (subCount > 24) + { + Logger::Error(Game::ERR_DROP, "How did you have 576 fastfiles?\n"); + } + + curTechsets_list.clear(); + techsets_list.clear(); + + for (int j = 0; j < subCount; ++j) + { + Game::XZoneInfo info; + info.name = Utils::String::VA("techsets%d", j); + info.allocFlags = Game::DB_ZONE_MOD; + info.freeFlags = 0; + + Game::DB_LoadXAssets(&info, 1, 0); + while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(10ms); // wait till its fully loaded + } - // last iteration - if (i != 0) - { // create csv with the techsets in it csvStr.clear(); - for (auto tech : curTechsets_list) + for (const auto& tech : curTechsets_list) { - std::string mat = ZoneBuilder::FindMaterialByTechnique(tech); + auto mat = ZoneBuilder::FindMaterialByTechnique(tech); if (mat.length() == 0) { - Logger::Print("Couldn't find a material for techset {}. Sort Keys will be incorrect.\n", tech); csvStr.append("techset," + tech + "\n"); } else @@ -1656,115 +1745,62 @@ namespace Components } } - std::string tempZoneFile = Utils::String::VA("zone_source/techsets/techsets%d.csv", subCount); - std::string tempZone = Utils::String::VA("techsets/techsets%d", subCount); + Utils::IO::WriteFile("zone_source/techsets/techsets.csv", csvStr); - Utils::IO::WriteFile(tempZoneFile, csvStr); + // set language back + Utils::Hook::Set(0x649E740, language); - Logger::Print("Building zone '{}'...\n", tempZone); - Zone(tempZone).build(); + Logger::Print("Building zone 'techsets/techsets'...\n"); + Zone("techsets/techsets").build(); + }); - // unload all zones + Command::Add("listassets", [](const Command::Params* params) + { + if (params->size() < 2) return; + Game::XAssetType type = Game::DB_GetXAssetNameType(params->get(1)); + + if (type != Game::XAssetType::ASSET_TYPE_INVALID) + { + Game::DB_EnumXAssets(type, [](Game::XAssetHeader header, void* data) + { + Game::XAsset asset = { *reinterpret_cast(data), header }; + Logger::Print("{}\n", Game::DB_GetXAssetName(&asset)); + }, &type, false); + } + }); + + Command::Add("loadtempzone", [](const Command::Params* params) + { + if (params->size() < 2) return; + + if (FastFiles::Exists(params->get(1))) + { + Game::XZoneInfo info; + info.name = params->get(1); + info.allocFlags = 0x80; + info.freeFlags = 0x0; + Game::DB_LoadXAssets(&info, 1, 0); + } + }); + + Command::Add("unloadtempzones", [](const Command::Params*) + { Game::XZoneInfo info; info.name = nullptr; info.allocFlags = 0x0; - info.freeFlags = Game::DB_ZONE_MOD; + info.freeFlags = 0x80; Game::DB_LoadXAssets(&info, 1, true); - - subCount++; - } - - // build final techsets fastfile - if (subCount > 24) - { - Logger::Error(Game::ERR_DROP, "How did you have 576 fastfiles?\n"); - } - - curTechsets_list.clear(); - techsets_list.clear(); - - for (int j = 0; j < subCount; ++j) - { - Game::XZoneInfo info; - info.name = Utils::String::VA("techsets%d", j); - info.allocFlags = Game::DB_ZONE_MOD; - info.freeFlags = 0; - - Game::DB_LoadXAssets(&info, 1, 0); - while (!Game::Sys_IsDatabaseReady()) std::this_thread::sleep_for(10ms); // wait till its fully loaded - } - - // create csv with the techsets in it - csvStr.clear(); - for (const auto& tech : curTechsets_list) - { - auto mat = ZoneBuilder::FindMaterialByTechnique(tech); - if (mat.length() == 0) - { - csvStr.append("techset," + tech + "\n"); - } - else - { - csvStr.append("material," + mat + "\n"); - } - } - - Utils::IO::WriteFile("zone_source/techsets/techsets.csv", csvStr); - - // set language back - Utils::Hook::Set(0x649E740, language); - - Logger::Print("Building zone 'techsets/techsets'...\n"); - Zone("techsets/techsets").build(); - }); - - Command::Add("listassets", [](const Command::Params* params) - { - if (params->size() < 2) return; - Game::XAssetType type = Game::DB_GetXAssetNameType(params->get(1)); - - if (type != Game::XAssetType::ASSET_TYPE_INVALID) - { - Game::DB_EnumXAssets(type, [](Game::XAssetHeader header, void* data) - { - Game::XAsset asset = { *reinterpret_cast(data), header }; - Logger::Print("{}\n", Game::DB_GetXAssetName(&asset)); - }, &type, false); - } - }); - - Command::Add("loadtempzone", [](const Command::Params* params) - { - if (params->size() < 2) return; - - if (FastFiles::Exists(params->get(1))) - { - Game::XZoneInfo info; - info.name = params->get(1); - info.allocFlags = 0x80; - info.freeFlags = 0x0; - Game::DB_LoadXAssets(&info, 1, 0); - } - }); - - Command::Add("unloadtempzones", [](const Command::Params*) - { - Game::XZoneInfo info; - info.name = nullptr; - info.allocFlags = 0x0; - info.freeFlags = 0x80; - Game::DB_LoadXAssets(&info, 1, true); - AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_RAWFILE, "default"); // Lock until zone is unloaded - }); + AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_RAWFILE, "default"); // Lock until zone is unloaded + }); Command::Add("materialInfoDump", [](const Command::Params*) - { - Game::DB_EnumXAssets(Game::ASSET_TYPE_MATERIAL, [](Game::XAssetHeader header, void*) { - Logger::Print("{}: {:#X} {:#X} {:#X}\n", - header.material->info.name, header.material->info.sortKey & 0xFF, header.material->info.gameFlags & 0xFF, header.material->stateFlags & 0xFF); - }, nullptr, false); - }); + Game::DB_EnumXAssets(Game::ASSET_TYPE_MATERIAL, [](Game::XAssetHeader header, void*) + { + Logger::Print("{}: {:#X} {:#X} {:#X}\n", + header.material->info.name, header.material->info.sortKey & 0xFF, header.material->info.gameFlags & 0xFF, header.material->stateFlags & 0xFF); + }, nullptr, false); + }); } } diff --git a/src/Components/Modules/Zones.cpp b/src/Components/Modules/Zones.cpp index 2d77c579..4da16137 100644 --- a/src/Components/Modules/Zones.cpp +++ b/src/Components/Modules/Zones.cpp @@ -1947,9 +1947,12 @@ namespace Components AssetHandler::Relocate(buffer + 0x20, buffer + 0x18, 0x30); AssetHandler::Relocate(buffer + 0x51, buffer + 0x48, 5); AssetHandler::Relocate(buffer + 0x58, buffer + 0x50, 0x10); - + + Game::Material* material = reinterpret_cast(buffer); // fix statebit - reinterpret_cast(buffer)->stateBitsEntry[47] = codol_material[0x50]; + material->stateBitsEntry[47] = codol_material[0x50]; + //check to fix distortion + if (material->info.sortKey == 44) material->info.sortKey = 43; } else if (Zones::ZoneVersion >= 359) { @@ -1993,6 +1996,9 @@ namespace Components // yes it was lol memcpy(&material->info.drawSurf.packed, material359.drawSurfBegin, 8); + //adding this here, situation as with later ff versions + if (material->info.sortKey == 44) material->info.sortKey = 43; + memcpy(&material->info.surfaceTypeBits, &material359.drawSurf[0], 6); // copies both surfaceTypeBits and hashIndex //material->drawSurf[8] = material359.drawSurf[0]; //material->drawSurf[9] = material359.drawSurf[1]; @@ -2046,6 +2052,8 @@ namespace Components Game::GfxWorld* world = reinterpret_cast(buffer); world->fogTypesAllowed = 3; + //all codol zones are like this pretty certain + reinterpret_cast(buffer)->sortKeyDistortion = 43; } return result; diff --git a/src/Game/BothGames.cpp b/src/Game/BothGames.cpp index e3986666..023be38a 100644 --- a/src/Game/BothGames.cpp +++ b/src/Game/BothGames.cpp @@ -11,4 +11,6 @@ namespace Game BG_IsWeaponValid_t BG_IsWeaponValid = BG_IsWeaponValid_t(0x415BA0); BG_GetEquippedWeaponIndex_t BG_GetEquippedWeaponIndex = BG_GetEquippedWeaponIndex_t(0x4D8BA0); BG_GetEquippedWeaponState_t BG_GetEquippedWeaponState = BG_GetEquippedWeaponState_t(0x4E79E0); + BG_PlayerHasWeapon_t BG_PlayerHasWeapon = BG_PlayerHasWeapon_t(0x4AB530); + BG_GetWeaponCompleteDef_t BG_GetWeaponCompleteDef = BG_GetWeaponCompleteDef_t(0x44CE00); } diff --git a/src/Game/BothGames.hpp b/src/Game/BothGames.hpp index 946979b5..d8b6707d 100644 --- a/src/Game/BothGames.hpp +++ b/src/Game/BothGames.hpp @@ -28,4 +28,10 @@ namespace Game typedef PlayerEquippedWeaponState*(*BG_GetEquippedWeaponState_t)(playerState_s* ps, unsigned int weaponIndex); extern BG_GetEquippedWeaponState_t BG_GetEquippedWeaponState; + + typedef int*(*BG_PlayerHasWeapon_t)(playerState_s* ps, unsigned int weaponIndex); + extern BG_PlayerHasWeapon_t BG_PlayerHasWeapon; + + typedef Game::WeaponCompleteDef*(*BG_GetWeaponCompleteDef_t)(unsigned int weaponIndex); + extern BG_GetWeaponCompleteDef_t BG_GetWeaponCompleteDef; } diff --git a/src/Game/Script.hpp b/src/Game/Script.hpp index 14ebbbb5..64182e73 100644 --- a/src/Game/Script.hpp +++ b/src/Game/Script.hpp @@ -221,6 +221,8 @@ namespace Game constexpr auto LOCAL_VAR_STACK_SIZE = 64; + constexpr auto SCRIPTDATA_DIR = "scriptdata"; + extern void IncInParam(); extern void Scr_AddBool(int value); diff --git a/src/Game/Structs.hpp b/src/Game/Structs.hpp index faadb458..d8fbb19c 100644 --- a/src/Game/Structs.hpp +++ b/src/Game/Structs.hpp @@ -1044,8 +1044,8 @@ namespace Game unsigned __int16 randomDataIntCount; unsigned __int16 numframes; char flags; - char boneCount[10]; - char notifyCount; + unsigned char boneCount[10]; + unsigned char notifyCount; char assetType; bool isDefault; unsigned int randomDataShortCount; @@ -3237,7 +3237,7 @@ namespace Game unsigned char* triEdgeIsWalkable; unsigned int borderCount; CollisionBorder* borders; - int partitionCount; + unsigned int partitionCount; CollisionPartition* partitions; unsigned int aabbTreeCount; CollisionAabbTree* aabbTrees; diff --git a/src/Steam/Steam.cpp b/src/Steam/Steam.cpp index e55b8c1f..37ec9361 100644 --- a/src/Steam/Steam.cpp +++ b/src/Steam/Steam.cpp @@ -130,7 +130,7 @@ namespace Steam } else { - Proxy::SetMod("IW4y: Modern Warfare 2"); + Proxy::SetMod("IW4x: Modern Warfare 2"); Proxy::RunGame(); } diff --git a/src/Utils/Utils.hpp b/src/Utils/Utils.hpp index d75e1130..eba34b59 100644 --- a/src/Utils/Utils.hpp +++ b/src/Utils/Utils.hpp @@ -82,6 +82,31 @@ namespace Utils Utils::Merge(&this->slots, obj.getSlots()); } + void disconnect(const Slot slot) + { + std::lock_guard _(this->mutex); + + if (slot) + { + this->slots.erase( + std::remove_if( + this->slots.begin(), + this->slots.end(), + [&](std::function& a) + { + if (a.target() == slot.target()) + { + return true; + } + + return false; + } + + ), this->slots.end() + ); + } + } + void connect(const Slot slot) { std::lock_guard _(this->mutex);