fix(world): remove non-retail portal exit fade
This commit is contained in:
parent
842bd89c16
commit
dded9e6b17
21 changed files with 919 additions and 347 deletions
|
|
@ -118,7 +118,7 @@ public sealed class StreamingController
|
|||
/// so the player arrives to a near-empty world (the "Fort Tethana only one landblock
|
||||
/// loaded" symptom) and their cell-walk can't root into neighbour cells yet (the
|
||||
/// transient walk-through-walls). The far ring still drains at the budget. The eager
|
||||
/// apply runs during the teleport hold (behind the fade), so the GPU spike is hidden.
|
||||
/// apply runs while the portal viewport replaces the world, so the GPU spike is hidden.
|
||||
/// </summary>
|
||||
public int PriorityRadius { get; set; }
|
||||
|
||||
|
|
@ -377,7 +377,7 @@ public sealed class StreamingController
|
|||
/// SYNCHRONOUSLY drop every resident landblock (render slot + physics + state) so none
|
||||
/// survives into the next frame stale, and no async unload can race a re-bake, then null
|
||||
/// the region so the next <see cref="NormalTick"/> re-bootstraps the WHOLE window fresh at
|
||||
/// the new origin (the near ring is priority-applied behind the fade; the rest streams in).
|
||||
/// the new origin (the near ring is priority-applied during portal travel; the rest streams in).
|
||||
/// A sealed-dungeon destination uses <see cref="PreCollapseToDungeon"/> instead.
|
||||
/// </summary>
|
||||
public void ForceReloadWindow()
|
||||
|
|
@ -405,7 +405,7 @@ public sealed class StreamingController
|
|||
/// (rendering at its old world position as "floating terrain at the horizon"), and rapid
|
||||
/// hops accumulated them faster than they cleared — a runaway resident count (951 observed
|
||||
/// vs a 625 window) that also dragged FPS. Loads inside the teleport near ring
|
||||
/// (<see cref="PriorityRadius"/>, applied behind the fade) likewise bypass the budget so the
|
||||
/// (<see cref="PriorityRadius"/>, applied during portal travel) likewise bypass the budget so the
|
||||
/// player materialises in a loaded world.
|
||||
/// </summary>
|
||||
private void DrainAndApply()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Classifies whether a local-player teleport crosses an AC landblock boundary.
|
||||
/// Cell identity comes from the complete retail <c>Position</c>, never from its
|
||||
/// frame coordinates: dungeon EnvCells may have valid negative local origins.
|
||||
/// Classifies both whether the player crosses an AC landblock boundary and
|
||||
/// whether acdream's asynchronous streaming origin must recenter. These are
|
||||
/// distinct while a teleport destination is aimed but not yet placed. Cell
|
||||
/// identity comes from the complete retail <c>Position</c>, never from its frame
|
||||
/// coordinates: dungeon EnvCells may have valid negative local origins.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail carries <c>Position::objcell_id</c> through
|
||||
|
|
@ -14,9 +16,12 @@ namespace AcDream.App.Streaming;
|
|||
/// </remarks>
|
||||
internal readonly record struct TeleportLandblockTransition(
|
||||
uint SourceLandblockId,
|
||||
uint DestinationLandblockId)
|
||||
uint DestinationLandblockId,
|
||||
uint StreamingCenterLandblockId)
|
||||
{
|
||||
public bool CrossesLandblock => SourceLandblockId != DestinationLandblockId;
|
||||
public bool ChangesStreamingCenter =>
|
||||
StreamingCenterLandblockId != DestinationLandblockId;
|
||||
|
||||
/// <summary>
|
||||
/// Build the transition from the player's current cell and the received
|
||||
|
|
@ -34,7 +39,8 @@ internal readonly record struct TeleportLandblockTransition(
|
|||
|
||||
return new TeleportLandblockTransition(
|
||||
sourceLandblockId,
|
||||
NormalizeLandblockId(destinationCellId));
|
||||
NormalizeLandblockId(destinationCellId),
|
||||
NormalizeLandblockId(currentStreamingCenterLandblockId));
|
||||
}
|
||||
|
||||
private static uint NormalizeLandblockId(uint cellOrLandblockId)
|
||||
|
|
|
|||
150
src/AcDream.App/Streaming/TeleportTransitCoordinator.cs
Normal file
150
src/AcDream.App/Streaming/TeleportTransitCoordinator.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Correlates F751 notifications with accepted Position packets without
|
||||
/// imposing presentation order on canonical physics. F751 deliberately does
|
||||
/// not consume retail's TELEPORT_TS, and the matching Position may be observed
|
||||
/// on either side of the notification.
|
||||
/// </summary>
|
||||
internal sealed class TeleportTransitCoordinator<TDestination>
|
||||
where TDestination : struct
|
||||
{
|
||||
private bool _hasLastStart;
|
||||
private ushort _lastStartSequence;
|
||||
private readonly Dictionary<ushort, TDestination> _bufferedDestinations = new();
|
||||
private bool _hasPendingStart;
|
||||
private ushort _pendingStartSequence;
|
||||
private bool _destinationAccepted;
|
||||
|
||||
public bool IsActive { get; private set; }
|
||||
public ushort Sequence { get; private set; }
|
||||
public bool HasPendingStart => _hasPendingStart;
|
||||
|
||||
/// <summary>Reject retransmitted or older F751 notifications.</summary>
|
||||
public bool CanBegin(ushort sequence) =>
|
||||
!_hasLastStart || IsNewer(_lastStartSequence, sequence);
|
||||
|
||||
/// <summary>
|
||||
/// Record a fresh notification lifetime. Presentation activation is a
|
||||
/// separate edge because acdream's optional developer camera can
|
||||
/// temporarily withdraw the normal player projection.
|
||||
/// </summary>
|
||||
public bool QueueStart(ushort sequence)
|
||||
{
|
||||
if (!CanBegin(sequence))
|
||||
return false;
|
||||
|
||||
PruneDestinationsOlderThan(sequence);
|
||||
IsActive = false;
|
||||
Sequence = 0;
|
||||
_hasPendingStart = true;
|
||||
_pendingStartSequence = sequence;
|
||||
_hasLastStart = true;
|
||||
_lastStartSequence = sequence;
|
||||
_destinationAccepted = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate the queued presentation and consume a destination that was
|
||||
/// accepted before the player projection became available.
|
||||
/// </summary>
|
||||
public bool Activate(out TDestination bufferedDestination)
|
||||
{
|
||||
bufferedDestination = default;
|
||||
if (!_hasPendingStart)
|
||||
return false;
|
||||
|
||||
IsActive = true;
|
||||
Sequence = _pendingStartSequence;
|
||||
_hasPendingStart = false;
|
||||
_pendingStartSequence = 0;
|
||||
_destinationAccepted = false;
|
||||
|
||||
if (!_bufferedDestinations.Remove(Sequence, out bufferedDestination))
|
||||
return false;
|
||||
|
||||
_destinationAccepted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offer an accepted Position packet. A matching active transit consumes
|
||||
/// its first packet even when TELEPORT_TS was already advanced. Without a
|
||||
/// matching notification, only a timestamp-advancing packet is buffered;
|
||||
/// ordinary movement can never become a future portal destination.
|
||||
/// </summary>
|
||||
public bool OfferDestination(
|
||||
ushort sequence,
|
||||
bool teleportTimestampAdvanced,
|
||||
TDestination destination,
|
||||
out TDestination acceptedDestination)
|
||||
{
|
||||
acceptedDestination = default;
|
||||
|
||||
// An advancing TELEPORT_TS makes every older destination impossible
|
||||
// to match a future F751. Retire those generations now so a skipped
|
||||
// notification cannot retain a stale destination through u16 wrap.
|
||||
if (teleportTimestampAdvanced)
|
||||
PruneDestinationsOlderThan(sequence);
|
||||
|
||||
if (IsActive && sequence == Sequence)
|
||||
{
|
||||
if (_destinationAccepted)
|
||||
return false;
|
||||
|
||||
_destinationAccepted = true;
|
||||
acceptedDestination = destination;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_hasPendingStart && sequence == _pendingStartSequence)
|
||||
{
|
||||
_bufferedDestinations.TryAdd(sequence, destination);
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort correlationSequence = _hasPendingStart
|
||||
? _pendingStartSequence
|
||||
: Sequence;
|
||||
bool mayBelongToFutureStart = (!IsActive && !_hasPendingStart)
|
||||
|| IsNewer(correlationSequence, sequence);
|
||||
if (!teleportTimestampAdvanced || !mayBelongToFutureStart)
|
||||
return false;
|
||||
|
||||
_bufferedDestinations.TryAdd(sequence, destination);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>End presentation while retaining duplicate and reorder history.</summary>
|
||||
public void EndActive()
|
||||
{
|
||||
IsActive = false;
|
||||
Sequence = 0;
|
||||
_hasPendingStart = false;
|
||||
_pendingStartSequence = 0;
|
||||
_destinationAccepted = false;
|
||||
}
|
||||
|
||||
/// <summary>Clear every correlation value at a network-session boundary.</summary>
|
||||
public void ClearSession()
|
||||
{
|
||||
EndActive();
|
||||
_hasLastStart = false;
|
||||
_lastStartSequence = 0;
|
||||
_bufferedDestinations.Clear();
|
||||
}
|
||||
|
||||
private static bool IsNewer(ushort current, ushort incoming)
|
||||
=> AcDream.Core.Physics.PhysicsTimestampGate.IsNewer(current, incoming);
|
||||
|
||||
private void PruneDestinationsOlderThan(ushort sequence)
|
||||
{
|
||||
foreach (ushort bufferedSequence in _bufferedDestinations.Keys.ToArray())
|
||||
{
|
||||
if (IsNewer(bufferedSequence, sequence))
|
||||
_bufferedDestinations.Remove(bufferedSequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue