Join destination scheduling to the canonical reveal generation, protect its share across every typed frame-budget dimension, and prevent stale work from clearing a replacement reservation. Remove forced incomplete materialization and project retail's centered portal wait cue while the authored tunnel remains active. Tests: Release build clean; 91 focused reservation/reveal tests; full solution 8,158 passed, 5 skipped. Co-authored-by: Codex <noreply@openai.com>
147 lines
6.1 KiB
C#
147 lines
6.1 KiB
C#
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// One evaluation of the destination domains guarded by
|
|
/// <see cref="WorldRevealReadinessBarrier"/>. Keeping the individual facts in
|
|
/// the canonical owner lets diagnostics and automation observe readiness
|
|
/// without duplicating (and eventually drifting from) the reveal predicate.
|
|
/// </summary>
|
|
internal readonly record struct WorldRevealReadinessSnapshot(
|
|
uint DestinationCell,
|
|
bool IsIndoor,
|
|
bool IsUnhydratable,
|
|
int RequiredRenderRadius,
|
|
bool IsRenderNeighborhoodReady,
|
|
bool AreCompositeTexturesReady,
|
|
bool IsCollisionReady)
|
|
{
|
|
public bool HasDestination => DestinationCell != 0u;
|
|
|
|
public bool IsReady => HasDestination
|
|
&& (IsUnhydratable
|
|
|| (IsRenderNeighborhoodReady
|
|
&& AreCompositeTexturesReady
|
|
&& IsCollisionReady));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Joins the render-publication, texture, and collision domains that must be
|
|
/// complete before the normal world viewport is revealed. Retail keeps
|
|
/// <c>SmartBox::position_update_complete</c> false while
|
|
/// <c>CellManager::blocking_for_cells</c> is set
|
|
/// (<c>SmartBox::UseTime</c>, 0x00455410). acdream loads those domains
|
|
/// asynchronously, so login and portal arrival must share this explicit
|
|
/// equivalent of retail's single blocking-cell edge.
|
|
/// </summary>
|
|
internal sealed class WorldRevealReadinessBarrier
|
|
{
|
|
internal const int OutdoorNeighborhoodRadius = 1;
|
|
|
|
private readonly Func<uint, int, bool> _isRenderNeighborhoodReady;
|
|
private readonly Func<uint, bool> _isSpawnCellReady;
|
|
private readonly Func<uint, int, bool> _isTerrainNeighborhoodReady;
|
|
private readonly Func<bool> _areCompositeTexturesReady;
|
|
private readonly Action<uint, int> _prepareCompositeTextures;
|
|
private readonly Action _invalidateCompositeTextures;
|
|
private readonly Func<uint, bool> _isSpawnClaimUnhydratable;
|
|
|
|
public WorldRevealReadinessBarrier(
|
|
Func<uint, int, bool> isRenderNeighborhoodReady,
|
|
Func<uint, bool> isSpawnCellReady,
|
|
Func<uint, int, bool> isTerrainNeighborhoodReady,
|
|
Func<bool> areCompositeTexturesReady,
|
|
Action<uint, int> prepareCompositeTextures,
|
|
Action invalidateCompositeTextures,
|
|
Func<uint, bool> isSpawnClaimUnhydratable)
|
|
{
|
|
_isRenderNeighborhoodReady = isRenderNeighborhoodReady
|
|
?? throw new ArgumentNullException(nameof(isRenderNeighborhoodReady));
|
|
_isSpawnCellReady = isSpawnCellReady
|
|
?? throw new ArgumentNullException(nameof(isSpawnCellReady));
|
|
_isTerrainNeighborhoodReady = isTerrainNeighborhoodReady
|
|
?? throw new ArgumentNullException(nameof(isTerrainNeighborhoodReady));
|
|
_areCompositeTexturesReady = areCompositeTexturesReady
|
|
?? throw new ArgumentNullException(nameof(areCompositeTexturesReady));
|
|
_prepareCompositeTextures = prepareCompositeTextures
|
|
?? throw new ArgumentNullException(nameof(prepareCompositeTextures));
|
|
_invalidateCompositeTextures = invalidateCompositeTextures
|
|
?? throw new ArgumentNullException(nameof(invalidateCompositeTextures));
|
|
_isSpawnClaimUnhydratable = isSpawnClaimUnhydratable
|
|
?? throw new ArgumentNullException(nameof(isSpawnClaimUnhydratable));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a new reveal lifetime. Texture readiness is destination-scoped;
|
|
/// carrying the prior login/portal result across a new destination would
|
|
/// open the barrier before that destination's composites were uploaded.
|
|
/// </summary>
|
|
public void Begin() => _invalidateCompositeTextures();
|
|
|
|
/// <summary>
|
|
/// Advances render-thread texture preparation after all required static
|
|
/// meshes for the destination neighborhood have been published.
|
|
/// </summary>
|
|
public void Prepare(uint destinationCell)
|
|
{
|
|
if (destinationCell == 0 || _isSpawnClaimUnhydratable(destinationCell))
|
|
return;
|
|
|
|
int radius = RequiredRenderRadius(destinationCell);
|
|
if (_isRenderNeighborhoodReady(destinationCell, radius))
|
|
_prepareCompositeTextures(destinationCell, radius);
|
|
}
|
|
|
|
/// <summary>
|
|
/// True only when the same destination is drawable and collidable. An
|
|
/// impossible indoor claim intentionally crosses the barrier so the
|
|
/// existing loud unhydratable-placement path can expose invalid server
|
|
/// data. A merely slow hydratable claim never crosses early.
|
|
/// </summary>
|
|
public bool IsReady(uint destinationCell)
|
|
=> Evaluate(destinationCell).IsReady;
|
|
|
|
/// <summary>
|
|
/// Evaluates each readiness domain once and returns the complete decision.
|
|
/// Consumers must use this snapshot rather than reimplementing the join.
|
|
/// </summary>
|
|
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
|
|
{
|
|
if (destinationCell == 0)
|
|
return default;
|
|
|
|
bool isIndoor = IsIndoor(destinationCell);
|
|
int radius = RequiredRenderRadius(destinationCell);
|
|
if (_isSpawnClaimUnhydratable(destinationCell))
|
|
{
|
|
return new WorldRevealReadinessSnapshot(
|
|
destinationCell,
|
|
isIndoor,
|
|
IsUnhydratable: true,
|
|
radius,
|
|
IsRenderNeighborhoodReady: false,
|
|
AreCompositeTexturesReady: false,
|
|
IsCollisionReady: false);
|
|
}
|
|
|
|
bool renderReady = _isRenderNeighborhoodReady(destinationCell, radius);
|
|
bool compositesReady = renderReady && _areCompositeTexturesReady();
|
|
bool collisionReady = renderReady && compositesReady
|
|
&& (isIndoor
|
|
? _isSpawnCellReady(destinationCell)
|
|
: _isTerrainNeighborhoodReady(destinationCell, radius));
|
|
|
|
return new WorldRevealReadinessSnapshot(
|
|
destinationCell,
|
|
isIndoor,
|
|
IsUnhydratable: false,
|
|
radius,
|
|
renderReady,
|
|
compositesReady,
|
|
collisionReady);
|
|
}
|
|
|
|
internal static int RequiredRenderRadius(uint destinationCell) =>
|
|
IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius;
|
|
|
|
private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;
|
|
}
|