fix(selection): port SmartBox click lighting pulse
Port the retail high/low material-lighting cadence for successful world clicks, keep it instance-scoped in the modern renderer, and restore authored lighting after 0.8 seconds. Correct the selection oracle and pin timing plus per-frame buffer lifecycle with tests. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
146a963aeb
commit
ea4f52ec51
11 changed files with 338 additions and 16 deletions
|
|
@ -0,0 +1,101 @@
|
|||
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 int _flipCount;
|
||||
private double _nextFlip;
|
||||
private RetailSelectionLighting _lighting;
|
||||
|
||||
public RetailSelectionLightingPulse(Func<double>? now = null)
|
||||
=> _now = now ?? MonotonicSeconds;
|
||||
|
||||
public void Start(uint serverGuid)
|
||||
{
|
||||
if (serverGuid == 0u)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
_serverGuid = serverGuid;
|
||||
_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, out RetailSelectionLighting lighting)
|
||||
{
|
||||
if (_flipCount != 0 && serverGuid != 0u && serverGuid == _serverGuid)
|
||||
{
|
||||
lighting = _lighting;
|
||||
return true;
|
||||
}
|
||||
|
||||
lighting = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_serverGuid = 0u;
|
||||
_flipCount = 0;
|
||||
_nextFlip = 0d;
|
||||
_lighting = default;
|
||||
}
|
||||
|
||||
private static double MonotonicSeconds()
|
||||
=> Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue