Merge pull request #887 from XLabsProject/develop

Release v0.7.9
This commit is contained in:
Edo 2023-03-31 13:45:38 +02:00 committed by GitHub
commit 626be45a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 705 additions and 454 deletions

2
.gitmodules vendored
View File

@ -17,7 +17,7 @@
[submodule "deps/mongoose"]
path = deps/mongoose
url = https://github.com/cesanta/mongoose.git
branch = 7.9
branch = 7.8
[submodule "deps/protobuf"]
path = deps/protobuf
url = https://github.com/google/protobuf.git

View File

@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog v0.3.0](http://keepachangelog.com/en/0.3.0/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.7.9] - 2023-03-31
### Added
- Game scripts can be loaded from the `scripts/mp`, `scripts/mp/<map name>` and `scripts/mp/<game type>` folders (#859)
- Add `ReadStream` GSC function (#862)
- Add `IString` GSC function (#877)
### Changed
- Test Clients will no longer receive names from the Xlabs Patreon website. The old behaviour was restored (#852)
- Enabled `OpenFile` GSC function (#862)
- Enabled `CloseFile` GSC function (#862)
- Chat system uses "reliable message" to mitigate message duplication (#873)
- The built-in GSC compiler no longer throws fatal errors when overriding a built-in function or method (IW3 behaviour) (#880)
- `CastFloat` GSC function was renamed to `Float` (#880)
### Fixed
- Fix bug where knife lounges would not work with a gamepad (#848)
- Fix rare RCon crash (#861)
- Fix bug where `sv_RandomBotNames` stopped working (#876)
- Fix crash caused by "Mongoose" (dependency) (#874)
### Known issues
- Sound issue fix is experimental as the bug is not fully understood.
## [0.7.8] - 2023-03-17
### Added
@ -32,7 +57,8 @@ The format is based on [Keep a Changelog v0.3.0](http://keepachangelog.com/en/0.
- Servers can no longer modify client Dvars that are saved in the client's config (#694)
- `banClient` and `muteClient` server commands do not apply to bots anymore (#730)
- Remove `zb_prefer_disk_assets` Dvar (#772)
- The max value of `perk_extendedMeleeRange`Dvar was increased (#782)
- The max value of `perk_extendedMeleeRange` Dvar was increased (#782)
- Test Clients will receive names from the Xlabs Patreon website in addition to the names from the bots.txt file (#771)
### Fixed
- Fix bug where`reloadmenus` command would not free resources used by custom menus (#740)

2
deps/mongoose vendored

@ -1 +1 @@
Subproject commit 4236405b90e051310aadda818e21c811e404b4d8
Subproject commit 0a265e79a67d7bfcdca27f2ccb98ccb474677ec6

View File

@ -1,6 +1,7 @@
#include <STDInclude.hpp>
#include <Utils/InfoString.hpp>
#include "Modules/ArenaLength.hpp"
#include "Modules/Bans.hpp"
#include "Modules/Bots.hpp"
#include "Modules/Branding.hpp"

View File

@ -73,7 +73,6 @@ namespace Components
#include "Modules/UIScript.hpp"
#include "Modules/ZoneBuilder.hpp"
#include "Modules/ArenaLength.hpp"
#include "Modules/AssetHandler.hpp"
#include "Modules/Dedicated.hpp"
#include "Modules/Events.hpp"

View File

@ -1,4 +1,5 @@
#include <STDInclude.hpp>
#include "ArenaLength.hpp"
namespace Components
{

View File

@ -12,56 +12,6 @@ namespace Assets
assert(header->data);
}
void IFxEffectDef::loadFxElemVisuals(Game::FxElemVisuals* visuals, char elemType, Components::ZoneBuilder::Zone* builder, Utils::Stream::Reader* reader)
{
switch (elemType)
{
case Game::FX_ELEM_TYPE_MODEL:
{
if (visuals->model)
{
visuals->model = Components::AssetHandler::FindAssetForZone(Game::XAssetType::ASSET_TYPE_XMODEL, reader->readString(), builder).model;
}
break;
}
case Game::FX_ELEM_TYPE_OMNI_LIGHT:
case Game::FX_ELEM_TYPE_SPOT_LIGHT:
break;
case Game::FX_ELEM_TYPE_SOUND:
{
if (visuals->soundName)
{
visuals->soundName = reader->readCString();
}
break;
}
case Game::FX_ELEM_TYPE_RUNNER:
{
if (visuals->effectDef.handle)
{
visuals->effectDef.handle = Components::AssetHandler::FindAssetForZone(Game::XAssetType::ASSET_TYPE_FX, reader->readString(), builder).fx;
}
break;
}
default:
{
if (visuals->material)
{
visuals->material = Components::AssetHandler::FindAssetForZone(Game::XAssetType::ASSET_TYPE_MATERIAL, reader->readString(), builder).material;
}
break;
}
}
}
void IFxEffectDef::loadFromIW4OF(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder)
{
header->fx = builder->getIW4OfApi()->read<Game::FxEffectDef>(Game::XAssetType::ASSET_TYPE_FX, name);
@ -169,7 +119,16 @@ namespace Assets
case 0xA:
{
builder->loadAssetByName(Game::XAssetType::ASSET_TYPE_SOUND, visuals->soundName, false);
if (visuals->soundName)
{
// Double "find call" but we have to because otherwise we'd crash on missing asset
// Sometimes Fx reference by name a sound that does not exist. IW oversight ?
// Never happens on iw3 but often happens on iw5, especially DLC maps.
if (Components::AssetHandler::FindAssetForZone(Game::XAssetType::ASSET_TYPE_SOUND, visuals->soundName, builder, false).data)
{
builder->loadAssetByName(Game::XAssetType::ASSET_TYPE_SOUND, visuals->soundName, false);
}
}
break;
}

View File

@ -18,7 +18,5 @@ namespace Assets
void loadEfx(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder);
void loadNative(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder);
void loadFromIW4OF(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder);
void loadFxElemVisuals(Game::FxElemVisuals* visuals, char elemType, Components::ZoneBuilder::Zone* builder, Utils::Stream::Reader* reader);
};
}

View File

@ -451,7 +451,7 @@ namespace Components
Utils::Hook::Set<DWORD>(0x4D0D60, 0xC301B0);
// Guid command
Command::Add("guid", [](Command::Params*)
Command::Add("guid", []
{
Logger::Print("Your guid: {:#X}\n", Steam::SteamUser()->GetSteamID().bits);
});

View File

@ -19,7 +19,7 @@ namespace Components
static unsigned __int64 GetKeyHash(const std::string& key);
static uint32_t GetSecurityLevel();
static void IncreaseSecurityLevel(uint32_t level, const std::string& command = "");
static void IncreaseSecurityLevel(uint32_t level, const std::string& command = {});
static uint32_t GetZeroBits(Utils::Cryptography::Token token, const std::string& publicKey);
static void IncrementToken(Utils::Cryptography::Token& token, Utils::Cryptography::Token& computeToken, const std::string& publicKey, uint32_t zeroBits, bool* cancel = nullptr, uint64_t* count = nullptr);

View File

@ -259,17 +259,17 @@ namespace Components
}
}
const auto num = std::atoi(input);
if (num < 0 || num >= *Game::svs_clientCount)
const auto clientNum = std::strtoul(input, nullptr, 10);
if (clientNum >= Game::MAX_CLIENTS)
{
Logger::Print("Bad client slot: {}\n", num);
Logger::Print("Bad client slot: {}\n", clientNum);
return;
}
const auto* cl = &Game::svs_clients[num];
if (cl->header.state == Game::CS_FREE)
const auto* cl = &Game::svs_clients[clientNum];
if (cl->header.state < Game::CS_ACTIVE)
{
Logger::Print("Client {} is not active\n", num);
Logger::Print("Client {} is not active\n", clientNum);
return;
}
@ -279,7 +279,7 @@ namespace Components
}
const std::string reason = params->size() < 3 ? "EXE_ERR_BANNED_PERM" : params->join(2);
BanClient(&Game::svs_clients[num], reason);
BanClient(&Game::svs_clients[clientNum], reason);
});
Command::Add("unbanClient", [](Command::Params* params)
@ -309,7 +309,7 @@ namespace Components
else if (type == "guid"s)
{
SteamID id;
id.bits = strtoull(params->get(2), nullptr, 16);
id.bits = std::strtoull(params->get(2), nullptr, 16);
UnbanClient(id);

View File

@ -1,8 +1,6 @@
#include <STDInclude.hpp>
#include <Utils/InfoString.hpp>
#include "Bots.hpp"
#include "ServerList.hpp"
#include "GSC/Script.hpp"
@ -14,7 +12,7 @@ namespace Components
{
std::vector<Bots::botData> Bots::BotNames;
Dvar::Var Bots::SVClanName;
Dvar::Var Bots::SVRandomBotNames;
struct BotMovementInfo
{
@ -25,7 +23,7 @@ namespace Components
bool active;
};
static BotMovementInfo g_botai[18];
static BotMovementInfo g_botai[Game::MAX_CLIENTS];
struct BotAction
{
@ -59,19 +57,6 @@ namespace Components
std::ranges::shuffle(BotNames, gen);
}
void Bots::UpdateBotNames()
{
const auto masterPort = (*Game::com_masterPort)->current.integer;
const auto* masterServerName = (*Game::com_masterServerName)->current.string;
Game::netadr_t master;
if (ServerList::GetMasterServer(masterServerName, masterPort, master))
{
Logger::Print("Getting bots...\n");
Network::Send(master, "getbots");
}
}
void Bots::LoadBotNames()
{
FileSystem::File bots("bots.txt");
@ -83,15 +68,8 @@ namespace Components
auto data = Utils::String::Split(bots.getBuffer(), '\n');
auto i = 0;
for (auto& entry : data)
{
if (i >= 18)
{
// Only parse 18 names from the file
break;
}
// Take into account for CR line endings
Utils::String::Replace(entry, "\r", "");
// Remove whitespace
@ -116,11 +94,10 @@ namespace Components
entry = entry.substr(0, pos);
}
BotNames.emplace_back(std::make_pair(entry, clanAbbrev));
++i;
BotNames.emplace_back(entry, clanAbbrev);
}
if (i)
if (SVRandomBotNames.get<bool>())
{
RandomizeBotNames();
}
@ -128,7 +105,7 @@ namespace Components
int Bots::BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int statStuff, int port)
{
static size_t botId = 0; // Loop over the BotNames vector
static std::size_t botId = 0; // Loop over the BotNames vector
static bool loadedNames = false; // Load file only once
std::string botName;
std::string clanName;
@ -152,11 +129,6 @@ namespace Components
clanName = "BOT"s;
}
if (const auto svClanName = SVClanName.get<std::string>(); !svClanName.empty())
{
clanName = svClanName;
}
return _snprintf_s(buffer, 0x400, _TRUNCATE, connectString, num, botName.data(), clanName.data(), protocol, checksum, statVer, statStuff, port);
}
@ -203,15 +175,14 @@ namespace Components
Game::Scr_AddBool(Game::SV_IsTestClient(ent->s.number) != 0);
}
void Bots::AddMethods()
void Bots::AddScriptMethods()
{
GSC::Script::AddMethMultiple(GScr_isTestClient, false, {"IsTestClient", "IsBot"}); // Usage: self IsTestClient();
GSC::Script::AddMethod("BotStop", [](Game::scr_entref_t entref) // Usage: <bot> BotStop();
GSC::Script::AddMethod("BotStop", [](const Game::scr_entref_t entref) // Usage: <bot> BotStop();
{
const auto* ent = GSC::Script::Scr_GetPlayerEntity(entref);
if (Game::SV_IsTestClient(ent->s.number) == 0)
if (!Game::SV_IsTestClient(ent->s.number))
{
Game::Scr_Error("BotStop: Can only call on a bot!");
return;
@ -222,18 +193,16 @@ namespace Components
g_botai[entref.entnum].active = true;
});
GSC::Script::AddMethod("BotWeapon", [](Game::scr_entref_t entref) // Usage: <bot> BotWeapon(<str>);
GSC::Script::AddMethod("BotWeapon", [](const Game::scr_entref_t entref) // Usage: <bot> BotWeapon(<str>);
{
const auto* ent = GSC::Script::Scr_GetPlayerEntity(entref);
if (Game::SV_IsTestClient(ent->s.number) == 0)
if (!Game::SV_IsTestClient(ent->s.number))
{
Game::Scr_Error("BotWeapon: Can only call on a bot!");
return;
}
const auto* weapon = Game::Scr_GetString(0);
if (!weapon || !*weapon)
{
g_botai[entref.entnum].weapon = 1;
@ -245,18 +214,16 @@ namespace Components
g_botai[entref.entnum].active = true;
});
GSC::Script::AddMethod("BotAction", [](Game::scr_entref_t entref) // Usage: <bot> BotAction(<str action>);
GSC::Script::AddMethod("BotAction", [](const Game::scr_entref_t entref) // Usage: <bot> BotAction(<str action>);
{
const auto* ent = GSC::Script::Scr_GetPlayerEntity(entref);
if (Game::SV_IsTestClient(ent->s.number) == 0)
if (!Game::SV_IsTestClient(ent->s.number))
{
Game::Scr_Error("BotAction: Can only call on a bot!");
return;
}
const auto* action = Game::Scr_GetString(0);
if (!action)
{
Game::Scr_ParamError(0, "BotAction: Illegal parameter!");
@ -286,11 +253,10 @@ namespace Components
Game::Scr_ParamError(0, "BotAction: Unknown action");
});
GSC::Script::AddMethod("BotMovement", [](Game::scr_entref_t entref) // Usage: <bot> BotMovement(<int>, <int>);
GSC::Script::AddMethod("BotMovement", [](const Game::scr_entref_t entref) // Usage: <bot> BotMovement(<int>, <int>);
{
const auto* ent = GSC::Script::Scr_GetPlayerEntity(entref);
if (Game::SV_IsTestClient(ent->s.number) == 0)
if (!Game::SV_IsTestClient(ent->s.number))
{
Game::Scr_Error("BotMovement: Can only call on a bot!");
return;
@ -399,30 +365,7 @@ namespace Components
Utils::Hook(0x441B80, G_SelectWeaponIndex_Hk, HOOK_JUMP).install()->quick();
Events::OnDvarInit([]
{
SVClanName = Dvar::Register<const char*>("sv_clanName", "", Game::DVAR_NONE, "The clan name for test clients");
});
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');
for (const auto& entry : botNames)
{
BotNames.emplace_back(std::make_pair(entry, "BOT"));
}
RandomizeBotNames();
}
});
SVRandomBotNames = Dvar::Register<bool>("sv_RandomBotNames", false, Game::DVAR_NONE, "Randomize the bots' names");
// Reset BotMovementInfo.active when client is dropped
Events::OnClientDisconnect([](const int clientNum)
@ -475,7 +418,7 @@ namespace Components
Spawn(count);
});
AddMethods();
AddScriptMethods();
// In case a loaded mod didn't call "BotStop" before the VM shutdown
Events::OnVMShutdown([]

View File

@ -11,17 +11,16 @@ namespace Components
using botData = std::pair< std::string, std::string>;
static std::vector<botData> BotNames;
static Dvar::Var SVClanName;
static Dvar::Var SVRandomBotNames;
static void RandomizeBotNames();
static void UpdateBotNames();
static void LoadBotNames();
static int BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int statStuff, int port);
static void Spawn(unsigned int count);
static void GScr_isTestClient(Game::scr_entref_t entref);
static void AddMethods();
static void AddScriptMethods();
static void BotAiAction(Game::client_t* cl);
static void SV_BotUserMove_Hk();

View File

@ -44,6 +44,9 @@ namespace Components
Utils::Hook::Nop(0x43EC96, 9);
Utils::Hook::Nop(0x4675C6, 9);
Utils::Hook::Nop(0x405A36, 9);
Utils::Hook::Nop(0x4CE656, 9);
Utils::Hook::Nop(0x461E66, 9);
Utils::Hook::Nop(0x4EB3F6, 9);
// Random checks scattered throughout the binary
Utils::Hook::Set<std::uint8_t>(0x499F90, 0xC3);
@ -56,13 +59,7 @@ namespace Components
Utils::Hook::Set<std::uint8_t>(0x461930, 0xC3);
Utils::Hook::Set<std::uint8_t>(0x430410, 0xC3);
// Used next to file system functions
Utils::Hook::Set<std::uint8_t>(0x47BC00, 0xC3);
// Looking for stuff in the registry
Utils::Hook::Nop(0x4826F8, 5);
// Live_Init
Utils::Hook::Nop(0x420937, 5);
}
}

View File

@ -63,13 +63,13 @@ namespace Components
if (IsMuted(player))
{
SendChat = false;
Game::SV_GameSendServerCommand(player - Game::g_entities, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"You are muted\"", 0x65));
Game::SV_GameSendServerCommand(player - Game::g_entities, Game::SV_CMD_RELIABLE, Utils::String::VA("%c \"You are muted\"", 0x65));
}
if (sv_disableChat.get<bool>())
{
SendChat = false;
Game::SV_GameSendServerCommand(player - Game::g_entities, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"Chat is disabled\"", 0x65));
Game::SV_GameSendServerCommand(player - Game::g_entities, Game::SV_CMD_RELIABLE, Utils::String::VA("%c \"Chat is disabled\"", 0x65));
}
// Message might be empty after processing the '/'
@ -302,7 +302,7 @@ namespace Components
});
Logger::Print("{} was muted\n", client->name);
Game::SV_GameSendServerCommand(client - Game::svs_clients, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"You were muted\"", 0x65));
Game::SV_GameSendServerCommand(client - Game::svs_clients, Game::SV_CMD_RELIABLE, Utils::String::VA("%c \"You were muted\"", 0x65));
}
void Chat::UnmuteClient(const Game::client_t* client)
@ -310,7 +310,7 @@ namespace Components
UnmuteInternal(client->steamID);
Logger::Print("{} was unmuted\n", client->name);
Game::SV_GameSendServerCommand(client - Game::svs_clients, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"You were unmuted\"", 0x65));
Game::SV_GameSendServerCommand(client - Game::svs_clients, Game::SV_CMD_RELIABLE, Utils::String::VA("%c \"You were unmuted\"", 0x65));
}
void Chat::UnmuteInternal(const std::uint64_t id, bool everyone)
@ -461,17 +461,17 @@ namespace Components
if (params->size() < 2) return;
auto message = params->join(1);
auto name = sv_sayName.get<std::string>();
const auto message = params->join(1);
const auto name = sv_sayName.get<std::string>();
if (!name.empty())
{
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_CAN_IGNORE, Utils::String::Format("{:c} \"{}: {}\"", 0x68, name, message));
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_RELIABLE, Utils::String::Format("{:c} \"{}: {}\"", 0x68, name, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "{}: {}\n", name, message);
}
else
{
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_CAN_IGNORE, Utils::String::Format("{:c} \"Console: {}\"", 0x68, message));
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_RELIABLE, Utils::String::Format("{:c} \"Console: {}\"", 0x68, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "Console: {}\n", message);
}
});
@ -486,19 +486,21 @@ namespace Components
if (params->size() < 3) return;
const auto client = std::atoi(params->get(1));
auto message = params->join(2);
auto name = sv_sayName.get<std::string>();
const auto parsedInput = std::strtoul(params->get(1), nullptr, 10);
const auto clientNum = static_cast<int>(std::min<std::size_t>(parsedInput, Game::MAX_CLIENTS));
const auto message = params->join(2);
const auto name = sv_sayName.get<std::string>();
if (!name.empty())
{
Game::SV_GameSendServerCommand(client, Game::SV_CMD_CAN_IGNORE, Utils::String::Format("{:c} \"{}: {}\"", 0x68, name.data(), message));
Logger::Print(Game::CON_CHANNEL_SERVER, "{} -> {}: {}\n", name, client, message);
Game::SV_GameSendServerCommand(clientNum, Game::SV_CMD_RELIABLE, Utils::String::Format("{:c} \"{}: {}\"", 0x68, name.data(), message));
Logger::Print(Game::CON_CHANNEL_SERVER, "{} -> {}: {}\n", name, clientNum, message);
}
else
{
Game::SV_GameSendServerCommand(client, Game::SV_CMD_CAN_IGNORE, Utils::String::Format("{:c} \"Console: {}\"", 0x68, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "Console -> {}: {}\n", client, message);
Game::SV_GameSendServerCommand(clientNum, Game::SV_CMD_RELIABLE, Utils::String::Format("{:c} \"Console: {}\"", 0x68, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "Console -> {}: {}\n", clientNum, message);
}
});
@ -512,8 +514,8 @@ namespace Components
if (params->size() < 2) return;
auto message = params->join(1);
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_CAN_IGNORE, Utils::String::Format("{:c} \"{}\"", 0x68, message));
const auto message = params->join(1);
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_RELIABLE, Utils::String::Format("{:c} \"{}\"", 0x68, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "Raw: {}\n", message);
});
@ -527,10 +529,12 @@ namespace Components
if (params->size() < 3) return;
const auto client = atoi(params->get(1));
std::string message = params->join(2);
Game::SV_GameSendServerCommand(client, Game::SV_CMD_CAN_IGNORE, Utils::String::Format("{:c} \"{}\"", 0x68, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "Raw -> {}: {}\n", client, message);
const auto parsedInput = std::strtoul(params->get(1), nullptr, 10);
const auto clientNum = static_cast<int>(std::min<std::size_t>(parsedInput, Game::MAX_CLIENTS));
const auto message = params->join(2);
Game::SV_GameSendServerCommand(clientNum, Game::SV_CMD_RELIABLE, Utils::String::Format("{:c} \"{}\"", 0x68, message));
Logger::Print(Game::CON_CHANNEL_SERVER, "Raw -> {}: {}\n", clientNum, message);
});
sv_sayName = Dvar::Register<const char*>("sv_sayName", "^7Console", Game::DVAR_NONE, "The alias of the server when broadcasting a chat message");
@ -620,6 +624,9 @@ namespace Components
Utils::Hook(0x4D00D4, PostSayStub, HOOK_CALL).install()->quick();
Utils::Hook(0x4D0110, PostSayStub, HOOK_CALL).install()->quick();
// Patch G_SayTo to use SV_CMD_RELIABLE
Utils::Hook::Set<std::uint8_t>(0x5DF7E3, Game::SV_CMD_RELIABLE);
// Change logic that does word splitting with new lines for chat messages to support fonticons
Utils::Hook(0x592E10, CG_AddToTeamChat_Stub, HOOK_JUMP).install()->quick();

View File

@ -61,31 +61,8 @@ namespace Components
void ClientCommand::AddCheatCommands()
{
Add("noclip", [](Game::gentity_s* ent, [[maybe_unused]] const Command::ServerParams* params)
{
if (!CheatsOk(ent))
return;
ent->client->flags ^= Game::PF_NOCLIP;
const auto entNum = ent->s.number;
Logger::Debug("Noclip toggled for entity {}", entNum);
Game::SV_GameSendServerCommand(entNum, Game::SV_CMD_CAN_IGNORE, VA("%c \"%s\"", 0x65, (ent->client->flags & Game::PF_NOCLIP) ? "GAME_NOCLIPON" : "GAME_NOCLIPOFF"));
});
Add("ufo", [](Game::gentity_s* ent, [[maybe_unused]] const Command::ServerParams* params)
{
if (!CheatsOk(ent))
return;
ent->client->flags ^= Game::PF_UFO;
const auto entNum = ent->s.number;
Logger::Debug("UFO toggled for entity {}", entNum);
Game::SV_GameSendServerCommand(entNum, Game::SV_CMD_CAN_IGNORE, VA("%c \"%s\"", 0x65, (ent->client->flags & Game::PF_UFO) ? "GAME_UFOON" : "GAME_UFOOFF"));
});
Add("noclip", Cmd_Noclip_f);
Add("ufo", Cmd_UFO_f);
Add("god", [](Game::gentity_s* ent, [[maybe_unused]] const Command::ServerParams* params)
{
@ -369,6 +346,21 @@ namespace Components
});
}
void ClientCommand::AddScriptMethods()
{
GSC::Script::AddMethod("Noclip", [](const Game::scr_entref_t entref) // gsc: self Noclip();
{
auto* ent = GSC::Script::Scr_GetPlayerEntity(entref);
Cmd_Noclip_f(ent, nullptr);
});
GSC::Script::AddMethod("Ufo", [](const Game::scr_entref_t entref) // gsc: self Ufo();
{
auto* ent = GSC::Script::Scr_GetPlayerEntity(entref);
Cmd_UFO_f(ent, nullptr);
});
}
const char* ClientCommand::EntInfoLine(const int entNum)
{
const auto* ent = &Game::g_entities[entNum];
@ -477,6 +469,32 @@ namespace Components
Logger::Print(Game::CON_CHANNEL_SERVER, "Done writing file.\n");
}
void ClientCommand::Cmd_Noclip_f(Game::gentity_s* ent, [[maybe_unused]] const Command::ServerParams* params)
{
if (!CheatsOk(ent))
return;
ent->client->flags ^= Game::PF_NOCLIP;
const auto entNum = ent->s.number;
Logger::Debug("Noclip toggled for entity {}", entNum);
Game::SV_GameSendServerCommand(entNum, Game::SV_CMD_CAN_IGNORE, VA("%c \"%s\"", 0x65, (ent->client->flags & Game::PF_NOCLIP) ? "GAME_NOCLIPON" : "GAME_NOCLIPOFF"));
}
void ClientCommand::Cmd_UFO_f(Game::gentity_s* ent, [[maybe_unused]] const Command::ServerParams* params)
{
if (!CheatsOk(ent))
return;
ent->client->flags ^= Game::PF_UFO;
const auto entNum = ent->s.number;
Logger::Debug("UFO toggled for entity {}", entNum);
Game::SV_GameSendServerCommand(entNum, Game::SV_CMD_CAN_IGNORE, VA("%c \"%s\"", 0x65, (ent->client->flags & Game::PF_UFO) ? "GAME_UFOON" : "GAME_UFOOFF"));
}
ClientCommand::ClientCommand()
{
AssertOffset(Game::playerState_s, stats, 0x150);
@ -486,6 +504,7 @@ namespace Components
AddCheatCommands();
AddScriptFunctions();
AddScriptMethods();
#ifdef _DEBUG
AddDevelopmentCommands();
#endif

View File

@ -16,10 +16,15 @@ namespace Components
static void ClientCommandStub(int clientNum);
static void AddCheatCommands();
static void AddDevelopmentCommands();
static void AddScriptFunctions();
static void AddScriptMethods();
static const char* EntInfoLine(int entNum);
static void G_DumpEntityDebugInfoToConsole(bool logfileOnly);
static void G_DumpEntityDebugInfoToCSV(const char* filenameSuffix);
static void Cmd_Noclip_f(Game::gentity_s* ent, const Command::ServerParams* params);
static void Cmd_UFO_f(Game::gentity_s* ent, const Command::ServerParams* params);
};
}

View File

@ -57,16 +57,16 @@ namespace Components
static Game::cmd_function_s* Allocate();
static void Add(const char* name, const std::function<void()>& callback);
static void Add(const char* name, const std::function<void(Command::Params*)>& callback);
static void Add(const char* name, const std::function<void(Params*)>& callback);
static void AddRaw(const char* name, void(*callback)(), bool key = false);
static void AddSV(const char* name, const std::function<void(Command::Params*)>& callback);
static void AddSV(const char* name, const std::function<void(Params*)>& callback);
static void Execute(std::string command, bool sync = true);
static Game::cmd_function_s* Find(const std::string& command);
private:
static std::unordered_map<std::string, std::function<void(Command::Params*)>> FunctionMap;
static std::unordered_map<std::string, std::function<void(Command::Params*)>> FunctionMapSV;
static std::unordered_map<std::string, std::function<void(Params*)>> FunctionMap;
static std::unordered_map<std::string, std::function<void(Params*)>> FunctionMapSV;
static void AddRawSV(const char* name, void(*callback)());

View File

@ -34,14 +34,14 @@ namespace Components
bool Console::SkipShutdown = false;
COLORREF Console::TextColor =
#if _DEBUG
#ifdef _DEBUG
RGB(255, 200, 117);
#else
RGB(120, 237, 122);
#endif
COLORREF Console::BackgroundColor =
#if _DEBUG
#ifdef _DEBUG
RGB(35, 21, 0);
#else
RGB(25, 32, 25);
@ -383,6 +383,7 @@ namespace Components
RefreshOutput();
#ifdef _DEBUG
if (IsDebuggerPresent())
{
while (true)
@ -390,6 +391,7 @@ namespace Components
std::this_thread::sleep_for(5s);
}
}
#endif
TerminateProcess(GetCurrentProcess(), EXIT_FAILURE);
}
@ -894,7 +896,7 @@ namespace Components
Utils::Hook(0x64DC6B, 0x64DCC2, HOOK_JUMP).install()->quick();
#ifdef _DEBUG
Console::AddConsoleCommand();
AddConsoleCommand();
#endif
if (Dedicated::IsEnabled() && !ZoneBuilder::IsEnabled())

View File

@ -14,6 +14,8 @@ namespace Components
Dvar::Var Dedicated::SVMOTD;
Dvar::Var Dedicated::COMLogFilter;
const Game::dvar_t* Dedicated::com_dedicated;
bool Dedicated::IsEnabled()
{
static std::optional<bool> flag;
@ -184,8 +186,6 @@ namespace Components
// Make sure all callbacks are handled
Scheduler::Loop(Steam::SteamAPI_RunCallbacks, Scheduler::Pipeline::SERVER);
SVLanOnly = Dvar::Register<bool>("sv_lanOnly", false, Game::DVAR_NONE, "Don't act as node");
Utils::Hook(0x60BE98, InitDedicatedServer, HOOK_CALL).install()->quick();
Utils::Hook::Set<BYTE>(0x683370, 0xC3); // steam sometimes doesn't like the server
@ -249,6 +249,26 @@ namespace Components
Events::OnDvarInit([]
{
SVMOTD = Dvar::Register<const char*>("sv_motd", "", Game::DVAR_NONE, "A custom message of the day for servers");
SVLanOnly = Dvar::Register<bool>("sv_lanOnly", false, Game::DVAR_NONE, "Don't act as node");
static const char* g_dedicatedEnumNames[] =
{
"listen server",
"dedicated LAN server",
"dedicated internet server",
nullptr,
};
// IW5MP Dedicated Server adds another flag. That flag should not exist on this version of IW4
com_dedicated = Game::Dvar_RegisterEnum("dedicated", g_dedicatedEnumNames, 2, Game::DVAR_ROM, "True if this is a dedicated server");
// Dedicated only behaviour from IW5MP Dedicated Server.
if (com_dedicated->current.integer != 1 && com_dedicated->current.integer != 2)
{
Game::DvarValue value;
value.integer = 0;
Game::Dvar_SetVariant(const_cast<Game::dvar_t*>(com_dedicated), value, Game::DVAR_SOURCE_INTERNAL);
}
});
// Post initialization point

View File

@ -12,6 +12,8 @@ namespace Components
static Dvar::Var SVMOTD;
static Dvar::Var COMLogFilter;
static const Game::dvar_t* com_dedicated;
static bool IsEnabled();
static bool IsRunning();

View File

@ -62,6 +62,8 @@ namespace Components
return;
}
char hostNameBuffer[256]{};
const auto* map = Game::UI_GetMapDisplayName((*Game::ui_mapname)->current.string);
const Game::StringTable* table;
@ -86,7 +88,6 @@ namespace Components
}
else
{
char hostNameBuffer[256]{};
TextRenderer::StripColors(Party::GetHostName().data(), hostNameBuffer, sizeof(hostNameBuffer));
TextRenderer::StripAllTextIcons(hostNameBuffer, hostNameBuffer, sizeof(hostNameBuffer));
@ -97,7 +98,7 @@ namespace Components
std::hash<Network::Address> hashFn;
const auto address = Party::Target();
DiscordPresence.partyId = Utils::String::VA("%zu", hashFn(address) ^ GetDiscordNonce());
DiscordPresence.partyId = Utils::String::VA("%s - %zu", hostNameBuffer, hashFn(address) ^ GetDiscordNonce());
DiscordPresence.joinSecret = address.getCString();
DiscordPresence.partySize = Game::cgArray[0].snap ? Game::cgArray[0].snap->numClients : 1;
DiscordPresence.partyMax = Party::GetMaxClients();

View File

@ -9,6 +9,8 @@
#include <mongoose.h>
#define MG_OVERRIDE_LOG_FN
namespace Components
{
static mg_mgr Mgr;
@ -26,6 +28,8 @@ namespace Components
volatile bool Download::Terminate;
bool Download::ServerRunning;
std::string Download::MongooseLogBuffer;
#pragma region Client
void Download::InitiateMapDownload(const std::string& map, bool needPassword)
@ -416,6 +420,19 @@ namespace Components
#pragma region Server
void Download::LogFn(char c, [[maybe_unused]] void* param)
{
// Truncate & print if buffer is 1024 characters in length or otherwise only print when we reached a 'new line'
if (!std::isprint(static_cast<unsigned char>(c)) || MongooseLogBuffer.size() == 1024)
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "{}\n", MongooseLogBuffer);
MongooseLogBuffer.clear();
return;
}
MongooseLogBuffer.push_back(c);
}
static std::string InfoHandler()
{
const auto status = ServerInfo::GetInfo();
@ -425,6 +442,7 @@ namespace Components
info["status"] = status.to_json();
info["host"] = host.to_json();
info["map_rotation"] = MapRotation::to_json();
info["dedicated"] = Dedicated::com_dedicated->current.value;
std::vector<nlohmann::json> players;
@ -432,23 +450,27 @@ namespace Components
for (auto i = 0; i < Game::MAX_CLIENTS; ++i)
{
std::unordered_map<std::string, nlohmann::json> playerInfo;
// Insert default values
playerInfo["score"] = 0;
playerInfo["ping"] = 0;
playerInfo["name"] = "";
playerInfo["test_client"] = 0;
if (Dedicated::IsRunning())
{
if (Game::svs_clients[i].header.state < Game::CS_CONNECTED) continue;
if (Game::svs_clients[i].header.state < Game::CS_ACTIVE) continue;
if (!Game::svs_clients[i].gentity || !Game::svs_clients[i].gentity->client) continue;
playerInfo["score"] = Game::SV_GameClientNum_Score(i);
playerInfo["ping"] = Game::svs_clients[i].ping;
playerInfo["name"] = Game::svs_clients[i].name;
playerInfo["test_client"] = Game::svs_clients[i].bIsTestClient;
}
else
{
// Score and ping are irrelevant
const auto* name = Game::PartyHost_GetMemberName(Game::g_lobbyData, i);
if (name == nullptr || *name == '\0') continue;
if (!name || !*name) continue;
playerInfo["name"] = name;
}
@ -457,7 +479,7 @@ namespace Components
}
info["players"] = players;
return {nlohmann::json(info).dump()};
return nlohmann::json(info).dump();
}
static std::string ListHandler()
@ -503,7 +525,7 @@ namespace Components
jsonList = fileList;
}
return {jsonList.dump()};
return jsonList.dump();
}
static std::string MapHandler()
@ -547,7 +569,7 @@ namespace Components
jsonList = fileList;
}
return {jsonList.dump()};
return jsonList.dump();
}
static void FileHandler(mg_connection* c, const mg_http_message* hm)
@ -663,6 +685,16 @@ namespace Components
{
if (!Flags::HasFlag("disable-mongoose"))
{
#ifdef _DEBUG
mg_log_set(MG_LL_INFO);
#else
mg_log_set(MG_LL_ERROR);
#endif
#ifdef MG_OVERRIDE_LOG_FN
mg_log_set_fn(LogFn, nullptr);
#endif
mg_mgr_init(&Mgr);
Network::OnStart([]

View File

@ -91,10 +91,14 @@ namespace Components
static volatile bool Terminate;
static bool ServerRunning;
static std::string MongooseLogBuffer;
static void DownloadProgress(FileDownload* fDownload, std::size_t bytes);
static void ModDownloader(ClientDownload* download);
static bool ParseModList(ClientDownload* download, const std::string& list);
static bool DownloadFile(ClientDownload* download, unsigned int index);
static void LogFn(char c, void* param);
};
}

View File

@ -432,6 +432,9 @@ namespace Components
// Uncheat ui_debugMode
Utils::Hook::Xor<std::uint8_t>(0x6312DE, Game::DVAR_CHEAT);
// Uncheat jump_slowdownEnable
Utils::Hook::Xor<std::uint32_t>(0x4EFABE, Game::DVAR_CHEAT);
// Hook dvar 'name' registration
Utils::Hook(0x40531C, Dvar_RegisterName, HOOK_CALL).install()->quick();

View File

@ -438,7 +438,7 @@ namespace Components
if (!Friends::IsOnline(user.lastTime)) return "Online";
if (user.server.getType() == Game::NA_BAD) return "Playing IW4x";
if (user.serverName.empty()) return Utils::String::VA("Playing on %s", user.server.getCString());
return Utils::String::VA("Playing %s on %s", Game::UI_LocalizeMapName(user.mapname.data()), user.serverName.data());
return Utils::String::VA("Playing %s on %s", Localization::LocalizeMapName(user.mapname.data()), user.serverName.data());
}
default:

