Skip to content

Commit

Permalink
custom chat warnings and some tech debt
Browse files Browse the repository at this point in the history
the abiliy to customize
- cheater joining (1/multi)
- cheater warning (1/multi)
messages, and the interval of those chat messages

fixes PazerOP#70

was the most requested feature, and thought "it can't be that bad right?"
  • Loading branch information
surepy committed Apr 14, 2023
1 parent 379c99e commit 9c29aad
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 42 deletions.
27 changes: 26 additions & 1 deletion tf2_bot_detector/Config/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,26 @@ void Settings::Deserialize(const nlohmann::json& json)
try_get_to_defaulted(*custom_values, m_KillLogsInChat, "kill_logs_in_chat", DEFAULTS.m_KillLogsInChat);

try_get_to_defaulted(*custom_values, m_AutoChatWarningsMarkedVSNotifications, "auto_chat_warnings_marked_vs_notifications", DEFAULTS.m_AutoChatWarningsMarkedVSNotifications);

try_get_to_defaulted(*custom_values, m_ChatWarningInterval, "chat_warning_interval", DEFAULTS.m_ChatWarningInterval);

// invalid settings, this shouldn't be a negative value or faster than tf2's chat speed cap.
if (m_ChatWarningInterval < 2) {
m_ChatWarningInterval = 10;
}

// custom warning messages
{
try_get_to_defaulted(*custom_values, m_UseCustomChatWarnings, "use_custom_chat_warnings", DEFAULTS.m_UseCustomChatWarnings);

try_get_to_defaulted(*custom_values, m_OneCheaterConnectingMessage, "one_cheater_connecting", DEFAULTS.m_OneCheaterConnectingMessage);
try_get_to_defaulted(*custom_values, m_MultipleCheaterConnectingMessage, "muti_cheater_connecting", DEFAULTS.m_MultipleCheaterConnectingMessage);

try_get_to_defaulted(*custom_values, m_OneCheaterWarningMessage, "one_cheater_warning", DEFAULTS.m_OneCheaterWarningMessage);
try_get_to_defaulted(*custom_values, m_MultipleCheaterWarningMessage, "muti_cheater_warning", DEFAULTS.m_MultipleCheaterWarningMessage);
}

try_get_to_defaulted(*custom_values, m_AutoChatWarningsMarkedVSNotifications, "auto_chat_warnings_marked_vs_notifications", DEFAULTS.m_AutoChatWarningsMarkedVSNotifications);
}

try_get_to_defaulted(*found, m_LocalSteamIDOverride, "local_steamid_override");
Expand Down Expand Up @@ -478,7 +498,12 @@ void Settings::Serialize(nlohmann::json& json) const
}
},
{ "auto_chat_warnings_connecting_party", m_AutoChatWarningsConnectingParty },
{ "kill_logs_in_chat", m_KillLogsInChat }
{ "chat_warning_interval", m_ChatWarningInterval },
{ "use_custom_chat_warnings", m_UseCustomChatWarnings },
{ "one_cheater_connecting", m_OneCheaterConnectingMessage },
{ "muti_cheater_connecting", m_MultipleCheaterConnectingMessage },
{ "one_cheater_warning", m_OneCheaterWarningMessage },
{ "muti_cheater_warning", m_MultipleCheaterWarningMessage }
}
},
{ "sleep_when_unfocused", m_SleepWhenUnfocused },
Expand Down
17 changes: 17 additions & 0 deletions tf2_bot_detector/Config/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,25 @@ namespace tf2_bot_detector
// (unimplemented)
bool m_AutoChatWarningsMarkedVSNotifications = true;

// put kill logs in chat
bool m_KillLogsInChat = false;

// how many times until chat warning, in seconds
int m_ChatWarningInterval = 10;

// TODO: wstring support?
// custom: custom chat warnings (most requested feature)
bool m_UseCustomChatWarnings = false;
// maybe?
// bool m_IgnoreBotLeaderPriority = false;
std::string m_OneCheaterConnectingMessage = "Heads up! There is a known cheater joining the other team! Name unknown until they fully join.";
std::string m_MultipleCheaterConnectingMessage = "Heads up! There are {} known cheaters joining the other team! Names unknown until they fully join.";

std::string m_OneCheaterWarningMessage = "Attention! There is a cheater on the other team named \"{}\". Please kick them!";
std::string m_MultipleCheaterWarningMessage = "Attention! There are {} cheaters on the other team named {}. Please kick them!";

// end custom

bool m_AutoChatWarnings = true;
bool m_AutoChatWarningsConnecting = false;
bool m_AutoVotekick = true;
Expand Down
2 changes: 1 addition & 1 deletion tf2_bot_detector/ConsoleLog/ConsoleLines.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ void KillNotificationLine::Print(const PrintArgs& args) const
chatColor = colorSettings.m_ChatLogFriendlyTeamFG;
}

// TODO: [POTENTIAL PERFORMANCE ISSUE] this is kind of a waste doing this same calculation over and over again, and might get out of hand pretty quickly.
// TODO: [POTENTIAL PERFORMANCE ISSUE/TECH DEBT] this is kind of a waste doing this same calculation over and over again, and might get out of hand pretty quickly.
// move into a seperate pre-calculated variable
// what this does is it just makes the color darker than the actual chat..
chatColor[0] = chatColor[0] / 2;
Expand Down
136 changes: 96 additions & 40 deletions tf2_bot_detector/ModeratorLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ namespace
PlayerMarks HasPlayerAttributes(const SteamID& id, const PlayerAttributesList& attributes, AttributePersistence persistence) const override;
bool InitiateVotekick(const IPlayer& player, KickReason reason, const PlayerMarks* marks = nullptr) override;

std::string GenerateCheaterWarnMessage(const std::vector<std::string>& names) const;

bool SetPlayerAttribute(const IPlayer& id, PlayerAttribute markType, AttributePersistence persistence, bool set = true, std::string proof = "") override;
bool SetPlayerAttribute(const SteamID& id, std::string name, PlayerAttribute markType, AttributePersistence persistence, bool set = true, std::string proof = "") override;

Expand Down Expand Up @@ -117,12 +119,15 @@ namespace

void OnRuleMatch(const ModerationRule& rule, const IPlayer& player, std::string reason = "none");

// How long inbetween accusations
// How long inbetween accusations, unused as we pull from settings.
static constexpr duration_t CHEATER_WARNING_INTERVAL = std::chrono::seconds(20);

// How long inbetween accusations, for people that are not using a forked version of tf2bd.
static constexpr duration_t CHEATER_WARNING_INTERVAL_DEFAULT = std::chrono::seconds(20);

// The soonest we can make an accusation after having seen an accusation in chat from a bot leader.
// This must be longer than CHEATER_WARNING_INTERVAL.
static constexpr duration_t CHEATER_WARNING_INTERVAL_NONLOCAL = CHEATER_WARNING_INTERVAL + std::chrono::seconds(10);
// This must be longer than CHEATER_WARNING_INTERVAL_DEFAULT.
static constexpr duration_t CHEATER_WARNING_INTERVAL_NONLOCAL = CHEATER_WARNING_INTERVAL_DEFAULT + std::chrono::seconds(10);

// How long we wait between determining someone is cheating and actually accusing them.
// This delay exists to give a bot leader a chance to make an accusation.
Expand Down Expand Up @@ -276,7 +281,7 @@ void ModeratorLogic::OnPlayerStatusUpdate(IWorldState& world, const IPlayer& pla
}
}

static bool IsCheaterConnectedWarning(const std::string_view& msg)
static bool IsBotDetectorMessage(const std::string_view& msg)
{
static const std::regex s_IngameWarning(
R"regex(Attention! There (?:is a|are \d+) cheaters? on the other team named .*\. Please kick them!)regex",
Expand All @@ -290,7 +295,7 @@ static bool IsCheaterConnectedWarning(const std::string_view& msg)

void ModeratorLogic::OnChatMsg(IWorldState& world, IPlayer& player, const std::string_view& msg)
{
bool botMsgDetected = IsCheaterConnectedWarning(msg);
bool botMsgDetected = IsBotDetectorMessage(msg);

// Check if it is a moderation message from someone else
if (m_Settings->m_AutoTempMute &&
Expand Down Expand Up @@ -372,7 +377,7 @@ void ModeratorLogic::OnLocalPlayerInitialized(IWorldState & world, bool initiali
);
++markedPlayerCount;

// don't warn again, we're gona warn here.
// don't warn again, we've already warned about this here.
player.GetOrCreateData<PlayerExtraData>().m_PartyWarned = true;
}
}
Expand Down Expand Up @@ -539,50 +544,83 @@ void ModeratorLogic::HandleConnectedEnemyCheaters(const std::vector<Cheater>& en

if (chatMsgCheaterNames.size() > 0)
{
constexpr char FMT_ONE_CHEATER[] = "Attention! There is a cheater on the other team named \"{}\". Please kick them!";
constexpr char FMT_MULTIPLE_CHEATERS[] = "Attention! There are {} cheaters on the other team named {}. Please kick them!";

constexpr size_t MAX_CHATMSG_LENGTH = 127;
constexpr size_t MAX_NAMES_LENGTH_ONE = MAX_CHATMSG_LENGTH - std::size(FMT_ONE_CHEATER) - 1 - 2;
constexpr size_t MAX_NAMES_LENGTH_MULTIPLE = MAX_CHATMSG_LENGTH - std::size(FMT_MULTIPLE_CHEATERS) - 1 - 1 - 2;

static_assert(MAX_NAMES_LENGTH_ONE >= 32);
mh::fmtstr<MAX_CHATMSG_LENGTH + 1> chatMsg;
if (chatMsgCheaterNames.size() == 1)
if (now >= m_NextCheaterWarningTime)
{
chatMsg.fmt(FMT_ONE_CHEATER, chatMsgCheaterNames.front());
if (m_Settings->m_AutoChatWarnings && m_ActionManager->QueueAction<ChatMessageAction>(GenerateCheaterWarnMessage(chatMsgCheaterNames)))
{
Log({ 1, 0, 0, 1 }, logMsg);
// used to be CHEATER_WARNING_INTERVAL
m_NextCheaterWarningTime = now + std::chrono::seconds(m_Settings->m_ChatWarningInterval);
}
}
else
{
assert(chatMsgCheaterNames.size() > 0);
std::string cheaters;
DebugLog("HandleEnemyCheaters(): Skipping cheater warnings for "s << to_seconds(m_NextCheaterWarningTime - now) << " seconds");
}
}
}

for (std::string cheaterNameStr : GetJoinedStrings(chatMsgCheaterNames.begin(), chatMsgCheaterNames.end(), ", "sv))
{
if (cheaters.empty() || cheaterNameStr.size() <= MAX_NAMES_LENGTH_MULTIPLE)
cheaters = std::move(cheaterNameStr);
else if (cheaterNameStr.size() > MAX_NAMES_LENGTH_MULTIPLE)
break;
std::string ModeratorLogic::GenerateCheaterWarnMessage(const std::vector<std::string>& names) const
{
// TODO: have the defauts in a const somewhere else idk
std::string one_cheater_warning = "Attention! There is a cheater on the other team named \"{}\". Please kick them!";
std::string multiple_cheater_warning = "Attention! There are {} cheaters on the other team named {}. Please kick them!";

if (m_Settings->m_UseCustomChatWarnings) {
// check if they're empty for whatever reason
if (!m_Settings->m_OneCheaterWarningMessage.empty()) {
try {
fmt::format(m_Settings->m_OneCheaterWarningMessage, 1);
one_cheater_warning = m_Settings->m_OneCheaterWarningMessage;
}
catch (fmt::format_error err) {
LogError("Our custom one cheater warning message is invalid; falling back to default.");
}

chatMsg.fmt(FMT_MULTIPLE_CHEATERS, chatMsgCheaterNames.size(), cheaters);
}

assert(chatMsg.size() <= 127);

if (now >= m_NextCheaterWarningTime)
{
if (m_Settings->m_AutoChatWarnings && m_ActionManager->QueueAction<ChatMessageAction>(chatMsg))
{
Log({ 1, 0, 0, 1 }, logMsg);
m_NextCheaterWarningTime = now + CHEATER_WARNING_INTERVAL;
if (!m_Settings->m_MultipleCheaterWarningMessage.empty()) {
try {
fmt::format(m_Settings->m_MultipleCheaterWarningMessage, fmt::arg("count", 2), fmt::arg("names", "a, b"));
one_cheater_warning = m_Settings->m_OneCheaterWarningMessage;
}
catch (fmt::format_error err) {
LogError("Our custom mutiple cheater warning message is invalid; falling back to default.");
}
}
else
}

constexpr size_t MAX_CHATMSG_LENGTH = 127;

size_t max_names_length_one = MAX_CHATMSG_LENGTH - one_cheater_warning.size() - 1 - 2;
size_t max_names_length_multiple = MAX_CHATMSG_LENGTH - multiple_cheater_warning.size() - 1 - 1 - 2;

assert(max_names_length_one >= 32);

mh::fmtstr<MAX_CHATMSG_LENGTH + 1> chatMsg;

if (names.size() == 1)
{
chatMsg.fmt(one_cheater_warning, names.front());
}
else
{
assert(names.size() > 0);
std::string cheaters;

for (std::string cheaterNameStr : GetJoinedStrings(names.begin(), names.end(), ", "sv))
{
DebugLog("HandleEnemyCheaters(): Skipping cheater warnings for "s << to_seconds(m_NextCheaterWarningTime - now) << " seconds");
if (cheaters.empty() || cheaterNameStr.size() <= max_names_length_multiple)
cheaters = std::move(cheaterNameStr);
else if (cheaterNameStr.size() > max_names_length_multiple)
break;
}

chatMsg.fmt(multiple_cheater_warning, fmt::arg("count", names.size()), fmt::arg("names", cheaters));
}

assert(chatMsg.size() <= 127);

return chatMsg.str();
}

void ModeratorLogic::HandleConnectingEnemyCheaters(const std::vector<Cheater>& connectingEnemyCheaters)
Expand Down Expand Up @@ -646,12 +684,30 @@ void ModeratorLogic::HandleConnectingEnemyCheaters(const std::vector<Cheater>& c
mh::fmtstr<128> chatMsg;
if (connectingEnemyCheaters.size() == 1)
{
chatMsg.puts("Heads up! There is a known cheater joining the other team! Name unknown until they fully join.");
if (m_Settings->m_UseCustomChatWarnings && !m_Settings->m_OneCheaterConnectingMessage.empty()) {
chatMsg.puts(m_Settings->m_OneCheaterConnectingMessage);
}
else {
// move this string to a const somewhere else idk
chatMsg.puts("Heads up! There is a known cheater joining the other team! Name unknown until they fully join.");
}
}
else
{
chatMsg.fmt("Heads up! There are {} known cheaters joining the other team! Names unknown until they fully join.",
connectingEnemyCheaters.size());
// move this string to a const somewhere else idk
std::string mutli_cheater_connecting = "Heads up! There are {} known cheaters joining the other team! Names unknown until they fully join.";

if (m_Settings->m_UseCustomChatWarnings && !m_Settings->m_MultipleCheaterConnectingMessage.empty()) {
try {
fmt::format(m_Settings->m_MultipleCheaterConnectingMessage, 1);
mutli_cheater_connecting = m_Settings->m_MultipleCheaterConnectingMessage;
}
catch (fmt::format_error err) {
LogError("Our cheater connecting message is invalid; falling back to default.");
}
}

chatMsg.fmt(mutli_cheater_connecting, connectingEnemyCheaters.size());
}

Log("Telling other team about "s << connectingEnemyCheaters.size() << " cheaters currently connecting");
Expand Down
75 changes: 75 additions & 0 deletions tf2_bot_detector/UI/SettingsWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ void SettingsWindow::OnDrawModerationSettings()
"This is needed because players can't vote until they have joined a team and picked a class. If we call a vote before enough people are ready, it might fail.");
}

ImGui::NewLine();

// Send warnings for connecting cheaters
{
if (ImGui::Checkbox("Chat message warnings for connecting cheaters", &m_Settings.m_AutoChatWarningsConnecting))
Expand All @@ -122,6 +124,79 @@ void SettingsWindow::OnDrawModerationSettings()
"Looks like: \"Heads up! There are N known cheaters joining the other team! Names unknown until they fully join.\"");
}

// Chat Warning Frequency
{
if (ImGui::SliderInt("Chat Warning Frequency", &m_Settings.m_ChatWarningInterval, 2, 60, "%d seconds"))
m_Settings.SaveFile();
ImGui::SetHoverTooltip("Delay between chat warnings");

if (m_Settings.m_ChatWarningInterval < 10) {
ImGui::TextFmt(
{ 1, 0, 0, 1 },
"WARN: YOU ARE SENDING MESSAGES WAY TOO FREQUENTLY! (<10s)\n"
"People usually find this chatspamming behavior annoying, and will likely kick **you** first instead!\n"
"(In fact, people find the default 10 second annoying, too!)\n"
"You have been warned!"
);
}
}

{
if (ImGui::Checkbox("Custom chat message warnings", &m_Settings.m_UseCustomChatWarnings))
m_Settings.SaveFile();
ImGui::SetHoverTooltip("Use a Custom chat warning, instead of the default \"Attention!\"...");
}

if (m_Settings.m_UseCustomChatWarnings)
{
ImGui::TextFmt(
{ 1, 0.5f, 0, 1 },
"NOTICE: enabling this setting will cause other bot detector users to not be able to recognize your message.\n"
"This in effect will cause the \"bot leader\" feature to not work, and multiple bot detector users may over-spam chat.\n"
"By using this option you understand that the above side-effect exists, and still want custom messages anyway."
);

ImGui::NewLine();

ImGui::TextFmt(
{ 1, 1, 1, 1 },
"Tip: format your strings using {}; if you want literal '{' or '}' do {{ and }} respectively"
);

ImGui::InputText("Cheater Joining", &m_Settings.m_OneCheaterConnectingMessage);
ImGui::InputText("Multiple Cheater Joining", &m_Settings.m_MultipleCheaterConnectingMessage);
try {
fmt::format(m_Settings.m_MultipleCheaterConnectingMessage, 1);
}
catch (fmt::format_error err) {
ImGui::TextFmt({ 1, 0, 0, 1 }, "Invalid string format! this takes 1 arguments maximum! (player count)");
}

ImGui::NewLine();

ImGui::InputText("Cheater Warning", &m_Settings.m_OneCheaterWarningMessage);
try {
fmt::format(m_Settings.m_OneCheaterWarningMessage, 1);
}
catch (fmt::format_error err) {
ImGui::TextFmt( { 1, 0, 0, 1 }, "Invalid string format! this takes 1 arguments maximum! (player count)");
}
ImGui::InputText("Multiple Cheater Warning", &m_Settings.m_MultipleCheaterWarningMessage);
try {
fmt::format(m_Settings.m_MultipleCheaterWarningMessage, fmt::arg("count", 2), fmt::arg("names", "a, b"));
}
catch (fmt::format_error err) {
ImGui::TextFmt({ 1, 0, 0, 1 }, "Invalid string format! this takes 2 arguments maximum! ({count}, {names})");
}

ImGui::NewLine();

if (ImGui::Button("Save Custom Messages"))
m_Settings.SaveFile();
}

ImGui::NewLine();

{
if (ImGui::Checkbox("Party message warnings for connecting cheaters", &m_Settings.m_AutoChatWarningsConnectingParty))
m_Settings.SaveFile();
Expand Down

0 comments on commit 9c29aad

Please sign in to comment.