refactor(runtime): own world reveal generation
This commit is contained in:
parent
4ab98b080e
commit
a6860d5563
26 changed files with 1294 additions and 502 deletions
385
src/AcDream.Runtime/World/RuntimeWorldTransitState.cs
Normal file
385
src/AcDream.Runtime/World/RuntimeWorldTransitState.cs
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace AcDream.Runtime.World;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical presentation-independent owner of one login/portal reveal
|
||||
/// generation. The graphical or headless host supplies typed destination
|
||||
/// readiness acknowledgements; Runtime owns which generation and destination
|
||||
/// those acknowledgements are allowed to advance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail ordering follows <c>SmartBox::TeleportPlayer @ 0x00453910</c>,
|
||||
/// <c>SmartBox::UseTime @ 0x00455410</c>, and
|
||||
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c>. Destination
|
||||
/// materialization resumes simulation before the later normal-world viewport
|
||||
/// edge.
|
||||
/// </remarks>
|
||||
public sealed class RuntimeWorldTransitState
|
||||
: IRuntimePortalView
|
||||
{
|
||||
public static readonly TimeSpan RetailWaitCueDelay =
|
||||
TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly Action<string> _log;
|
||||
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
|
||||
private long _nextGeneration;
|
||||
|
||||
public RuntimeWorldTransitState(Action<string>? log = null)
|
||||
{
|
||||
_log = log ?? (_ => { });
|
||||
}
|
||||
|
||||
public RuntimePortalSnapshot Snapshot => _snapshot;
|
||||
public bool IsWorldSimulationAvailable =>
|
||||
_snapshot.WorldSimulationAvailable;
|
||||
public int DiagnosticFailureCount { get; private set; }
|
||||
public Exception? LastDiagnosticFailure { get; private set; }
|
||||
|
||||
public long BeginReveal(
|
||||
RuntimePortalKind kind,
|
||||
uint destinationCell)
|
||||
{
|
||||
if (kind is RuntimePortalKind.None)
|
||||
throw new ArgumentOutOfRangeException(nameof(kind));
|
||||
if (destinationCell == 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||
|
||||
if (_snapshot.Generation != 0
|
||||
&& !_snapshot.Cancelled
|
||||
&& !_snapshot.WorldViewportObserved)
|
||||
{
|
||||
Log("superseded", _snapshot);
|
||||
}
|
||||
|
||||
long generation = checked(++_nextGeneration);
|
||||
int failures = _snapshot.InvariantFailureCount;
|
||||
int materializations = _snapshot.PortalMaterializationCount;
|
||||
_snapshot = new RuntimePortalSnapshot(
|
||||
generation,
|
||||
kind,
|
||||
new RuntimeDestinationReadiness(
|
||||
generation,
|
||||
destinationCell,
|
||||
IsIndoor: IsIndoor(destinationCell),
|
||||
IsUnhydratable: false,
|
||||
RequiredRenderRadius: 0,
|
||||
IsRenderNeighborhoodReady: false,
|
||||
AreCompositeTexturesReady: false,
|
||||
IsCollisionReady: false),
|
||||
Materialized: false,
|
||||
Completed: false,
|
||||
Cancelled: false,
|
||||
WorldViewportObserved: false,
|
||||
WorldSimulationAvailable: false,
|
||||
InvariantFailureCount: failures,
|
||||
WaitCueShown: false,
|
||||
PortalMaterializationCount: materializations);
|
||||
Log("begin", _snapshot);
|
||||
return generation;
|
||||
}
|
||||
|
||||
public bool AcknowledgeDestinationReadiness(
|
||||
in RuntimeDestinationReadiness acknowledgement)
|
||||
{
|
||||
if (!ValidateActive(
|
||||
acknowledgement.Generation,
|
||||
acknowledgement.DestinationCell,
|
||||
"readiness"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_snapshot.Readiness.IsReady)
|
||||
{
|
||||
if (_snapshot.Readiness == acknowledgement)
|
||||
return false;
|
||||
|
||||
FailInvariant(
|
||||
"readiness-changed-after-ready",
|
||||
$"accepted={DescribeReadiness(_snapshot.Readiness)} "
|
||||
+ $"actual={DescribeReadiness(acknowledgement)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isIndoor = IsIndoor(acknowledgement.DestinationCell);
|
||||
int requiredRenderRadius = isIndoor ? 0 : 1;
|
||||
if (acknowledgement.IsIndoor != isIndoor
|
||||
|| acknowledgement.RequiredRenderRadius
|
||||
!= requiredRenderRadius)
|
||||
{
|
||||
FailInvariant(
|
||||
"invalid-readiness-shape",
|
||||
$"indoor={acknowledgement.IsIndoor} "
|
||||
+ $"expectedIndoor={isIndoor} "
|
||||
+ $"radius={acknowledgement.RequiredRenderRadius} "
|
||||
+ $"expectedRadius={requiredRenderRadius}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_snapshot.Readiness == acknowledgement)
|
||||
return false;
|
||||
|
||||
_snapshot = _snapshot with { Readiness = acknowledgement };
|
||||
Log("readiness", _snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AcknowledgeMaterialized(
|
||||
long generation,
|
||||
uint destinationCell)
|
||||
{
|
||||
if (!ValidateActive(generation, destinationCell, "materialized"))
|
||||
return false;
|
||||
if (_snapshot.Materialized)
|
||||
return false;
|
||||
if (!_snapshot.Readiness.IsReady)
|
||||
{
|
||||
FailInvariant("materialized-before-ready", null);
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = _snapshot.PortalMaterializationCount;
|
||||
if (_snapshot.Kind == RuntimePortalKind.Portal)
|
||||
count = checked(count + 1);
|
||||
_snapshot = _snapshot with
|
||||
{
|
||||
Materialized = true,
|
||||
WorldSimulationAvailable = true,
|
||||
PortalMaterializationCount = count,
|
||||
};
|
||||
Log("materialized", _snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AcknowledgeWorldViewportVisible(long generation)
|
||||
{
|
||||
if (!ValidateGeneration(
|
||||
generation,
|
||||
"world-visible",
|
||||
allowCompleted: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_snapshot.WorldViewportObserved)
|
||||
return false;
|
||||
if (!_snapshot.Readiness.IsReady)
|
||||
{
|
||||
FailInvariant("viewport-before-ready", null);
|
||||
return false;
|
||||
}
|
||||
|
||||
_snapshot = _snapshot with { WorldViewportObserved = true };
|
||||
Log("world-visible", _snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ObserveWait(long generation, TimeSpan elapsed)
|
||||
{
|
||||
if (generation == 0
|
||||
|| generation != _snapshot.Generation
|
||||
|| _snapshot.Cancelled
|
||||
|| _snapshot.Completed
|
||||
|| elapsed < RetailWaitCueDelay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_snapshot.WaitCueShown)
|
||||
{
|
||||
_snapshot = _snapshot with { WaitCueShown = true };
|
||||
SafeLog(
|
||||
$"[world-reveal] event=wait-cue "
|
||||
+ $"elapsedMs={elapsed.TotalMilliseconds:F0} "
|
||||
+ Describe(_snapshot));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Complete(long generation)
|
||||
{
|
||||
if (!ValidateGeneration(
|
||||
generation,
|
||||
"complete",
|
||||
allowCompleted: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_snapshot.Completed)
|
||||
return false;
|
||||
if (!_snapshot.Readiness.IsReady)
|
||||
{
|
||||
FailInvariant("complete-before-ready", null);
|
||||
return false;
|
||||
}
|
||||
if (_snapshot.Kind == RuntimePortalKind.Portal
|
||||
&& !_snapshot.Materialized)
|
||||
{
|
||||
FailInvariant("portal-complete-before-materialized", null);
|
||||
return false;
|
||||
}
|
||||
|
||||
_snapshot = _snapshot with
|
||||
{
|
||||
Completed = true,
|
||||
WorldSimulationAvailable = true,
|
||||
};
|
||||
Log("complete", _snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Cancel(long generation)
|
||||
{
|
||||
if (generation == 0
|
||||
|| generation != _snapshot.Generation
|
||||
|| _snapshot.Cancelled
|
||||
|| _snapshot.Completed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_snapshot = _snapshot with
|
||||
{
|
||||
Cancelled = true,
|
||||
WorldSimulationAvailable = true,
|
||||
};
|
||||
Log("cancel", _snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears session-visible state while retaining the monotonically
|
||||
/// increasing generation counter so a delayed prior-session host
|
||||
/// acknowledgement cannot match a future generation.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
_snapshot = RuntimePortalSnapshot.Idle;
|
||||
}
|
||||
|
||||
private bool ValidateActive(
|
||||
long generation,
|
||||
uint destinationCell,
|
||||
string eventName)
|
||||
{
|
||||
if (!ValidateGeneration(
|
||||
generation,
|
||||
eventName,
|
||||
allowCompleted: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (destinationCell == 0u
|
||||
|| destinationCell != _snapshot.DestinationCell)
|
||||
{
|
||||
FailInvariant(
|
||||
$"{eventName}-destination-mismatch",
|
||||
$"expected=0x{_snapshot.DestinationCell:X8} "
|
||||
+ $"actual=0x{destinationCell:X8}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateGeneration(
|
||||
long generation,
|
||||
string eventName,
|
||||
bool allowCompleted)
|
||||
{
|
||||
if (generation == 0 || generation != _snapshot.Generation)
|
||||
{
|
||||
LogRejected(
|
||||
$"{eventName}-generation-mismatch",
|
||||
$"expected={_snapshot.Generation} actual={generation}");
|
||||
return false;
|
||||
}
|
||||
if (_snapshot.Cancelled
|
||||
|| (!allowCompleted && _snapshot.Completed))
|
||||
{
|
||||
LogRejected(
|
||||
$"{eventName}-after-terminal",
|
||||
$"completed={_snapshot.Completed} "
|
||||
+ $"cancelled={_snapshot.Cancelled}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void FailInvariant(string reason, string? detail)
|
||||
{
|
||||
_snapshot = _snapshot with
|
||||
{
|
||||
InvariantFailureCount =
|
||||
checked(_snapshot.InvariantFailureCount + 1),
|
||||
};
|
||||
string suffix =
|
||||
string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
|
||||
SafeLog(
|
||||
$"[world-reveal] event=invariant-failure "
|
||||
+ $"reason={reason}{suffix} {Describe(_snapshot)}");
|
||||
}
|
||||
|
||||
private void LogRejected(string reason, string? detail)
|
||||
{
|
||||
string suffix =
|
||||
string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
|
||||
SafeLog(
|
||||
$"[world-reveal] event=rejected "
|
||||
+ $"reason={reason}{suffix} {Describe(_snapshot)}");
|
||||
}
|
||||
|
||||
private void Log(string eventName, RuntimePortalSnapshot snapshot) =>
|
||||
SafeLog($"[world-reveal] event={eventName} {Describe(snapshot)}");
|
||||
|
||||
private void SafeLog(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
_log(message);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
DiagnosticFailureCount =
|
||||
checked(DiagnosticFailureCount + 1);
|
||||
LastDiagnosticFailure = error;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Describe(RuntimePortalSnapshot snapshot)
|
||||
{
|
||||
RuntimeDestinationReadiness 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} "
|
||||
+ $"simulation={snapshot.WorldSimulationAvailable} "
|
||||
+ $"failures={snapshot.InvariantFailureCount}");
|
||||
}
|
||||
|
||||
private static string DescribeReadiness(
|
||||
in RuntimeDestinationReadiness readiness) =>
|
||||
string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"indoor={readiness.IsIndoor},"
|
||||
+ $"unhydratable={readiness.IsUnhydratable},"
|
||||
+ $"radius={readiness.RequiredRenderRadius},"
|
||||
+ $"render={readiness.IsRenderNeighborhoodReady},"
|
||||
+ $"composites={readiness.AreCompositeTexturesReady},"
|
||||
+ $"collision={readiness.IsCollisionReady}");
|
||||
|
||||
private static bool IsIndoor(uint cellId) =>
|
||||
(cellId & 0xFFFFu) >= 0x0100u;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue