Publish the retail blocking-for-cells edge before deferred recenter work, freeze old-world presentation/simulation/audio, and advance full-window retirement from exact metered entity and owner cursors. This removes synchronous portal teardown without allowing retained owners to remain observable.
109 lines
3.3 KiB
C#
109 lines
3.3 KiB
C#
using AcDream.App.Audio;
|
|
using AcDream.Core.Selection;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// Read-only generation gate shared by world simulation and presentation.
|
|
/// A hard login/portal reveal boundary makes the prior world unavailable
|
|
/// immediately while its physical owners retire over later frames.
|
|
/// </summary>
|
|
public interface IWorldGenerationAvailability
|
|
{
|
|
bool IsWorldAvailable { get; }
|
|
long QuiescedGeneration { get; }
|
|
}
|
|
|
|
internal sealed class AlwaysAvailableWorldGeneration
|
|
: IWorldGenerationAvailability
|
|
{
|
|
public static AlwaysAvailableWorldGeneration Instance { get; } = new();
|
|
public bool IsWorldAvailable => true;
|
|
public long QuiescedGeneration => 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single-writer state behind <see cref="IWorldGenerationAvailability"/>.
|
|
/// Superseding reveal generations stay continuously quiesced; only the exact
|
|
/// active generation may reopen the world.
|
|
/// </summary>
|
|
internal sealed class WorldGenerationAvailabilityState
|
|
: IWorldGenerationAvailability
|
|
{
|
|
public bool IsWorldAvailable => QuiescedGeneration == 0;
|
|
public long QuiescedGeneration { get; private set; }
|
|
|
|
public void Begin(long generation)
|
|
{
|
|
if (generation <= 0)
|
|
throw new ArgumentOutOfRangeException(nameof(generation));
|
|
|
|
QuiescedGeneration = generation;
|
|
}
|
|
|
|
public bool End(long generation)
|
|
{
|
|
if (generation <= 0 || QuiescedGeneration != generation)
|
|
return false;
|
|
|
|
QuiescedGeneration = 0;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns the immediate observable edge of retail
|
|
/// <c>SmartBox::UseTime @ 0x00455410</c>'s <c>blocking_for_cells</c>
|
|
/// branch. Deferred teardown may retain memory, but the old generation cannot
|
|
/// keep rendering, targeting, ticking, or producing world audio.
|
|
/// </summary>
|
|
internal sealed class WorldGenerationQuiescence
|
|
{
|
|
private readonly WorldGenerationAvailabilityState _availability;
|
|
private readonly SelectionState _selection;
|
|
private readonly GpuWorldState _world;
|
|
private readonly IWorldAudioQuiescence? _audio;
|
|
|
|
public WorldGenerationQuiescence(
|
|
WorldGenerationAvailabilityState availability,
|
|
SelectionState selection,
|
|
GpuWorldState world,
|
|
IWorldAudioQuiescence? audio)
|
|
{
|
|
_availability = availability
|
|
?? throw new ArgumentNullException(nameof(availability));
|
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
|
_audio = audio;
|
|
}
|
|
|
|
public IWorldGenerationAvailability Availability => _availability;
|
|
|
|
public void Begin(long generation)
|
|
{
|
|
bool firstEdge = _availability.IsWorldAvailable;
|
|
uint? selected = firstEdge ? _selection.SelectedObjectId : null;
|
|
bool clearWorldSelection =
|
|
selected is uint selectedGuid
|
|
&& _world.IsLiveEntityVisible(selectedGuid);
|
|
|
|
_availability.Begin(generation);
|
|
if (!firstEdge)
|
|
return;
|
|
|
|
if (clearWorldSelection)
|
|
{
|
|
_selection.Clear(
|
|
SelectionChangeSource.System,
|
|
SelectionChangeReason.Cleared);
|
|
}
|
|
|
|
_audio?.SuspendWorldAudio();
|
|
}
|
|
|
|
public void End(long generation)
|
|
{
|
|
if (_availability.End(generation))
|
|
_audio?.ResumeWorldAudio();
|
|
}
|
|
}
|