View File

@ -4,17 +4,100 @@
namespace Components::GSC
{
const char* IO::QueryStrings[] = { R"(..)", R"(../)", R"(..\)" };
const char* IO::ForbiddenStrings[] = { R"(..)", R"(../)", R"(..\)" };
FILE* IO::openScriptIOFileHandle;
std::filesystem::path IO::Path;
void IO::GScr_OpenFile()
{
const auto* filepath = Game::Scr_GetString(0);
const auto* mode = Game::Scr_GetString(1);
for (std::size_t i = 0; i < std::extent_v<decltype(ForbiddenStrings)>; ++i)
{
if (std::strstr(filepath, ForbiddenStrings[i]) != nullptr)
{
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "OpenFile: directory traversal is not allowed!\n");
Game::Scr_AddInt(-1);
return;
}
}
if (mode != "read"s)
{
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "Valid openfile modes are 'read'\n");
Game::Scr_AddInt(-1);
return;
}
if (openScriptIOFileHandle)
{
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "OpenFile failed. {} files already open\n", 1);
Game::Scr_AddInt(-1);
return;
}
const auto scriptData = Path / "scriptdata"s / filepath;
_set_errno(0);
const auto result = fopen_s(&openScriptIOFileHandle, scriptData.string().data(), "r");
if (result || !openScriptIOFileHandle)
{
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "OpenFile failed. '{}'", result);
Game::Scr_AddInt(-1);
return;
}
Game::Scr_AddInt(1);
}
void IO::GScr_ReadStream()
{
if (!openScriptIOFileHandle)
{
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "ReadStream failed. File stream was not opened\n");
return;
}
char line[1024]{};
if (std::fgets(line, sizeof(line), openScriptIOFileHandle) != nullptr)
{
Game::Scr_AddString(line);
return;
}
Logger::Warning(Game::CON_CHANNEL_PARSERSCRIPT, "ReadStream failed.\n");
if (std::feof(openScriptIOFileHandle))
{
Logger::Print(Game::CON_CHANNEL_PARSERSCRIPT, "ReadStream: EOF reached\n");
}
}
void IO::GScr_CloseFile()
{
if (!openScriptIOFileHandle)
{
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "CloseFile failed. File stream was not opened\n");
Game::Scr_AddInt(-1);
return;
}
Game::Scr_AddInt(std::fclose(openScriptIOFileHandle));
openScriptIOFileHandle = nullptr;
}
void IO::AddScriptFunctions()
{
Script::AddFunction("FileWrite", [] // gsc: FileWrite(<filepath>, <string>, <mode>)
{
const auto* path = Game::Scr_GetString(0);
auto* text = Game::Scr_GetString(1);
auto* mode = Game::Scr_GetString(2);
const auto* filepath = Game::Scr_GetString(0);
const auto* text = Game::Scr_GetString(1);
const auto* mode = Game::Scr_GetString(2);
if (!path)
if (!filepath)
{
Game::Scr_ParamError(0, "FileWrite: filepath is not defined!");
return;
@ -26,117 +109,124 @@ namespace Components::GSC
return;
}
for (std::size_t i = 0; i < ARRAYSIZE(QueryStrings); ++i)
for (std::size_t i = 0; i < std::extent_v<decltype(ForbiddenStrings)>; ++i)
{
if (std::strstr(path, QueryStrings[i]) != nullptr)
if (std::strstr(filepath, ForbiddenStrings[i]) != nullptr)
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "FileWrite: directory traversal is not allowed!\n");
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "FileWrite: directory traversal is not allowed!\n");
return;
}
}
if (mode != "append"s && mode != "write"s)
{
Logger::Warning(Game::CON_CHANNEL_SCRIPT, "FileWrite: mode not defined or was wrong, defaulting to 'write'\n");
Logger::Warning(Game::CON_CHANNEL_PARSERSCRIPT, "FileWrite: mode not defined or was wrong, defaulting to 'write'\n");
mode = "write";
}
const auto* scriptData = Utils::String::VA("%s/%s", "scriptdata", path);
if (mode == "write"s)
{
FileSystem::FileWriter(scriptData).write(text);
}
else if (mode == "append"s)
{
FileSystem::FileWriter(scriptData, true).write(text);
}
const auto append = mode == "append"s;
const auto scriptData = Path / "scriptdata"s / filepath;
Utils::IO::WriteFile(scriptData.string(), text, append);
});
Script::AddFunction("FileRead", [] // gsc: FileRead(<filepath>)
{
const auto* path = Game::Scr_GetString(0);
if (!path)
const auto* filepath = Game::Scr_GetString(0);
if (!filepath)
{
Game::Scr_ParamError(0, "FileRead: filepath is not defined!");
return;
}
for (std::size_t i = 0; i < ARRAYSIZE(QueryStrings); ++i)
for (std::size_t i = 0; i < std::extent_v<decltype(ForbiddenStrings)>; ++i)
{
if (std::strstr(path, QueryStrings[i]) != nullptr)
if (std::strstr(filepath, ForbiddenStrings[i]) != nullptr)
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "FileRead: directory traversal is not allowed!\n");
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "FileRead: directory traversal is not allowed!\n");
return;
}
}
const auto* scriptData = Utils::String::VA("%s/%s", "scriptdata", path);
const auto scriptData = Path / "scriptdata"s / filepath;
const auto file = FileSystem::FileReader(scriptData);
if (!file.exists())
std::string file;
if (!Utils::IO::ReadFile(scriptData.string(), &file))
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "FileRead: file '{}' not found!\n", scriptData);
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "FileRead: file '{}' not found!\n", scriptData.string());
return;
}
auto buffer = file.getBuffer();
buffer = buffer.substr(0, 1024 - 1); // 1024 is the max string size for the SL system
Game::Scr_AddString(buffer.data());
file = file.substr(0, 1024 - 1); // 1024 is the max string size for the SL system
Game::Scr_AddString(file.data());
});
Script::AddFunction("FileExists", [] // gsc: FileExists(<filepath>)
{
const auto* path = Game::Scr_GetString(0);
if (!path)
const auto* filepath = Game::Scr_GetString(0);
if (!filepath)
{
Game::Scr_ParamError(0, "FileExists: filepath is not defined!");
return;
}
for (std::size_t i = 0; i < ARRAYSIZE(QueryStrings); ++i)
for (std::size_t i = 0; i < std::extent_v<decltype(ForbiddenStrings)>; ++i)
{
if (std::strstr(path, QueryStrings[i]) != nullptr)
if (std::strstr(filepath, ForbiddenStrings[i]) != nullptr)
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "FileExists: directory traversal is not allowed!\n");
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "FileExists: directory traversal is not allowed!\n");
return;
}
}
const auto* scriptData = Utils::String::VA("%s/%s", "scriptdata", path);
Game::Scr_AddBool(FileSystem::FileReader(scriptData).exists());
const auto scriptData = Path / "scriptdata"s / filepath;
Game::Scr_AddBool(Utils::IO::FileExists(scriptData.string()));
});
Script::AddFunction("FileRemove", [] // gsc: FileRemove(<filepath>)
{
const auto* path = Game::Scr_GetString(0);
if (!path)
const auto* filepath = Game::Scr_GetString(0);
if (!filepath)
{
Game::Scr_ParamError(0, "FileRemove: filepath is not defined!");
return;
}
for (std::size_t i = 0; i < ARRAYSIZE(QueryStrings); ++i)
for (std::size_t i = 0; i < std::extent_v<decltype(ForbiddenStrings)>; ++i)
{
if (std::strstr(path, QueryStrings[i]) != nullptr)
if (std::strstr(filepath, ForbiddenStrings[i]) != nullptr)
{
Logger::Print("FileRemove: directory traversal is not allowed!\n");
Logger::PrintError(Game::CON_CHANNEL_PARSERSCRIPT, "FileRemove: directory traversal is not allowed!\n");
return;
}
}
const auto p = "scriptdata"s / std::filesystem::path(path);
const auto folder = p.parent_path().string();
const auto file = p.filename().string();
Game::Scr_AddInt(FileSystem::_DeleteFile(folder, file));
const auto scriptData = Path / "scriptdata"s / filepath;
Game::Scr_AddBool(Utils::IO::RemoveFile(scriptData.string()));
});
Script::AddFunction("ReadStream", GScr_ReadStream);
}
IO::IO()
{
openScriptIOFileHandle = nullptr;
Path = "userraw"s;
AddScriptFunctions();
Utils::Hook::Set<Game::BuiltinFunction>(0x79A858, GScr_OpenFile);
Utils::Hook::Set<int>(0x79A85C, 0);
Utils::Hook::Set<Game::BuiltinFunction>(0x79A864, GScr_CloseFile);
Utils::Hook::Set<int>(0x79A868, 0);
Events::OnVMShutdown([]
{
if (openScriptIOFileHandle)
{
std::fclose(openScriptIOFileHandle);
openScriptIOFileHandle = nullptr;
}
});
}
}

View File

@ -8,7 +8,15 @@ namespace Components::GSC
IO();
private:
static const char* QueryStrings[];
static const char* ForbiddenStrings[];
static FILE* openScriptIOFileHandle;
static std::filesystem::path Path;
static void GScr_OpenFile();
static void GScr_ReadStream();
static void GScr_CloseFile();
static void AddScriptFunctions();
};

View File

@ -72,16 +72,20 @@ namespace Components::GSC
const auto* op = Game::Scr_GetString(1);
const auto b = GetInt64Arg(2, true);
if (const auto itr = Operations.find(op); itr != Operations.end())
{
Game::Scr_AddString(Utils::String::VA("%lld", itr->second(a, b)));
return;
if (const auto itr = Operations.find(op); itr != Operations.end())
{
Game::Scr_AddString(Utils::String::VA("%lld", itr->second(a, b)));
return;
}
}
if (const auto itr = Comparisons.find(op); itr != Comparisons.end())
{
Game::Scr_AddBool(itr->second(a, b));
return;
if (const auto itr = Comparisons.find(op); itr != Comparisons.end())
{
Game::Scr_AddBool(itr->second(a, b));
return;
}
}
Game::Scr_ParamError(1, "Invalid int64 operation");

View File

@ -35,24 +35,23 @@ namespace Components::GSC
Game::Scr_StartupGameType();
}
// Do not use C++ objects because Scr_LoadScript may longjmp
void Script::GScr_LoadGameTypeScript_Stub()
void Script::LoadCustomScriptsFromFolder(const char* dir)
{
// Clear handles (from previous GSC loading session)
ScriptMainHandles.clear();
ScriptInitHandles.clear();
char path[MAX_OSPATH]{};
char searchPath[MAX_OSPATH]{};
char path[MAX_PATH]{};
strncpy_s(searchPath, dir, _TRUNCATE);
strncat_s(searchPath, "/", _TRUNCATE);
auto numFiles = 0;
const auto** files = Game::FS_ListFiles("scripts/", "gsc", Game::FS_LIST_ALL, &numFiles, 10);
const auto** files = Game::FS_ListFiles(searchPath, "gsc", Game::FS_LIST_ALL, &numFiles, 10);
for (auto i = 0; i < numFiles; ++i)
{
const auto* scriptFile = files[i];
Logger::Print("Loading script {}...\n", scriptFile);
const auto len = sprintf_s(path, "%s/%s", "scripts", scriptFile);
const auto len = sprintf_s(path, "%s/%s", dir, scriptFile);
if (len == -1)
{
continue;
@ -63,7 +62,7 @@ namespace Components::GSC
if (!Game::Scr_LoadScript(path))
{
Logger::Print("Script {} encountered an error while loading. (doesn't exist?)", path);
Logger::Print("Script {} encountered an error while loading. A compilation error is the most likely cause\n", path);
continue;
}
@ -72,12 +71,14 @@ namespace Components::GSC
const auto initHandle = Game::Scr_GetFunctionHandle(path, "init");
if (initHandle != 0)
{
Logger::Debug("Loaded '{}::init'", path);
ScriptInitHandles.insert_or_assign(path, initHandle);
}
const auto mainHandle = Game::Scr_GetFunctionHandle(path, "main");
if (mainHandle != 0)
{
Logger::Debug("Loaded '{}::main'", path);
ScriptMainHandles.insert_or_assign(path, mainHandle);
}
@ -85,6 +86,34 @@ namespace Components::GSC
}
Game::FS_FreeFileList(files, 10);
}
void Script::LoadCustomScripts()
{
LoadCustomScriptsFromFolder("scripts");
// Game specific
const auto* gameDir = "scripts/mp";
LoadCustomScriptsFromFolder(gameDir);
// Map specific
const auto* mapDir = Utils::String::Format("scripts/mp/{}", (*Game::sv_mapname)->current.string);
LoadCustomScriptsFromFolder(mapDir);
// Mode specific
const auto* modeDir = Utils::String::Format("scripts/mp/{}", (*Game::g_gametype)->current.string);
LoadCustomScriptsFromFolder(modeDir);
}
// Do not use C++ objects because Scr_LoadScript may longjmp and crash or leak memory
void Script::GScr_LoadGameTypeScript_Stub()
{
// Clear handles (from previous GSC loading session)
ScriptMainHandles.clear();
ScriptInitHandles.clear();
LoadCustomScripts();
Game::GScr_LoadGameTypeScript();
}
@ -255,5 +284,9 @@ namespace Components::GSC
Utils::Hook(0x4EC8DD, BuiltIn_GetMethodStub, HOOK_CALL).install()->quick(); // Scr_GetMethod
Utils::Hook(0x5F41A3, SetExpFogStub, HOOK_CALL).install()->quick();
// Restore IW3's compiler behaviour when dealing with 'overriding builtin function'
Utils::Hook::Nop(0x613EDA, 2); // Scr_GetFunction
Utils::Hook::Nop(0x613EF0, 2); // Scr_GetMethod
}
}

