fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -81,6 +81,7 @@ public sealed class DatSoundCache
|
|||
private readonly Dictionary<uint, LinkedListNode<WaveEntry>> _waveEntries = new();
|
||||
private readonly LinkedList<WaveEntry> _waveLru = new();
|
||||
private readonly long _maxWaveBytes;
|
||||
private readonly Action<uint>? _afterInitialWaveMiss;
|
||||
private long _residentWaveBytes;
|
||||
private long _hits;
|
||||
private long _misses;
|
||||
|
|
@ -98,11 +99,24 @@ public sealed class DatSoundCache
|
|||
/// exercised deterministically without allocating tens of megabytes.
|
||||
/// </summary>
|
||||
public DatSoundCache(IDatObjectSource dats, long maxWaveBytes)
|
||||
: this(dats, maxWaveBytes, afterInitialWaveMiss: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic concurrency seam used to park a caller after its fast
|
||||
/// resident lookup but before the authoritative locked recheck.
|
||||
/// </summary>
|
||||
internal DatSoundCache(
|
||||
IDatObjectSource dats,
|
||||
long maxWaveBytes,
|
||||
Action<uint>? afterInitialWaveMiss)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1);
|
||||
_dats = dats;
|
||||
_maxWaveBytes = maxWaveBytes;
|
||||
_afterInitialWaveMiss = afterInitialWaveMiss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,14 +142,37 @@ public sealed class DatSoundCache
|
|||
return null;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _misses);
|
||||
_afterInitialWaveMiss?.Invoke(waveId);
|
||||
|
||||
Lazy<WaveData?> lazy = _inflight.GetOrAdd(
|
||||
waveId,
|
||||
static (id, self) => new Lazy<WaveData?>(
|
||||
() => self.DecodeUncached(id),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication),
|
||||
this);
|
||||
Lazy<WaveData?> lazy;
|
||||
lock (_gate)
|
||||
{
|
||||
// A caller can pause after the fast miss while another caller
|
||||
// decodes, admits, and removes the in-flight Lazy. Recheck the
|
||||
// authoritative caches and acquire/create the Lazy under the same
|
||||
// gate used by admission so that stale caller cannot start a
|
||||
// second decode after publication.
|
||||
if (_waveEntries.TryGetValue(waveId, out var node))
|
||||
{
|
||||
Interlocked.Increment(ref _hits);
|
||||
Touch(node);
|
||||
return node.Value.Wave;
|
||||
}
|
||||
|
||||
if (_negativeWaveIds.ContainsKey(waveId))
|
||||
{
|
||||
Interlocked.Increment(ref _hits);
|
||||
return null;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _misses);
|
||||
lazy = _inflight.GetOrAdd(
|
||||
waveId,
|
||||
static (id, self) => new Lazy<WaveData?>(
|
||||
() => self.DecodeUncached(id),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication),
|
||||
this);
|
||||
}
|
||||
try
|
||||
{
|
||||
WaveData? decoded = lazy.Value;
|
||||
|
|
|
|||
|
|
@ -179,14 +179,28 @@ public sealed class ChatLog
|
|||
|
||||
/// <summary>PlayerKilled (0x019E) — death announcement.</summary>
|
||||
/// <remarks>
|
||||
/// Death messages are routed as <see cref="ChatKind.System"/> so they
|
||||
/// share styling with other server announcements. The
|
||||
/// Retail <c>ClientCombatSystem::HandlePlayerDeathEvent @0x0056C320</c>
|
||||
/// suppresses this bystander-facing line when the local player is either
|
||||
/// the victim or killer; those participants receive their dedicated
|
||||
/// 0x01AC/0x01AD notification instead. Death messages that survive that
|
||||
/// gate are routed as <see cref="ChatKind.System"/> so they share styling
|
||||
/// with other server announcements. The
|
||||
/// <c>SenderGuid</c> field carries the victim guid; the
|
||||
/// <c>ChannelId</c> field carries the killer guid (a small misuse
|
||||
/// of the field but avoids a schema change).
|
||||
/// </remarks>
|
||||
public void OnPlayerKilled(string deathMessage, uint victimGuid, uint killerGuid)
|
||||
public void OnPlayerKilled(
|
||||
string deathMessage,
|
||||
uint victimGuid,
|
||||
uint killerGuid,
|
||||
uint localPlayerGuid = 0u)
|
||||
{
|
||||
if (localPlayerGuid != 0u
|
||||
&& (localPlayerGuid == victimGuid || localPlayerGuid == killerGuid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Append(new ChatEntry(
|
||||
Kind: ChatKind.System,
|
||||
Sender: "",
|
||||
|
|
|
|||
|
|
@ -192,11 +192,12 @@ public sealed class PhysicsTimestampGate
|
|||
_timestamps[ForcePosition] = forcePosition;
|
||||
|
||||
// SmartBox::HandleReceivedPosition 0x00453FD0: a fresh local
|
||||
// FORCE_POSITION whose teleport is exactly equal blips immediately,
|
||||
// assigns POSITION_TS directly (even equal/older), preserves the
|
||||
// current heading, sends a position event, and returns WITHOUT
|
||||
// advancing TELEPORT_TS.
|
||||
if (teleport == _timestamps[Teleport])
|
||||
// FORCE_POSITION whose teleport stamp is not older (equal OR
|
||||
// newer) blips immediately, assigns POSITION_TS directly (even
|
||||
// equal/older), preserves the current heading, sends a position
|
||||
// event, and returns WITHOUT advancing TELEPORT_TS. The same
|
||||
// wrap-safe predicate is used by IsFreshTeleportStart above.
|
||||
if (!IsNewer(teleport, _timestamps[Teleport]))
|
||||
{
|
||||
_timestamps[Position] = position;
|
||||
return PositionTimestampDisposition.ForcePosition;
|
||||
|
|
|
|||
|
|
@ -16,26 +16,13 @@ namespace AcDream.Core.Physics;
|
|||
/// transitional blending.
|
||||
///
|
||||
/// <para>
|
||||
/// This exact advance-with-wrap-then-lerp/slerp algorithm already exists as
|
||||
/// an inline, App-layer-only implementation for the "legacy" (no
|
||||
/// <see cref="AnimationSequencer"/>) NPC idle-cycle path —
|
||||
/// <c>LiveEntityAnimationPresenter.Present</c>'s non-sequencer branch
|
||||
/// (<c>CurrFrame += legacyAdvanceSeconds * Framerate</c> with the same
|
||||
/// modulo wrap) and its private <c>TryResolvePartFrame</c> helper (the same
|
||||
/// frame-bracket lerp/slerp). That call site has a live entity, a
|
||||
/// This is the shared implementation used by both the "legacy" (no
|
||||
/// <see cref="AnimationSequencer"/>) NPC idle-cycle path and the chargen
|
||||
/// preview. The live presenter has a live entity, a
|
||||
/// <c>LiveEntityRuntime</c> membership, and per-tick elapsed time supplied by
|
||||
/// the render loop; the chargen preview has none of that (there is no live
|
||||
/// entity — character creation hasn't happened yet), so it cannot reuse that
|
||||
/// class directly. Rather than re-typing the same formula a second time,
|
||||
/// this Core, pure, unit-testable class is the shared primitive: the
|
||||
/// chargen preview (<c>AcDream.App.Rendering.ChargenPreviewAnimator</c>)
|
||||
/// consumes it directly, and it is safe for a future pass to redirect
|
||||
/// <c>LiveEntityAnimationPresenter</c>'s inline copy through it as a
|
||||
/// behavior-preserving mechanical follow-up (not done here — that file is
|
||||
/// live, heavily tested production entity-rendering code with zero relation
|
||||
/// to this preview-only feature, so touching it is out of this slice's
|
||||
/// blast radius by design, not oversight). Tracked as
|
||||
/// <c>docs/ISSUES.md</c> #403 so the follow-up has an owner.
|
||||
/// entity — character creation hasn't happened yet), so neither consumer can
|
||||
/// own the primitive. Keeping it here prevents the two paths from drifting.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailAnimationCyclePlayback
|
||||
|
|
@ -43,14 +30,15 @@ public static class RetailAnimationCyclePlayback
|
|||
/// <summary>
|
||||
/// Advances <paramref name="currFrame"/> by <c>elapsedSeconds * framerate</c>
|
||||
/// and wraps it back into <c>[lowFrame, highFrame]</c> with the SAME modulo
|
||||
/// shape <c>LiveEntityAnimationPresenter.Present</c>'s legacy branch uses
|
||||
/// shape retail playback and the live presenter's legacy branch use
|
||||
/// (<c>over % (span + 1)</c>, not a plain clamp — a frame position that
|
||||
/// overshoots the end by more than one span wraps around more than once
|
||||
/// rather than sticking at the boundary, matching a long stall/resume).
|
||||
/// Returns <paramref name="currFrame"/> unchanged for a degenerate cycle
|
||||
/// (<paramref name="highFrame"/> <= <paramref name="lowFrame"/>), a
|
||||
/// non-positive <paramref name="framerate"/>, or a non-positive
|
||||
/// <paramref name="elapsedSeconds"/>.
|
||||
/// (<paramref name="highFrame"/> <= <paramref name="lowFrame"/>) or a
|
||||
/// non-positive <paramref name="elapsedSeconds"/>. A negative framerate
|
||||
/// advances backward and clamps at the low frame, matching the former
|
||||
/// live-presenter implementation exactly.
|
||||
/// </summary>
|
||||
public static float Advance(
|
||||
float currFrame,
|
||||
|
|
@ -60,7 +48,7 @@ public static class RetailAnimationCyclePlayback
|
|||
float elapsedSeconds)
|
||||
{
|
||||
int span = highFrame - lowFrame;
|
||||
if (span <= 0 || framerate <= 0f || elapsedSeconds <= 0f)
|
||||
if (span <= 0 || elapsedSeconds <= 0f)
|
||||
return currFrame;
|
||||
|
||||
float next = currFrame + elapsedSeconds * framerate;
|
||||
|
|
@ -84,8 +72,7 @@ public static class RetailAnimationCyclePlayback
|
|||
/// back to <paramref name="lowFrame"/>). Returns <c>false</c> — with
|
||||
/// <c>default</c> outputs — when <paramref name="partIndex"/> is outside
|
||||
/// the bracketing frame's part list, matching
|
||||
/// <c>LiveEntityAnimationPresenter.TryResolvePartFrame</c>'s no-
|
||||
/// sequence-frames branch exactly.
|
||||
/// the live presenter's no-sequence-frames branch exactly.
|
||||
/// </summary>
|
||||
public static bool TryInterpolatePart(
|
||||
Animation animation,
|
||||
|
|
|
|||
|
|
@ -505,6 +505,15 @@ public sealed class WorldTimeService
|
|||
/// <summary>Current sky lighting state.</summary>
|
||||
public SkyKeyframe CurrentSky => _sky.Interpolate((float)DayFraction);
|
||||
|
||||
/// <summary>
|
||||
/// Interpolate this clock's active day-group provider at an explicit day
|
||||
/// fraction without changing the clock. Retail <c>LScape::SetDay</c>
|
||||
/// uses this distinction: the sky continues advancing normally while
|
||||
/// landscape lighting is sampled at noon.
|
||||
/// </summary>
|
||||
public SkyKeyframe SkyAtDayFraction(float dayFraction) =>
|
||||
_sky.Interpolate(dayFraction);
|
||||
|
||||
/// <summary>Convenience: current sun direction from derived sky state.</summary>
|
||||
public Vector3 CurrentSunDirection =>
|
||||
SkyStateProvider.SunDirectionFromKeyframe(CurrentSky);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue