ddnet/src/game/client/components/spectator.cpp
Robert Müller c6cc45471a Allow spectator mode to be changed while demo is paused
By updating and rendering the current tick again when changing the spectator mode while the demo playback is paused.

Refactoring: Extract `IDemoPlayer::ETickOffset`, `IDemoPlayer::SeekTick` and `CMenus::DemoSeekTick`.
2022-08-28 17:52:49 +02:00

440 lines
14 KiB
C++

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <climits>
#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/textrender.h>
#include <game/generated/protocol.h>
#include <game/client/animstate.h>
#include <game/client/render.h>
#include <game/localization.h>
#include "camera.h"
#include "spectator.h"
#include <game/client/gameclient.h>
bool CSpectator::CanChangeSpectator()
{
// Don't change SpectatorID when not spectating
return m_pClient->m_Snap.m_SpecInfo.m_Active;
}
void CSpectator::SpectateNext(bool Reverse)
{
int CurIndex = -1;
const CNetObj_PlayerInfo **paPlayerInfos = m_pClient->m_Snap.m_apInfoByDDTeamName;
// m_SpectatorID may be uninitialized if m_Active is false
if(m_pClient->m_Snap.m_SpecInfo.m_Active)
{
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(paPlayerInfos[i] && paPlayerInfos[i]->m_ClientID == m_pClient->m_Snap.m_SpecInfo.m_SpectatorID)
{
CurIndex = i;
break;
}
}
}
int Start;
if(CurIndex != -1)
{
if(Reverse)
Start = CurIndex - 1;
else
Start = CurIndex + 1;
}
else
{
if(Reverse)
Start = -1;
else
Start = 0;
}
int Increment = Reverse ? -1 : 1;
for(int i = 0; i < MAX_CLIENTS; i++)
{
int PlayerIndex = (Start + i * Increment) % MAX_CLIENTS;
// % in C++ takes the sign of the dividend, not divisor
if(PlayerIndex < 0)
PlayerIndex += MAX_CLIENTS;
const CNetObj_PlayerInfo *pPlayerInfo = paPlayerInfos[PlayerIndex];
if(pPlayerInfo && pPlayerInfo->m_Team != TEAM_SPECTATORS)
{
Spectate(pPlayerInfo->m_ClientID);
break;
}
}
}
void CSpectator::ConKeySpectator(IConsole::IResult *pResult, void *pUserData)
{
CSpectator *pSelf = (CSpectator *)pUserData;
if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK)
pSelf->m_Active = pResult->GetInteger(0) != 0;
else
pSelf->m_Active = false;
}
void CSpectator::ConSpectate(IConsole::IResult *pResult, void *pUserData)
{
CSpectator *pSelf = (CSpectator *)pUserData;
if(!pSelf->CanChangeSpectator())
return;
pSelf->Spectate(pResult->GetInteger(0));
}
void CSpectator::ConSpectateNext(IConsole::IResult *pResult, void *pUserData)
{
CSpectator *pSelf = (CSpectator *)pUserData;
if(!pSelf->CanChangeSpectator())
return;
pSelf->SpectateNext(false);
}
void CSpectator::ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData)
{
CSpectator *pSelf = (CSpectator *)pUserData;
if(!pSelf->CanChangeSpectator())
return;
pSelf->SpectateNext(true);
}
void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData)
{
CSpectator *pSelf = (CSpectator *)pUserData;
if(!pSelf->CanChangeSpectator())
return;
CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap;
int SpectatorID = Snap.m_SpecInfo.m_SpectatorID;
int NewSpectatorID = -1;
vec2 CurPosition(pSelf->m_pClient->m_Camera.m_Center);
if(SpectatorID != SPEC_FREEVIEW)
{
const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorID].m_Cur;
CurPosition.x = CurCharacter.m_X;
CurPosition.y = CurCharacter.m_Y;
}
int ClosestDistance = INT_MAX;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(i == SpectatorID || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || (SpectatorID == SPEC_FREEVIEW && i == Snap.m_LocalClientID))
continue;
const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur;
int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y));
if(NewSpectatorID == -1 || Distance < ClosestDistance)
{
NewSpectatorID = i;
ClosestDistance = Distance;
}
}
if(NewSpectatorID > -1)
pSelf->Spectate(NewSpectatorID);
}
CSpectator::CSpectator()
{
OnReset();
m_OldMouseX = m_OldMouseY = 0.0f;
}
void CSpectator::OnConsoleInit()
{
Console()->Register("+spectate", "", CFGFLAG_CLIENT, ConKeySpectator, this, "Open spectator mode selector");
Console()->Register("spectate", "i[spectator-id]", CFGFLAG_CLIENT, ConSpectate, this, "Switch spectator mode");
Console()->Register("spectate_next", "", CFGFLAG_CLIENT, ConSpectateNext, this, "Spectate the next player");
Console()->Register("spectate_previous", "", CFGFLAG_CLIENT, ConSpectatePrevious, this, "Spectate the previous player");
Console()->Register("spectate_closest", "", CFGFLAG_CLIENT, ConSpectateClosest, this, "Spectate the closest player");
}
bool CSpectator::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
{
if(!m_Active)
return false;
UI()->ConvertMouseMove(&x, &y, CursorType);
m_SelectorMouse += vec2(x, y);
return true;
}
void CSpectator::OnRelease()
{
OnReset();
}
void CSpectator::OnRender()
{
if(!m_Active)
{
if(m_WasActive)
{
if(m_SelectedSpectatorID != NO_SELECTION)
Spectate(m_SelectedSpectatorID);
m_WasActive = false;
}
return;
}
if(!m_pClient->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
m_Active = false;
m_WasActive = false;
return;
}
m_WasActive = true;
m_SelectedSpectatorID = NO_SELECTION;
// draw background
float Width = 400 * 3.0f * Graphics()->ScreenAspect();
float Height = 400 * 3.0f;
float ObjWidth = 300.0f;
float FontSize = 20.0f;
float BigFontSize = 20.0f;
float StartY = -190.0f;
float LineHeight = 60.0f;
float TeeSizeMod = 1.0f;
float RoundRadius = 30.0f;
bool Selected = false;
int TotalPlayers = 0;
int PerLine = 8;
float BoxMove = -10.0f;
float BoxOffset = 0.0f;
for(auto &pInfo : m_pClient->m_Snap.m_apInfoByDDTeamName)
{
if(!pInfo || pInfo->m_Team == TEAM_SPECTATORS)
continue;
++TotalPlayers;
}
if(TotalPlayers > 32)
{
FontSize = 18.0f;
LineHeight = 30.0f;
TeeSizeMod = 0.7f;
PerLine = 16;
RoundRadius = 10.0f;
BoxMove = 3.0f;
BoxOffset = 6.0f;
}
if(TotalPlayers > 16)
{
ObjWidth = 600.0f;
}
Graphics()->MapScreen(0, 0, Width, Height);
Graphics()->DrawRect(Width / 2.0f - ObjWidth, Height / 2.0f - 300.0f, ObjWidth * 2, 600.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_ALL, 20.0f);
// clamp mouse position to selector area
m_SelectorMouse.x = clamp(m_SelectorMouse.x, -(ObjWidth - 20.0f), ObjWidth - 20.0f);
m_SelectorMouse.y = clamp(m_SelectorMouse.y, -280.0f, 280.0f);
// draw selections
if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == SPEC_FREEVIEW) ||
(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW))
{
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f), Height / 2.0f - 280.0f, 270.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
}
if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientID >= 0 && m_pClient->m_DemoSpecID == SPEC_FOLLOW)
{
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 310.0f), Height / 2.0f - 280.0f, 270.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
}
if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 290 + 10.0f) &&
m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
{
m_SelectedSpectatorID = SPEC_FREEVIEW;
Selected = true;
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f);
TextRender()->Text(0, Width / 2.0f - (ObjWidth - 60.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Free-View"), -1.0f);
if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientID >= 0)
{
Selected = false;
if(m_SelectorMouse.x > -(ObjWidth - 290.0f) && m_SelectorMouse.x <= -(ObjWidth - 590.0f) &&
m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
{
m_SelectedSpectatorID = SPEC_FOLLOW;
Selected = true;
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f);
TextRender()->Text(0, Width / 2.0f - (ObjWidth - 350.0f), Height / 2.0f - 280.0f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Follow"), -1.0f);
}
float x = -(ObjWidth - 35.0f), y = StartY;
int OldDDTeam = -1;
for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i)
{
if(!m_pClient->m_Snap.m_apInfoByDDTeamName[i] || m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_Team == TEAM_SPECTATORS)
continue;
++Count;
if(Count == PerLine + 1 || (Count > PerLine + 1 && (Count - 1) % PerLine == 0))
{
x += 290.0f;
y = StartY;
}
const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_apInfoByDDTeamName[i];
int DDTeam = m_pClient->m_Teams.Team(pInfo->m_ClientID);
int NextDDTeam = 0;
for(int j = i + 1; j < MAX_CLIENTS; j++)
{
const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_apInfoByDDTeamName[j];
if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS)
continue;
NextDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID);
break;
}
if(OldDDTeam == -1)
{
for(int j = i - 1; j >= 0; j--)
{
const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_apInfoByDDTeamName[j];
if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS)
continue;
OldDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID);
break;
}
}
if(DDTeam != TEAM_FLOCK)
{
ColorRGBA Color = color_cast<ColorRGBA>(ColorHSLA(DDTeam / 64.0f, 1.0f, 0.5f, 0.5f));
int Corners = 0;
if(OldDDTeam != DDTeam)
Corners |= IGraphics::CORNER_TL | IGraphics::CORNER_TR;
if(NextDDTeam != DDTeam)
Corners |= IGraphics::CORNER_BL | IGraphics::CORNER_BR;
Graphics()->DrawRect(Width / 2.0f + x - 10.0f + BoxOffset, Height / 2.0f + y + BoxMove, 270.0f - BoxOffset, LineHeight, Color, Corners, RoundRadius);
}
OldDDTeam = DDTeam;
if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID))
{
Graphics()->DrawRect(Width / 2.0f + x - 10.0f + BoxOffset, Height / 2.0f + y + BoxMove, 270.0f - BoxOffset, LineHeight, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, RoundRadius);
}
Selected = false;
if(m_SelectorMouse.x >= x - 10.0f && m_SelectorMouse.x < x + 260.0f &&
m_SelectorMouse.y >= y - (LineHeight / 6.0f) && m_SelectorMouse.y < y + (LineHeight * 5.0f / 6.0f))
{
m_SelectedSpectatorID = m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID;
Selected = true;
}
float TeeAlpha;
if(Client()->State() == IClient::STATE_DEMOPLAYBACK &&
!m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_Active)
{
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.25f);
TeeAlpha = 0.5f;
}
else
{
TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f);
TeeAlpha = 1.0f;
}
TextRender()->Text(0, Width / 2.0f + x + 50.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_aName, 220.0f);
// flag
if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS &&
m_pClient->m_Snap.m_pGameDataObj && (m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID || m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID))
{
Graphics()->BlendNormal();
if(m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID)
Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue);
else
Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed);
Graphics()->QuadsBegin();
Graphics()->QuadsSetSubset(1, 0, 0, 1);
float Size = LineHeight;
IGraphics::CQuadItem QuadItem(Width / 2.0f + x - LineHeight / 5.0f, Height / 2.0f + y - LineHeight / 3.0f, Size / 2.0f, Size);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_RenderInfo;
TeeInfo.m_Size *= TeeSizeMod;
CAnimState *pIdleState = CAnimState::GetIdle();
vec2 OffsetToMid;
RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid);
vec2 TeeRenderPos(Width / 2.0f + x + 20.0f, Height / 2.0f + y + BoxMove + LineHeight / 2.0f + OffsetToMid.y);
RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos, TeeAlpha);
if(m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_Friend)
{
ColorRGBA rgb = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageFriendColor));
TextRender()->TextColor(rgb.WithAlpha(1.f));
TextRender()->Text(0, Width / 2.0f + x - TeeInfo.m_Size / 2.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, "", 220.0f);
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
}
y += LineHeight;
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
RenderTools()->RenderCursor(m_SelectorMouse + vec2(Width, Height) / 2, 48.0f);
}
void CSpectator::OnReset()
{
m_WasActive = false;
m_Active = false;
m_SelectedSpectatorID = NO_SELECTION;
}
void CSpectator::Spectate(int SpectatorID)
{
if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
m_pClient->m_DemoSpecID = clamp(SpectatorID, (int)SPEC_FOLLOW, MAX_CLIENTS - 1);
// The tick must be rendered for the spectator mode to be updated, so we do it manually when demo playback is paused
if(DemoPlayer()->BaseInfo()->m_Paused)
GameClient()->m_Menus.DemoSeekTick(IDemoPlayer::TICK_CURRENT);
return;
}
if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SpectatorID)
return;
CNetMsg_Cl_SetSpectatorMode Msg;
Msg.m_SpectatorID = SpectatorID;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
}