View File

@ -39,6 +39,9 @@ namespace Components::GSC
static std::unordered_map<std::string, int> ScriptMainHandles;
static std::unordered_map<std::string, int> ScriptInitHandles;
static void LoadCustomScriptsFromFolder(const char* dir);
static void LoadCustomScripts();
static void Scr_LoadGameType_Stub();
static void Scr_StartupGameType_Stub();
static void GScr_LoadGameTypeScript_Stub();

View File

@ -35,14 +35,14 @@ namespace Components::GSC
void ScriptExtension::GScr_AddFieldsForEntityStub()
{
for (const auto& [offset, field] : CustomEntityFields)
for (const auto& field : CustomEntityFields | std::views::values)
{
Game::Scr_AddClassField(Game::ClassNum::CLASS_NUM_ENTITY, field.name, field.ofs);
}
Utils::Hook::Call<void()>(0x4A7CF0)(); // GScr_AddFieldsForClient
for (const auto& [offset, field] : CustomClientFields)
for (const auto& field : CustomClientFields | std::views::values)
{
Game::Scr_AddClassField(Game::ClassNum::CLASS_NUM_ENTITY, field.name, field.ofs);
}

View File

@ -5,6 +5,8 @@
namespace Components::GSC
{
using namespace Utils::String;
void String::AddScriptFunctions()
{
Script::AddFunction("ToUpper", [] // gsc: ToUpper(<string>)
@ -108,7 +110,7 @@ namespace Components::GSC
});
// Func present on IW5
Script::AddFunction("CastFloat", [] // gsc: CastFloat()
Script::AddFunction("Float", [] // gsc: Float()
{
switch (Game::Scr_GetType(0))
{
@ -122,7 +124,7 @@ namespace Components::GSC
Game::Scr_AddFloat(static_cast<float>(Game::Scr_GetInt(0)));
break;
default:
Game::Scr_ParamError(0, Utils::String::VA("cannot cast %s to float", Game::Scr_GetTypeName(0)));
Game::Scr_ParamError(0, VA("cannot cast %s to float", Game::Scr_GetTypeName(0)));
break;
}
});
@ -141,6 +143,22 @@ namespace Components::GSC
Game::Scr_AddInt(result);
});
Script::AddFunction("IString", [] // gsc: IString(<string>)
{
if (Game::Scr_GetType(0) != Game::VAR_STRING)
{
Game::Scr_ParamError(0, VA("cannot cast %s to istring", Game::Scr_GetTypeName(0)));
return;
}
const auto value = Game::Scr_GetConstString(0);
Game::SL_AddRefToString(value);
Game::Scr_AddIString(Game::Scr_GetString(0));
Game::SL_RemoveRefToString(value);
});
}
String::String()

