fix: complete retail parity stability pass
All checks were successful
CI / linux-portable (push) Successful in 3m41s
CI / windows-gate (push) Successful in 6m49s
CI / release (push) Successful in 3m22s

This commit is contained in:
Erik 2026-08-28 20:01:39 +02:00
parent d3df4cb20a
commit f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions

View file

@ -142,11 +142,6 @@ public sealed class DatSoundCacheTests
[Trait("Status", "KnownFailure")]
public void GetWave_ConcurrentSameId_PublishesOneCanonicalWaveAndDecodesOnce()
{
// #321: a stale caller can pass the resident-cache check, pause, and
// reach _inflight after the winning caller has decoded, admitted, and
// removed its Lazy. That second Lazy performs a duplicate decode.
// R3 preserves this product defect as an explicit known-failure lane;
// fixing the cache algorithm is outside the test-only cleanup scope.
var dats = new FakeDatObjectSource();
dats.AddWave(1, MakePcmWave(1, dataBytes: 1000));
var cache = new DatSoundCache(dats, maxWaveBytes: 10_000);
@ -161,6 +156,55 @@ public sealed class DatSoundCacheTests
Assert.Equal(1, dats.GetCallCount(1));
}
[Fact]
public async Task GetWave_StaleMissCannotDecodeAgainAfterWinnerPublishes()
{
using var winnerDecodeEntered = new ManualResetEventSlim();
using var releaseWinnerDecode = new ManualResetEventSlim();
using var staleMissParked = new ManualResetEventSlim();
using var releaseStaleMiss = new ManualResetEventSlim();
var dats = new FakeDatObjectSource
{
BeforeWaveGet = _ =>
{
winnerDecodeEntered.Set();
if (!releaseWinnerDecode.Wait(TimeSpan.FromSeconds(5)))
throw new TimeoutException("winner decode was never released");
},
};
dats.AddWave(1, MakePcmWave(1, dataBytes: 1000));
int initialMissOrdinal = 0;
var cache = new DatSoundCache(
dats,
maxWaveBytes: 10_000,
afterInitialWaveMiss: _ =>
{
if (Interlocked.Increment(ref initialMissOrdinal) != 2)
return;
staleMissParked.Set();
if (!releaseStaleMiss.Wait(TimeSpan.FromSeconds(5)))
throw new TimeoutException("stale miss was never released");
});
Task<WaveData?> winner = Task.Run(() => cache.GetWave(1));
Assert.True(winnerDecodeEntered.Wait(TimeSpan.FromSeconds(5)));
Task<WaveData?> stale = Task.Run(() => cache.GetWave(1));
Assert.True(staleMissParked.Wait(TimeSpan.FromSeconds(5)));
releaseWinnerDecode.Set();
WaveData? canonical = await winner;
releaseStaleMiss.Set();
WaveData? recovered = await stale;
Assert.NotNull(canonical);
Assert.Same(canonical, recovered);
Assert.Equal(1, dats.GetCallCount(1));
}
[Fact]
public void GetWave_ConcurrentMissingId_NeverThrowsAndAllReturnNull()
{
@ -214,6 +258,8 @@ public sealed class DatSoundCacheTests
public void AddWave(uint id, Wave wave) => _waves[id] = wave;
public Action<uint>? BeforeWaveGet { get; init; }
public int GetCallCount(uint id)
{
lock (_gate)
@ -225,6 +271,7 @@ public sealed class DatSoundCacheTests
{
if (typeof(T) == typeof(Wave))
{
BeforeWaveGet?.Invoke(fileId);
lock (_gate)
{
_callCounts.TryGetValue(fileId, out int count);

View file

@ -125,7 +125,9 @@ public sealed class ChatLogTests
{
var log = new ChatLog();
log.OnPlayerKilled("Caith was killed by a Drudge.",
victimGuid: 0x12345678u, killerGuid: 0x90ABCDEFu);
victimGuid: 0x12345678u,
killerGuid: 0x90ABCDEFu,
localPlayerGuid: 0x11111111u);
var e = log.Snapshot()[0];
Assert.Equal(ChatKind.System, e.Kind);
Assert.Equal("Caith was killed by a Drudge.", e.Text);
@ -133,6 +135,25 @@ public sealed class ChatLogTests
Assert.Equal(0x90ABCDEFu, e.ChannelId); // killer guid stashed here
}
[Theory]
[InlineData(0x12345678u)]
[InlineData(0x90ABCDEFu)]
public void OnPlayerKilled_SuppressesVictimAndKiller(uint localPlayerGuid)
{
var log = new ChatLog();
int appended = 0;
log.EntryAppended += _ => appended++;
log.OnPlayerKilled(
"Caith was killed by a Drudge.",
victimGuid: 0x12345678u,
killerGuid: 0x90ABCDEFu,
localPlayerGuid);
Assert.Empty(log.Snapshot());
Assert.Equal(0, appended);
}
// OnWeenieError-specific tests (plain code, interpolation, silent
// client-control statuses) were removed here — REJECT-review rework
// (SHOULD-FIX 3, docs/research/2026-08-09-ch2-review-findings.md)
@ -230,7 +251,11 @@ public sealed class ChatLogTests
public void OnPlayerKilled_LogTextType_IsDefault()
{
var log = new ChatLog();
log.OnPlayerKilled("Caith was killed by a Drudge.", 0x1u, 0x2u);
log.OnPlayerKilled(
"Caith was killed by a Drudge.",
0x1u,
0x2u,
localPlayerGuid: 0x3u);
Assert.Equal(0x00u, log.Snapshot()[0].LogTextType);
}

View file

@ -265,15 +265,28 @@ public class PhysicsTimestampGateTests
}
[Fact]
public void FreshForceWithNewerTeleport_UsesNormalPositionPath()
public void FreshForceWithNewerTeleport_UsesForcePathWithoutConsumingTeleport()
{
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(10, 0, 0, 0, 20, 0, 30, 0, 1);
Assert.Equal(PositionTimestampDisposition.Apply,
Assert.Equal(PositionTimestampDisposition.ForcePosition,
gate.TryAcceptPositionEvent(1, 11, 21, 31, isLocalPlayer: true));
Assert.Equal((ushort)11, gate.PositionTimestamp);
Assert.Equal((ushort)21, gate.TeleportTimestamp);
Assert.Equal((ushort)20, gate.TeleportTimestamp);
Assert.Equal((ushort)31, gate.ForcePositionTimestamp);
}
[Fact]
public void FreshForceWithWrappedNewerTeleport_UsesForcePathWithoutConsumingTeleport()
{
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(10, 0, 0, 0, ushort.MaxValue, 0, 30, 0, 1);
Assert.Equal(PositionTimestampDisposition.ForcePosition,
gate.TryAcceptPositionEvent(1, 11, 0, 31, isLocalPlayer: true));
Assert.Equal((ushort)11, gate.PositionTimestamp);
Assert.Equal(ushort.MaxValue, gate.TeleportTimestamp);
Assert.Equal((ushort)31, gate.ForcePositionTimestamp);
}

View file

@ -76,13 +76,22 @@ public sealed class RetailAnimationCyclePlaybackTests
}
[Fact]
public void Advance_NonPositiveFramerateOrElapsed_ReturnsCurrFrameUnchanged()
public void Advance_ZeroFramerateOrNonPositiveElapsed_ReturnsCurrFrameUnchanged()
{
Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(3f, 0, 29, framerate: 0f, elapsedSeconds: 1f));
Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(3f, 0, 29, framerate: 30f, elapsedSeconds: 0f));
Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(3f, 0, 29, framerate: 30f, elapsedSeconds: -1f));
}
[Fact]
public void Advance_NegativeFramerate_MovesBackwardAndClampsAtLowFrame()
{
Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(
5f, lowFrame: 0, highFrame: 29, framerate: -2f, elapsedSeconds: 1f));
Assert.Equal(0f, RetailAnimationCyclePlayback.Advance(
1f, lowFrame: 0, highFrame: 29, framerate: -2f, elapsedSeconds: 1f));
}
[Fact]
public void TryInterpolatePart_ExactFrame_ReturnsThatFramesPose()
{