using System;
using System.Collections.Generic;
namespace AcDream.App.Streaming;
///
/// Pure data type describing the set of landblocks currently considered
/// "visible" by the streaming system. Given a center landblock (x, y) and
/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window.
///
public sealed class StreamingRegion
{
public int CenterX { get; private set; }
public int CenterY { get; private set; }
public int Radius { get; }
// Strictly the (2r+1)×(2r+1) window (clamped to world bounds).
private readonly HashSet _visible = new();
// Everything currently loaded: window + hysteresis-retained landblocks.
private readonly HashSet _resident = new();
///
/// Landblock IDs in the current visible window in the AC 8.8 coordinate
/// form: (lbX << 24) | (lbY << 16) | 0xFFFF. The trailing
/// 0xFFFF selects the LandBlock dat (terrain heightmap); the
/// matching LandBlockInfo (static-object metadata) is at 0xFFFE
/// on the same coordinates and is resolved internally by
/// LandblockLoader.
///
///
/// This set is strictly the (2r+1)×(2r+1) window; it does NOT include
/// hysteresis-retained landblocks outside the window. Use
/// to enumerate everything actually loaded.
///
///
public IReadOnlyCollection Visible => _visible;
///
/// Every landblock currently loaded: the current visible window plus any
/// landblocks being held in memory by the hysteresis buffer (not yet past
/// the Radius + 2 unload threshold).
///
public IReadOnlyCollection Resident => _resident;
public StreamingRegion(int cx, int cy, int radius)
{
Radius = radius;
Recenter(cx, cy);
}
private void Recenter(int cx, int cy)
{
CenterX = cx;
CenterY = cy;
_visible.Clear();
for (int dx = -Radius; dx <= Radius; dx++)
{
for (int dy = -Radius; dy <= Radius; dy++)
{
int nx = cx + dx;
int ny = cy + dy;
if (nx < 0 || nx > 0xFF || ny < 0 || ny > 0xFF)
continue;
_visible.Add(EncodeLandblockId(nx, ny));
}
}
// On first construction, resident == visible.
_resident.UnionWith(_visible);
}
///
/// Encode a landblock at (lbX, lbY) into the AC dat id form. Always uses
/// the 0xFFFF terminator (LandBlock = terrain). The earlier
/// version of this method used 0xFFFE by mistake — that's the
/// LandBlockInfo id, and asking LandblockLoader.Load to read a
/// LandBlock at the LandBlockInfo coords corrupts the dat reader's
/// buffer position, returning a half-populated LandBlock.Height[]
/// array which renders as wildly distorted "ball with spikes" terrain.
///
internal static uint EncodeLandblockId(int lbX, int lbY)
=> ((uint)lbX << 24) | ((uint)lbY << 16) | 0xFFFFu;
///
/// Recompute the visible window around a new center and return the
/// delta vs. the previous state. Hysteresis: landblocks aren't unloaded
/// until they're further than Radius + 2 from the new center,
/// so boundary crossings don't thrash.
///
public RegionDiff RecenterTo(int newCx, int newCy)
{
// Snapshot the old resident set so we can diff against it.
var oldResident = new HashSet(_resident);
// Recompute _visible strictly as the new window.
Recenter(newCx, newCy);
// Loads = entries in the new window not yet in the resident set.
var toLoad = new List();
foreach (var id in _visible)
if (!oldResident.Contains(id))
toLoad.Add(id);
// Unloads = resident entries outside the hysteresis threshold
// (|dx| > Radius+2 OR |dy| > Radius+2).
int unloadThreshold = Radius + 2;
var toUnload = new List();
foreach (var id in oldResident)
{
if (_visible.Contains(id)) continue; // still in window, keep
int lbX = (int)((id >> 24) & 0xFFu);
int lbY = (int)((id >> 16) & 0xFFu);
int dx = Math.Abs(lbX - newCx);
int dy = Math.Abs(lbY - newCy);
if (dx > unloadThreshold || dy > unloadThreshold)
toUnload.Add(id);
}
// Update resident: (oldResident ∪ newVisible) ∖ toUnload.
_resident.UnionWith(_visible);
foreach (var id in toUnload)
_resident.Remove(id);
return new RegionDiff(toLoad, toUnload);
}
}
///
/// Output of : the landblocks to
/// start loading (newly entered the visible window) and the landblocks to
/// unload (fell outside the unload threshold, which is Radius + 2).
/// Both lists are disjoint from the current
/// set; the caller hands them to LandblockStreamer as jobs.
///
public readonly record struct RegionDiff(
IReadOnlyList ToLoad,
IReadOnlyList ToUnload);