2121#include < iomanip>
2222#include < chrono>
2323#include < cmath>
24+ #include < limits>
25+ #include < glm/gtx/norm.hpp>
2426
2527#include < Games/Skyrim/Interface/UI.h>
2628#include < Games/Skyrim/Camera/PlayerCamera.h>
@@ -97,7 +99,8 @@ void PartyMapOverlayService::OnPartyPositions(const NotifyPartyPositions& aMsg)
9799
98100 for (const auto & e : aMsg.Entries )
99101 {
100- m_last[e.PlayerId ] = LastInfo{glm::vec3{e.Position .x , e.Position .y , e.Position .z }, tick};
102+ glm::vec3 pos{e.Position .x , e.Position .y , e.Position .z };
103+ StoreLastInfo (e.PlayerId , pos, tick);
101104
102105 WorldspaceInfo info{};
103106 if (e.WorldSpaceId )
@@ -115,7 +118,7 @@ void PartyMapOverlayService::OnPartyPositions(const NotifyPartyPositions& aMsg)
115118 // Track last known position per worldspace for cross-world projection
116119 if (info.HasWorld )
117120 {
118- m_lastPerWorld[e.PlayerId ][info.WorldSpaceFormId ] = glm::vec3{e. Position . x , e. Position . y , e. Position . z } ;
121+ m_lastPerWorld[e.PlayerId ][info.WorldSpaceFormId ] = pos ;
119122 }
120123 }
121124}
@@ -175,13 +178,13 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
175178 {
176179 const auto & pc = view.get <PlayerComponent>(e);
177180 const auto & interp = view.get <InterpolationComponent>(e);
178- m_last[ pc.Id ] = LastInfo{ interp.Position , tick} ;
181+ StoreLastInfo ( pc.Id , interp.Position , tick) ;
179182 }
180183
181184 // Periodically send our own position to the server for party tracking
182185 {
183186 static auto s_lastPosSend = std::chrono::steady_clock::time_point{};
184- constexpr auto cPosInterval = std::chrono::milliseconds (250 );
187+ constexpr auto cPosInterval = std::chrono::milliseconds (100 );
185188 const auto now = std::chrono::steady_clock::now ();
186189 if (now - s_lastPosSend >= cPosInterval)
187190
@@ -222,7 +225,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
222225
223226 // Build and send pins to CEF overlay at a throttled rate
224227 static auto s_lastSend = std::chrono::steady_clock::time_point{};
225- constexpr auto cDelayBetweenUpdates = std::chrono::milliseconds (33 ); // ~30 FPS for smoother pins
228+ constexpr auto cDelayBetweenUpdates = std::chrono::milliseconds (16 ); // ~60 FPS for tighter tracking
226229 const auto now = std::chrono::steady_clock::now ();
227230 if (now - s_lastSend < cDelayBetweenUpdates)
228231 return ;
@@ -271,6 +274,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
271274 os.setf (std::ios::fixed); os << std::setprecision (1 );
272275 os << " [" ;
273276 bool first = true ;
277+ const auto sampleNow = std::chrono::steady_clock::now ();
274278
275279 for (uint32_t pid : members)
276280 {
@@ -285,11 +289,29 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
285289 const bool hasWorld = (itWs != m_worlds.end ()) && itWs->second .HasWorld && itWs->second .WorldSpaceFormId != 0 ;
286290 float sx = 0 .f , sy = 0 .f ;
287291 bool drew = false ;
292+ const auto & lastInfo = itPos->second ;
293+
294+ glm::vec3 worldPos = lastInfo.Pos ;
295+ float dtSeconds = 0 .0f ;
296+ if (tick > lastInfo.Tick )
297+ dtSeconds = static_cast <float >(tick - lastInfo.Tick ) / 1000 .0f ;
298+
299+ if (lastInfo.SampleTime .time_since_epoch ().count () != 0 )
300+ {
301+ const float realDt = std::chrono::duration_cast<std::chrono::duration<float >>(sampleNow - lastInfo.SampleTime ).count ();
302+ dtSeconds = std::max (dtSeconds, realDt);
303+ }
304+
305+ constexpr float cMaxPredictionSeconds = 1 .0f ;
306+ dtSeconds = std::clamp (dtSeconds, 0 .0f , cMaxPredictionSeconds);
307+ const bool usingPrediction = dtSeconds > 0 .0f && glm::length2 (lastInfo.Velocity ) > std::numeric_limits<float >::epsilon ();
308+ if (usingPrediction)
309+ worldPos += lastInfo.Velocity * dtSeconds;
288310
289311 // 1) If member already in display worldspace, project directly
290312 if (hasWorld && itWs->second .WorldSpaceFormId == dispWsId)
291313 {
292- NiPoint3 wpos (itPos-> second . Pos );
314+ NiPoint3 wpos (worldPos );
293315 NiPoint3 spos{};
294316 if (HUDMenuUtils::WorldPtToScreenPt3 (wpos, spos))
295317 {
@@ -305,7 +327,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
305327 if (auto * pFromWs = static_cast <TESWorldSpace*>(TESForm::GetById (itWs->second .WorldSpaceFormId )))
306328 {
307329 glm::vec3 dstPos{};
308- if (WorldMapProjector::Convert (pFromWs, itPos-> second . Pos , pDispWs, dstPos))
330+ if (WorldMapProjector::Convert (pFromWs, worldPos , pDispWs, dstPos))
309331 {
310332 NiPoint3 wpos (dstPos);
311333 NiPoint3 spos{};
@@ -323,7 +345,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
323345 if (!drew && hasWorld && dispWsId != 0 && itWs->second .WorldSpaceFormId != dispWsId)
324346 {
325347 glm::vec3 dstPos{};
326- if (ComputeCrossWorldApprox (pid, itWs->second .WorldSpaceFormId , itPos-> second . Pos , dispWsId, dstPos))
348+ if (ComputeCrossWorldApprox (pid, itWs->second .WorldSpaceFormId , worldPos , dispWsId, dstPos))
327349 {
328350 NiPoint3 wpos (dstPos);
329351 NiPoint3 spos{};
@@ -379,11 +401,24 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
379401 if (itLS != m_lastScreen.end ())
380402 {
381403 const uint64_t dt = tick - itLS->second .Tick ;
382- // Time constant ~150ms for quick but smooth convergence
383- float alpha = 1 .0f - std::exp (-static_cast <float >(dt) / 150 .0f );
384- if (alpha < 0 .f ) alpha = 0 .f ; if (alpha > 1 .f ) alpha = 1 .f ;
385- sx = itLS->second .sx + (sx - itLS->second .sx ) * alpha;
386- sy = itLS->second .sy + (sy - itLS->second .sy ) * alpha;
404+ float alpha = 1 .0f ;
405+ if (dt > 0 )
406+ {
407+ constexpr float cSmoothingMs = 45 .0f ;
408+ alpha = 1 .0f - std::exp (-static_cast <float >(dt) / cSmoothingMs);
409+ alpha = std::clamp (alpha, 0 .7f , 1 .0f );
410+ }
411+
412+ const float dx = sx - itLS->second .sx ;
413+ const float dy = sy - itLS->second .sy ;
414+ constexpr float cTeleportPixelsSq = 600 .0f * 600 .0f ;
415+ if ((dx * dx + dy * dy) > cTeleportPixelsSq)
416+ alpha = 1 .0f ;
417+ else if (usingPrediction && dtSeconds >= 0 .2f )
418+ alpha = 1 .0f ;
419+
420+ sx = itLS->second .sx + dx * alpha;
421+ sy = itLS->second .sy + dy * alpha;
387422 }
388423
389424 // Cache last projected (smoothed) position
@@ -444,4 +479,61 @@ void PartyMapOverlayService::SetWaypointFor(uint32_t aPlayerId) noexcept
444479 m_world.GetDispatcher ().trigger (SetWaypointEvent (pos, itWs->second .WorldSpaceFormId ));
445480}
446481
482+ void PartyMapOverlayService::StoreLastInfo (uint32_t aPlayerId, const glm::vec3& aPos, uint64_t aTick) noexcept
483+ {
484+ constexpr float cMinDtSeconds = 0 .001f ;
485+ constexpr float cMaxDtSeconds = 6 .0f ;
486+ constexpr float cTeleportResetDistanceSq = 4096 .0f * 4096 .0f ;
487+ constexpr float cMaxSpeed = 16000 .0f ; // units per second, avoids runaway extrapolation
488+ constexpr float cMaxSpeedSq = cMaxSpeed * cMaxSpeed;
489+ constexpr float cVelocityBlend = 0 .25f ; // keep some of the previous vector to reduce jitter
490+
491+ const auto now = std::chrono::steady_clock::now ();
492+
493+ LastInfo info{};
494+ info.Pos = aPos;
495+ info.Tick = aTick;
496+ info.Velocity = {};
497+ info.SampleTime = now;
498+
499+ auto itPrev = m_last.find (aPlayerId);
500+ if (itPrev != m_last.end ())
501+ {
502+ const uint64_t tickDelta = (aTick > itPrev->second .Tick ) ? (aTick - itPrev->second .Tick ) : 0ull ;
503+ float dtSeconds = tickDelta > 0 ? static_cast <float >(tickDelta) / 1000 .0f : 0 .0f ;
504+
505+ if (itPrev->second .SampleTime .time_since_epoch ().count () != 0 )
506+ {
507+ const float realDtSeconds = std::chrono::duration_cast<std::chrono::duration<float >>(now - itPrev->second .SampleTime ).count ();
508+ if (realDtSeconds >= cMinDtSeconds)
509+ dtSeconds = std::max (dtSeconds, realDtSeconds);
510+ }
511+
512+ if (dtSeconds < cMinDtSeconds)
513+ {
514+ info.Velocity = itPrev->second .Velocity ;
515+ }
516+ else
517+ {
518+ const glm::vec3 delta = aPos - itPrev->second .Pos ;
519+ const float distSq = glm::length2 (delta);
447520
521+ if (dtSeconds <= cMaxDtSeconds && distSq <= cTeleportResetDistanceSq)
522+ {
523+ glm::vec3 newVelocity = delta / dtSeconds;
524+ if (glm::length2 (newVelocity) > cMaxSpeedSq)
525+ {
526+ newVelocity = {};
527+ }
528+ else if (glm::length2 (itPrev->second .Velocity ) > 0 .0f )
529+ {
530+ newVelocity = itPrev->second .Velocity * cVelocityBlend + newVelocity * (1 .0f - cVelocityBlend);
531+ }
532+
533+ info.Velocity = newVelocity;
534+ }
535+ }
536+ }
537+
538+ m_last[aPlayerId] = info;
539+ }
0 commit comments