Merge branch 'develop' into codo_461_dump

# Conflicts:
#	src/Components/Modules/ZoneBuilder.cpp
#	src/Components/Modules/Zones.cpp
This commit is contained in:
Louvenarde 2023-12-17 00:30:54 +01:00
commit d0e7ec7787
44 changed files with 1237 additions and 569 deletions

View File

@ -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

2
deps/GSL vendored

@ -1 +1 @@
Subproject commit afaaa71bcee45d9c90c21f8bd3ba2b12902242e9
Subproject commit 4300304ef24c247b3db0255763f46b9f95c3a83d

@ -1 +1 @@
Subproject commit 6af596a010eebf727e5d914bf9a01903c14ae128
Subproject commit 700a2ae858c27568d95e21ce8c5f941d36c4a6c4

2
deps/libtomcrypt vendored

@ -1 +1 @@
Subproject commit 93f5348c47d3578091a4ee5b90f4add216b46d1b
Subproject commit b96e96cf8b22a931e8e91098ac37bc72f9e2f033

2
deps/libtommath vendored

@ -1 +1 @@
Subproject commit c6a00c26ca2192c713a36227fdd84d126cdc95b9
Subproject commit 3746c58f29a1ebea15046932bbc9dacc35b4b214

2
deps/pdcurses vendored

@ -1 +1 @@
Subproject commit 0eb254ae43cdd1f532d515accac59377087883f6
Subproject commit 5c62af03e9a05e3a3ae8c8354c1234b772dcf4b0

2
deps/rapidjson vendored

@ -1 +1 @@
Subproject commit 973dc9c06dcd3d035ebd039cfb9ea457721ec213
Subproject commit 476ffa2fd272243275a74c36952f210267dc3088

2
deps/zlib vendored

@ -1 +1 @@
Subproject commit 48c3741002aca9dae84e9f2288ca149af14c9128
Subproject commit 79a0e447a0dfa32979420cb21cfb96d684b2c9d5

View File

@ -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

View File

@ -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

View File

@ -355,9 +355,13 @@ namespace Components
AssetHandler::TypeCallbacks[type] = callback;
}
void AssetHandler::OnLoad(Utils::Slot<AssetHandler::RestrictCallback> callback)
std::function<void()> AssetHandler::OnLoad(Utils::Slot<AssetHandler::RestrictCallback> callback)
{
AssetHandler::RestrictSignal.connect(callback);
return [callback](){
AssetHandler::RestrictSignal.disconnect(callback);
};
}
void AssetHandler::ClearRelocations()

View File

@ -23,7 +23,7 @@ namespace Components
~AssetHandler();
static void OnFind(Game::XAssetType type, Utils::Slot<Callback> callback);
static void OnLoad(Utils::Slot<RestrictCallback> callback);
static std::function<void()> OnLoad(Utils::Slot<RestrictCallback> callback);
static void ClearRelocations();
static void Relocate(void* start, void* to, DWORD size = 4);

View File

@ -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::TracerDef>(Game::XAssetType::ASSET_TYPE_TRACER, name);
}
void ITracerDef::mark(Game::XAssetHeader header, Components::ZoneBuilder::Zone* builder)

View File

@ -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::WeaponCompleteDef>(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);
}
}
);
}
}

View File

@ -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;

View File

@ -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<unsigned short>();
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<Game::XAnimNotifyInfo>();
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);
}

View File

@ -306,7 +306,7 @@ namespace Assets
Game::CollisionPartition* destPartitions = buffer->dest<Game::CollisionPartition>();
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];

View File

@ -21,7 +21,8 @@ namespace Components
{
0xf4d2c30b712ac6e3,
0xf7e33c4081337fa3,
0x6f5597f103cc50e9
0x6f5597f103cc50e9,
0xecd542eee54ffccf,
};
bool Auth::HasAccessToReservedSlot;

View File

@ -19,12 +19,17 @@ namespace Components
std::size_t Bots::BotDataIndex;
std::vector<Bots::botData> 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::botData> Bots::LoadBotNames()
{
std::vector<botData> 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<botData>
{
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<int8_t>(rightInt);
g_botai[entref.entnum].active = true;
});
GSC::Script::AddMethod("BotMeleeParams", [](const Game::scr_entref_t entref) // Usage: <bot> BotMeleeParams(<float>, <float>);
{
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<int>(static_cast<int>(Game::Scr_GetFloat(1)), std::numeric_limits<unsigned char>::min(), std::numeric_limits<unsigned char>::max());
g_botai[entref.entnum].meleeYaw = yaw;
g_botai[entref.entnum].meleeDist = static_cast<int8_t>(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<uint16_t>(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<uint16_t>(i);
break;
}
}
}
}
@ -446,14 +516,19 @@ namespace Components
}
}
count = std::min(Game::MAX_CLIENTS, count);
count = std::clamp<std::size_t>(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
{

View File

@ -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<std::string, std::string>;
static const Game::dvar_t* sv_randomBotNames;
static const Game::dvar_t* sv_replaceBots;
static std::size_t BotDataIndex;
static std::vector<botData> RemoteBotNames;
static void UpdateBotNames();
static std::vector<botData> 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();

View File

@ -1,6 +1,8 @@
#include <STDInclude.hpp>
#include "ClientCommand.hpp"
#include "Weapon.hpp"
#include "GSC/Script.hpp"
using namespace Utils::String;
@ -381,9 +383,16 @@ namespace Components
Game::XModel* model = nullptr;
if (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;

View File

@ -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));

View File

@ -435,8 +435,41 @@ namespace Components
MongooseLogBuffer.push_back(c);
}
static std::optional<std::string> 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<std::string> 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<std::string> ListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
std::optional<std::string> 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<std::string> MapHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
std::optional<std::string> 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<std::string> FileHandler(mg_connection* c, const mg_http_message* hm)
std::optional<std::string> 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<std::string> ServerListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
std::optional<std::string> Download::ServerListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
std::vector<std::string> 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<std::optional<std::string>(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;

View File

@ -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<std::string> 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<std::string> ListHandler(mg_connection* c, const mg_http_message* hm);
static std::optional<std::string> InfoHandler(mg_connection* c, const mg_http_message* hm);
static std::optional<std::string> ServerListHandler(mg_connection* c, const mg_http_message* hm);
static std::optional<std::string> MapHandler(mg_connection* c, const mg_http_message* hm);
};
}

View File

@ -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());
});

View File

@ -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<unsigned int>(std::max(0, static_cast<int>(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;
}
}

View File

@ -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;

View File

@ -208,7 +208,6 @@ namespace Components
nlohmann::json MapRotation::to_json()
{
assert(!DedicatedRotation.empty());
return DedicatedRotation.to_json();
}

View File

@ -112,7 +112,8 @@ namespace Components
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();

View File

@ -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());

View File

@ -9,16 +9,18 @@ namespace Components
{
std::unordered_map<std::uint32_t, int> RCon::RateLimit;
std::vector<std::size_t> RCon::RconAddresses;
std::vector<std::size_t> 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: {} <command>\n", params->get(0));
return;
}
const auto command = params->join(1);
auto* addr = reinterpret_cast<Game::netadr_t*>(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<Game::netadr_t*>(0xA5EA44);
Network::Address target(addr);
@ -91,9 +126,9 @@ namespace Components
Network::Address address(params->get(1));
const auto hash = std::hash<std::uint32_t>()(*reinterpret_cast<const std::uint32_t*>(&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<int>())
if (lastTime && (time - lastTime) < RConTimeout.get<int>())
{
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<int>())
if ((time - i->second) > RConTimeout.get<int>())
{
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<std::string>();
const auto svPassword = RConPassword.get<std::string>();
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<bool>())
if (RConLogRequests.get<bool>())
#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<bool>())
#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<char*>(publicKey), sizeof(publicKey)));
RConKey.set(std::string(reinterpret_cast<char*>(publicKey), sizeof(publicKey)));
RconContainer.timestamp = 0;
RConContainer.timestamp = 0;
Events::OnDvarInit([]
{
RconPassword = Dvar::Register<const char*>("rcon_password", "", Game::DVAR_NONE, "The password for rcon");
RconLogRequests = Dvar::Register<bool>("rcon_log_requests", false, Game::DVAR_NONE, "Print remote commands in log");
RconTimeout = Dvar::Register<int>("rcon_timeout", 500, 100, 10000, Game::DVAR_NONE, "");
RConPassword = Dvar::Register<const char*>("rcon_password", "", Game::DVAR_NONE, "The password for rcon");
RConLogRequests = Dvar::Register<bool>("rcon_log_requests", false, Game::DVAR_NONE, "Print remote commands in log");
RConTimeout = Dvar::Register<int>("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<std::uint32_t>()(*reinterpret_cast<const std::uint32_t*>(&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<std::uint32_t>()(*reinterpret_cast<const std::uint32_t*>(&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");
}
}

View File

@ -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<std::uint32_t, int> RateLimit;
static std::vector<std::size_t> RconAddresses;
static std::vector<std::size_t> 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);
};
}

View File

@ -1,5 +1,6 @@
#include <STDInclude.hpp>
#include <Utils/InfoString.hpp>
#include <Utils/WebIO.hpp>
#include "Discovery.hpp"
#include "Events.hpp"
@ -10,10 +11,14 @@
#include "Toast.hpp"
#include "UIFeeder.hpp"
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
namespace Components
{
bool ServerList::SortAsc = true;
int ServerList::SortKey = static_cast<std::underlying_type_t<Column>>(Column::Ping);
bool ServerList::SortAsc = false;
int ServerList::SortKey = static_cast<std::underlying_type_t<Column>>(Column::Players);
unsigned int ServerList::CurrentServer = 0;
ServerList::Container ServerList::RefreshContainer;
@ -24,6 +29,8 @@ namespace Components
std::vector<unsigned int> 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();
}
}

