fix(A.5 T4-T6): bootstrap guard + dead enum + test cleanups

Code review on commits 7bcabab/fb6b61e/326b698 flagged 2 Important +
4 Minor issues. Apply all fixes:

Important:
- Two-tier RecenterTo + MarkResidentFromBootstrap now throw
  InvalidOperationException on misuse — calling RecenterTo before the
  bootstrap silently emitted the entire window as fresh loads (no
  demotes/unloads since _tierResidence was empty), a correctness hazard
  that produced no exception. Calling MarkResidentFromBootstrap twice
  silently dropped accumulated tier state. Both now crash loudly via
  a _bootstrapped flag.
- Dropped TierResidence.None from the enum — never assigned, never
  checked; absence from the dictionary already encodes "not resident."

Minor:
- Renamed test: RecenterTo_FirstTick_* → ComputeFirstTickDiff_FirstTick_*
  (the test calls ComputeFirstTickDiff, not RecenterTo).
- Strengthened RecenterTo_PlayerWalks_NullToFar_* with assertions for
  ToPromote.Count==3 (the x=102 column promoting Far→Near) and
  ToUnload.Empty (everything within hysteresis).
- Replaced System.Math.Abs with Math.Abs in new code to match the
  file's existing `using System;` convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-09 22:49:35 +02:00
parent 326b698161
commit 1658882439
2 changed files with 40 additions and 10 deletions

View file

@ -26,6 +26,13 @@ public sealed class StreamingRegion
// Two-tier residence tracking: maps each resident LB to its current tier.
private readonly Dictionary<uint, TierResidence> _tierResidence = new();
// Set true after MarkResidentFromBootstrap. The two-tier RecenterTo
// requires this state to be seeded; calling RecenterTo before the
// bootstrap silently emits the entire window as fresh loads (no demotes,
// no unloads, since _tierResidence is empty), which is a correctness
// hazard. The flag converts that into a loud InvalidOperationException.
private bool _bootstrapped;
/// <summary>
/// Landblock IDs in the current visible window in the AC 8.8 coordinate
/// form: <c>(lbX &lt;&lt; 24) | (lbY &lt;&lt; 16) | 0xFFFF</c>. The trailing
@ -132,6 +139,12 @@ public sealed class StreamingRegion
/// </summary>
public void MarkResidentFromBootstrap()
{
if (_bootstrapped)
throw new InvalidOperationException(
"MarkResidentFromBootstrap was already called; calling it again would " +
"reset accumulated tier-residence state and silently drop differential " +
"data built up by interim RecenterTo calls.");
_tierResidence.Clear();
for (int dx = -FarRadius; dx <= FarRadius; dx++)
{
@ -140,14 +153,15 @@ public sealed class StreamingRegion
int nx = CenterX + dx;
int ny = CenterY + dy;
if (nx < 0 || nx > 0xFF || ny < 0 || ny > 0xFF) continue;
int absDx = System.Math.Abs(dx);
int absDy = System.Math.Abs(dy);
int absDx = Math.Abs(dx);
int absDy = Math.Abs(dy);
var id = EncodeLandblockId(nx, ny);
_tierResidence[id] = (absDx <= NearRadius && absDy <= NearRadius)
? TierResidence.Near
: TierResidence.Far;
}
}
_bootstrapped = true;
}
/// <summary>
@ -165,6 +179,13 @@ public sealed class StreamingRegion
/// </summary>
public TwoTierDiff RecenterTo(int newCx, int newCy)
{
if (!_bootstrapped)
throw new InvalidOperationException(
"Two-tier RecenterTo called before MarkResidentFromBootstrap. " +
"First call ComputeFirstTickDiff to enqueue the bootstrap loads, " +
"then MarkResidentFromBootstrap to seed _tierResidence, then RecenterTo " +
"for subsequent observer moves.");
int nearUnloadThreshold = NearRadius + 2;
int farUnloadThreshold = FarRadius + 2;
@ -183,8 +204,8 @@ public sealed class StreamingRegion
int nx = newCx + dx;
int ny = newCy + dy;
if (nx < 0 || nx > 0xFF || ny < 0 || ny > 0xFF) continue;
int absDx = System.Math.Abs(dx);
int absDy = System.Math.Abs(dy);
int absDx = Math.Abs(dx);
int absDy = Math.Abs(dy);
bool inNear = absDx <= NearRadius && absDy <= NearRadius;
var id = EncodeLandblockId(nx, ny);
newCenterIds.Add(id);
@ -213,9 +234,9 @@ public sealed class StreamingRegion
var current = kvp.Value;
int lbX = (int)((id >> 24) & 0xFFu);
int lbY = (int)((id >> 16) & 0xFFu);
int absDx = System.Math.Abs(lbX - newCx);
int absDy = System.Math.Abs(lbY - newCy);
int distance = System.Math.Max(absDx, absDy); // Chebyshev
int absDx = Math.Abs(lbX - newCx);
int absDy = Math.Abs(lbY - newCy);
int distance = Math.Max(absDx, absDy); // Chebyshev
if (newCenterIds.Contains(id))
{
@ -317,6 +338,8 @@ public readonly record struct RegionDiff(
IReadOnlyList<uint> ToUnload);
/// <summary>
/// Tracks which tier a landblock currently occupies in the two-tier streaming model.
/// Tracks which tier a landblock currently occupies in the two-tier streaming
/// model. Absence from the dictionary encodes "not resident"; the enum has no
/// None member to avoid suggesting a third runtime state.
/// </summary>
internal enum TierResidence { None, Far, Near }
internal enum TierResidence { Far, Near }