acdream/src/AcDream.App/Rendering/Selection/RetailSelectionLightingPulse.cs
Erik 047a4c83b5 fix(interaction): bind selection to live incarnations
Carry local WorldEntity identity through render hits, lighting pulses, and deferred movement actions so GUID reuse cannot target a replacement. Reset all session-owned selection and ItemHolder state and prevent combat auto-target during teardown.
2026-07-21 07:07:58 +02:00

111 lines
3.1 KiB
C#

using System.Diagnostics;
namespace AcDream.App.Rendering.Selection;
/// <summary>
/// Replacement values passed by retail to CPhysicsObj::SetLighting.
/// CMaterial receives luminosity first and diffuse second.
/// </summary>
internal readonly record struct RetailSelectionLighting(float Luminosity, float Diffuse)
{
public static readonly RetailSelectionLighting Normal = new(0f, 1f);
public static readonly RetailSelectionLighting Low = new(0f, 0.35f);
public static readonly RetailSelectionLighting High = new(0.99f, 1f);
}
/// <summary>
/// Retail SmartBox click-confirmation lighting pulse.
///
/// UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @ 0x004E5AD0
/// applies HIGH immediately. Global_Loop @ 0x004E5620 then advances
/// LOW/HIGH/LOW/RESTORE at 0.2-second intervals. A new click restores the old
/// target implicitly and starts the sequence again on the new target.
/// </summary>
internal sealed class RetailSelectionLightingPulse
{
internal const double FlipIntervalSeconds = 0.2;
private readonly Func<double> _now;
private uint _serverGuid;
private uint _localEntityId;
private int _flipCount;
private double _nextFlip;
private RetailSelectionLighting _lighting;
public RetailSelectionLightingPulse(Func<double>? now = null)
=> _now = now ?? MonotonicSeconds;
public void Start(uint serverGuid, uint localEntityId)
{
if (serverGuid == 0u || localEntityId == 0u)
{
Clear();
return;
}
_serverGuid = serverGuid;
_localEntityId = localEntityId;
_flipCount = 1;
_lighting = RetailSelectionLighting.High;
_nextFlip = _now() + FlipIntervalSeconds;
}
/// <summary>
/// Advances at most one flip per frame, matching retail Global_Loop. The
/// next deadline is based on the current frame time, so a long frame does
/// not replay multiple material mutations in one draw.
/// </summary>
public void Tick()
{
if (_flipCount == 0)
return;
double now = _now();
if (now < _nextFlip)
return;
int nextCount = _flipCount + 1;
if (nextCount >= 5)
{
Clear();
return;
}
_flipCount = nextCount;
_lighting = (nextCount & 1) != 0
? RetailSelectionLighting.High
: RetailSelectionLighting.Low;
_nextFlip = now + FlipIntervalSeconds;
}
public bool TryGet(
uint serverGuid,
uint localEntityId,
out RetailSelectionLighting lighting)
{
if (_flipCount != 0
&& serverGuid != 0u
&& localEntityId != 0u
&& serverGuid == _serverGuid
&& localEntityId == _localEntityId)
{
lighting = _lighting;
return true;
}
lighting = default;
return false;
}
public void Clear()
{
_serverGuid = 0u;
_localEntityId = 0u;
_flipCount = 0;
_nextFlip = 0d;
_lighting = default;
}
private static double MonotonicSeconds()
=> Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
}