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,17 @@
|
|||
namespace AcDream.App.Rendering.Selection;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies the transient material lighting override which retail applies to
|
||||
/// a world object after SmartBox resolves a click.
|
||||
/// </summary>
|
||||
internal interface IRetailSelectionLightingSource
|
||||
{
|
||||
/// <summary>Advances the retail SmartBox flip cadence once for this frame.</summary>
|
||||
void TickLighting();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CMaterial luminosity/diffuse replacement for an
|
||||
/// object, or false when its authored material values must be restored.
|
||||
/// </summary>
|
||||
bool TryGetLighting(uint serverGuid, out RetailSelectionLighting lighting);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -9,9 +9,10 @@ namespace AcDream.App.Rendering.Selection;
|
|||
/// The renderer builds one frame while input queries the previously completed
|
||||
/// frame, avoiding partial visibility state during multi-slice portal drawing.
|
||||
/// </summary>
|
||||
internal sealed class RetailSelectionScene : IRetailSelectionRenderSink
|
||||
internal sealed class RetailSelectionScene : IRetailSelectionRenderSink, IRetailSelectionLightingSource
|
||||
{
|
||||
private readonly RetailSelectionGeometryCache _geometry;
|
||||
private readonly RetailSelectionLightingPulse _lightingPulse;
|
||||
private List<RetailSelectionPart> _building = new();
|
||||
private List<RetailSelectionPart> _published = new();
|
||||
private readonly HashSet<PartKey> _buildingKeys = new();
|
||||
|
|
@ -21,8 +22,26 @@ internal sealed class RetailSelectionScene : IRetailSelectionRenderSink
|
|||
|
||||
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
|
||||
|
||||
public RetailSelectionScene(RetailSelectionGeometryCache geometry)
|
||||
=> _geometry = geometry ?? throw new ArgumentNullException(nameof(geometry));
|
||||
public RetailSelectionScene(
|
||||
RetailSelectionGeometryCache geometry,
|
||||
RetailSelectionLightingPulse? lightingPulse = null)
|
||||
{
|
||||
_geometry = geometry ?? throw new ArgumentNullException(nameof(geometry));
|
||||
_lightingPulse = lightingPulse ?? new RetailSelectionLightingPulse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts retail's SmartBox click pulse independently of persistent target
|
||||
/// selection. Examine, use, and targeted-use clicks receive the same pulse.
|
||||
/// </summary>
|
||||
public void BeginLightingPulse(uint serverGuid)
|
||||
=> _lightingPulse.Start(serverGuid);
|
||||
|
||||
public void TickLighting()
|
||||
=> _lightingPulse.Tick();
|
||||
|
||||
public bool TryGetLighting(uint serverGuid, out RetailSelectionLighting lighting)
|
||||
=> _lightingPulse.TryGet(serverGuid, out lighting);
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue