From 59287680c6f2725565d0a797826a279500892c6b Mon Sep 17 00:00:00 2001 From: wtfseanscool <93678452+wtfseanscool@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:54:56 -0400 Subject: [PATCH 1/2] Add team invite button feature - clean implementation Features: - Interactive invite buttons in scoreboard for each player - Auto-join functionality for TEAM_FLOCK players (join empty team + lock + invite) - Normal invite functionality for players already on teams - Mouse cursor support in scoreboard for button interaction - Visual feedback (checkmarks, cooldowns, disabled states) - State tracking and cooldown management - Automatic cleanup when players leave/join Files modified: - src/game/client/components/scoreboard.h: Added invite button methods and member variables - src/game/client/components/scoreboard.cpp: Added full invite button implementation --- src/game/client/components/scoreboard.cpp | 347 +++++++++++++++++++++- src/game/client/components/scoreboard.h | 22 ++ 2 files changed, 368 insertions(+), 1 deletion(-) diff --git a/src/game/client/components/scoreboard.cpp b/src/game/client/components/scoreboard.cpp index 97893a13d03..75ace05cdf3 100644 --- a/src/game/client/components/scoreboard.cpp +++ b/src/game/client/components/scoreboard.cpp @@ -18,8 +18,12 @@ #include #include +#include "SDL_mouse.h" + CScoreboard::CScoreboard() { + InviteButtonInitialize(); + OnReset(); } @@ -41,6 +45,8 @@ void CScoreboard::OnInit() void CScoreboard::OnReset() { + InviteButtonReset(); + m_Active = false; m_ServerRecord = -1.0f; } @@ -48,6 +54,12 @@ void CScoreboard::OnReset() void CScoreboard::OnRelease() { m_Active = false; + + if(m_MouseModeWasAbsolute) + { + Input()->MouseModeRelative(); + m_MouseModeWasAbsolute = false; + } } void CScoreboard::OnMessage(int MsgType, void *pRawMsg) @@ -327,8 +339,12 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, const float PingLength = 55.0f; const float PingOffset = Scoreboard.x + Scoreboard.w - PingLength - 20.0f; const float CountryOffset = PingOffset - CountryLength; + + const float InviteButtonLength = m_InviteButtonWidth; + const float InviteButtonOffset = CountryOffset - InviteButtonLength - 5.0f; + const float ClanOffset = NameOffset + NameLength + 5.0f; - const float ClanLength = CountryOffset - ClanOffset - 5.0f; + const float ClanLength = InviteButtonOffset - ClanOffset - 5.0f; // render headlines const float HeadlineFontsize = 22.0f; @@ -340,6 +356,8 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, TextRender()->Text(NameOffset, HeadlineY, HeadlineFontsize, Localize("Name")); const char *pClanLabel = Localize("Clan"); TextRender()->Text(ClanOffset + (ClanLength - TextRender()->TextWidth(HeadlineFontsize, pClanLabel)) / 2.0f, HeadlineY, HeadlineFontsize, pClanLabel); + const char *pInviteLabel = Localize("Invite"); + TextRender()->Text(InviteButtonOffset + (InviteButtonLength - TextRender()->TextWidth(HeadlineFontsize, pInviteLabel)) / 2.0f, HeadlineY, HeadlineFontsize, pInviteLabel); const char *pPingLabel = Localize("Ping"); TextRender()->Text(PingOffset + PingLength - TextRender()->TextWidth(HeadlineFontsize, pPingLabel), HeadlineY, HeadlineFontsize, pPingLabel); @@ -591,6 +609,88 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, Cursor.m_LineWidth = ClanLength; TextRender()->TextEx(&Cursor, ClientData.m_aClan); } + + if(ShouldShowInviteButton(pInfo->m_ClientId)) + { + CUIRect InviteButton; + InviteButton.x = InviteButtonOffset; + InviteButton.y = Row.y + (Row.h - m_InviteButtonHeight) / 2.0f; + InviteButton.w = InviteButtonLength; + InviteButton.h = m_InviteButtonHeight; + + vec2 NativeMousePos = Input()->NativeMousePos(); + float ScreenWidth = Graphics()->ScreenWidth(); + float ScreenHeight = Graphics()->ScreenHeight(); + + const float ScoreboardHeight = 400.0f * 3.0f; // Same as other Height variable + const float ScoreboardWidth = ScoreboardHeight * Graphics()->ScreenAspect(); + + // Convert from native screen coordinates to scoreboard coordinate system + float ScoreboardMouseX = (NativeMousePos.x / ScreenWidth) * ScoreboardWidth; + float ScoreboardMouseY = (NativeMousePos.y / ScreenHeight) * ScoreboardHeight; + + // Manual button collision detection using native coordinates + bool ManualHover = (ScoreboardMouseX >= InviteButton.x && ScoreboardMouseX <= InviteButton.x + InviteButton.w && + ScoreboardMouseY >= InviteButton.y && ScoreboardMouseY <= InviteButton.y + InviteButton.h); + + + bool InviteSent = m_aInviteSent[pInfo->m_ClientId]; + bool GlobalCooldownActive = IsGlobalInviteCooldownActive(); + bool IsDisabled = GlobalCooldownActive && !InviteSent; + + ColorRGBA ButtonColor, BorderColor; + float BorderWidth = 0.0f; + float ButtonRadius = 3.0f; + const char *pButtonText; + + if(InviteSent) + { + ButtonColor = ColorRGBA(0.05f, 0.25f, 0.05f, 0.5f); + BorderColor = ColorRGBA(0.0f, 0.2f, 0.0f, 0.6f); + TextColor = ColorRGBA(0.7f, 0.9f, 0.7f, 1.0f); + BorderWidth = 1.0f; + pButtonText = "✓"; + } + else + { + ButtonColor = IsDisabled ? ColorRGBA(0.1f, 0.1f, 0.1f, 0.2f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f); + BorderColor = IsDisabled ? ColorRGBA(0.2f, 0.2f, 0.2f, 0.3f) : ColorRGBA(0.3f, 0.3f, 0.3f, 0.5f); + TextColor = IsDisabled ? ColorRGBA(0.4f, 0.4f, 0.4f, 0.6f) : ColorRGBA(0.9f, 0.9f, 0.9f, 0.9f); + BorderWidth = 1.0f; + + pButtonText = "+"; + } + + CUIRect ButtonRect = InviteButton; + + ButtonRect.Draw(ButtonColor, IGraphics::CORNER_ALL, ButtonRadius); + + if(BorderWidth > 0.0f) + { + CUIRect BorderRect = InviteButton; + BorderRect.x -= BorderWidth; + BorderRect.y -= BorderWidth; + BorderRect.w += BorderWidth * 2.0f; + BorderRect.h += BorderWidth * 2.0f; + + BorderRect.Draw(BorderColor, IGraphics::CORNER_ALL, ButtonRadius + BorderWidth); + ButtonRect.Draw(ButtonColor, IGraphics::CORNER_ALL, ButtonRadius); + } + + TextRender()->TextColor(TextColor); + float TextSize = FontSize * 0.8f; + + Ui()->DoLabel(&ButtonRect, pButtonText, TextSize, TEXTALIGN_MC); + + TextRender()->TextColor(TextRender()->DefaultTextColor()); + + bool ManualClick = ManualHover && Ui()->MouseButtonClicked(0); + + if(ManualClick && !InviteSent && !IsDisabled) + { + OnInviteButtonClick(pInfo->m_ClientId, ClientData.m_aName); + } + } // country flag GameClient()->m_CountryFlags.Render(ClientData.m_Country, ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), @@ -661,7 +761,30 @@ void CScoreboard::OnRender() return; if(!IsActive()) + { + if(m_MouseModeWasAbsolute) + { + Input()->MouseModeRelative(); + m_MouseModeWasAbsolute = false; + } return; + } + + if(!m_MouseModeWasAbsolute) + { + Input()->MouseModeAbsolute(); + Ui()->SetEnabled(true); + m_MouseModeWasAbsolute = true; + + int ScreenWidth = Graphics()->ScreenWidth(); + int ScreenHeight = Graphics()->ScreenHeight(); + SDL_WarpMouseInWindow(nullptr, ScreenWidth / 2, ScreenHeight / 2); + } + + Ui()->MapScreen(); + Ui()->Update(); + + UpdateTeamStateTracking(); // if the score board is active, then we should clear the motd message as well if(GameClient()->m_Motd.IsActive()) @@ -865,3 +988,225 @@ const char *CScoreboard::GetTeamName(int Team) const else return nullptr; } +void CScoreboard::InviteButtonInitialize() +{ + m_InviteButtonWidth = 50.0f; + m_InviteButtonHeight = 20.0f; + + m_LastLocalTeam = TEAM_FLOCK; + m_InAutoJoinSequence = false; + + m_MouseModeWasAbsolute = false; + + std::fill(m_aInviteSent, m_aInviteSent + SERVER_MAX_CLIENTS, false); + + std::fill(m_aPlayerWasActive, m_aPlayerWasActive + SERVER_MAX_CLIENTS, false); + + m_GlobalInviteCooldownEnd = 0; +} + +void CScoreboard::InviteButtonReset() +{ + m_LastLocalTeam = TEAM_FLOCK; + m_InAutoJoinSequence = false; + + if(m_MouseModeWasAbsolute) + { + Input()->MouseModeRelative(); + m_MouseModeWasAbsolute = false; + } + + std::fill(m_aInviteSent, m_aInviteSent + SERVER_MAX_CLIENTS, false); + + std::fill(m_aPlayerWasActive, m_aPlayerWasActive + SERVER_MAX_CLIENTS, false); + + m_GlobalInviteCooldownEnd = 0; +} + +bool CScoreboard::ShouldShowInviteButton(int ClientId) const +{ + if(!GameClient()->m_Snap.m_pLocalInfo) + return false; + + if(ClientId < 0 || ClientId >= SERVER_MAX_CLIENTS || !GameClient()->m_Snap.m_apPlayerInfos[ClientId]) + return false; + + if(!GameClient()->m_aClients[ClientId].m_Active) + return false; + + int TargetTeam = GameClient()->m_Snap.m_apPlayerInfos[ClientId]->m_Team; + if(TargetTeam == TEAM_SPECTATORS) + return false; + + int LocalPlayerId = GameClient()->m_aLocalIds[g_Config.m_ClDummy]; + if(LocalPlayerId < 0 || LocalPlayerId >= SERVER_MAX_CLIENTS) + return false; + + int LocalTeam = GameClient()->m_Teams.Team(LocalPlayerId); + + if(ClientId == LocalPlayerId) + return false; + + int TargetDDTeam = GameClient()->m_Teams.Team(ClientId); + + if(LocalTeam == TargetDDTeam && LocalTeam != TEAM_FLOCK) + return false; + + return true; +} + +void CScoreboard::OnInviteButtonClick(int ClientId, const char *pPlayerName) +{ + if(m_aInviteSent[ClientId]) + return; + + if(IsGlobalInviteCooldownActive()) + return; + + if(!pPlayerName || pPlayerName[0] == '\0') + return; + + if(ClientId < 0 || ClientId >= SERVER_MAX_CLIENTS || !GameClient()->m_Snap.m_apPlayerInfos[ClientId]) + return; + + if(!GameClient()->m_aClients[ClientId].m_Active) + return; + + if(!ShouldShowInviteButton(ClientId)) + return; + + if(str_comp(pPlayerName, GameClient()->m_aClients[ClientId].m_aName) != 0) + return; + + int LocalPlayerId = GameClient()->m_aLocalIds[g_Config.m_ClDummy]; + if(LocalPlayerId < 0 || LocalPlayerId >= SERVER_MAX_CLIENTS) + return; + + int LocalTeam = GameClient()->m_Teams.Team(LocalPlayerId); + + m_aInviteSent[ClientId] = true; + int64_t CooldownDuration = (int64_t)g_Config.m_SvInviteFrequency * time_freq(); + m_GlobalInviteCooldownEnd = time_get() + CooldownDuration; + + // Special handling for TEAM_FLOCK players + if(LocalTeam == TEAM_FLOCK) + { + int EmptyTeam = FindNextEmptyTeam(); + if(EmptyTeam == -1) + { + m_aInviteSent[ClientId] = false; + m_GlobalInviteCooldownEnd = 0; + return; + } + + m_InAutoJoinSequence = true; + + // Join the empty team first + char aJoinCommand[64]; + str_format(aJoinCommand, sizeof(aJoinCommand), "/team %d", EmptyTeam); + GameClient()->m_Chat.SendChat(0, aJoinCommand); + + // Lock the team + GameClient()->m_Chat.SendChat(0, "/lock"); + + char aInviteCommand[256]; + str_format(aInviteCommand, sizeof(aInviteCommand), "/invite %s", pPlayerName); + GameClient()->m_Chat.SendChat(0, aInviteCommand); + } + else + { + char aInviteCommand[256]; + str_format(aInviteCommand, sizeof(aInviteCommand), "/invite %s", pPlayerName); + GameClient()->m_Chat.SendChat(0, aInviteCommand); + } +} + +bool CScoreboard::IsGlobalInviteCooldownActive() const +{ + return time_get() < m_GlobalInviteCooldownEnd; +} + +void CScoreboard::UpdateTeamStateTracking() +{ + int CurrentLocalTeam = TEAM_FLOCK; + int MainPlayerId = GameClient()->m_aLocalIds[0]; + int DummyPlayerId = GameClient()->m_aLocalIds[1]; + + if(MainPlayerId >= 0 && MainPlayerId < SERVER_MAX_CLIENTS && GameClient()->m_aClients[MainPlayerId].m_Active) + { + CurrentLocalTeam = GameClient()->m_Teams.Team(MainPlayerId); + } + else if(DummyPlayerId >= 0 && DummyPlayerId < SERVER_MAX_CLIENTS && GameClient()->m_aClients[DummyPlayerId].m_Active) + { + CurrentLocalTeam = GameClient()->m_Teams.Team(DummyPlayerId); + } + + for(int i = 0; i < SERVER_MAX_CLIENTS; i++) + { + bool IsCurrentlyActive = GameClient()->m_Snap.m_apPlayerInfos[i] && GameClient()->m_aClients[i].m_Active; + + if(m_aPlayerWasActive[i] && !IsCurrentlyActive) + { + ResetInviteState(i); + } + else if(!m_aPlayerWasActive[i] && IsCurrentlyActive) + { + ResetInviteState(i); + } + + m_aPlayerWasActive[i] = IsCurrentlyActive; + } + + if(m_LastLocalTeam != CurrentLocalTeam) + { + if(!m_InAutoJoinSequence) + { + ResetInviteState(); + } + else + { + // Clear the auto-join flag after the team change is processed + m_InAutoJoinSequence = false; + } + m_LastLocalTeam = CurrentLocalTeam; + } +} + +void CScoreboard::ResetInviteState(int ClientId) +{ + if(ClientId == -1) + { + std::fill(m_aInviteSent, m_aInviteSent + SERVER_MAX_CLIENTS, false); + m_GlobalInviteCooldownEnd = 0; + } + else if(ClientId >= 0 && ClientId < SERVER_MAX_CLIENTS) + { + m_aInviteSent[ClientId] = false; + } +} + +int CScoreboard::FindNextEmptyTeam() const +{ + // Check teams 1-63 for empty slots + for(int Team = 1; Team < TEAM_SUPER; Team++) + { + bool TeamEmpty = true; + + // Check if any player is in this team + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(GameClient()->m_Snap.m_apPlayerInfos[i] && + GameClient()->m_aClients[i].m_Active && + GameClient()->m_Teams.Team(i) == Team) + { + TeamEmpty = false; + break; + } + } + + if(TeamEmpty) + return Team; + } + + return -1; // No empty team found +} \ No newline at end of file diff --git a/src/game/client/components/scoreboard.h b/src/game/client/components/scoreboard.h index cbf728ebf51..0685780442f 100644 --- a/src/game/client/components/scoreboard.h +++ b/src/game/client/components/scoreboard.h @@ -30,6 +30,28 @@ class CScoreboard : public CComponent static void ConKeyScoreboard(IConsole::IResult *pResult, void *pUserData); const char *GetTeamName(int Team) const; + void InviteButtonInitialize(); + void InviteButtonReset(); + bool ShouldShowInviteButton(int ClientId) const; + void OnInviteButtonClick(int ClientId, const char *pPlayerName); + void UpdateTeamStateTracking(); + void ResetInviteState(int ClientId = -1); + bool IsGlobalInviteCooldownActive() const; + int FindNextEmptyTeam() const; + + float m_InviteButtonWidth; + float m_InviteButtonHeight; + bool m_aInviteSent[64]; + + int64_t m_GlobalInviteCooldownEnd; + + int m_LastLocalTeam; + bool m_InAutoJoinSequence; + + bool m_aPlayerWasActive[64]; + + bool m_MouseModeWasAbsolute; + bool m_Active; float m_ServerRecord; From 4111170fb9cfdeff7d042ade3ea4a5331256fad0 Mon Sep 17 00:00:00 2001 From: wtfseanscool <93678452+wtfseanscool@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:37:27 -0400 Subject: [PATCH 2/2] update: Team invite buttons Made the following changes: - When in an active run, AND not frozen, hide the invite buttons (alternative can disable, doesnt matter), and restore regular mouse interaction when the scoreboard is open Bugs fixed: - No longer shows when we are in spectator / in super - Added server join cooldown to handle internal delay after join / map switch --- src/game/client/components/scoreboard.cpp | 55 +++++++++++++++++++++-- src/game/client/components/scoreboard.h | 6 +++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/game/client/components/scoreboard.cpp b/src/game/client/components/scoreboard.cpp index 75ace05cdf3..f778a41e8bf 100644 --- a/src/game/client/components/scoreboard.cpp +++ b/src/game/client/components/scoreboard.cpp @@ -636,7 +636,8 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, bool InviteSent = m_aInviteSent[pInfo->m_ClientId]; bool GlobalCooldownActive = IsGlobalInviteCooldownActive(); - bool IsDisabled = GlobalCooldownActive && !InviteSent; + bool ServerJoinCooldownActive = IsServerJoinCooldownActive(); + bool IsDisabled = (GlobalCooldownActive && !InviteSent) || ServerJoinCooldownActive; ColorRGBA ButtonColor, BorderColor; float BorderWidth = 0.0f; @@ -770,7 +771,9 @@ void CScoreboard::OnRender() return; } - if(!m_MouseModeWasAbsolute) + bool ShouldEnableMouseMode = !ShouldDisableInteraction(); + + if(!m_MouseModeWasAbsolute && ShouldEnableMouseMode) { Input()->MouseModeAbsolute(); Ui()->SetEnabled(true); @@ -780,6 +783,12 @@ void CScoreboard::OnRender() int ScreenHeight = Graphics()->ScreenHeight(); SDL_WarpMouseInWindow(nullptr, ScreenWidth / 2, ScreenHeight / 2); } + else if(m_MouseModeWasAbsolute && !ShouldEnableMouseMode) + { + Input()->MouseModeRelative(); + Ui()->SetEnabled(false); + m_MouseModeWasAbsolute = false; + } Ui()->MapScreen(); Ui()->Update(); @@ -1003,6 +1012,7 @@ void CScoreboard::InviteButtonInitialize() std::fill(m_aPlayerWasActive, m_aPlayerWasActive + SERVER_MAX_CLIENTS, false); m_GlobalInviteCooldownEnd = 0; + m_ServerJoinTime = time_get(); } void CScoreboard::InviteButtonReset() @@ -1021,6 +1031,7 @@ void CScoreboard::InviteButtonReset() std::fill(m_aPlayerWasActive, m_aPlayerWasActive + SERVER_MAX_CLIENTS, false); m_GlobalInviteCooldownEnd = 0; + m_ServerJoinTime = time_get(); } bool CScoreboard::ShouldShowInviteButton(int ClientId) const @@ -1042,11 +1053,20 @@ bool CScoreboard::ShouldShowInviteButton(int ClientId) const if(LocalPlayerId < 0 || LocalPlayerId >= SERVER_MAX_CLIENTS) return false; - int LocalTeam = GameClient()->m_Teams.Team(LocalPlayerId); + if(GameClient()->m_Snap.m_pLocalInfo->m_Team == TEAM_SPECTATORS) + return false; + + if(ShouldDisableInteraction()) + return false; if(ClientId == LocalPlayerId) return false; + int LocalTeam = GameClient()->m_Teams.Team(LocalPlayerId); + + if (LocalTeam == TEAM_SUPER) + return false; + int TargetDDTeam = GameClient()->m_Teams.Team(ClientId); if(LocalTeam == TargetDDTeam && LocalTeam != TEAM_FLOCK) @@ -1063,6 +1083,9 @@ void CScoreboard::OnInviteButtonClick(int ClientId, const char *pPlayerName) if(IsGlobalInviteCooldownActive()) return; + if(IsServerJoinCooldownActive()) + return; + if(!pPlayerName || pPlayerName[0] == '\0') return; @@ -1126,6 +1149,32 @@ bool CScoreboard::IsGlobalInviteCooldownActive() const return time_get() < m_GlobalInviteCooldownEnd; } +bool CScoreboard::IsServerJoinCooldownActive() const +{ + const int64_t ServerJoinCooldownDuration = 1 * time_freq(); // 1 second + return time_get() < m_ServerJoinTime + ServerJoinCooldownDuration; +} + +bool CScoreboard::IsLocalPlayerInActiveRun() const +{ + return GameClient()->LastRaceTick() >= 0; +} + +bool CScoreboard::IsLocalPlayerFrozen() const +{ + int LocalPlayerId = GameClient()->m_aLocalIds[g_Config.m_ClDummy]; + if(LocalPlayerId < 0 || LocalPlayerId >= SERVER_MAX_CLIENTS) + return false; + + const auto &ClientData = GameClient()->m_aClients[LocalPlayerId]; + return ClientData.m_FreezeEnd > 0; +} + +bool CScoreboard::ShouldDisableInteraction() const +{ + return IsLocalPlayerInActiveRun() && !IsLocalPlayerFrozen(); +} + void CScoreboard::UpdateTeamStateTracking() { int CurrentLocalTeam = TEAM_FLOCK; diff --git a/src/game/client/components/scoreboard.h b/src/game/client/components/scoreboard.h index 0685780442f..bd58f31fd47 100644 --- a/src/game/client/components/scoreboard.h +++ b/src/game/client/components/scoreboard.h @@ -37,6 +37,10 @@ class CScoreboard : public CComponent void UpdateTeamStateTracking(); void ResetInviteState(int ClientId = -1); bool IsGlobalInviteCooldownActive() const; + bool IsServerJoinCooldownActive() const; + bool IsLocalPlayerInActiveRun() const; + bool IsLocalPlayerFrozen() const; + bool ShouldDisableInteraction() const; int FindNextEmptyTeam() const; float m_InviteButtonWidth; @@ -49,6 +53,8 @@ class CScoreboard : public CComponent bool m_InAutoJoinSequence; bool m_aPlayerWasActive[64]; + + int64_t m_ServerJoinTime; bool m_MouseModeWasAbsolute;