refactor(world): extract live projection mechanics
Move appearance rebinding, collision construction, default-pose resolution, local shadow ownership, and exact leave-world presentation into focused owners. Preserve retail parent ordering with staged validation, committed recovery, recursive attached-subtree withdrawal, and retryable exact teardown across parent, pickup, position, unwield, and pose-loss edges. Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
c87bac31a0
commit
69a2ca0c6d
26 changed files with 4172 additions and 430 deletions
|
|
@ -0,0 +1,175 @@
|
|||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Plugins;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Owns retail <c>CPhysicsObj::leave_world</c>'s App projection tail for a
|
||||
/// still-live exact record. Component projection is removed before canonical
|
||||
/// runtime withdrawal, matching the shipped failure/retry boundary.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityProjectionWithdrawalController
|
||||
{
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly ProjectileController _projectiles;
|
||||
private readonly WorldGameState _worldState;
|
||||
private readonly WorldEvents _worldEvents;
|
||||
private readonly ShadowObjectRegistry _shadows;
|
||||
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||
private readonly LocalPlayerShadowState _localPlayerShadow;
|
||||
|
||||
public LiveEntityProjectionWithdrawalController(
|
||||
LiveEntityRuntime runtime,
|
||||
ProjectileController projectiles,
|
||||
WorldGameState worldState,
|
||||
WorldEvents worldEvents,
|
||||
ShadowObjectRegistry shadows,
|
||||
EntityEffectPoseRegistry effectPoses,
|
||||
LocalPlayerShadowState localPlayerShadow)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_projectiles = projectiles ?? throw new ArgumentNullException(nameof(projectiles));
|
||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||
_worldEvents = worldEvents ?? throw new ArgumentNullException(nameof(worldEvents));
|
||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||
_effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses));
|
||||
_localPlayerShadow = localPlayerShadow
|
||||
?? throw new ArgumentNullException(nameof(localPlayerShadow));
|
||||
}
|
||||
|
||||
public bool Withdraw(uint serverGuid, uint localPlayerGuid)
|
||||
{
|
||||
if (!_runtime.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| record.WorldEntity is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Withdraw(record, localPlayerGuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Withdraws only the captured incarnation and projection operation. A
|
||||
/// leave-world observer may accept a fresh same-generation Position and
|
||||
/// reproject this record; that newer projection must survive the displaced
|
||||
/// outer operation just as a newer INSTANCE_TS must.
|
||||
/// </summary>
|
||||
public bool Withdraw(LiveEntityRecord record, uint localPlayerGuid)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!_runtime.IsCurrentRecord(record) || record.WorldEntity is null)
|
||||
return false;
|
||||
|
||||
return Withdraw(
|
||||
record,
|
||||
record.PositionAuthorityVersion,
|
||||
record.ProjectionMutationVersion,
|
||||
localPlayerGuid);
|
||||
}
|
||||
|
||||
public bool Withdraw(
|
||||
LiveEntityRecord record,
|
||||
ulong positionAuthorityVersion,
|
||||
ulong projectionMutationVersion,
|
||||
uint localPlayerGuid)
|
||||
{
|
||||
ExactProjectionWithdrawalOutcome outcome = WithdrawExact(
|
||||
record,
|
||||
positionAuthorityVersion,
|
||||
projectionMutationVersion,
|
||||
localPlayerGuid);
|
||||
if (outcome.Failure is not null)
|
||||
throw outcome.Failure;
|
||||
return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed;
|
||||
}
|
||||
|
||||
public ExactProjectionWithdrawalOutcome WithdrawExact(
|
||||
LiveEntityRecord record,
|
||||
ulong positionAuthorityVersion,
|
||||
ulong projectionMutationVersion,
|
||||
uint localPlayerGuid)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!_runtime.IsCurrentRecord(record)
|
||||
|| record.WorldEntity is null
|
||||
|| record.PositionAuthorityVersion != positionAuthorityVersion
|
||||
|| record.ProjectionMutationVersion != projectionMutationVersion)
|
||||
{
|
||||
return new(
|
||||
ExactProjectionWithdrawalDisposition.Superseded,
|
||||
Failure: null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LeaveWorld(record, localPlayerGuid);
|
||||
bool completed = _runtime.WithdrawLiveEntityProjection(
|
||||
record,
|
||||
positionAuthorityVersion,
|
||||
projectionMutationVersion);
|
||||
return new(
|
||||
completed
|
||||
? ExactProjectionWithdrawalDisposition.Completed
|
||||
: ExactProjectionWithdrawalDisposition.Superseded,
|
||||
Failure: null);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
bool current = _runtime.IsCurrentRecord(record);
|
||||
ExactProjectionWithdrawalDisposition disposition =
|
||||
current
|
||||
&& record.PositionAuthorityVersion == positionAuthorityVersion
|
||||
&& record.ProjectionMutationVersion == projectionMutationVersion
|
||||
&& record.IsSpatiallyProjected
|
||||
? ExactProjectionWithdrawalDisposition.Pending
|
||||
: current
|
||||
&& record.PositionAuthorityVersion == positionAuthorityVersion
|
||||
&& !record.IsSpatiallyProjected
|
||||
? ExactProjectionWithdrawalDisposition.Completed
|
||||
: ExactProjectionWithdrawalDisposition.Superseded;
|
||||
return new(disposition, error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes only the shorter world-facing component projection. This exact
|
||||
/// overload is also used by logical teardown after the active GUID table
|
||||
/// may already expose a replacement incarnation.
|
||||
/// </summary>
|
||||
public void LeaveWorld(LiveEntityRecord record, uint localPlayerGuid)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.WorldEntity is not { } entity)
|
||||
return;
|
||||
|
||||
bool retainedProjectileShadow = _projectiles.LeaveWorld(record);
|
||||
if (!retainedProjectileShadow)
|
||||
_shadows.Suspend(entity.Id);
|
||||
_worldState.RemoveById(entity.Id);
|
||||
_worldEvents.ForgetEntity(entity.Id);
|
||||
if (record.ServerGuid == localPlayerGuid
|
||||
&& (!_runtime.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
||||
|| ReferenceEquals(current, record)))
|
||||
{
|
||||
_localPlayerShadow.Clear();
|
||||
}
|
||||
|
||||
// Pose-attached effects keep logical ownership while cell-less. A
|
||||
// missing pose suppresses stale anchors; re-entry republishes the same
|
||||
// WorldEntity frames without replaying create-time resources.
|
||||
_effectPoses.Remove(entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public enum ExactProjectionWithdrawalDisposition
|
||||
{
|
||||
Completed,
|
||||
Pending,
|
||||
Superseded,
|
||||
}
|
||||
|
||||
public readonly record struct ExactProjectionWithdrawalOutcome(
|
||||
ExactProjectionWithdrawalDisposition Disposition,
|
||||
Exception? Failure);
|
||||
Loading…
Add table
Add a link
Reference in a new issue