158 lines
5.2 KiB
C#
158 lines
5.2 KiB
C#
using System.Globalization;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
internal enum WorldRevealKind
|
|
{
|
|
Login,
|
|
Portal,
|
|
}
|
|
|
|
internal readonly record struct WorldRevealLifecycleSnapshot(
|
|
long Generation,
|
|
WorldRevealKind Kind,
|
|
WorldRevealReadinessSnapshot Readiness,
|
|
bool Materialized,
|
|
bool Completed,
|
|
bool Cancelled,
|
|
bool WorldViewportObserved,
|
|
int InvariantFailureCount)
|
|
{
|
|
public bool IsActive => Generation != 0 && !Cancelled;
|
|
public bool IsReady => Readiness.IsReady;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Observes logical login/portal reveal generations without owning rendering or
|
|
/// changing gameplay state. The owner turns the canonical readiness decision
|
|
/// into deterministic lifecycle markers for connected automation and records a
|
|
/// hard diagnostic failure if normal world geometry becomes visible early.
|
|
/// </summary>
|
|
internal sealed class WorldRevealLifecycleTelemetry
|
|
{
|
|
private readonly Action<string> _log;
|
|
private WorldRevealLifecycleSnapshot _snapshot;
|
|
private long _nextGeneration;
|
|
private int _portalMaterializationCount;
|
|
|
|
public WorldRevealLifecycleTelemetry(Action<string>? log = null)
|
|
{
|
|
_log = log ?? (_ => { });
|
|
}
|
|
|
|
public WorldRevealLifecycleSnapshot Snapshot => _snapshot;
|
|
public int PortalMaterializationCount => _portalMaterializationCount;
|
|
|
|
public long Begin(WorldRevealKind kind)
|
|
{
|
|
if (_snapshot.Generation != 0
|
|
&& !_snapshot.Cancelled
|
|
&& !_snapshot.WorldViewportObserved)
|
|
{
|
|
Log("superseded", _snapshot);
|
|
}
|
|
|
|
long generation = checked(++_nextGeneration);
|
|
_snapshot = new WorldRevealLifecycleSnapshot(
|
|
generation,
|
|
kind,
|
|
default,
|
|
Materialized: false,
|
|
Completed: false,
|
|
Cancelled: false,
|
|
WorldViewportObserved: false,
|
|
InvariantFailureCount: _snapshot.InvariantFailureCount);
|
|
Log("begin", _snapshot);
|
|
return generation;
|
|
}
|
|
|
|
public void ObserveReadiness(WorldRevealReadinessSnapshot readiness)
|
|
{
|
|
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
|
return;
|
|
|
|
if (_snapshot.Readiness == readiness)
|
|
return;
|
|
|
|
if (_snapshot.Readiness.DestinationCell != 0u
|
|
&& readiness.DestinationCell != 0u
|
|
&& _snapshot.Readiness.DestinationCell != readiness.DestinationCell)
|
|
{
|
|
FailInvariant(
|
|
"destination-changed",
|
|
$"from=0x{_snapshot.Readiness.DestinationCell:X8} to=0x{readiness.DestinationCell:X8}");
|
|
}
|
|
|
|
_snapshot = _snapshot with { Readiness = readiness };
|
|
Log("readiness", _snapshot);
|
|
}
|
|
|
|
public void ObserveMaterialized()
|
|
{
|
|
if (_snapshot.Generation == 0 || _snapshot.Cancelled || _snapshot.Materialized)
|
|
return;
|
|
|
|
_snapshot = _snapshot with { Materialized = true };
|
|
if (_snapshot.Kind == WorldRevealKind.Portal)
|
|
_portalMaterializationCount++;
|
|
Log("materialized", _snapshot);
|
|
}
|
|
|
|
public void ObserveWorldViewportVisible()
|
|
{
|
|
if (_snapshot.Generation == 0 || _snapshot.Cancelled || _snapshot.WorldViewportObserved)
|
|
return;
|
|
|
|
if (!_snapshot.Readiness.IsReady)
|
|
FailInvariant("viewport-before-ready", null);
|
|
|
|
_snapshot = _snapshot with { WorldViewportObserved = true };
|
|
Log("world-visible", _snapshot);
|
|
}
|
|
|
|
public void Complete()
|
|
{
|
|
if (_snapshot.Generation == 0 || _snapshot.Cancelled || _snapshot.Completed)
|
|
return;
|
|
|
|
_snapshot = _snapshot with { Completed = true };
|
|
Log("complete", _snapshot);
|
|
}
|
|
|
|
public void Cancel()
|
|
{
|
|
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
|
return;
|
|
|
|
_snapshot = _snapshot with { Cancelled = true };
|
|
Log("cancel", _snapshot);
|
|
}
|
|
|
|
private void FailInvariant(string reason, string? detail)
|
|
{
|
|
_snapshot = _snapshot with
|
|
{
|
|
InvariantFailureCount = checked(_snapshot.InvariantFailureCount + 1),
|
|
};
|
|
string suffix = string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
|
|
_log($"[world-reveal] event=invariant-failure reason={reason}{suffix} {Describe(_snapshot)}");
|
|
}
|
|
|
|
private void Log(string eventName, WorldRevealLifecycleSnapshot snapshot) =>
|
|
_log($"[world-reveal] event={eventName} {Describe(snapshot)}");
|
|
|
|
private static string Describe(WorldRevealLifecycleSnapshot snapshot)
|
|
{
|
|
WorldRevealReadinessSnapshot ready = snapshot.Readiness;
|
|
return string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"generation={snapshot.Generation} kind={snapshot.Kind} " +
|
|
$"cell=0x{ready.DestinationCell:X8} indoor={ready.IsIndoor} " +
|
|
$"radius={ready.RequiredRenderRadius} unhydratable={ready.IsUnhydratable} " +
|
|
$"render={ready.IsRenderNeighborhoodReady} composites={ready.AreCompositeTexturesReady} " +
|
|
$"collision={ready.IsCollisionReady} ready={ready.IsReady} " +
|
|
$"materialized={snapshot.Materialized} completed={snapshot.Completed} " +
|
|
$"cancelled={snapshot.Cancelled} visible={snapshot.WorldViewportObserved} " +
|
|
$"failures={snapshot.InvariantFailureCount}");
|
|
}
|
|
}
|