diff --git a/docs/cvars.md b/docs/cvars.md
index f7692ed3..d9c7d80a 100644
--- a/docs/cvars.md
+++ b/docs/cvars.md
@@ -262,7 +262,7 @@
|sar_hud_ghost_spec|0|Show the name of the ghost you're currently spectating.|
|sar_hud_grounded|0|Draws the state of player being on ground.|
|sar_hud_groundframes|0|Draws the number of ground frames since last landing. Setting it to 2 preserves the value.|
-|sar_hud_groundspeed|0|Draw the speed of the player upon leaving the ground.
0 = Default,
1 = Groundspeed,
2 = Groundspeed (Gain)|
+|sar_hud_groundspeed|0|Draw the speed of the player upon leaving the ground.
0 = Default
1 = Groundspeed
2 = Groundspeed (Gain)|
|sar_hud_hide_text|cmd|sar_hud_hide_text \ - hides the nth text value in the HUD|
|sar_hud_inspection|0|Draws entity inspection data.|
|sar_hud_jump|0|Draws current jump distance.|
@@ -503,6 +503,7 @@
|sar_scrollspeed_y|210|Scroll speed HUD y offset.|
|sar_seamshot_finder|0|Enables or disables seamshot finder overlay.|
|sar_session|cmd|sar_session - prints the current tick of the server since it has loaded|
+|sar_set_promo_items_state|cmd|sar_set_promo_items_state \... - enables coop promotional items on spawn.|
|sar_show_entinp|0|Print all entity inputs to console.|
|sar_skiptodemo|cmd|sar_skiptodemo \ - skip demos in demo queue to this demo|
|sar_speedrun_autoreset_clear|cmd|sar_speedrun_autoreset_clear - stop using the autoreset file|
diff --git a/src/Cheats.cpp b/src/Cheats.cpp
index 3cd1769d..511fb323 100644
--- a/src/Cheats.cpp
+++ b/src/Cheats.cpp
@@ -331,6 +331,7 @@ Memory::Patch *g_floorReportalPatch;
Memory::Patch *g_coopLoadingDotsPatch;
Memory::Patch *g_autoGrabPatchServer;
Memory::Patch *g_autoGrabPatchClient;
+Memory::Patch *g_promoFlagsPatch;
void Cheats::Init() {
sv_laser_cube_autoaim = Variable("sv_laser_cube_autoaim");
@@ -399,6 +400,14 @@ void Cheats::Init() {
g_autoGrabPatchClient->Restore();
}
+ g_promoFlagsPatch = new Memory::Patch();
+ auto portal2PromoFlags = Memory::Scan(MODULE("server"), Offsets::Portal2PromoFlagsSig, Offsets::Portal2PromoFlagsOff);
+ if (portal2PromoFlags) {
+ unsigned char promoFlagsByte = 0x00;
+ g_promoFlagsPatch->Execute(portal2PromoFlags, &promoFlagsByte, 1); // Note: Has to be active before map loads.
+ g_promoFlagsPatch->Restore();
+ }
+
Variable::RegisterAll();
Command::RegisterAll();
}
@@ -422,6 +431,8 @@ void Cheats::Shutdown() {
SAFE_DELETE(g_autoGrabPatchServer);
g_autoGrabPatchClient->Restore();
SAFE_DELETE(g_autoGrabPatchClient);
+ g_promoFlagsPatch->Restore();
+ SAFE_DELETE(g_promoFlagsPatch);
}
@@ -589,3 +600,37 @@ void Cheats::CheckAutoGrab() {
g_autoGrabPatchClient->Restore();
}
}
+
+DECL_AUTO_COMMAND_COMPLETION(sar_set_promo_items_state, ({"skins", "helmet", "antenna"})) // TODO: Add support for autofilling multiple args.
+CON_COMMAND_F_COMPLETION(sar_set_promo_items_state, "sar_set_promo_items_state ... - enables coop promotional items on spawn.\n", FCVAR_CHEAT, AUTOCOMPLETION_FUNCTION(sar_set_promo_items_state)) {
+ if (!g_promoFlagsPatch || !g_promoFlagsPatch->IsInit()) {
+ return console->Print("sar_set_promo_items_state is not available.\n");
+ }
+
+ if (args.ArgC() < 2) {
+ return console->Print(sar_set_promo_items_state.ThisPtr()->m_pszHelpString);
+ }
+
+ unsigned char targetFlags = 0;
+ for (int i = 1; i < args.ArgC(); i++) {
+ if (strcasecmp(args[i], "off") == 0) {
+ g_promoFlagsPatch->Restore();
+ return;
+ }
+ if (strcasecmp(args[i], "all") == 0) {
+ targetFlags = 0b111;
+ break;
+ }
+ if (strcasecmp(args[i], "skins") == 0) {;
+ targetFlags |= 0b001;
+ } else if (strcasecmp(args[i], "helmet") == 0) {
+ targetFlags |= 0b010;
+ } else if (strcasecmp(args[i], "antenna") == 0) {
+ targetFlags |= 0b100;
+ } else {
+ return console->Print(sar_set_promo_items_state.ThisPtr()->m_pszHelpString);
+ }
+ }
+ g_promoFlagsPatch->Restore();
+ g_promoFlagsPatch->Execute(&targetFlags, 1);
+}
diff --git a/src/Cheats.hpp b/src/Cheats.hpp
index 28141e5e..95a055c1 100644
--- a/src/Cheats.hpp
+++ b/src/Cheats.hpp
@@ -50,3 +50,5 @@ extern Variable hide_gun_when_holding;
extern Variable r_flashlightbrightness;
extern Command sar_togglewait;
+
+extern Memory::Patch *g_promoFlagsPatch;
diff --git a/src/Modules/Server.cpp b/src/Modules/Server.cpp
index 2bfa3623..164a4744 100644
--- a/src/Modules/Server.cpp
+++ b/src/Modules/Server.cpp
@@ -285,6 +285,7 @@ DETOUR(Server::PlayerRunCommand, CUserCmd *cmd, void *moveHelper) {
Cheats::AutoStrafe(slot, thisptr, cmd);
Cheats::CheckFloorReportals();
+ if (!sv_cheats.GetBool()) g_promoFlagsPatch->Restore(); // We only want to check this once per map load, to preserve the intended behavior.
inputHud.SetInputInfo(slot, cmd->buttons, {cmd->sidemove, cmd->forwardmove, cmd->upmove});
diff --git a/src/Offsets/Portal 2 9568.hpp b/src/Offsets/Portal 2 9568.hpp
index e616cf66..d132ec06 100644
--- a/src/Offsets/Portal 2 9568.hpp
+++ b/src/Offsets/Portal 2 9568.hpp
@@ -523,6 +523,10 @@ SIGSCAN_DEFAULT(FloorReportalBranch, "75 7D 8B 8E C0 04 00 00",
SIGSCAN_DEFAULT(CPortal_Player__PollForUseEntity_CheckMP, "74 ? ? ? 8B 82 ? ? ? ? FF D0 84 C0 74 ? 8B CE",
"74 ? 8B 10 83 EC 0C 50 FF 92 88 00 00 00 83 C4 10 84 C0 ? ? ? ? ? ? ? ? ? ? ? ? ? 00 00") // "OnJump" xref -> CPortal_Player:PreThink -> CBasePlayer::PreThink -> CBasePlayer::ItemPreFrame -> CBasePlayer::PlayerUse -> CPortal_Player vtable offset -> CPortal_Player::PlayerUse -> Second function call from disassembly -> CPortal_Player::PollForUseEntity -> jz instruction
+SIGSCAN_DEFAULT(Portal2PromoFlagsSig, "F6 05 ? ? ? ? 02 74 ? 8B CE",
+ "A1 ? ? ? ? A8 02") // "#P2_WearableType_Flag" xref -> CPortal_Player::GiveDefaultItems -> bitwise & -> portal2 promo flag
+OFFSET_DEFAULT(Portal2PromoFlagsOff, 2, 1)
+
// VPhysics
OFFSET_EMPTY(DestroyEnvironment)
OFFSET_EMPTY(GetActiveEnvironmentByIndex)
diff --git a/src/Utils/Memory.cpp b/src/Utils/Memory.cpp
index 8c883161..c58d57ab 100644
--- a/src/Utils/Memory.cpp
+++ b/src/Utils/Memory.cpp
@@ -245,6 +245,7 @@ Memory::Patch::~Patch() {
}
bool Memory::Patch::Execute() {
if (this->isPatched) return true; // already executed
+ if (!this->IsInit()) return false;
unsigned char *tmpPatch = new unsigned char[this->size];
// We create another patch, because this->patch is gonna be deleted
memcpy(tmpPatch, this->patch, this->size);
diff --git a/src/Utils/Memory.hpp b/src/Utils/Memory.hpp
index b281f9a3..593506e5 100644
--- a/src/Utils/Memory.hpp
+++ b/src/Utils/Memory.hpp
@@ -50,6 +50,13 @@ namespace Memory {
bool Execute(uintptr_t location, unsigned char (&bytes)[size]) {
return Execute(location, bytes, size);
}
+ bool Execute(unsigned char *bytes, size_t size) {
+ return Execute(location, bytes, size);
+ }
+ template
+ bool Execute(unsigned char (&bytes)[size]) {
+ return Execute(location, bytes, size);
+ }
bool Restore();
bool IsPatched();
bool IsInit();