fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue