feat(A.5 T5): StreamingRegion two-tier RecenterTo + TierResidence tracking
Adds TierResidence enum (None/Far/Near), _tierResidence dictionary seeded by MarkResidentFromBootstrap, and the canonical two-tier RecenterTo overload returning TwoTierDiff. Pass 1 walks the new far window and emits ToLoadFar / ToLoadNear / ToPromote; Pass 2 walks prior residents and emits ToDemote / ToUnload using Chebyshev hysteresis thresholds (NearRadius+2 / FarRadius+2). EncodeLandblockIdForTest exposes the encoding rule to test assemblies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7bcababf82
commit
fb6b61e8ef
2 changed files with 165 additions and 1 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
|
|
@ -22,6 +23,9 @@ public sealed class StreamingRegion
|
|||
// Everything currently loaded: window + hysteresis-retained landblocks.
|
||||
private readonly HashSet<uint> _resident = new();
|
||||
|
||||
// Two-tier residence tracking: maps each resident LB to its current tier.
|
||||
private readonly Dictionary<uint, TierResidence> _tierResidence = new();
|
||||
|
||||
/// <summary>
|
||||
/// Landblock IDs in the current visible window in the AC 8.8 coordinate
|
||||
/// form: <c>(lbX << 24) | (lbY << 16) | 0xFFFF</c>. The trailing
|
||||
|
|
@ -121,6 +125,142 @@ public sealed class StreamingRegion
|
|||
ToUnload: System.Array.Empty<uint>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call once after <see cref="ComputeFirstTickDiff"/> to seed
|
||||
/// <c>_tierResidence</c> with the initial window. Every LB in the inner
|
||||
/// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far.
|
||||
/// </summary>
|
||||
public void MarkResidentFromBootstrap()
|
||||
{
|
||||
_tierResidence.Clear();
|
||||
for (int dx = -FarRadius; dx <= FarRadius; dx++)
|
||||
{
|
||||
for (int dy = -FarRadius; dy <= FarRadius; dy++)
|
||||
{
|
||||
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);
|
||||
var id = EncodeLandblockId(nx, ny);
|
||||
_tierResidence[id] = (absDx <= NearRadius && absDy <= NearRadius)
|
||||
? TierResidence.Near
|
||||
: TierResidence.Far;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-visible wrapper around <see cref="EncodeLandblockId"/> so test
|
||||
/// assemblies can build expected IDs without duplicating the encoding rule.
|
||||
/// </summary>
|
||||
internal static uint EncodeLandblockIdForTest(int lbX, int lbY)
|
||||
=> EncodeLandblockId(lbX, lbY);
|
||||
|
||||
/// <summary>
|
||||
/// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2.
|
||||
/// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null
|
||||
/// unload. Requires <see cref="MarkResidentFromBootstrap"/> (or a prior
|
||||
/// call to this method) to have seeded <c>_tierResidence</c>.
|
||||
/// </summary>
|
||||
public TwoTierDiff RecenterTo(int newCx, int newCy)
|
||||
{
|
||||
int nearUnloadThreshold = NearRadius + 2;
|
||||
int farUnloadThreshold = FarRadius + 2;
|
||||
|
||||
var toLoadFar = new List<uint>();
|
||||
var toLoadNear = new List<uint>();
|
||||
var toPromote = new List<uint>();
|
||||
var toDemote = new List<uint>();
|
||||
var toUnload = new List<uint>();
|
||||
|
||||
// Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote.
|
||||
var newCenterIds = new HashSet<uint>();
|
||||
for (int dx = -FarRadius; dx <= FarRadius; dx++)
|
||||
{
|
||||
for (int dy = -FarRadius; dy <= FarRadius; dy++)
|
||||
{
|
||||
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);
|
||||
bool inNear = absDx <= NearRadius && absDy <= NearRadius;
|
||||
var id = EncodeLandblockId(nx, ny);
|
||||
newCenterIds.Add(id);
|
||||
|
||||
if (!_tierResidence.TryGetValue(id, out var current))
|
||||
{
|
||||
// Not resident at all — fresh load.
|
||||
if (inNear) toLoadNear.Add(id);
|
||||
else toLoadFar.Add(id);
|
||||
_tierResidence[id] = inNear ? TierResidence.Near : TierResidence.Far;
|
||||
}
|
||||
else if (current == TierResidence.Far && inNear)
|
||||
{
|
||||
// Was Far, now inside Near ring — promote.
|
||||
toPromote.Add(id);
|
||||
_tierResidence[id] = TierResidence.Near;
|
||||
}
|
||||
// Near→Near and Far→Far are no-ops.
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: check previously-resident LBs for demote / unload.
|
||||
foreach (var kvp in _tierResidence.ToArray())
|
||||
{
|
||||
var id = kvp.Key;
|
||||
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
|
||||
|
||||
if (newCenterIds.Contains(id))
|
||||
{
|
||||
// Still in the far window — only Near→Far demote possible here.
|
||||
if (current == TierResidence.Near && (absDx > NearRadius || absDy > NearRadius))
|
||||
{
|
||||
if (distance > nearUnloadThreshold)
|
||||
{
|
||||
toDemote.Add(id);
|
||||
_tierResidence[id] = TierResidence.Far;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Outside the new window — demote / unload by threshold.
|
||||
if (current == TierResidence.Near)
|
||||
{
|
||||
if (distance > nearUnloadThreshold)
|
||||
{
|
||||
toDemote.Add(id);
|
||||
_tierResidence[id] = TierResidence.Far;
|
||||
if (distance > farUnloadThreshold)
|
||||
{
|
||||
toUnload.Add(id);
|
||||
_tierResidence.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (current == TierResidence.Far)
|
||||
{
|
||||
if (distance > farUnloadThreshold)
|
||||
{
|
||||
toUnload.Add(id);
|
||||
_tierResidence.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CenterX = newCx;
|
||||
CenterY = newCy;
|
||||
|
||||
return new TwoTierDiff(toLoadFar, toLoadNear, toPromote, toDemote, toUnload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recompute the visible window around a new center and return the
|
||||
/// delta vs. the previous state. Hysteresis: landblocks aren't unloaded
|
||||
|
|
@ -166,7 +306,7 @@ public sealed class StreamingRegion
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Output of <see cref="StreamingRegion.RecenterTo"/>: the landblocks to
|
||||
/// Output of <see cref="StreamingRegion.RecenterToSingleTier"/>: the landblocks to
|
||||
/// start loading (newly entered the visible window) and the landblocks to
|
||||
/// unload (fell outside the unload threshold, which is <c>Radius + 2</c>).
|
||||
/// Both lists are disjoint from the current <see cref="StreamingRegion.Visible"/>
|
||||
|
|
@ -175,3 +315,8 @@ public sealed class StreamingRegion
|
|||
public readonly record struct RegionDiff(
|
||||
IReadOnlyList<uint> ToLoad,
|
||||
IReadOnlyList<uint> ToUnload);
|
||||
|
||||
/// <summary>
|
||||
/// Tracks which tier a landblock currently occupies in the two-tier streaming model.
|
||||
/// </summary>
|
||||
internal enum TierResidence { None, Far, Near }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue