refactor(runtime): acknowledge exact world host projections

This commit is contained in:
Erik 2026-07-26 18:27:41 +02:00
parent 73d0b54e38
commit 18d17d8bb1
17 changed files with 1677 additions and 72 deletions

View file

@ -192,8 +192,19 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
$"{checkpoint.Environment.ActiveDayGroupIndex}:" +
$"{(int)checkpoint.Environment.Weather}:" +
$"{(int)checkpoint.Environment.EnvironOverride};" +
$"environment-owner={checkpoint.EnvironmentOwnership.IsInitialized}:" +
$"{checkpoint.EnvironmentOwnership.DayGroupDefinitionCount}:" +
$"{checkpoint.EnvironmentOwnership.ActiveDayGroupCount};" +
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady};" +
$"transit-owner={checkpoint.TransitOwnership.BufferedTeleportDestinationCount}:" +
$"{checkpoint.TransitOwnership.PendingTeleportStartCount}:" +
$"{checkpoint.TransitOwnership.ActiveTeleportCount}:" +
$"{checkpoint.TransitOwnership.AcceptedTeleportDestinationCount}:" +
$"{checkpoint.TransitOwnership.ActiveRevealCount}:" +
$"{checkpoint.TransitOwnership.PendingDestinationReadinessCount}:" +
$"{checkpoint.TransitOwnership.HostProjectionCount}:" +
$"{checkpoint.TransitOwnership.PendingHostAcknowledgementCount}")));
}
public void OnLifecycle(in RuntimeLifecycleDelta delta) =>

View file

