From 7c45688fd255c2fdff55ec2968c5e10b000a0a44 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Wed, 28 Aug 2024 15:06:13 +0800 Subject: [PATCH] Add spectator cursor --- datasrc/network.py | 9 +++++ src/engine/shared/config_variables.h | 3 ++ src/game/client/components/hud.cpp | 29 ++++++++++--- src/game/client/gameclient.cpp | 56 ++++++++++++++++++++++++++ src/game/client/gameclient.h | 6 +++ src/game/server/entities/character.cpp | 18 +++++++++ src/game/server/entities/character.h | 2 + src/game/server/player.cpp | 23 +++++++++++ 8 files changed, 141 insertions(+), 5 deletions(-) diff --git a/datasrc/network.py b/datasrc/network.py index 04c02748b..38345f491 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -378,6 +378,15 @@ Objects = [ NetEventEx("MapSoundWorld:Common", "map-sound-world@netevent.ddnet.org", [ NetIntAny("m_SoundId"), ]), + + # Spectating cursor + NetObjectEx("SpecCursor", "spec-cursor@netobj.ddnet.org", [ + NetIntAny("m_Weapon"), + NetIntAny("m_TargetX"), + NetIntAny("m_TargetY"), + NetIntAny("m_DeltaPrevTargetX"), + NetIntAny("m_DeltaPrevTargetY"), + ]), ] Messages = [ diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 15f10e22a..c34725cc6 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -73,6 +73,9 @@ MACRO_CONFIG_INT(ClShowfps, cl_showfps, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, MACRO_CONFIG_INT(ClShowpred, cl_showpred, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame prediction time in milliseconds") MACRO_CONFIG_INT(ClEyeWheel, cl_eye_wheel, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show eye wheel along together with emotes") MACRO_CONFIG_INT(ClEyeDuration, cl_eye_duration, 999999, 1, 999999, CFGFLAG_CLIENT | CFGFLAG_SAVE, "How long the eyes emotes last") +MACRO_CONFIG_INT(ClSpecCursor, cl_spec_cursor, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable the cursor of spectating player if available") +MACRO_CONFIG_INT(ClSpecCursorInterp, cl_spec_cursor_interp, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interpolate the cursor of spectating player") +MACRO_CONFIG_INT(ClSpecCursorDemo, cl_spec_cursor_demo, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show cursor during demo playback if available") MACRO_CONFIG_INT(ClAirjumpindicator, cl_airjumpindicator, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show the air jump indicator") MACRO_CONFIG_INT(ClThreadsoundloading, cl_threadsoundloading, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Load sound files threaded") diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp index 903443d67..fbdeb92ae 100644 --- a/src/game/client/components/hud.cpp +++ b/src/game/client/components/hud.cpp @@ -585,16 +585,35 @@ void CHud::RenderTeambalanceWarning() void CHud::RenderCursor() { - if(!m_pClient->m_Snap.m_pLocalCharacter || Client()->State() == IClient::STATE_DEMOPLAYBACK) + int CurWeapon; + vec2 TargetPos; + + if(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_pLocalCharacter) + { + // render local cursor + CurWeapon = m_pClient->m_Snap.m_pLocalCharacter->m_Weapon % NUM_WEAPONS; + TargetPos = m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy]; + } + else if(g_Config.m_ClSpecCursor && m_pClient->m_Snap.m_pSpecCursor && m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) + { + // render spec cursor + CurWeapon = m_pClient->m_Snap.m_pSpecCursor->m_Weapon % NUM_WEAPONS; + TargetPos = m_pClient->m_Snap.m_SpecInfo.m_Position + m_pClient->m_Snap.m_DisplayCursorPos; + } + else if(g_Config.m_ClSpecCursorDemo && m_pClient->m_Snap.m_pSpecCursor && Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_pLocalCharacter) + { + CurWeapon = m_pClient->m_Snap.m_pLocalCharacter->m_Weapon % NUM_WEAPONS; + TargetPos = m_pClient->m_LocalCharacterPos + m_pClient->m_Snap.m_DisplayCursorPos; + } + else + { return; + } RenderTools()->MapScreenToInterface(m_pClient->m_Camera.m_Center.x, m_pClient->m_Camera.m_Center.y); - - // render cursor - int CurWeapon = m_pClient->m_Snap.m_pLocalCharacter->m_Weapon % NUM_WEAPONS; Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); Graphics()->TextureSet(m_pClient->m_GameSkin.m_aSpriteWeaponCursors[CurWeapon]); - Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_aCursorOffset[CurWeapon], m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy].x, m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy].y); + Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_aCursorOffset[CurWeapon], TargetPos.x, TargetPos.y); } void CHud::PrepareAmmoHealthAndArmorQuads() diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 1f450fa04..4e6c73cbf 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -678,6 +678,8 @@ void CGameClient::UpdatePositions() if(!m_MultiViewActivated && m_MultiView.m_IsInit) ResetMultiView(); + UpdateSpectatorCursor(); + UpdateRenderedCharacters(); } @@ -1791,6 +1793,11 @@ void CGameClient::OnNewSnapshot() m_aSwitchStateTeam[g_Config.m_ClDummy] = -1; GotSwitchStateTeam = true; } + else if(Item.m_Type == NETOBJTYPE_SPECCURSOR) + { + m_Snap.m_pSpecCursor = (const CNetObj_SpecCursor *)Item.m_pData; + m_Snap.m_pPrevSpecCursor = (const CNetObj_SpecCursor *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_SPECCURSOR, Item.m_Id); + } } } @@ -2945,6 +2952,55 @@ void CGameClient::UpdatePrediction() m_GameWorld.NetObjEnd(); } +void CGameClient::UpdateSpectatorCursor() +{ + if(m_Snap.m_pSpecCursor) + { + const float IntraTickSincePrev = Client()->IntraGameTickSincePrev(g_Config.m_ClDummy); + + // Decode previous cursor position from current snapshot + const int TargetX = m_Snap.m_pSpecCursor->m_TargetX; + const int TargetY = m_Snap.m_pSpecCursor->m_TargetY; + + const int PrevTargetX = TargetX + m_Snap.m_pSpecCursor->m_DeltaPrevTargetX; + const int PrevTargetY = TargetY + m_Snap.m_pSpecCursor->m_DeltaPrevTargetY; + + int PrevPrevTargetX, PrevPrevTargetY; + int PrevPrevPrevTargetX, PrevPrevPrevTargetY; + + if(m_Snap.m_pPrevSpecCursor) + { + PrevPrevTargetX = m_Snap.m_pPrevSpecCursor->m_TargetX; + PrevPrevTargetY = m_Snap.m_pPrevSpecCursor->m_TargetY; + PrevPrevPrevTargetX = PrevPrevTargetX + m_Snap.m_pPrevSpecCursor->m_DeltaPrevTargetX; + PrevPrevPrevTargetY = PrevPrevTargetY + m_Snap.m_pPrevSpecCursor->m_DeltaPrevTargetY; + } + else + { + PrevPrevTargetX = PrevTargetX; + PrevPrevTargetY = PrevTargetY; + PrevPrevPrevTargetX = PrevTargetX; + PrevPrevPrevTargetY = PrevTargetX; + } + + if(IntraTickSincePrev <= 1.0f) + { + float Intra = g_Config.m_ClSpecCursorInterp ? IntraTickSincePrev : 1.0f; + m_Snap.m_DisplayCursorPos = mix(vec2(PrevPrevPrevTargetX, PrevPrevPrevTargetY), vec2(PrevPrevTargetX, PrevPrevTargetY), Intra); + } + else if(IntraTickSincePrev <= 2.0f) + { + float Intra = g_Config.m_ClSpecCursorInterp ? IntraTickSincePrev - 1.0f : 1.0f; + m_Snap.m_DisplayCursorPos = mix(vec2(PrevPrevTargetX, PrevPrevTargetY), vec2(PrevTargetX, PrevTargetY), Intra); + } + else + { + float Intra = g_Config.m_ClSpecCursorInterp ? IntraTickSincePrev - 2.0f : 1.0f; + m_Snap.m_DisplayCursorPos = mix(vec2(PrevTargetX, PrevTargetY), vec2(TargetX, TargetY), Intra); + } + } +} + void CGameClient::UpdateRenderedCharacters() { for(int i = 0; i < MAX_CLIENTS; i++) diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 175c0d414..76e6d1910 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -313,6 +313,8 @@ public: const CNetObj_PlayerInfo *m_pLocalInfo; const CNetObj_SpectatorInfo *m_pSpectatorInfo; const CNetObj_SpectatorInfo *m_pPrevSpectatorInfo; + const CNetObj_SpecCursor *m_pSpecCursor; + const CNetObj_SpecCursor *m_pPrevSpecCursor; const CNetObj_Flag *m_apFlags[2]; const CNetObj_GameInfo *m_pGameInfoObj; const CNetObj_GameData *m_pGameDataObj; @@ -338,6 +340,9 @@ public: vec2 m_Position; } m_SpecInfo; + // cursor data + vec2 m_DisplayCursorPos; + // struct CCharacterInfo { @@ -794,6 +799,7 @@ private: int m_aShowOthers[NUM_DUMMIES]; void UpdatePrediction(); + void UpdateSpectatorCursor(); void UpdateRenderedCharacters(); int m_aLastUpdateTick[MAX_CLIENTS] = {0}; diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp index 1594cf173..040d778fc 100644 --- a/src/game/server/entities/character.cpp +++ b/src/game/server/entities/character.cpp @@ -1170,6 +1170,24 @@ void CCharacter::SnapCharacter(int SnappingClient, int Id) } } +void CCharacter::SnapSpecCursor(int SnappingClient) +{ + CNetObj_SpecCursor *pCursorInfo = static_cast(Server()->SnapNewItem(NETOBJTYPE_SPECCURSOR, SnappingClient, sizeof(CNetObj_SpecCursor))); + if(pCursorInfo) + { + pCursorInfo->m_Weapon = GetActiveWeapon(); + + pCursorInfo->m_TargetX = m_Input.m_TargetX; + pCursorInfo->m_TargetY = m_Input.m_TargetY; + + /* + ensure info density at SERVER_TICK_SPEED even if sv_high_bandwidth is 0 + */ + pCursorInfo->m_DeltaPrevTargetX = m_PrevInput.m_TargetX - m_Input.m_TargetX; + pCursorInfo->m_DeltaPrevTargetY = m_PrevInput.m_TargetY - m_Input.m_TargetY; + } +} + bool CCharacter::CanSnapCharacter(int SnappingClient) { if(SnappingClient == SERVER_DEMO_CLIENT) diff --git a/src/game/server/entities/character.h b/src/game/server/entities/character.h index 54651f410..11acb7462 100644 --- a/src/game/server/entities/character.h +++ b/src/game/server/entities/character.h @@ -100,6 +100,8 @@ public: void AddVelocity(vec2 Addition); void ApplyMoveRestrictions(); + void SnapSpecCursor(int SnappingClient); + private: // player controlling this character class CPlayer *m_pPlayer; diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 2b638cc8b..b2dc69b48 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -410,6 +410,29 @@ void CPlayer::Snap(int SnappingClient) } } + if(m_ClientId == SnappingClient && !Server()->IsSixup(SnappingClient)) + { + /* + Snap spectator cursors for local players by default. + The information is not visible to local player unless they recorded demo, which the spectator cursor will act as local player's cursor. + */ + CPlayer *pCursorOwner = this; + + if(m_Team == TEAM_SPECTATORS || IsPaused()) + { + pCursorOwner = m_SpectatorId != SPEC_FREEVIEW ? GameServer()->m_apPlayers[m_SpectatorId] : NULL; + } + + if(pCursorOwner) + { + CCharacter *pCursorOwnerChar = pCursorOwner->GetCharacter(); + if(pCursorOwnerChar && pCursorOwnerChar->IsAlive()) + { + pCursorOwnerChar->SnapSpecCursor(m_ClientId); + } + } + } + CNetObj_DDNetPlayer *pDDNetPlayer = Server()->SnapNewItem(id); if(!pDDNetPlayer) return;