Files
GTASource/game/frontend/HudMarkerManager.cpp
T
expvintl 419f2e4752 init
2025-02-23 17:40:52 +08:00

1320 lines
38 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// FILE : HudMarkerManager.cpp
// PURPOSE : Manages 2D world markers (instancing, layout, etc)
// AUTHOR : Aaron Baumbach (Ruffian)
// STARTED : 06/02/2020
//
/////////////////////////////////////////////////////////////////////////////////
#include "HudMarkerManager.h"
#include "HudMarkerRenderer.h"
#include "grcore/debugdraw.h"
#include "frontend/MiniMap.h"
#include "frontend/Map/BlipEnums.h"
#include "frontend/HudTools.h"
#include "frontend/ui_channel.h"
#include "Camera/Base/BaseCamera.h"
#include "Camera/CamInterface.h"
#include "Camera/Viewports/ViewportManager.h"
#include "scene/world/GameWorld.h"
#include "script/script_hud.h"
#include "system/controlMgr.h"
#include "Peds/ped.h"
static bool g_HudMarkerOcclusionTestEnabled = false;
static float g_HudMarkerFocusClearGracePeriod = 0.3f;
FRONTEND_OPTIMISATIONS()
CHudMarkerManager* CHudMarkerManager::sm_instance = nullptr;
void CHudMarkerManager::Init(unsigned Mode)
{
uiDisplayf("CHudMarkerManager::Init - Mode=[%d]", Mode);
if(Mode == INIT_CORE)
{
uiAssertf(sm_instance == nullptr, "HudMarkerManager instance exists during INIT_CORE");
sm_instance = rage_new CHudMarkerManager();
}
if (sm_instance)
{
sm_instance->Init_Internal(Mode);
}
}
void CHudMarkerManager::Shutdown(unsigned Mode)
{
uiDisplayf("CHudMarkerManager::Shutdown - Mode=[%d]", Mode);
if (sm_instance)
{
sm_instance->Shutdown_Internal(Mode);
}
if (Mode == INIT_CORE)
{
if (sm_instance)
{
delete sm_instance;
sm_instance = nullptr;
}
}
}
void CHudMarkerManager::DeviceLost()
{
}
void CHudMarkerManager::DeviceReset()
{
if (sm_instance)
{
sm_instance->DeviceReset_Internal();
}
}
#if __BANK
void CHudMarkerManager::InitWidgets()
{
bkBank *pBank = BANKMGR.FindBank(UI_DEBUG_BANK_NAME);
if (!pBank) // create the bank if not found
{
pBank = &BANKMGR.CreateBank(UI_DEBUG_BANK_NAME);
}
if (uiVerify(sm_instance) && pBank)
{
pBank->AddButton("Create HudMarker widgets", datCallback(MFA(CHudMarkerManager::CreateBankWidgets), (datBase*)sm_instance));
}
}
void CHudMarkerManager::ShutdownWidgets()
{
bkBank *pBank = BANKMGR.FindBank(UI_DEBUG_BANK_NAME);
if (pBank)
{
pBank->Destroy();
}
}
#endif //__BANK
bool CHudMarkerManager::IsInitialised() const
{
return m_markerStates.GetCount() > 0;
}
void CHudMarkerManager::SetIsEnabled(bool IsEnabled)
{
if (m_isEnabled != IsEnabled)
{
m_isEnabled = IsEnabled;
}
}
bool CHudMarkerManager::ShouldRender() const
{
return m_isEnabled && !m_hideThisFrame;
}
void CHudMarkerManager::UpdateMovieConfig()
{
if (m_renderer)
{
m_renderer->UpdateMovieConfig();
}
}
void CHudMarkerManager::Update()
{
PF_AUTO_PUSH_TIMEBAR("CHudMarkerManager::Update()");
if (CSystem::IsThisThreadId(SYS_THREAD_RENDER)) // only on UT
{
uiAssertf(0, "CHudMarkerManager::Update - can only be called on the UpdateThread!");
return;
}
//Early out if the system is disabled
if (!m_isEnabled)
{
return;
}
#if __BANK
m_numActive = m_activeIds.GetCount();
m_numDebug = m_debugMarkers.GetCount();
#endif
const grcViewport* GameViewport = gVpMan.GetCurrentGameGrcViewport();
if (GameViewport == nullptr)
{
uiAssertf(0, "CHudMarkerManager::Update - failed to resolve GrcViewport");
return;
}
CPed* FollowPlayer = CGameWorld::FindFollowPlayer();
if (FollowPlayer == nullptr)
{
uiAssertf(0, "CHudMarkerManager::Update - failed to resolve FollowPlayer");
return;
}
//Determine frame visibility
if (!CScriptHud::bDisplayHud ||
gVpMan.AreWidescreenBordersActive() ||
CScriptHud::bDontDisplayHudOrRadarThisFrame ||
!CPauseMenu::GetMenuPreference(PREF_DISPLAY_HUD) ||
!CVfxHelper::ShouldRenderInGameUI())
{
m_hideThisFrame = true;
}
//Reset transient variables
m_cachedAspectRatio = CHudTools::GetAspectRatio();
m_cachedAspectMultiplier = CHudTools::GetAspectRatioMultiplier();
m_cachedPlayerPedPosition = FollowPlayer->GetPreviousPosition();
m_cachedMinimapArea = CalculateMinimapArea();
m_markersWithDisplayText.Reset(false);
fwRect ClampBoundary = CalculateClampBoundary();
//Pre update group init
m_isAnyGroupSolo = false;
for (HudMarkerGroupID ID = HudMarkerGroupID::Min(); ID.IsValid(); ++ID)
{
m_markerGroups[ID].Reset();
m_isAnyGroupSolo |= m_markerGroups[ID].IsSolo;
}
//Early out if we have no markers
if (m_activeIds.IsEmpty())
{
return;
}
//Sort: world space distance, descending (as we reverse iterate)
std::stable_sort(m_activeIds.begin(), m_activeIds.end(), [this](HudMarkerID A, HudMarkerID B)
{
return (m_markerStates[A].PedDistance > m_markerStates[B].PedDistance) ||
(m_markerStates[B].IsFocused) ||
(m_markerStates[B].IsWaypoint && !m_markerStates[A].IsFocused);
});
uiAssert(m_occlusionTestFrameSkip >= 0);
int OcclusionTestFrameSpread = m_occlusionTestFrameSkip + 1;
m_occlusionTestFrame = ++m_occlusionTestFrame % OcclusionTestFrameSpread;
//Update state of active markers
for (int i = m_activeIds.GetCount() - 1; i >= 0; --i) //reverse iterate so we can safely remove
{
const auto& ID = m_activeIds[i];
auto& MarkerState = m_markerStates[ID];
auto GroupPtr = MarkerState.GroupID.IsValid() ? &m_markerGroups[MarkerState.GroupID] : nullptr;
const bool CanPerformOcclusionTest = (i % OcclusionTestFrameSpread) == m_occlusionTestFrame;
if (MarkerState.IsPendingRemoval)
{
if (MarkerState.HasRenderableShutdown) //Wait for renderer to cleanup
{
RemoveMarker_Internal(i);
}
}
else if (UpdateMarker(MarkerState, GroupPtr, ClampBoundary, FollowPlayer, *GameViewport, CanPerformOcclusionTest))
{
if (MarkerState.IsVisible && !MarkerState.IsClamped && MarkerState.DistanceTextAlpha > 0)
{
m_markersWithDisplayText.PushAndGrow(ID);
}
}
else
{
MarkerState.IsPendingRemoval = true;
}
}
UpdateWaypoint();
#if __BANK
if (ShouldRender())
{
RenderDebug(ClampBoundary);
}
#endif
}
void CHudMarkerManager::AddDrawListCommands()
{
//Add draw list commands
bool DidRenderThisFrame = false;
if (m_renderer && ShouldRender())
{
m_renderer->AddDrawListCommands(m_activeIds, m_markerStates);
DidRenderThisFrame = true;
}
//Handle change in system visibility
if (DidRenderThisFrame != m_didRenderLastFrame)
{
if (!DidRenderThisFrame) //If we've hidden all the markers this frame, reset their alphas so they fade back in...
{
for (int i = 0; i < m_activeIds.GetCount(); ++i)
{
const auto& ID = m_activeIds[i];
auto& MarkerState = m_markerStates[ID];
MarkerState.Alpha = 0;
MarkerState.Scale = 0;
}
}
}
m_didRenderLastFrame = DidRenderThisFrame;
m_hideThisFrame = false;
}
bool CHudMarkerManager::IsMarkerValid(HudMarkerID ID) const
{
return ID.IsValid() && (m_activeIds.Find(ID) != -1) && !m_markerStates[ID].IsPendingRemoval;
}
HudMarkerID CHudMarkerManager::CreateMarker(const SHudMarkerState& InitState)
{
HudMarkerID ID;
if (!uiVerify(IsInitialised()))
{
return ID;
}
#if __BANK
m_waterMark++;
m_highWaterMark = MAX(m_waterMark, m_highWaterMark);
#endif
if (!m_freeIds.IsEmpty())
{
ID = m_freeIds.Pop();
m_activeIds.Push(ID);
SHudMarkerState& State = m_markerStates[ID];
State = InitState;
State.ID = ID;
uiDebugf1("CHudMarkerManager::CreateMarker - ID=[%d] ActiveCount[%d]", ID.Get(), m_activeIds.GetCount());
}
else
{
#if __BANK
uiAssertf(m_highWaterMark == MAX_NUM_MARKERS, "CHudMarkerManager::CreateMarker - Max num reached - HighWaterMark=[%d]", m_highWaterMark);
uiWarningf("CHudMarkerManager::CreateMarker - FAILED - HighWaterMark=[%d] - consider increasing MAX_NUM_MARKERS", m_highWaterMark);
#endif
}
return ID;
}
HudMarkerID CHudMarkerManager::CreateMarkerForBlip(s32 BlipID, SHudMarkerState* outState)
{
if (CMiniMap::IsBlipIdInUse(BlipID))
{
if (auto IDPtr = m_blipMarkerMap.Access(BlipID))
{
return *IDPtr; //there is already a marker for this blip
}
SHudMarkerState NewState;
NewState.BlipID = BlipID;
if (outState)
{
*outState = NewState;
}
uiDebugf1("CHudMarkerManager::CreateMarkerForBlip - BlipID=[%d]", BlipID);
auto ID = CreateMarker(NewState);
if (ID.IsValid())
{
m_blipMarkerMap.Insert(BlipID, ID);
}
return ID;
}
uiWarningf("CHudMarkerManager::CreateMarkerForBlip - FAILED - BlipID=[%d] not in use", BlipID);
return HudMarkerID();
}
const SHudMarkerState* CHudMarkerManager::GetMarkerState(HudMarkerID ID) const
{
if (IsMarkerValid(ID))
{
return &m_markerStates[ID];
}
return nullptr;
}
SHudMarkerState* CHudMarkerManager::GetMarkerState(HudMarkerID ID)
{
if (IsMarkerValid(ID))
{
return &m_markerStates[ID];
}
return nullptr;
}
const SHudMarkerGroup* CHudMarkerManager::GetMarkerGroup(HudMarkerGroupID ID) const
{
if (ID.IsValid())
{
return &m_markerGroups[ID];
}
return nullptr;
}
SHudMarkerGroup* CHudMarkerManager::GetMarkerGroup(HudMarkerGroupID ID)
{
if (ID.IsValid())
{
return &m_markerGroups[ID];
}
return nullptr;
}
bool CHudMarkerManager::RemoveMarker(HudMarkerID& ID)
{
if (IsMarkerValid(ID))
{
m_markerStates[ID].IsPendingRemoval = true;
return true;
}
ID.Invalidate();
return false;
}
CHudMarkerManager::CHudMarkerManager()
: m_isEnabled(true)
, m_hideThisFrame(false)
, m_didRenderLastFrame(false)
, m_clampMode(ClampMode_Radial)
, m_clampInsetX(0.05f)
, m_clampInsetY(0.15f)
, m_clampCloseProximityMinDistance(50.0f)
, m_clampCloseProximityMaxDistance(200.0f)
, m_clampCloseProximityInset(0.0f)
, m_markerScreenHalfSize(0.04f)
, m_fadeOverDistanceAlpha(200)
, m_fadeOverDistanceMin(100.0f)
, m_fadeOverDistanceMax(500.0f)
, m_scaleMin(0.75f)
, m_scaleMax(1.25f)
, m_scaleDistanceMin(100.0f)
, m_scaleDistanceMax(500.0f)
, m_occludedAlpha(200)
, m_hideDistanceTextWhenClamped(true)
, m_occlusionTestFrame(0)
, m_occlusionTestFrameSkip(15)
, m_minimapArea(0.0f, 1.0f, 0.15f, 0.8f)
, m_minimapAreaBig(0.0f, 1.0f, 0.23f, 0.58f)
, m_isAnyGroupSolo(false)
, m_focusRadius(0.1f)
, m_lastFocusTime(0)
, m_pendingFocusInput(false)
, m_focusInputPressedTime(0)
#if __BANK
, m_waterMark(0)
, m_highWaterMark(0)
, m_renderDebugMarkers(false)
, m_renderDebugText(false)
, m_renderClampBoundary(false)
, m_renderMinimapArea(false)
, m_renderFocusRadius(false)
, m_pushPopNum(1)
, m_pushGroup(HudMarkerGroupID::Min())
, m_pushRange(200.0f)
, m_numActive(0)
, m_numDebug(0)
#endif
{
m_renderer = rage_new CHudMarkerRenderer();
FatalAssert(m_renderer);
uiDisplayf("CHudMarkerManager - alloc[%d]", (int)sizeof(*this));
}
CHudMarkerManager::~CHudMarkerManager()
{
delete m_renderer;
m_renderer = nullptr;
}
void CHudMarkerManager::Init_Internal(unsigned Mode)
{
if (Mode == INIT_SESSION)
{
m_markerStates.Reset();
for (int i = 0; i < m_markerStates.GetMaxCount(); i++)
{
m_markerStates.Push(SHudMarkerState());
}
m_activeIds.Reset();
m_freeIds.Reset();
for (HudMarkerID ID = HudMarkerID::Max(); ID.IsValid(); --ID)
{
m_freeIds.Push(ID);
}
m_markerGroups.Reset();
for (int i = 0; i < m_markerGroups.GetMaxCount(); i++)
{
m_markerGroups.Push(SHudMarkerGroup());
}
m_focusedMarker.Invalidate();
m_lastFocusTime = 0;
m_pendingFocusInput = false;
m_focusInputPressedTime = 0;
m_pendingFocusSelection.Invalidate();
m_activeWaypointMarker.Invalidate();
}
if (m_renderer)
{
m_renderer->Init(Mode);
}
}
void CHudMarkerManager::Shutdown_Internal(unsigned Mode)
{
if (m_renderer)
{
m_renderer->Shutdown(Mode);
}
if (Mode == SHUTDOWN_CORE)
{
#if __BANK
ShutdownWidgets();
#endif
}
else if (Mode == SHUTDOWN_SESSION)
{
m_markerStates.Reset();
}
}
void CHudMarkerManager::DeviceReset_Internal()
{
if (m_renderer)
{
m_renderer->DeviceReset();
}
}
void CHudMarkerManager::RemoveMarker_Internal(int ActiveIndex)
{
#if __BANK
m_waterMark--;
#endif
if (uiVerify(ActiveIndex >= 0 && ActiveIndex < m_activeIds.GetMaxCount()))
{
const auto& ID = m_activeIds[ActiveIndex];
if (m_markerStates[ID].BlipID != INVALID_BLIP_ID)
{
m_blipMarkerMap.Delete(m_markerStates[ID].BlipID);
}
m_markerStates[ID].~SHudMarkerState(); //call destructor directly as we're not actually freeing up the element
m_freeIds.Push(m_activeIds[ActiveIndex]);
m_activeIds.DeleteFast(ActiveIndex);
uiDebugf1("CHudMarkerManager::RemoveMarker_Internal - ID=[%d]", ID.Get());
}
}
//Updates the marker state
//Return: false if the marker should be removed
bool CHudMarkerManager::UpdateMarker(SHudMarkerState& State, SHudMarkerGroup* Group, const fwRect& ClampBoundary, CPed* FollowPed, const grcViewport& Viewport, bool CanPerformOcclusionTest) const
{
if (State.BlipID != INVALID_BLIP_ID && !UpdateMarkerFromBlip(State.BlipID, State))
{
State.IsVisible = false;
return false;
}
State.PedDistance = (State.WorldPosition - FollowPed->GetPreviousPosition()).XYMag();
State.CamDistanceSq = (State.WorldPosition - VEC3V_TO_VECTOR3(Viewport.GetCameraPosition())).XYMag2();
bool ShouldClamp = State.ClampEnabled;
float MinDistance = 0.0f;
float MaxDistance = State.PedCullDistance;
bool SkipMaxDistanceCheck = false;
int TargetAlpha = 255;
int TargetDistanceTextAlpha = 255;
if (m_hideDistanceTextWhenClamped && State.IsClamped && !State.IsWaypoint)
{
TargetDistanceTextAlpha = 0;
}
//Resolve group behaviours
if (Group)
{
SkipMaxDistanceCheck |= Group->NumVisible < Group->ForceVisibleNum;
MinDistance = Group->MinDistance;
MaxDistance = Group->MaxDistance;
if (Group->NumVisible < Group->MaxVisible && !Group->IsMuted && (!m_isAnyGroupSolo || Group->IsSolo))
{
Group->NumVisible++;
if (ShouldClamp && Group->NumClamped < Group->MaxClampNum)
{
if (Group->NumClamped == 0 && Group->AlwaysShowDistanceTextForClosest)
{
TargetDistanceTextAlpha = 255;
}
Group->NumClamped++;
}
else
{
ShouldClamp = false;
}
if (State.PedDistance < Group->MinTextDistance)
{
TargetDistanceTextAlpha = 0;
}
}
else
{
ShouldClamp = false;
TargetAlpha = 0;
}
}
//Force waypoint behaviours
if (State.IsWaypoint)
{
SkipMaxDistanceCheck = true;
ShouldClamp = true;
}
ProjectMarker(State, ShouldClamp, ClampBoundary, Viewport);
//Hide for one frame to make sure we're fully initialised
if (!State.IsInitialised)
{
State.IsVisible = false;
State.IsInitialised = true;
}
//Perform alpha fading
if (State.IsVisible)
{
if (State.IsFocused || State.IsWaypoint)
{
TargetAlpha = 255;
}
else if (!SkipMaxDistanceCheck && State.PedDistance > MaxDistance)
{
TargetAlpha = 0;
}
else
{
const float alphaOverDistance = 1.0f - ClampRange(State.PedDistance, m_fadeOverDistanceMin, m_fadeOverDistanceMax);
TargetAlpha = (int)MIN(TargetAlpha, m_fadeOverDistanceAlpha + ((255 - m_fadeOverDistanceAlpha) * alphaOverDistance));
//Disable occlusion while clamped
if (State.IsClamped)
{
CanPerformOcclusionTest = false;
State.IsOccluded = false;
}
//Occlusion test
if (g_HudMarkerOcclusionTestEnabled && CanPerformOcclusionTest)
{
//TODO: PERF: consider making this async?
Vector3 vTraceStart = State.WorldPosition;
Vector3 vTraceEnd = VEC3V_TO_VECTOR3(Viewport.GetCameraPosition());
static const s32 iCollisionFlags = ArchetypeFlags::GTA_ALL_MAP_TYPES;
static const s32 iIgnoreFlags = WorldProbe::LOS_IGNORE_SHOOT_THRU | WorldProbe::LOS_IGNORE_SHOOT_THRU_FX | WorldProbe::LOS_NO_RESULT_SORTING;
WorldProbe::CShapeTestProbeDesc probeDesc;
WorldProbe::CShapeTestFixedResults<> probeResult;
probeDesc.SetResultsStructure(&probeResult);
probeDesc.SetStartAndEnd(vTraceStart, vTraceEnd);
probeDesc.SetIncludeFlags(iCollisionFlags);
probeDesc.SetExcludeEntity(FollowPed);
probeDesc.SetOptions(iIgnoreFlags);
probeDesc.SetContext(WorldProbe::EHud);
taskAssertf(FPIsFinite(vTraceStart.x) && FPIsFinite(vTraceStart.y) && FPIsFinite(vTraceStart.z), "CHudMarkerManager::UpdateMarker: Occlusion");
State.IsOccluded = WorldProbe::GetShapeTestManager()->SubmitTest(probeDesc);
}
if (State.IsOccluded)
{
TargetAlpha = MIN(TargetAlpha, m_occludedAlpha);
}
else if (State.PedDistance < MinDistance)
{
TargetAlpha = 0;
}
}
//Hide overlaping distance text - assumes distance accending order
float OverlapDist = m_markerScreenHalfSize * 2.0f * State.Scale;
for (int i = 0; i < m_markersWithDisplayText.GetCount(); ++i)
{
if (const auto OtherMarkerState = GetMarkerState(m_markersWithDisplayText[i]))
{
const Vector2 AdjustedSP(State.ScreenPosition.x, State.ScreenPosition.y);
const Vector2 OtherAdjustedSP(OtherMarkerState->ScreenPosition.x, OtherMarkerState->ScreenPosition.y);
const Vector2 Delta = AdjustedSP - OtherAdjustedSP;
if (abs(Delta.x) < (OverlapDist / m_cachedAspectRatio) && abs(Delta.y) < OverlapDist)
{
TargetDistanceTextAlpha = 0;
break;
}
}
}
//Smooth Alpha
if (State.Alpha != TargetAlpha)
{
int Step = (int)MAX(1.0f, 255.0f * 4.0f * fwTimer::GetTimeStep());
State.Alpha = (u8)(State.Alpha + Clamp(TargetAlpha - State.Alpha, -Step, Step));
}
//Smooth Distance Text Alpha
TargetDistanceTextAlpha = MIN(TargetDistanceTextAlpha, TargetAlpha);
if (State.DistanceTextAlpha != TargetDistanceTextAlpha)
{
int Step = (int)MAX(1.0f, 255.0f * 4.0f * fwTimer::GetTimeStep());
State.DistanceTextAlpha = (u8)(State.DistanceTextAlpha + Clamp(TargetDistanceTextAlpha - State.DistanceTextAlpha, -Step, Step));
}
//Update visibility given alpha (perf)
State.IsVisible = State.Alpha != 0;
}
else
{
State.Alpha = 0;
State.DistanceTextAlpha = 0;
}
//Update Scale
if (State.IsVisible)
{
float TargetScale = 1.0f;
if (State.IsWaypoint)
{
TargetScale = m_scaleMax;
}
else if (State.IsFocused)
{
TargetScale = m_scaleMax + 0.1f;
}
else
{
const float tDist = ClampRange(State.PedDistance, m_scaleDistanceMin, m_scaleDistanceMax);
TargetScale = Lerp(1.0f - tDist, m_scaleMin, m_scaleMax);
}
const float Step = fwTimer::GetTimeStep() / 0.3f;
State.Scale = State.Scale + Clamp(TargetScale - State.Scale, -Step, Step);
}
return true;
}
bool CHudMarkerManager::UpdateMarkerFromBlip(s32 BlipID, SHudMarkerState& State) const
{
if (CMiniMap::IsBlipIdInUse(BlipID))
{
auto Blip = CMiniMap::GetBlip(BlipID);
State.BlipID = BlipID;
State.WorldPosition = CMiniMap::GetBlipPositionValue(Blip);
eBLIP_COLOURS BlipColor = CMiniMap::GetBlipColourValue(Blip);
if (BlipColor != BLIP_COLOUR_USE_COLOUR32)
{
State.Color = CMiniMap_Common::GetColourFromBlipSettings(BlipColor, CMiniMap::IsFlagSet(Blip, BLIP_FLAG_BRIGHTNESS));
}
else
{
State.Color = CMiniMap::GetBlipColour32Value(Blip);
}
State.Alpha = MIN(State.Alpha, (u8)CMiniMap::GetBlipAlphaValue(Blip));
State.IconIndex = CMiniMap::GetBlipObjectNameId(Blip);
return true;
}
return false;
}
void CHudMarkerManager::UpdateWaypoint()
{
const bool bIsWaypointSelectionAllowed =
CPlayerInfo::IsHardAiming() && //Player must be aiming
!CScriptHud::bUsingMissionCreator &&
!CMiniMap::IsInGolfMap() &&
!CMiniMap::GetInPrologue() &&
!CMiniMap::GetBlockWaypoint() &&
NetworkInterface::CanSetWaypoint(CGameWorld::FindLocalPlayer());
float MinFocusDistance = m_focusRadius;
HudMarkerID BestFocusMarker;
sWaypointStruct ActiveWaypoint;
CMiniMap::GetActiveWaypoint(ActiveWaypoint);
HudMarkerID AssociatedWaypointMarker;
const u32 TimeMS = sysTimer::GetSystemMsTime();
const rage::InputType SelectInput = INPUT_HUDMARKER_SELECT;
const ioValue& InputValue = CControlMgr::GetMainPlayerControl().GetValue(SelectInput);
//Loop active markers
for (int i = m_activeIds.GetCount() - 1; i >= 0; --i)
{
const auto& ID = m_activeIds[i];
auto& MarkerState = m_markerStates[ID];
if (MarkerState.IsPendingRemoval)
continue;
//Determine associated waypoint status (there can only be one)
MarkerState.IsWaypoint = false;
if (ActiveWaypoint.iBlipIndex != INVALID_BLIP_ID && !AssociatedWaypointMarker.IsValid())
{
if (ActiveWaypoint.iAssociatedBlipId == MarkerState.BlipID || //Is blip associated?
(ActiveWaypoint.vPos - Vector2(MarkerState.WorldPosition.x, MarkerState.WorldPosition.y)).Mag2() < 0.1f) //Is blip very close?
{
AssociatedWaypointMarker = ID;
MarkerState.IsWaypoint = true;
}
}
//Determine best focus marker
if (bIsWaypointSelectionAllowed && !MarkerState.IsWaypoint && MarkerState.IsVisible && !MarkerState.IsClamped)
{
static const Vector2 CenterOffset(0.5f, 0.5f);
Vector2 CenterAlignedPos = MarkerState.ScreenPosition - CenterOffset;
CenterAlignedPos.x *= m_cachedAspectRatio;
const float DistanceFromCenter = CenterAlignedPos.Mag();
if (DistanceFromCenter < MinFocusDistance)
{
MinFocusDistance = DistanceFromCenter;
BestFocusMarker = ID;
}
}
}
//Update active waypoint marker
m_activeWaypointMarker.Invalidate();
if (AssociatedWaypointMarker.IsValid())
{
m_activeWaypointMarker = AssociatedWaypointMarker;
}
else if (ActiveWaypoint.iBlipIndex != INVALID_BLIP_ID)
{
//No associated marker was found so create a transient one
if (!m_transientWaypointMarker.IsValid())
{
SHudMarkerState NewMarkerState;
NewMarkerState.IsWaypoint = true;
NewMarkerState.ClampEnabled = true;
NewMarkerState.PedCullDistance = FLT_MAX;
NewMarkerState.WorldHeightOffset = 1.0f; //Raise it off the ground
m_transientWaypointMarker = CreateMarker(NewMarkerState);
}
if (auto pWaypointMarkerState = GetMarkerState(m_transientWaypointMarker))
{
if (pWaypointMarkerState->BlipID != ActiveWaypoint.iBlipIndex)
{
UpdateMarkerFromBlip(ActiveWaypoint.iBlipIndex, *pWaypointMarkerState);
pWaypointMarkerState->IconIndex = -1;
}
}
m_activeWaypointMarker = m_transientWaypointMarker;
}
//Cleanup transient marker if it's no longer needed
if (m_transientWaypointMarker != m_activeWaypointMarker)
{
RemoveMarker(m_transientWaypointMarker);
}
//Update Selection Focus
if (m_focusedMarker != BestFocusMarker)
{
if (!bIsWaypointSelectionAllowed || BestFocusMarker.IsValid() || TimeMS > m_lastFocusTime + (int)(g_HudMarkerFocusClearGracePeriod * 1000.0f))
{
if (auto MarkerState = GetMarkerState(m_focusedMarker))
{
MarkerState->IsFocused = false;
}
if (auto MarkerState = GetMarkerState(BestFocusMarker))
{
MarkerState->IsFocused = true;
}
m_focusedMarker = BestFocusMarker;
m_lastFocusTime = TimeMS;
}
}
else if (m_focusedMarker.IsValid())
{
m_lastFocusTime = TimeMS;
}
//When button is pressed, start pending focus selection
if (InputValue.IsPressed() && bIsWaypointSelectionAllowed)
{
m_pendingFocusSelection.Invalidate();
m_pendingFocusInput = true;
m_focusInputPressedTime = sysTimer::GetSystemMsTime();
}
//Update pending focus selection if valid
if (m_pendingFocusInput && m_focusedMarker.IsValid())
{
m_pendingFocusSelection = m_focusedMarker;
}
//When the button is released and input is in a valid state (not exclusive to another control)
if (m_pendingFocusInput && InputValue.IsEnabled() && InputValue.IsUp())
{
//And input was released within 150ms (so we don't trigger during "look-behind")
if (sysTimer::GetSystemMsTime() < m_focusInputPressedTime + 150)
{
//Set waypoint to focus (if any)
if (IsMarkerValid(m_pendingFocusSelection))
{
const auto& MarkerState = m_markerStates[m_pendingFocusSelection];
ObjectId objectId = 0;
if (CMiniMap::IsBlipIdInUse(MarkerState.BlipID))
{
if (auto pBlip = CMiniMap::GetBlip(MarkerState.BlipID))
{
if (netObject* pNetObj = NetworkUtils::GetNetworkObjectFromEntity(CMiniMap::FindBlipEntity(pBlip)))
{
objectId = pNetObj->GetObjectID();
}
}
}
CMiniMap::SwitchOnWaypoint(MarkerState.WorldPosition.x, MarkerState.WorldPosition.y, objectId);
}
//Or clear waypoint
else
{
CMiniMap::SwitchOffWaypoint();
}
}
m_pendingFocusInput = false;
m_pendingFocusSelection.Invalidate();
}
}
fwRect CHudMarkerManager::CalculateClampBoundary() const
{
const bool ConsiderSuperWidescreen = true;
float x0, x1, y0, y1;
CHudTools::GetMinSafeZone(x0, y0, x1, y1, m_cachedAspectRatio, ConsiderSuperWidescreen);
const float AspectMultiplier = MIN(1.0f, m_cachedAspectMultiplier);
const float Width = (x1 - x0) / AspectMultiplier;
const float Height = y1 - y0;
fwRect ClampBoundary(x0, y1, x1, y0);
ClampBoundary.left += m_clampInsetX * Width;
ClampBoundary.right -= m_clampInsetX * Width;
ClampBoundary.top += m_clampInsetY * Height;
ClampBoundary.bottom -= m_clampInsetY * Height;
return ClampBoundary;
}
fwRect CHudMarkerManager::CalculateMinimapArea() const
{
const bool ConsiderSuperWidescreen = true;
float x0, x1, y0, y1;
CHudTools::GetMinSafeZone(x0, y0, x1, y1, m_cachedAspectRatio, ConsiderSuperWidescreen);
fwRect Area = CMiniMap::IsInBigMap() ? m_minimapAreaBig : m_minimapArea;
//Add safe zone offset (assuming bottom left aligned)
Area.left += x0;
Area.right += x0;
Area.top -= 1.0f - y1;
Area.bottom -= 1.0f - y1;
return Area;
}
//Projects State.WorldPosition to HUD(0-1), clamping to boundary if required
//Results written into State
void CHudMarkerManager::ProjectMarker(SHudMarkerState& State, bool ShouldClamp, const fwRect& ClampBoundary, const grcViewport& Viewport) const
{
State.IsVisible = false;
State.IsClamped = false;
//Project
const Vec4V WorldPosition(State.WorldPosition.x, State.WorldPosition.y, State.WorldPosition.z + State.WorldHeightOffset, 1.0f);
const Vec4V ProjectedPosition = Multiply(Viewport.GetFullCompositeMtx(), WorldPosition);
if (IsZeroAll(ProjectedPosition.GetW()))
{
return;
}
const bool IsBehindCamera = ProjectedPosition.GetWf() < 0;
//Perspective divide
State.ScreenPosition.Set(ProjectedPosition.GetXf() / ProjectedPosition.GetWf(), ProjectedPosition.GetYf() / ProjectedPosition.GetWf());
//Convert to 0-1 space (Note: origin is top left)
State.ScreenPosition.x /= Viewport.GetWidth();
State.ScreenPosition.y /= Viewport.GetHeight();
//Coords are inverted in X behind the camera, so flip them back
if (IsBehindCamera)
{
State.ScreenPosition.x = 1.0f - State.ScreenPosition.x;
State.ScreenPosition.y = 1.0f - State.ScreenPosition.y;
}
Vector2 ClampCenter; ClampBoundary.GetCentre(ClampCenter.x, ClampCenter.y);
//Do clamp if requested
if (ShouldClamp)
{
State.IsVisible = true;
Vector2 ClampedPosition = State.ScreenPosition;
if (m_clampMode == ClampMode_None)
{
//do nothing
}
else if (m_clampMode == ClampMode_Radial)
{
Vector2 HalfSize(ClampBoundary.GetWidth() * 0.5f, abs(ClampBoundary.GetHeight()) * 0.5f);
//Apply proximity inset
float ProximityInset = 1.0f - ClampRange(State.PedDistance, m_clampCloseProximityMinDistance, m_clampCloseProximityMaxDistance);
if (ProximityInset > 0.0f)
{
ProximityInset *= m_clampCloseProximityInset;
HalfSize.x -= ProximityInset / m_cachedAspectRatio;
HalfSize.y -= ProximityInset;
}
//Frame position for unit circle about "ClampCenter"
ClampedPosition.x = (ClampedPosition.x - ClampCenter.x) / HalfSize.x;
ClampedPosition.y = (ClampedPosition.y - ClampCenter.y) / HalfSize.y;
//Clamp to unit circle (forcefully if behind the camera)
const float Mag2 = ClampedPosition.Mag2();
if (Mag2 > 1.0f || IsBehindCamera)
{
ClampedPosition.InvScale(Sqrtf(Mag2));
State.IsClamped = true;
}
//Transform back
ClampedPosition = ClampCenter + (ClampedPosition * HalfSize);
}
else if (m_clampMode == ClampMode_Rectangular)
{
//Force-clamp markers behind the camera
if (IsBehindCamera)
{
ClampedPosition -= ClampCenter;
ClampedPosition.NormalizeSafe();
ClampedPosition += ClampCenter;
}
//Find intersection with boundary
atFixedArray<Vector2, 2> Intersections;
if (CHudTools::LineRectIntersection(ClampCenter, ClampedPosition, ClampBoundary, Intersections))
{
ClampedPosition = Intersections[0];
State.IsClamped = true;
}
}
//Update rotation (before position)
Vector2 Direction = ClampedPosition - ClampCenter;
State.ScreenRotation = atan2(Direction.x, -Direction.y);
//Update position
State.ScreenPosition = ClampedPosition;
}
else //Unclamped
{
//Not visible if behind the camera
State.IsVisible = !IsBehindCamera;
}
if (State.IsVisible)
{
//Not visible if off screen
const float HalfSize = m_markerScreenHalfSize * State.Scale;
const float MinScreenLocX = -HalfSize / m_cachedAspectRatio;
const float MaxScreenLocX = 1.0f + (HalfSize / m_cachedAspectRatio);
const float MinScreenLocY = -HalfSize;
const float MaxScreenLocY = 1.0f + (HalfSize);
State.IsVisible = State.ScreenPosition.x >= MinScreenLocX && State.ScreenPosition.x <= MaxScreenLocX &&
State.ScreenPosition.y >= MinScreenLocY && State.ScreenPosition.y <= MaxScreenLocY;
//Minimap overlap cull
if (State.ScreenPosition.x < m_cachedMinimapArea.right && State.ScreenPosition.y > m_cachedMinimapArea.top)
{
State.IsVisible = false;
}
}
}
//START_DEBUG===========================================
#if __BANK
void CHudMarkerManager::CreateBankWidgets()
{
static bool bBankCreated = false;
bkBank *pBank = BANKMGR.FindBank(UI_DEBUG_BANK_NAME);
if (!uiVerify(pBank) || bBankCreated)
{
return;
}
bBankCreated = true;
pBank->PushGroup("HUD Markers");
{
pBank->AddText("Num Active", &m_numActive, true);
pBank->PushGroup("Clamping");
{
static const char* ClampModeOptions[ClampMode_NUM] = { "None", "Radial", "Rectangular" };
pBank->AddCombo("Clamp Mode", &m_clampMode, ClampMode_NUM, ClampModeOptions);
pBank->AddToggle("Draw Clamp Boundary", &m_renderClampBoundary);
pBank->AddSlider("Clamp Inset X", &m_clampInsetX, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Clamp Inset Y", &m_clampInsetY, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Clamp Close Proximity Min Distance", &m_clampCloseProximityMinDistance, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Clamp Close Proximity Max Distance", &m_clampCloseProximityMaxDistance, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Clamp Close Proximity Inset", &m_clampCloseProximityInset, 0.0f, 1.0f, 0.01f);
pBank->AddSeparator();
pBank->AddToggle("Hide Distance Text When Clamped", &m_hideDistanceTextWhenClamped);
}
pBank->PopGroup();
pBank->PushGroup("Alpha/Scale");
{
pBank->AddSlider("Marker Screen Half Size", &m_markerScreenHalfSize, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Scale Min", &m_scaleMin, 0.0f, 2.0f, 0.01f);
pBank->AddSlider("Scale Max", &m_scaleMax, 0.0f, 2.0f, 0.01f);
pBank->AddSlider("Scale Distance Min", &m_scaleDistanceMin, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Scale Distance Max", &m_scaleDistanceMax, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Fade Over Distance Alpha", &m_fadeOverDistanceAlpha, 0, 255, 0);
pBank->AddSlider("Fade Over Distance Min", &m_fadeOverDistanceMin, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Fade Over Distance Max", &m_fadeOverDistanceMax, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Occluded Alpha", &m_occludedAlpha, 0, 255, 0);
pBank->AddSlider("Occlusion Frame Skip", &m_occlusionTestFrameSkip, 0, 30, 1);
}
pBank->PopGroup();
pBank->PushGroup("Minimap Overlap Culling");
{
pBank->AddToggle("Render Debug", &m_renderMinimapArea);
pBank->AddSeparator();
pBank->AddTitle("Minimap Area");
pBank->AddSlider("Left", &m_minimapArea.left, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Right", &m_minimapArea.right, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Top", &m_minimapArea.top, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Bottom", &m_minimapArea.bottom, 0.0f, 1.0f, 0.01f);
pBank->AddSeparator();
pBank->AddTitle("Minimap Area (Big)");
pBank->AddSlider("Left", &m_minimapAreaBig.left, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Right", &m_minimapAreaBig.right, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Top", &m_minimapAreaBig.top, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Bottom", &m_minimapAreaBig.bottom, 0.0f, 1.0f, 0.01f);
}
pBank->PopGroup();
pBank->PushGroup("Groups", false);
{
for (HudMarkerGroupID ID = HudMarkerGroupID::Min(); ID.IsValid(); ++ID)
{
char GroupName[64];
formatf(GroupName, "Group_%d", ID.Get(), NELEM(GroupName));
pBank->PushGroup(GroupName, false);
{
auto& Group = m_markerGroups[ID];
pBank->AddText("Num Visible", &Group.NumVisible, true);
pBank->AddText("Num Clamped", &Group.NumClamped, true);
pBank->AddSeparator();
pBank->AddSlider("Max Clamped", &Group.MaxClampNum, 0, HudMarkerID::Size(), 1);
pBank->AddSlider("Force Visible", &Group.ForceVisibleNum, 0, HudMarkerID::Size(), 1);
pBank->AddSlider("Max Visible", &Group.MaxVisible, 0, HudMarkerID::Size(), 1);
pBank->AddSlider("Min Distance", &Group.MinDistance, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Max Distance", &Group.MaxDistance, 0.0f, 10000.0f, 1.0f);
pBank->AddSlider("Min Text Distance", &Group.MinTextDistance, 0.0f, 10000.0f, 1.0f);
//pBank->AddToggle("Solo", &Group.IsSolo);
//pBank->AddToggle("Muted", &Group.IsMuted);
}
pBank->PopGroup();
}
}
pBank->PopGroup();
pBank->PushGroup("Picking");
{
pBank->AddToggle("Render Debug", &m_renderFocusRadius);
pBank->AddSlider("Focus Radius", &m_focusRadius, 0.0f, 1.0f, 0.01f);
pBank->AddSlider("Focus Clear Grace Period", &g_HudMarkerFocusClearGracePeriod, 0.0f, 1.0f, 0.01f);
}
pBank->PopGroup();
if (m_renderer)
{
m_renderer->CreateBankWidgets(pBank);
}
pBank->PushGroup("Test");
{
pBank->AddToggle("Draw Debug Markers", &m_renderDebugMarkers);
pBank->AddToggle("Draw Debug Text", &m_renderDebugText);
pBank->AddText("Num Debug", &m_numDebug, true);
pBank->AddButton("Push Marker", datCallback(MFA(CHudMarkerManager::PushDebugMarker), (datBase*)this));
pBank->AddButton("Pop Marker", datCallback(MFA(CHudMarkerManager::PopDebugMarker), (datBase*)this));
pBank->AddSlider("Push/Pop Num", &m_pushPopNum, 1, HudMarkerID::Size(), 1);
pBank->AddSlider("Push Group", &m_pushGroup, HudMarkerGroupID::Min(), HudMarkerGroupID::Max(), 1);
pBank->AddSlider("Push Range", &m_pushRange, 1.0f, 500.0f, 1);
}
pBank->PopGroup();
}
pBank->PopGroup();
}
void CHudMarkerManager::PushDebugMarker()
{
CPed* FollowPlayer = CGameWorld::FindFollowPlayer();
if (FollowPlayer == nullptr)
{
uiAssertf(0, "CHudMarkerManager::CreateRandomMarker - failed to resolve FollowPlayer");
return;
}
for (int i = 0; i < m_pushPopNum; i++)
{
if (!m_debugMarkers.IsFull())
{
SHudMarkerState NewState;
const Vector3 RandOffset(fwRandom::GetRandomNumberInRange(-m_pushRange, m_pushRange), fwRandom::GetRandomNumberInRange(-m_pushRange, m_pushRange), fwRandom::GetRandomNumberInRange(0.0f, 50.0f));
NewState.WorldPosition = FollowPlayer->GetPreviousPosition() + RandOffset;
NewState.Color = Color32(fwRandom::GetRandomNumberInRange(0, 255), fwRandom::GetRandomNumberInRange(0, 255), fwRandom::GetRandomNumberInRange(0, 255), 255);
NewState.IconIndex = fwRandom::GetRandomNumberInRange(0, eBlipSprite_MAX_VALUE);
NewState.GroupID = HudMarkerGroupID::IsValid(m_pushGroup) ? m_pushGroup : HudMarkerGroupID::Rand();
NewState.ClampEnabled = (rand() % 2) != 0;
auto ID = CreateMarker(NewState);
if (ID.IsValid())
{
m_debugMarkers.Push(ID);
}
else
{
break;
}
}
else
{
break;
}
}
}
void CHudMarkerManager::PopDebugMarker()
{
for (int i = 0; i < m_pushPopNum; i++)
{
if (!m_debugMarkers.IsEmpty())
{
auto ID = m_debugMarkers.Pop();
RemoveMarker(ID);
}
else
{
break;
}
}
}
void CHudMarkerManager::RenderDebug(const fwRect& BoundaryRect)
{
PF_AUTO_PUSH_TIMEBAR("CHudMarkerRenderer::RenderDebugMarkersUT()");
if (!CSystem::IsThisThreadId(SYS_THREAD_UPDATE)) // only on UT
{
sfAssertf(0, "CHudMarkerRenderer::RenderDebugUT can only be called on the UpdateThread!");
return;
}
//Sort: Camera distance, descending
std::stable_sort(m_activeIds.begin(), m_activeIds.end(), [this](HudMarkerID A, HudMarkerID B)
{
return m_markerStates[A].CamDistanceSq > m_markerStates[B].CamDistanceSq;
});
const Vector2 MarkerHalfSize(m_markerScreenHalfSize / m_cachedAspectRatio, m_markerScreenHalfSize);
const Vector2 DebugHalfSize(0.01f / m_cachedAspectRatio, 0.01f);
const Vector2 DebugOutlineHalfSize = DebugHalfSize * 1.3f;
for (int i = 0; i < m_activeIds.GetCount(); ++i)
{
const auto& ID = m_activeIds[i];
const auto& State = m_markerStates[ID];
if (State.IsVisible)
{
if (m_renderDebugMarkers)
{
grcDebugDraw::RectAxisAligned(State.ScreenPosition - MarkerHalfSize * State.Scale, State.ScreenPosition + MarkerHalfSize * State.Scale, Color32(255, 255, 255, 64));
const Color32 OutlineColor(State.IsClamped ? Color32(255, 0, 0) : (State.IsFocused ? Color32(255, 255, 255) : Color32(0, 0, 0)));
grcDebugDraw::RectAxisAligned(State.ScreenPosition - DebugOutlineHalfSize * State.Scale, State.ScreenPosition + DebugOutlineHalfSize * State.Scale, OutlineColor);
grcDebugDraw::RectAxisAligned(State.ScreenPosition - DebugHalfSize * State.Scale, State.ScreenPosition + DebugHalfSize * State.Scale, State.Color);
}
if (m_renderDebugText)
{
char DebugText[128];
formatf(DebugText, "ID=%d\ndepth=%d\ngroup=%d\nclamp=%s\nped=%.1f\ncam=%.1f", (int)State.ID, i, (int)State.GroupID, State.ClampEnabled ? "true" : "false", State.PedDistance, sqrt(State.CamDistanceSq), NELEM(DebugText)); // unique instance name based on the unique id
grcDebugDraw::TextFontPush(grcSetup::GetFixedWidthFont());
grcDebugDraw::Text(State.ScreenPosition, DD_ePCS_NormalisedZeroToOne, Color32(225, 225, 225), DebugText);
grcDebugDraw::TextFontPop();
}
}
}
if (m_renderClampBoundary)
{
Vector2 Min(BoundaryRect.left, BoundaryRect.top);
Vector2 Max(BoundaryRect.right, BoundaryRect.bottom);
grcDebugDraw::RectAxisAligned(Min, Max, Color32(255, 255, 255, 64));
}
if (m_renderMinimapArea)
{
Vector2 Min(m_cachedMinimapArea.left, m_cachedMinimapArea.top);
Vector2 Max(m_cachedMinimapArea.right, m_cachedMinimapArea.bottom);
grcDebugDraw::RectAxisAligned(Min, Max, Color32(255, 0, 255, 64));
}
if (m_renderFocusRadius)
{
Vector2 Min(m_cachedMinimapArea.left, m_cachedMinimapArea.top);
Vector2 Max(m_cachedMinimapArea.right, m_cachedMinimapArea.bottom);
grcDebugDraw::Circle(Vector2(0.5f, 0.5f), m_focusRadius, Color32(255, 0, 255, 64));
}
}
#endif //__BANK
//END_DEBUG===========================================