View File

@ -21,7 +21,7 @@ namespace Components
{Game::GPAD_UP, Game::K_DPAD_UP},
{Game::GPAD_DOWN, Game::K_DPAD_DOWN},
{Game::GPAD_LEFT, Game::K_DPAD_LEFT},
{Game::GPAD_RIGHT, Game::K_DPAD_RIGHT}
{Game::GPAD_RIGHT, Game::K_DPAD_RIGHT},
};
Game::StickToCodeMap_t Gamepad::analogStickList[4]
@ -712,6 +712,8 @@ namespace Components
void Gamepad::AimAssist_UpdateGamePadInput(const Game::AimInput* input, Game::AimOutput* output)
{
assert(input);
assert(output);
AssertIn(input->localClientNum, Game::STATIC_MAX_LOCAL_CLIENTS);
auto& aaGlob = Game::aaGlobArray[input->localClientNum];
@ -724,13 +726,11 @@ namespace Components
Game::AimAssist_UpdateTweakables(input->localClientNum);
Game::AimAssist_UpdateAdsLerp(input);
AimAssist_ApplyTurnRates(input, output);
// Automelee has already been done by keyboard so don't do it again
Game::AimAssist_ApplyAutoMelee(input, output);
AimAssist_ApplyLockOn(input, output);
}
aaGlob.prevButtons = input->buttons;
aaGlob.prevButtons = input->buttons;
}
}
void Gamepad::CL_RemoteControlMove_GamePad(const int localClientNum, Game::usercmd_s* cmd)
@ -960,6 +960,10 @@ namespace Components
aimInput.forwardAxis = forward;
aimInput.rightAxis = side;
AimAssist_UpdateGamePadInput(&aimInput, &aimOutput);
cmd->meleeChargeDist = aimOutput.meleeChargeDist;
cmd->meleeChargeYaw = aimOutput.meleeChargeYaw;
clientActive.clViewangles[0] = aimOutput.pitch;
clientActive.clViewangles[1] = aimOutput.yaw;
}

View File

@ -226,7 +226,7 @@ namespace Components
Command::Add("ipcping", []([[maybe_unused]] Command::Params* params)
{
Logger::Print("Sending ping to pipe!\n");
Write("ping", "");
Write("ping", {});
});
}

View File

@ -1,4 +1,5 @@
#include <STDInclude.hpp>
#include "ArenaLength.hpp"
namespace Components
{
@ -330,6 +331,25 @@ namespace Components
return pszString;
}
const char* Localization::LocalizeMapName(const char* mapName)
{
for (int i = 0; i < *Game::arenaCount; ++i)
{
if (!_stricmp(ArenaLength::NewArenas[i].mapName, mapName))
{
auto* uiName = &ArenaLength::NewArenas[i].uiName[0];
if ((uiName[0] == 'M' && uiName[1] == 'P') || (uiName[0] == 'P' && uiName[1] == 'A')) // MPUI/PATCH
{
return Get(uiName);
}
return uiName;
}
}
return mapName;
}
Localization::Localization()
{
SetCredits();

View File

@ -14,6 +14,8 @@ namespace Components
static std::optional<std::string> PrefixOverride;
static void ParseOutput(const std::function<void(Game::LocalizeEntry*)>& callback);
static const char* LocalizeMapName(const char* mapName);
private:
static std::recursive_mutex LocalizeMutex;
static std::unordered_map<std::string, Game::LocalizeEntry*> LocalizeMap;

View File

@ -28,25 +28,24 @@ namespace Components
void Logger::MessagePrint(const int channel, const std::string& msg)
{
std::string out = msg;
// Filter out coloured strings for stdout
if (out[0] == '^' && out[1] != '\0')
static const auto shouldPrint = []() -> bool
{
out = out.substr(2);
}
return Flags::HasFlag("stdout") || Loader::IsPerformingUnitTests();
}();
if (Flags::HasFlag("stdout") || Loader::IsPerformingUnitTests())
if (shouldPrint)
{
printf("%s", out.data());
fflush(stdout);
std::printf("%s", msg.data());
std::fflush(stdout);
return;
}
#ifdef _DEBUG
if (!IsConsoleReady())
{
OutputDebugStringA(out.data());
OutputDebugStringA(msg.data());
}
#endif
if (!Game::Sys_IsMainThread())
{
@ -141,7 +140,7 @@ namespace Components
void Logger::NetworkLog(const char* data, bool gLog)
{
if (data == nullptr)
if (!data)
{
return;
}
@ -163,7 +162,7 @@ namespace Components
va_end(ap);
const auto time = Game::level->time / 1000;
const auto len = _snprintf_s(string, _TRUNCATE, "%3i:%i%i %s", time / 60, time % 60 / 10, time % 60 % 10, string2);
const auto len = sprintf_s(string, "%3i:%i%i %s", time / 60, time % 60 / 10, time % 60 % 10, string2);
if (Game::level->logFile)
{
@ -183,25 +182,29 @@ namespace Components
jz returnPrint
pushad
push [esp + 28h]
call PrintMessagePipe
add esp, 4h
popad
retn
ret
returnPrint:
pushad
push 0
push [esp + 2Ch]
push 0 // gLog
push [esp + 2Ch] // data
call NetworkLog
add esp, 8h
popad
push esi
mov esi, [esp + 0Ch]
push 4AA835h
retn
ret
}
}

View File

@ -1,5 +1,6 @@
#include <STDInclude.hpp>
#include "ArenaLength.hpp"
#include "FastFiles.hpp"
#include "RawFiles.hpp"
#include "StartupMessages.hpp"
@ -92,9 +93,9 @@ namespace Components
Game::unzClose(this->searchPath.iwd->handle);
auto _free = Utils::Hook::Call<void(void*)>(0x6B5CF2);
_free(this->searchPath.iwd->buildBuffer);
_free(this->searchPath.iwd);
// Use game's free function
Utils::Hook::Call<void(void*)>(0x6B5CF2)(this->searchPath.iwd->buildBuffer);
Utils::Hook::Call<void(void*)>(0x6B5CF2)(this->searchPath.iwd);
ZeroMemory(&this->searchPath, sizeof this->searchPath);
}
@ -619,8 +620,8 @@ namespace Components
{
if (entry.is_directory())
{
auto zoneName = entry.path().filename().string();
auto mapPath = std::format("{}\\{}.ff", entry.path().string(), zoneName);
const auto zoneName = entry.path().filename().string();
const auto mapPath = std::format("{}\\{}.ff", entry.path().string(), zoneName);
if (Utils::IO::FileExists(mapPath))
{
FoundCustomMaps.push_back(zoneName);
@ -659,7 +660,7 @@ namespace Components
if (error)
{
Logger::Error(Game::ERR_DISCONNECT, "Missing DLC pack {} ({}) containing map {} ({}).\nPlease download it to play this map.",
pack.name, pack.index, Game::UI_LocalizeMapName(mapname.data()), mapname);
pack.name, pack.index, Localization::LocalizeMapName(mapname.data()), mapname);
}
return dlcIsTrue;
@ -758,8 +759,8 @@ namespace Components
Maps::AddDlc({ 1, "Stimulus Pack", {"mp_complex", "mp_compact", "mp_storm", "mp_overgrown", "mp_crash"} });
Maps::AddDlc({ 2, "Resurgence Pack", {"mp_abandon", "mp_vacant", "mp_trailerpark", "mp_strike", "mp_fuel2"} });
Maps::AddDlc({ 3, "IW4x Classics", {"mp_nuked", "mp_cross_fire", "mp_cargoship", "mp_bloc", "mp_killhouse", "mp_bog_sh", "mp_cargoship_sh", "mp_shipment_long", "mp_rust_long", "mp_firingrange", "mp_bloc_sh", "mp_crash_tropical", "mp_estate_tropical", "mp_fav_tropical", "mp_storm_spring"} });
Maps::AddDlc({ 4, "Call Of Duty 4 Pack", {"mp_farm", "mp_backlot", "mp_pipeline", "mp_countdown", "mp_crash_snow", "mp_carentan", "mp_broadcast", "mp_showdown", "mp_convoy"} });
Maps::AddDlc({ 3, "IW4x Classics", {"mp_nuked", "mp_cross_fire", "mp_cargoship", "mp_bloc", "mp_killhouse", "mp_bog_sh", "mp_cargoship_sh", "mp_shipment", "mp_shipment_long", "mp_rust_long", "mp_firingrange", "mp_bloc_sh", "mp_crash_tropical", "mp_estate_tropical", "mp_fav_tropical", "mp_storm_spring"} });
Maps::AddDlc({ 4, "Call Of Duty 4 Pack", {"mp_farm", "mp_backlot", "mp_pipeline", "mp_countdown", "mp_crash_snow", "mp_carentan", "mp_broadcast", "mp_showdown", "mp_convoy", "mp_citystreets"} });
Maps::AddDlc({ 5, "Modern Warfare 3 Pack", {"mp_dome", "mp_hardhat", "mp_paris", "mp_seatown", "mp_bravo", "mp_underground", "mp_plaza2", "mp_village", "mp_alpha"}});
Maps::UpdateDlcStatus();

View File

@ -6,13 +6,21 @@ namespace Components
// Packet interception
std::unordered_map<std::string, Network::NetworkCallback> Network::CL_Callbacks;
Network::Address::Address()
{
ZeroMemory(&this->address, sizeof(Game::netadr_t));
this->setType(Game::NA_BAD);
}
Network::Address::Address(const std::string& addrString)
{
ZeroMemory(&this->address, sizeof(Game::netadr_t));
Game::NET_StringToAdr(addrString.data(), &this->address);
}
Network::Address::Address(sockaddr* addr)
{
ZeroMemory(&this->address, sizeof(Game::netadr_t));
Game::SockadrToNetadr(addr, &this->address);
}
@ -23,7 +31,7 @@ namespace Components
void Network::Address::setPort(unsigned short port)
{
this->address.port = htons(port);
this->address.port = ::htons(port);
}
unsigned short Network::Address::getPort() const
@ -76,7 +84,7 @@ namespace Components
this->toSockAddr(reinterpret_cast<sockaddr*>(addr));
}
Game::netadr_t* Network::Address::get()
const Game::netadr_t* Network::Address::get() const noexcept
{
return &this->address;
}
@ -88,7 +96,7 @@ namespace Components
std::string Network::Address::getString() const
{
return {this->getCString()};
return std::string{ this->getCString() };
}
bool Network::Address::isLocal() const noexcept
@ -148,7 +156,7 @@ namespace Components
StartupSignal.connect(callback);
}
void Network::Send(Game::netsrc_t type, Address target, const std::string& data)
void Network::Send(Game::netsrc_t type, const Address& target, const std::string& data)
{
// Do not use NET_OutOfBandPrint. It only supports non-binary data!
@ -159,12 +167,12 @@ namespace Components
SendRaw(type, target, rawData);
}
void Network::Send(Address target, const std::string& data)
void Network::Send(const Address& target, const std::string& data)
{
Send(Game::netsrc_t::NS_CLIENT1, target, data);
}
void Network::SendRaw(Game::netsrc_t type, Address target, const std::string& data)
void Network::SendRaw(Game::netsrc_t type, const Address& target, const std::string& data)
{
if (!target.isValid()) return;
@ -172,12 +180,12 @@ namespace Components
Game::Sys_SendPacket(type, data.size(), data.data(), *target.get());
}
void Network::SendRaw(Address target, const std::string& data)
void Network::SendRaw(const Address& target, const std::string& data)
{
SendRaw(Game::NS_CLIENT1, target, data);
}
void Network::SendCommand(Game::netsrc_t type, Address target, const std::string& command, const std::string& data)
void Network::SendCommand(Game::netsrc_t type, const Address& target, const std::string& command, const std::string& data)
{
// Use space as separator (possible separators are '\n', ' ').
// Though, our handler only needs exactly 1 char as separator and doesn't care which char it is.
@ -190,7 +198,7 @@ namespace Components
Send(type, target, packet);
}
void Network::SendCommand(Address target, const std::string& command, const std::string& data)
void Network::SendCommand(const Address& target, const std::string& command, const std::string& data)
{
SendCommand(Game::NS_CLIENT1, target, command, data);
}
@ -293,8 +301,8 @@ namespace Components
const std::string data(reinterpret_cast<char*>(message->data) + offset, message->cursize - offset);
auto address_ = Address(address);
handler->second(address_, data);
auto target = Address{ address };
handler->second(target, data);
return true;
}

View File

@ -8,7 +8,7 @@ namespace Components
class Address
{
public:
Address() { setType(Game::NA_BAD); }
Address();
Address(const std::string& addrString);
Address(sockaddr* addr);
Address(sockaddr addr) : Address(&addr) {}
@ -33,7 +33,7 @@ namespace Components
[[nodiscard]] sockaddr getSockAddr();
void toSockAddr(sockaddr* addr);
void toSockAddr(sockaddr_in* addr);
Game::netadr_t* get();
[[nodiscard]] const Game::netadr_t* get() const noexcept;
[[nodiscard]] const char* getCString() const;
[[nodiscard]] std::string getString() const;
@ -57,16 +57,16 @@ namespace Components
static void OnStart(const Utils::Slot<CallbackRaw>& callback);
// Send quake-styled binary data
static void Send(Address target, const std::string& data);
static void Send(Game::netsrc_t type, Address target, const std::string& data);
static void Send(const Address& target, const std::string& data);
static void Send(Game::netsrc_t type, const Address& target, const std::string& data);
// Allows sending raw data without quake header
static void SendRaw(Address target, const std::string& data);
static void SendRaw(Game::netsrc_t type, Address target, const std::string& data);
static void SendRaw(const Address& target, const std::string& data);
static void SendRaw(Game::netsrc_t type, const Address& target, const std::string& data);
// Send quake-style command using binary data
static void SendCommand(Address target, const std::string& command, const std::string& data = "");
static void SendCommand(Game::netsrc_t type, Address target, const std::string& command, const std::string& data = "");
static void SendCommand(const Address& target, const std::string& command, const std::string& data = {});
static void SendCommand(Game::netsrc_t type, const Address& target, const std::string& command, const std::string& data = {});
static void Broadcast(unsigned short port, const std::string& data);
static void BroadcastRange(unsigned int min, unsigned int max, const std::string& data);

View File

@ -6,6 +6,26 @@ namespace Components
{
Dvar::Var PlayerName::sv_allowColoredNames;
bool PlayerName::IsBadChar(int c)
{
if (c == '%')
{
return true;
}
if (c == '~')
{
return true;
}
if (c < 32 || c > 126)
{
return true;
}
return false;
}
void PlayerName::UserInfoCopy(char* buffer, const char* name, const int size)
{
if (!sv_allowColoredNames.get<bool>())
@ -71,13 +91,8 @@ namespace Components
while (i < size - 1 && dest[i] != '\0')
{
// Check for various illegal characters
if (dest[i] == '%')
{
return false;
}
if (std::iscntrl(static_cast<unsigned char>(dest[i])))
const auto c = static_cast<unsigned char>(dest[i]);
if (IsBadChar(c))
{
return false;
}
@ -119,7 +134,7 @@ namespace Components
{
sv_allowColoredNames = Dvar::Register<bool>("sv_allowColoredNames", true, Game::DVAR_NONE, "Allow colored names on the server");
// Disable SV_UpdateUserinfo_f, to block changing the name ingame
// Disable SV_UpdateUserinfo_f to block changing the name ingame
Utils::Hook::Set<BYTE>(0x6258D0, 0xC3);
// Allow colored names ingame. Hook placed in ClientUserinfoChanged

View File

@ -16,6 +16,8 @@ namespace Components
// Message used when kicking players
static constexpr auto INVALID_NAME_MSG = "Invalid name detected";
static bool IsBadChar(int c);
static char* CleanStrStub(char* string);
static void ClientCleanName();

View File

@ -143,6 +143,63 @@ namespace Components
}
}
void RCon::RconExecuter(const Network::Address& address, std::string data)
{
Utils::String::Trim(data);
const auto pos = data.find_first_of(' ');
if (pos == std::string::npos)
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon request from {}\n", address.getString());
return;
}
auto password = data.substr(0, pos);
auto command = data.substr(pos + 1);
// B3 sends the password inside quotes :S
if (!password.empty() && password[0] == '"' && password.back() == '"')
{
password.pop_back();
password.erase(password.begin());
}
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());
return;
}
if (svPassword != password)
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon password sent from {}\n", address.getString());
return;
}
static std::string outputBuffer;
outputBuffer.clear();
#ifndef _DEBUG
if (RconLogRequests.get<bool>())
#endif
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing RCon request from {}: {}\n", address.getString(), command);
}
Logger::PipeOutput([](const std::string& output)
{
outputBuffer.append(output);
});
Command::Execute(command, true);
Logger::PipeOutput(nullptr);
Network::SendCommand(address, "print", outputBuffer);
outputBuffer.clear();
}
RCon::RCon()
{
Events::OnSVInit(AddCommands);
@ -213,61 +270,11 @@ namespace Components
RateLimitCleanup(time);
std::string data_ = data;
Utils::String::Trim(data_);
const auto pos = data.find_first_of(' ');
if (pos == std::string::npos)
auto rconData = data;
Scheduler::Once([address, s = std::move(rconData)]
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon request from {}\n", address.getString());
return;
}
auto password = data.substr(0, pos);
auto command = data.substr(pos + 1);
// B3 sends the password inside quotes :S
if (!password.empty() && password[0] == '"' && password.back() == '"')
{
password.pop_back();
password.erase(password.begin());
}
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());
return;
}
if (svPassword != password)
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon password sent from {}\n", address.getString());
return;
}
static std::string outputBuffer;
outputBuffer.clear();
#ifndef DEBUG
if (RconLogRequests.get<bool>())
#endif
{
Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing RCon request from {}: {}\n", address.getString(), command);
}
Logger::PipeOutput([](const std::string& output)
{
outputBuffer.append(output);
});
Command::Execute(command, true);
Logger::PipeOutput(nullptr);
Network::SendCommand(address, "print", outputBuffer);
outputBuffer.clear();
RconExecuter(address, s);
}, Scheduler::Pipeline::MAIN);
});
Network::OnClientPacket("rconRequest", [](const Network::Address& address, [[maybe_unused]] const std::string& data)

View File

@ -47,5 +47,7 @@ namespace Components
static bool IsRateLimitCheckDisabled();
static bool RateLimitCheck(const Network::Address& address, int time);
static void RateLimitCleanup(int time);
static void RconExecuter(const Network::Address& address, std::string data);
};
}

View File

@ -142,7 +142,7 @@ namespace Components
const char* data = Game::Scr_AddSourceBuffer(nullptr, file.getName().data(), nullptr, false);
if (data != nullptr)
if (data)
{
Utils::IO::WriteFile("raw/" + file.getName(), data);
Logger::Print("File '{}' written to raw!\n", file.getName());

View File

@ -64,7 +64,7 @@ namespace Components
Dvar::Var("uiSi_KillCam").set("@MENU_NO");
Dvar::Var("uiSi_ffType").set("@MENU_DISABLED");
Dvar::Var("uiSi_MapName").set(serverInfo->mapname);
Dvar::Var("uiSi_MapNameLoc").set(Game::UI_LocalizeMapName(serverInfo->mapname.data()));
Dvar::Var("uiSi_MapNameLoc").set(Localization::LocalizeMapName(serverInfo->mapname.data()));
Dvar::Var("uiSi_GameType").set(Game::UI_LocalizeGameType(serverInfo->gametype.data()));
Dvar::Var("uiSi_ModName").set("");
Dvar::Var("uiSi_aimAssist").set(serverInfo->aimassist ? "@MENU_YES" : "@MENU_NO");
@ -220,7 +220,7 @@ namespace Components
const auto* client = Game::svs_clients[i].gentity->client;
const auto team = client->sess.cs.team;
if (team == Game::TEAM_SPECTATOR)
if (Game::svs_clients[i].bIsTestClient || team == Game::TEAM_SPECTATOR)
{
continue;
}
@ -233,7 +233,7 @@ namespace Components
{
// Score and ping are irrelevant
const auto* namePtr = Game::PartyHost_GetMemberName(reinterpret_cast<Game::PartyData*>(0x1081C00), i);
if (!namePtr || !namePtr[0]) continue;
if (!namePtr || !*namePtr) continue;
name = namePtr;
}
@ -261,7 +261,7 @@ namespace Components
Dvar::Var("uiSi_Hardcore").set(info.get("g_hardcore") == "0" ? "@MENU_DISABLED" : "@MENU_ENABLED");
Dvar::Var("uiSi_KillCam").set(info.get("scr_game_allowkillcam") == "0" ? "@MENU_NO" : "@MENU_YES");
Dvar::Var("uiSi_MapName").set(info.get("mapname"));
Dvar::Var("uiSi_MapNameLoc").set(Game::UI_LocalizeMapName(info.get("mapname").data()));
Dvar::Var("uiSi_MapNameLoc").set(Localization::LocalizeMapName(info.get("mapname").data()));
Dvar::Var("uiSi_GameType").set(Game::UI_LocalizeGameType(info.get("g_gametype").data()));
Dvar::Var("uiSi_ModName").set("");
Dvar::Var("uiSi_aimAssist").set(info.get("aimAssist") == "0" ? "@MENU_DISABLED" : "@MENU_ENABLED");

View File

@ -118,13 +118,13 @@ namespace Components
{
if (!sorting && !Maps::CheckMapInstalled(server->mapname))
{
return Utils::String::VA("^1%s", Game::UI_LocalizeMapName(server->mapname.data()));
return Utils::String::VA("^1%s", Localization::LocalizeMapName(server->mapname.data()));
}
return Game::UI_LocalizeMapName(server->mapname.data());
return Localization::LocalizeMapName(server->mapname.data());
}
return Utils::String::VA("^3%s", Game::UI_LocalizeMapName(server->mapname.data()));
return Utils::String::VA("^3%s", Localization::LocalizeMapName(server->mapname.data()));
}
case Column::Players:

View File

@ -311,7 +311,7 @@ namespace Components
if (item < Demos.size())
{
auto info = Demos.at(item);
return Utils::String::VA("%s on %s", Game::UI_LocalizeGameType(info.gametype.data()), Game::UI_LocalizeMapName(info.mapname.data()));
return Utils::String::VA("%s on %s", Game::UI_LocalizeGameType(info.gametype.data()), Localization::LocalizeMapName(info.mapname.data()));
}
return "";
@ -330,7 +330,7 @@ namespace Components
asctime_s(buffer, sizeof buffer, &time);
Dvar::Var("ui_demo_mapname").set(info.mapname);
Dvar::Var("ui_demo_mapname_localized").set(Game::UI_LocalizeMapName(info.mapname.data()));
Dvar::Var("ui_demo_mapname_localized").set(Localization::LocalizeMapName(info.mapname.data()));
Dvar::Var("ui_demo_gametype").set(Game::UI_LocalizeGameType(info.gametype.data()));
Dvar::Var("ui_demo_length").set(Utils::String::FormatTimeSpan(info.length));
Dvar::Var("ui_demo_author").set(info.author);

View File

@ -290,7 +290,7 @@ namespace Components
return maps.at(index).data();
}
#ifdef DEBUG
#ifdef _DEBUG
if (IsDebuggerPresent())
{
__debugbreak();

View File

@ -41,14 +41,14 @@ namespace Components
Game::msg_t msg{};
const auto clientNum = client - Game::svs_clients;
const auto msg_buf_large = std::make_unique<unsigned char[]>(0x10000);
const auto msg_buf_large = std::make_unique<unsigned char[]>(0x20000);
auto* msg_buf = msg_buf_large.get();
assert(VoicePacketCount[clientNum] >= 0);
if (client->header.state == Game::CS_ACTIVE && VoicePacketCount[clientNum])
{
Game::MSG_Init(&msg, msg_buf, 0x10000);
Game::MSG_Init(&msg, msg_buf, 0x20000);
assert(msg.cursize == 0);
assert(msg.bit == 0);

View File

@ -97,7 +97,7 @@ namespace Game
const dvar_t** ip = reinterpret_cast<const dvar_t**>(0x64A1DF8);
const dvar_t** port = reinterpret_cast<const dvar_t**>(0x64A3004);
__declspec(naked) void Dvar_SetVariant(dvar_t*, DvarValue, DvarSetSource)
__declspec(naked) void Dvar_SetVariant(dvar_t* /*dvar*/, DvarValue /*value*/, DvarSetSource /*source*/)
{
static DWORD Dvar_SetVariant_t = 0x647400;

View File

@ -149,6 +149,6 @@ namespace Game
extern const dvar_t** ip;
extern const dvar_t** port;
extern void Dvar_SetVariant(dvar_t* var, DvarValue value, DvarSetSource source);
extern void Dvar_SetVariant(dvar_t* dvar, DvarValue value, DvarSetSource source);
extern void Dvar_SetFromStringFromSource(const dvar_t* dvar, const char* string, DvarSetSource source);
}

View File

@ -103,6 +103,7 @@ namespace Game
MSG_ReadDeltaUsercmdKey_t MSG_ReadDeltaUsercmdKey = MSG_ReadDeltaUsercmdKey_t(0x491F00);
MSG_ReadBitsCompress_t MSG_ReadBitsCompress = MSG_ReadBitsCompress_t(0x4DCC30);
MSG_WriteBitsCompress_t MSG_WriteBitsCompress = MSG_WriteBitsCompress_t(0x4319D0);
Huff_offsetReceive_t Huff_offsetReceive = Huff_offsetReceive_t(0x466060);
NetadrToSockadr_t NetadrToSockadr = NetadrToSockadr_t(0x4B4B40);
@ -389,6 +390,8 @@ namespace Game
bool* s_havePlaylists = reinterpret_cast<bool*>(0x1AD3680);
huffman_t* msgHuff = reinterpret_cast<huffman_t*>(0x1CB9EC0);
const char* TableLookup(StringTable* stringtable, int row, int column)
{
if (!stringtable || !stringtable->values || row >= stringtable->rowCount || column >= stringtable->columnCount) return "";
@ -399,25 +402,6 @@ namespace Game
return value;
}
const char* UI_LocalizeMapName(const char* mapName)
{
for (int i = 0; i < *arenaCount; ++i)
{
if (!_stricmp(Components::ArenaLength::NewArenas[i].mapName, mapName))
{
char* uiName = &Components::ArenaLength::NewArenas[i].uiName[0];
if ((uiName[0] == 'M' && uiName[1] == 'P') || (uiName[0] == 'P' && uiName[1] == 'A')) // MPUI/PATCH
{
return SEH_StringEd_GetString(uiName);
}
return uiName;
}
}
return mapName;
}
const char* UI_LocalizeGameType(const char* gameType)
{
if (!gameType || !*gameType)

View File

@ -263,6 +263,9 @@ namespace Game
typedef int(*MSG_WriteBitsCompress_t)(bool trainHuffman, const unsigned char* from, unsigned char* to, int size);
extern MSG_WriteBitsCompress_t MSG_WriteBitsCompress;
typedef void(*Huff_offsetReceive_t)(nodetype* node, int* ch, const unsigned char* fin, int* offset);
extern Huff_offsetReceive_t Huff_offsetReceive;
typedef void(*NetadrToSockadr_t)(netadr_t *a, sockaddr *s);
extern NetadrToSockadr_t NetadrToSockadr;
@ -727,6 +730,8 @@ namespace Game
extern bool* s_havePlaylists;
extern huffman_t* msgHuff;
constexpr auto MAX_MSGLEN = 0x20000;
ScreenPlacement* ScrPlace_GetFullPlacement();
@ -737,7 +742,6 @@ namespace Game
void Menu_SetNextCursorItem(UiContext* ctx, menuDef_t* currentMenu, int unk = 1);
void Menu_SetPrevCursorItem(UiContext* ctx, menuDef_t* currentMenu, int unk = 1);
const char* TableLookup(StringTable* stringtable, int row, int column);
const char* UI_LocalizeMapName(const char* mapName);
const char* UI_LocalizeGameType(const char* gameType);
float UI_GetScoreboardLeft(void*);

View File

@ -11011,6 +11011,31 @@ namespace Game
snd_alias_list_t* aliasList;
};
struct nodetype
{
nodetype* left;
nodetype* right;
nodetype* parent;
int weight;
int symbol;
};
struct huff_t
{
int blocNode;
int blocPtrs;
nodetype* tree;
nodetype* loc[257];
nodetype** freelist;
nodetype nodeList[768];
nodetype* nodePtrs[768];
};
struct huffman_t
{
huff_t compressDecompress;
};
#pragma endregion
#ifndef IDA

View File

@ -46,6 +46,7 @@
#include <type_traits>
#include <map>
#include <set>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>

View File

@ -7,8 +7,8 @@ namespace Utils::String
{
const char* VA(const char* fmt, ...)
{
static VAProvider<4, 100> globalProvider;
static thread_local VAProvider<8, 256> provider;
static VAProvider<4, 256> globalProvider;
static thread_local VAProvider<8, 1024> provider;
va_list ap;
va_start(ap, fmt);
@ -24,7 +24,7 @@ namespace Utils::String
std::string ToLower(const std::string& text)
{
std::string result;
std::ranges::transform(text, std::back_inserter(result), [](const unsigned char input)
std::ranges::transform(text, std::back_inserter(result), [](const unsigned char input) -> char
{
return static_cast<char>(std::tolower(input));
});
@ -35,7 +35,7 @@ namespace Utils::String
std::string ToUpper(const std::string& text)
{
std::string result;
std::ranges::transform(text, std::back_inserter(result), [](const unsigned char input)
std::ranges::transform(text, std::back_inserter(result), [](const unsigned char input) -> char
{
return static_cast<char>(std::toupper(input));
});
@ -45,7 +45,7 @@ namespace Utils::String
bool Compare(const std::string& lhs, const std::string& rhs)
{
return std::ranges::equal(lhs, rhs, [](const unsigned char a, const unsigned char b)
return std::ranges::equal(lhs, rhs, [](const unsigned char a, const unsigned char b) -> bool
{
return std::tolower(a) == std::tolower(b);
});

View File

@ -11,69 +11,68 @@ namespace Utils::String
public:
static_assert(Buffers != 0 && MinBufferSize != 0, "Buffers and MinBufferSize mustn't be 0");
VAProvider() : currentBuffer(0) {}
~VAProvider() = default;
VAProvider() : currentBuffer_(0) {}
[[nodiscard]] const char* get(const char* format, va_list ap)
{
++this->currentBuffer %= ARRAY_COUNT(this->stringPool);
auto entry = &this->stringPool[this->currentBuffer];
++this->currentBuffer_ %= ARRAY_COUNT(this->stringPool_);
auto entry = &this->stringPool_[this->currentBuffer_];
if (!entry->size || !entry->buffer)
if (!entry->size_ || !entry->buffer_)
{
throw std::runtime_error("String pool not initialized");
}
while (true)
{
const auto res = vsnprintf_s(entry->buffer, entry->size, _TRUNCATE, format, ap);
const auto res = vsnprintf_s(entry->buffer_, entry->size_, _TRUNCATE, format, ap);
if (res > 0) break; // Success
if (res == 0) return ""; // Error
entry->doubleSize();
}
return entry->buffer;
return entry->buffer_;
}
private:
class Entry
{
public:
Entry(std::size_t _size = MinBufferSize) : size(_size), buffer(nullptr)
Entry(std::size_t size = MinBufferSize) : size_(size), buffer_(nullptr)
{
if (this->size < MinBufferSize) this->size = MinBufferSize;
if (this->size_ < MinBufferSize) this->size_ = MinBufferSize;
this->allocate();
}
~Entry()
{
if (this->buffer) Memory::GetAllocator()->free(this->buffer);
this->size = 0;
this->buffer = nullptr;
if (this->buffer_) Memory::GetAllocator()->free(this->buffer_);
this->size_ = 0;
this->buffer_ = nullptr;
}
void allocate()
{
if (this->buffer) Memory::GetAllocator()->free(this->buffer);
this->buffer = Memory::GetAllocator()->allocateArray<char>(this->size + 1);
if (this->buffer_) Memory::GetAllocator()->free(this->buffer_);
this->buffer_ = Memory::GetAllocator()->allocateArray<char>(this->size_ + 1);
}
void doubleSize()
{
this->size *= 2;
this->size_ *= 2;
this->allocate();
}
std::size_t size;
char* buffer;
std::size_t size_;
char* buffer_;
};
std::size_t currentBuffer;
Entry stringPool[Buffers];
std::size_t currentBuffer_;
Entry stringPool_[Buffers];
};
template <typename Arg> // This should display a nice "null" instead of a number
template <typename Arg> // This should display a nice "nullptr" instead of a number
static void SanitizeFormatArgs(Arg& arg)
{
if constexpr (std::is_same_v<Arg, char*> || std::is_same_v<Arg, const char*>)