View File

@ -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);

View File

@ -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<DWORD>(0x4F76FB, 0x12EC + (sizeof(bg_sharedAmmoCaps) - (1200 * 4)));
// Move arg4 pointers
Utils::Hook::Set<DWORD>(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<DWORD>(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<BYTE>(0x44F240, 0xEB);
// Skip double loading for fs_game
Utils::Hook::Set<BYTE>(0x4081FD, 0xEB);

View File

@ -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;

View File

@ -30,7 +30,7 @@ namespace Components
buffer(0xC800000),
zoneName(name),
dataMap("zone_source/" + name + ".csv"),
branding{nullptr},
branding{ nullptr },
assetDepth(0),
iw4ofApi(getIW4OfApiParams())
{
@ -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<char>(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];
@ -545,7 +545,7 @@ namespace Components
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<int>(brandingLen), getAllocator()->duplicateString(zoneBranding)};
this->branding = { this->zoneName.data(), 0, static_cast<int>(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.
@ -792,6 +792,14 @@ namespace Components
return AssetHandler::FindAssetForZone(static_cast<Game::XAssetType>(type), name, this).data;
};
params.request_mark_asset = [this](int type, void* data) -> void
{
Game::XAsset asset {static_cast<Game::XAssetType>(type), {data}};
AssetHandler::ZoneMark(asset, this);
this->addRawAsset(static_cast<Game::XAssetType>(type), data);
};
params.fs_read_file = [](const std::string& filename) -> std::string
{
auto file = FileSystem::File(filename);
@ -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);
@ -1111,13 +1119,8 @@ namespace Components
params.find_other_asset = [](int type, const std::string& name) -> void*
{
if (ZoneBuilder::DumpingZone.empty())
{
return Game::DB_FindXAssetHeader(static_cast<Game::XAssetType>(type), name.data()).data;
}
// Do not deadlock the DB
return nullptr;
return Game::DB_FindXAssetHeader(static_cast<Game::XAssetType>(type), name.data()).data;
};
params.fs_read_file = [](const std::string& filename) -> std::string
@ -1272,7 +1275,7 @@ namespace Components
auto jmp = 0x5B9841 - 0x5B97B6 + 1 + 0xFD + 0x100 - 515;
Utils::Hook::Set(0x5B97B6 +1, static_cast<short>(jmp));
AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader asset, const std::string& name, bool* /*restrict*/)
AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader /* asset*/, const std::string& name, bool* /*restrict*/)
{
if (!ZoneBuilder::TraceZone.empty() && ZoneBuilder::TraceZone == FastFiles::Current())
{
@ -1281,8 +1284,9 @@ namespace Components
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,30 +1330,50 @@ 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<asset_t> 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);
}
});
Command::Add("dumpzone", [](const Command::Params* params)
{
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("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("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);
}
}
}
Logger::Print("Unloading zone '{}'...\n", zone);
info.freeFlags = Game::DB_ZONE_MOD;
info.allocFlags = 0;

View File

@ -1948,8 +1948,11 @@ namespace Components
AssetHandler::Relocate(buffer + 0x51, buffer + 0x48, 5);
AssetHandler::Relocate(buffer + 0x58, buffer + 0x50, 0x10);
Game::Material* material = reinterpret_cast<Game::Material*>(buffer);
// fix statebit
reinterpret_cast<Game::Material*>(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<Game::GfxWorld*>(buffer);
world->fogTypesAllowed = 3;
//all codol zones are like this pretty certain
reinterpret_cast<Game::GfxWorld*>(buffer)->sortKeyDistortion = 43;
}
return result;

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -130,7 +130,7 @@ namespace Steam
}
else
{
Proxy::SetMod("IW4y: Modern Warfare 2");
Proxy::SetMod("IW4x: Modern Warfare 2");
Proxy::RunGame();
}

View File

@ -82,6 +82,31 @@ namespace Utils
Utils::Merge(&this->slots, obj.getSlots());
}
void disconnect(const Slot<T> slot)
{
std::lock_guard<std::recursive_mutex> _(this->mutex);
if (slot)
{
this->slots.erase(
std::remove_if(
this->slots.begin(),
this->slots.end(),
[&](std::function<T>& a)
{
if (a.target<T>() == slot.target<T>())
{
return true;
}
return false;
}
), this->slots.end()
);
}
}
void connect(const Slot<T> slot)
{
std::lock_guard<std::recursive_mutex> _(this->mutex);