Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/Misc/InGameChat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
#include <Spawner/Spawner.h>
#include <Utilities/Macro.h>
#include <HouseClass.h>
#include <MessageListClass.h>
#include <Unsorted.h>
#include <Windows.h>

// This corrects the processing of Unicode player names
// and prohibits incoming messages from players with whom chat is disabled
Expand All @@ -38,15 +40,99 @@ struct GlobalPacket_NetMessage
byte Color;
byte CRC;
};

#pragma pack(pop)

static bool inline IsDisableChatEnabled()
{
return Spawner::Enabled && Spawner::GetConfig()->DisableChat;
}

// Don't send message to others when DisableChat is active.
// Mirrors: hack 0x0055EF38, 0x0055EF3E in chat_disable.asm
DEFINE_HOOK(0x55EF38, MessageSend_DisableChat, 0x6)
{
static int LastDisableChatFeedbackFrame = -1000;

if (IsDisableChatEnabled())
{
const int currentFrame = Unsorted::CurrentFrame;

if (currentFrame - LastDisableChatFeedbackFrame >= 90)
{
MessageListClass::Instance.PrintMessage(L"Chat is disabled. Message not sent.");
LastDisableChatFeedbackFrame = currentFrame;
}
Comment on lines +55 to +65
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LastDisableChatFeedbackFrame is a function-local static that persists across matches. If Unsorted::CurrentFrame resets to 0 on a new game, currentFrame - LastDisableChatFeedbackFrame will be negative and the feedback message may never display again until the frame counter surpasses the previous match’s value. Consider resetting the stored frame when the current frame goes backwards (or move this state into a global that gets reset in the game-start reset hook).

Copilot uses AI. Check for mistakes.

return 0x55F056; // skip the send
}

return 0; // execute original: cmp edi, ebx; mov [esp+0x14], ebx
}

// After receiving a message, don't play sound if AddMessage returned NULL
// (i.e. the message was suppressed). Mirrors: hack 0x0048D97E in chat_disable.asm
DEFINE_HOOK(0x48D97E, NetworkCallBack_NetMessage_Sound, 0x5)
{
if (!Spawner::Enabled)
return 0;

if (!R->EAX<void*>())
return 0x48D99A; // skip sound

return 0; // execute original: mov eax, [0x8871E0]
}

// In diplomacy dialog, make chat checkbox non-interactive for each player,
// matching the existing Player_MuteSWLaunches disabled-checkbox behavior.
// Hook point is after `push 0` (lParam), so jumping to 0x657FC0 preserves stack layout.
DEFINE_HOOK(0x657F95, RadarClass_Diplomacy_DisableChatToggleUI, 0x2)
{
return IsDisableChatEnabled()
? 0x657FC0
: 0;
}

// The non-interactive branch (loc_657FC0) sets BM_SETCHECK(1) before disabling.
// Force it back to OFF for DisableChat so visuals match the intended locked state.
DEFINE_HOOK(0x657FDB, RadarClass_Diplomacy_ForceDisabledChatVisualOff, 0x5)
{
if (IsDisableChatEnabled())
{
GET(HWND, hWnd, EBP);
SendMessageA(hWnd, BM_SETCHECK, BST_UNCHECKED, 0);
}

return 0;
}

// Continuously enforce DisableChat by resetting ChatMask every frame,
// preventing re-enabling chat from the alliance menu.
DEFINE_HOOK(0x55DDA5, MainLoop_AfterRender_DisableChat, 0x5)
{
auto const Original = reinterpret_cast<int(__thiscall*)(void*)>(0x5D4430);
GET(void*, pThis, ECX);
Original(pThis);

if (IsDisableChatEnabled())
{
for (int i = 0; i < 8; ++i)
Game::ChatMask[i] = false;
}

return 0x55DDAA;
}

DEFINE_HOOK(0x48D92B, NetworkCallBack_NetMessage_Print, 0x5)
{
if (!Spawner::Enabled)
return 0;

enum { SkipMessage = 0x48DAD3, PrintMessage = 0x48D937 };

if (IsDisableChatEnabled())
return SkipMessage;

const int houseIndex = GlobalPacket_NetMessage::Instance.HouseIndex;

if (houseIndex < 8 && Game::ChatMask[houseIndex])
Expand Down
1 change: 1 addition & 0 deletions src/Spawner/Spawner.Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI)
ContinueWithoutHumans = pINI->ReadBool(pSettingsSection, "ContinueWithoutHumans", ContinueWithoutHumans);
DefeatedBecomesObserver = pINI->ReadBool(pSettingsSection, "DefeatedBecomesObserver", DefeatedBecomesObserver);
Observer_ShowAIOnSidebar = pINI->ReadBool(pSettingsSection, "Observer.ShowAIOnSidebar", Observer_ShowAIOnSidebar);
DisableChat = pINI->ReadBool(pSettingsSection, "DisableChat", DisableChat);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/Spawner/Spawner.Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class SpawnerConfig
bool ContinueWithoutHumans;
bool DefeatedBecomesObserver;
bool Observer_ShowAIOnSidebar;
bool DisableChat;

SpawnerConfig() // default values
// Game Mode Options
Expand Down Expand Up @@ -238,6 +239,7 @@ class SpawnerConfig
, ContinueWithoutHumans { false }
, DefeatedBecomesObserver { false }
, Observer_ShowAIOnSidebar { false }
, DisableChat { false }
{ }

void LoadFromINIFile(CCINIClass* pINI);
Expand Down