@ -128,6 +128,55 @@ public readonly record struct RuntimeDestinationReadiness(
&& IsCollisionReady));
}
[Flags]
public enum RuntimeWorldHostAcknowledgementStage
{
None = 0,
ProjectionRegistered = 1 << 0,
SimulationReleaseProjected = 1 << 1,
DestinationReservationReleased = 1 << 2,
TerminalProjected = 1 << 3,
}
/// <summary>
/// Exact identity of one graphical or direct host projection attached to a
/// Runtime reveal generation. It contains no presentation object.
/// </summary>
public readonly record struct RuntimeWorldHostProjectionToken(
long Generation,
uint DestinationCell)
{
public bool IsValid => Generation != 0 && DestinationCell != 0u;
}
public readonly record struct RuntimeWorldHostAcknowledgement(
RuntimeWorldHostProjectionToken Projection,
RuntimeWorldHostAcknowledgementStage Stage);
public readonly record struct RuntimeWorldHostProjectionSnapshot(
RuntimeWorldHostProjectionToken Token,
RuntimeWorldHostAcknowledgementStage PendingAcknowledgements,
bool ProjectionRegistered,
bool SimulationReleaseProjected,
bool DestinationReservationReleased,
bool IsSuperseding)
{
public int PendingAcknowledgementCount =>
Count(PendingAcknowledgements);
private static int Count(RuntimeWorldHostAcknowledgementStage stages)
{
int value = (int)stages;
int count = 0;
while (value != 0)
{
value &= value - 1;
count++;
}
return count;
}
}
public readonly record struct RuntimePortalSnapshot(
long Generation,
RuntimePortalKind Kind,
@ -166,6 +215,7 @@ public readonly record struct RuntimePortalSnapshot(
public interface IRuntimePortalView
{
RuntimePortalSnapshot Snapshot { get; }
RuntimeWorldTransitOwnershipSnapshot Ownership { get; }
}
public readonly record struct RuntimeStateCheckpoint(
@ -184,7 +234,9 @@ public readonly record struct RuntimeStateCheckpoint(
RuntimeActionSnapshot Actions,
RuntimeMovementSnapshot Movement,
RuntimeWorldEnvironmentSnapshot Environment,
RuntimePortalSnapshot Portal);
RuntimeWorldEnvironmentOwnershipSnapshot EnvironmentOwnership,
RuntimePortalSnapshot Portal,
RuntimeWorldTransitOwnershipSnapshot TransitOwnership);
public interface IGameRuntimeView
{

View file

@ -96,9 +96,15 @@ public readonly record struct RuntimeWorldEnvironmentSnapshot(
WeatherKind Weather,
EnvironOverride EnvironOverride);
public readonly record struct RuntimeWorldEnvironmentOwnershipSnapshot(
bool IsInitialized,
int DayGroupDefinitionCount,
int ActiveDayGroupCount);
public interface IRuntimeWorldEnvironmentView
{
RuntimeWorldEnvironmentSnapshot Snapshot { get; }
RuntimeWorldEnvironmentOwnershipSnapshot Ownership { get; }
}
/// <summary>
@ -152,6 +158,15 @@ public sealed class RuntimeWorldEnvironmentState
Weather.Kind,
Weather.Override);
public RuntimeWorldEnvironmentOwnershipSnapshot Ownership =>
CaptureOwnership();
public RuntimeWorldEnvironmentOwnershipSnapshot CaptureOwnership() =>
new(
IsInitialized,
_definition?.DayGroups.Count ?? 0,
ActiveDayGroupIndex >= 0 ? 1 : 0);
public void Initialize(RuntimeWorldEnvironmentDefinition definition)
{
ArgumentNullException.ThrowIfNull(definition);

View file

@ -2,6 +2,27 @@ using System.Globalization;
namespace AcDream.Runtime.World;
public readonly record struct RuntimeWorldTransitOwnershipSnapshot(
int BufferedTeleportDestinationCount,
int PendingTeleportStartCount,
int ActiveTeleportCount,
int AcceptedTeleportDestinationCount,
int ActiveRevealCount,
int PendingDestinationReadinessCount,
int HostProjectionCount,
int PendingHostAcknowledgementCount)
{
public bool IsSessionIdle =>
BufferedTeleportDestinationCount == 0
&& PendingTeleportStartCount == 0
&& ActiveTeleportCount == 0
&& AcceptedTeleportDestinationCount == 0
&& ActiveRevealCount == 0
&& PendingDestinationReadinessCount == 0
&& HostProjectionCount == 0
&& PendingHostAcknowledgementCount == 0;
}
/// <summary>
/// Canonical presentation-independent owner of local F751/Position
/// correlation and one login/portal reveal generation. The graphical or
@ -21,12 +42,34 @@ namespace AcDream.Runtime.World;
public sealed class RuntimeWorldTransitState
: IRuntimePortalView
{
private sealed class HostProjectionRecord(
RuntimeWorldHostProjectionToken token)
{
public RuntimeWorldHostProjectionToken Token { get; } = token;
public RuntimeWorldHostAcknowledgementStage Pending { get; set; } =
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered;
public bool ProjectionRegistered { get; set; }
public bool SimulationReleaseProjected { get; set; }
public bool DestinationReservationReleased { get; set; }
public bool IsSuperseding { get; set; }
public RuntimeWorldHostProjectionSnapshot Snapshot => new(
Token,
Pending,
ProjectionRegistered,
SimulationReleaseProjected,
DestinationReservationReleased,
IsSuperseding);
}
public static readonly TimeSpan RetailWaitCueDelay =
TimeSpan.FromSeconds(5);
private readonly Action<string> _log;
private readonly Dictionary<ushort, RuntimeTeleportDestination>
_bufferedDestinations = [];
private readonly Dictionary<long, HostProjectionRecord>
_hostProjections = [];
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
private long _nextGeneration;
private bool _hasLastTeleportStart;
@ -45,6 +88,8 @@ public sealed class RuntimeWorldTransitState
}
public RuntimePortalSnapshot Snapshot => _snapshot;
public RuntimeWorldTransitOwnershipSnapshot Ownership =>
CaptureOwnership();
public bool IsWorldSimulationAvailable =>
_snapshot.WorldSimulationAvailable;
public bool IsTeleportActive => _teleportActive;
@ -57,6 +102,30 @@ public sealed class RuntimeWorldTransitState
public int DiagnosticFailureCount { get; private set; }
public Exception? LastDiagnosticFailure { get; private set; }
public RuntimeWorldTransitOwnershipSnapshot CaptureOwnership()
{
int pendingHostAcknowledgements = 0;
foreach (HostProjectionRecord host in _hostProjections.Values)
{
pendingHostAcknowledgements = checked(
pendingHostAcknowledgements
+ host.Snapshot.PendingAcknowledgementCount);
}
bool revealActive = _snapshot.Generation != 0
&& !_snapshot.Completed
&& !_snapshot.Cancelled;
return new RuntimeWorldTransitOwnershipSnapshot(
_bufferedDestinations.Count,
_hasPendingTeleportStart ? 1 : 0,
_teleportActive ? 1 : 0,
_hasAcceptedDestination ? 1 : 0,
revealActive ? 1 : 0,
revealActive && !_snapshot.IsReady ? 1 : 0,
_hostProjections.Count,
pendingHostAcknowledgements);
}
/// <summary>
/// Starts initial-login reveal. Portal reveals must instead claim the
/// exact F751-correlated destination through
@ -69,15 +138,30 @@ public sealed class RuntimeWorldTransitState
/// Atomically claims the one accepted Position paired with the active F751
/// sequence and starts its reveal generation.
/// </summary>
public bool CanBeginPortalReveal(
ushort teleportSequence,
uint destinationCell)
{
if (!_teleportActive
|| teleportSequence != _activeTeleportSequence
|| !_hasAcceptedDestination)
{
return false;
}
RuntimeTeleportDestination destination = _acceptedDestination;
return destination.TeleportSequence == teleportSequence
&& destination.CellId != 0u
&& destination.CellId == destinationCell;
}
public bool TryBeginPortalReveal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
generation = 0;
if (!_teleportActive
|| teleportSequence != _activeTeleportSequence
|| !_hasAcceptedDestination)
if (!CanBeginPortalReveal(teleportSequence, destinationCell))
{
LogRejected(
"portal-begin-without-destination",
@ -89,19 +173,6 @@ public sealed class RuntimeWorldTransitState
}
RuntimeTeleportDestination destination = _acceptedDestination;
if (destination.TeleportSequence != teleportSequence
|| destination.CellId == 0u
|| destination.CellId != destinationCell)
{
FailInvariant(
"portal-begin-destination-mismatch",
$"sequence={teleportSequence} "
+ $"acceptedSequence={destination.TeleportSequence} "
+ $"expectedCell=0x{destination.CellId:X8} "
+ $"actualCell=0x{destinationCell:X8}");
return false;
}
generation = BeginRevealCore(
RuntimePortalKind.Portal,
destinationCell);
@ -110,6 +181,159 @@ public sealed class RuntimeWorldTransitState
return true;
}
/// <summary>
/// Registers one presentation-independent host projection against the
/// exact active reveal. Graphical and direct hosts receive the same token.
/// </summary>
public bool TryRegisterHostProjection(
long generation,
uint destinationCell,
out RuntimeWorldHostProjectionToken token)
{
token = new RuntimeWorldHostProjectionToken(
generation,
destinationCell);
if (!token.IsValid
|| generation != _snapshot.Generation
|| destinationCell != _snapshot.DestinationCell
|| _snapshot.Cancelled
|| _snapshot.Completed)
{
LogRejected(
"host-register-mismatch",
$"generation={generation} "
+ $"cell=0x{destinationCell:X8}");
token = default;
return false;
}
if (_hostProjections.TryGetValue(
generation,
out HostProjectionRecord? existing))
{
if (existing.Token == token)
return true;
LogRejected(
"host-register-conflict",
$"generation={generation} "
+ $"expectedCell=0x{existing.Token.DestinationCell:X8} "
+ $"actualCell=0x{destinationCell:X8}");
token = default;
return false;
}
_hostProjections.Add(
generation,
new HostProjectionRecord(token));
return true;
}
public bool TryGetHostProjection(
in RuntimeWorldHostProjectionToken token,
out RuntimeWorldHostProjectionSnapshot projection)
{
if (token.IsValid
&& _hostProjections.TryGetValue(
token.Generation,
out HostProjectionRecord? host)
&& host.Token == token)
{
projection = host.Snapshot;
return true;
}
projection = default;
return false;
}
/// <summary>
/// Marks the graphical reservation-release callback as due before the
/// host executes it. A thrown callback therefore leaves an exact pending
/// acknowledgement instead of losing the unfinished suffix.
/// </summary>
public bool RequireDestinationReservationRelease(
in RuntimeWorldHostProjectionToken token)
{
if (!TryGetHostRecord(token, out HostProjectionRecord? host))
return false;
if (host.DestinationReservationReleased)
return false;
host.Pending |= RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased;
return true;
}
/// <summary>
/// Converts an older host projection into an exact withdrawal transaction
/// while a replacement reveal keeps world simulation quiesced.
/// </summary>
public bool BeginHostProjectionSupersession(
in RuntimeWorldHostProjectionToken token)
{
if (!TryGetHostRecord(token, out HostProjectionRecord? host))
return false;
BeginHostProjectionSupersession(host);
return true;
}
public bool AcknowledgeHostProjection(
in RuntimeWorldHostAcknowledgement acknowledgement)
{
RuntimeWorldHostAcknowledgementStage stage =
acknowledgement.Stage;
if (!IsSingleStage(stage)
|| !TryGetHostRecord(
acknowledgement.Projection,
out HostProjectionRecord? host)
|| (host.Pending & stage) == 0)
{
return false;
}
switch (stage)
{
case RuntimeWorldHostAcknowledgementStage.ProjectionRegistered:
if (host.IsSuperseding)
return false;
host.ProjectionRegistered = true;
break;
case RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected:
if (host.Token.Generation == _snapshot.Generation
&& !_snapshot.WorldSimulationAvailable)
{
return false;
}
host.SimulationReleaseProjected = true;
break;
case RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased:
host.DestinationReservationReleased = true;
break;
case RuntimeWorldHostAcknowledgementStage.TerminalProjected:
if (!host.IsSuperseding
&& (host.Token.Generation != _snapshot.Generation
|| (!_snapshot.Completed
&& !_snapshot.Cancelled)))
{
return false;
}
if ((host.Pending & ~stage) != 0)
return false;
host.Pending &= ~stage;
_hostProjections.Remove(host.Token.Generation);
return true;
default:
return false;
}
host.Pending &= ~stage;
return true;
}
private long BeginRevealCore(
RuntimePortalKind kind,
uint destinationCell)
@ -125,6 +349,12 @@ public sealed class RuntimeWorldTransitState
{
Log("superseded", _snapshot);
}
if (_hostProjections.TryGetValue(
_snapshot.Generation,
out HostProjectionRecord? superseded))
{
BeginHostProjectionSupersession(superseded);
}
long generation = checked(++_nextGeneration);
int failures = _snapshot.InvariantFailureCount;
@ -372,6 +602,10 @@ public sealed class RuntimeWorldTransitState
WorldSimulationAvailable = true,
PortalMaterializationCount = count,
};
RequireHostStage(
generation,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
Log("materialized", _snapshot);
return true;
}
@ -444,11 +678,16 @@ public sealed class RuntimeWorldTransitState
return false;
}
bool simulationWasAvailable =
_snapshot.WorldSimulationAvailable;
_snapshot = _snapshot with
{
Completed = true,
WorldSimulationAvailable = true,
};
RequireTerminalHostProjection(
generation,
requireSimulationRelease: !simulationWasAvailable);
Log("complete", _snapshot);
return true;
}
@ -463,11 +702,16 @@ public sealed class RuntimeWorldTransitState
return false;
}
bool simulationWasAvailable =
_snapshot.WorldSimulationAvailable;
_snapshot = _snapshot with
{
Cancelled = true,
WorldSimulationAvailable = true,
};
RequireTerminalHostProjection(
generation,
requireSimulationRelease: !simulationWasAvailable);
Log("cancel", _snapshot);
return true;
}
@ -479,6 +723,17 @@ public sealed class RuntimeWorldTransitState
/// </summary>
public void ResetSession()
{
if (_hostProjections.Count != 0)
{
RuntimeWorldTransitOwnershipSnapshot ownership =
CaptureOwnership();
throw new InvalidOperationException(
"World transit cannot reset while a host projection "
+ "still owns an acknowledgement suffix "
+ $"(hosts={ownership.HostProjectionCount}, "
+ $"pending={ownership.PendingHostAcknowledgementCount}).");
}
_snapshot = RuntimePortalSnapshot.Idle;
EndTeleport();
_hasLastTeleportStart = false;
@ -486,6 +741,105 @@ public sealed class RuntimeWorldTransitState
_bufferedDestinations.Clear();
}
private bool TryGetHostRecord(
in RuntimeWorldHostProjectionToken token,
out HostProjectionRecord host)
{
if (token.IsValid
&& _hostProjections.TryGetValue(
token.Generation,
out HostProjectionRecord? candidate)
&& candidate.Token == token)
{
host = candidate;
return true;
}
host = null!;
return false;
}
private void RequireHostStage(
long generation,
RuntimeWorldHostAcknowledgementStage stage)
{
if (!_hostProjections.TryGetValue(
generation,
out HostProjectionRecord? host))
{
return;
}
bool alreadyAcknowledged = stage switch
{
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered =>
host.ProjectionRegistered,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected =>
host.SimulationReleaseProjected,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased =>
host.DestinationReservationReleased,
_ => false,
};
if (!alreadyAcknowledged)
host.Pending |= stage;
}
private void RequireTerminalHostProjection(
long generation,
bool requireSimulationRelease)
{
if (!_hostProjections.TryGetValue(
generation,
out HostProjectionRecord? host))
{
return;
}
// A host that is terminating no longer has to finish acquiring a
// projection. It must instead release every resource it did acquire.
host.Pending &=
~RuntimeWorldHostAcknowledgementStage.ProjectionRegistered;
if (requireSimulationRelease
&& !host.SimulationReleaseProjected)
{
host.Pending |= RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected;
}
if (!host.DestinationReservationReleased)
{
host.Pending |= RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased;
}
host.Pending |=
RuntimeWorldHostAcknowledgementStage.TerminalProjected;
}
private static void BeginHostProjectionSupersession(
HostProjectionRecord host)
{
host.IsSuperseding = true;
host.Pending &=
~(RuntimeWorldHostAcknowledgementStage.ProjectionRegistered
| RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
if (!host.DestinationReservationReleased)
{
host.Pending |= RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased;
}
host.Pending |=
RuntimeWorldHostAcknowledgementStage.TerminalProjected;
}
private static bool IsSingleStage(
RuntimeWorldHostAcknowledgementStage stage)
{
int value = (int)stage;
return value != 0 && (value & (value - 1)) == 0;
}
private bool IsCurrentPortalDestination(
long generation,
ushort teleportSequence,