fix(physics): C4 route 5 — projectile authoritative placement (#276 partial)
Ports retail's missile Position handling into the canonical Runtime
placement owner instead of the deleted ApplyAuthoritativePosition
short-circuit. The Create/residence-window halves of the projectile
pipeline (RuntimeProjectile binding, TryBind's adopted-body branch,
the collision/shadow registration) were already canonical from prior
slices; this closes the remaining gap — how an ACCEPTED Position for
an in-flight missile is classified, placed, and presented.
Byte-decode (Step 1 hard gate, before any code was written):
CPhysicsObj::MoveOrTeleport @0x00516330-0x00516438 disassembled from
the PDB-paired binary (Capstone, x86 32-bit thiscall). `ret 0x10`
establishes four stack args; [esp+0x7c] (arg5, the velocity pointer)
is never referenced in any of the three branches (teleport/near/far).
The retail reviewer independently reproduced this by searching the
whole function body for the `24 7c` mod/rm+disp8 encoding a
`[esp+0x7c]` read would require and found zero occurrences. This
retired a fabricated `?? Vector3.Zero` fallback in the deleted method
— retail's PositionPack::UnPack initializes an absent velocity to
zero and MoveOrTeleport never installs it; the projectile's Vector
channel (RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector)
remains the sole velocity authority for a missile. D-P5 in the
contract; the Runtime seam commits no velocity from the Position
packet at all.
The unbound-missile fix: RuntimeEntityObjectLifetime's
ClassifyRemoteAcceptedPosition now derives ProjectileAuthoritative
from a CONJUNCTIVE predicate — the Missile bit AND a bound
RuntimeProjectile whose Body is the canonical PhysicsBody — never the
bit alone. Retail places every non-player CPhysicsObj unconditionally
(there is no missile-specific placement gate in MoveOrTeleport or its
callers), so an unbindable or not-yet-bound missile taking the
ordinary remote tail is retail-faithful, not a fallback: the earlier
bit-only discriminator would have silently frozen it instead.
AP-141 records this as a deliberate, recorded divergence, not
fidelity. Retail mechanically WOULD arm a missile's ConstrainTo leash
on any nonzero MoveOrTeleport return: HandleReceivedPosition
@0x00453FD0's only kind test is player-vs-not, ConstrainTo
@0x00454272 has no kind test of its own, and CPhysicsObj::ConstrainTo
@0x00510520 creates a PositionManager on demand via
MakePositionManager @0x00510523 if one doesn't exist. acdream
deliberately does not construct that EntityPhysicsHost/
PositionManager/InterpolationManager chain for a ballistic body — the
route-5b split the C4 route 5 contract rejected — so a live missile
never shows an armed leash and never catches up via the near/
UnroutedCatchUp policy. This divergence is safe specifically because
ACE never sends UpdatePosition for a missile
(references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:
333-334, SendUpdatePosition() commented out inside the
PhysicsState.Missile branch at :265) — every half of this row is
deterministic-test-gated only, never exercised against a real server.
AP-141 also records the surviving ConstrainTo re-anchor divergence
under clause (b): for the adopted-body case (TryBind's shared-body
branch — an ordinary remote whose Missile bit is set by a later
State packet, so it still carries a live RemoteMotion), acdream now
ports retail's teleport-branch and far-branch StopInterpolating
action (Interp.Clear()), but never re-arms or re-anchors the
inherited ConstrainTo leash the way retail's HandleReceivedPosition
@0x00454254/@0x00454272 does on every nonzero return. The risk
column's earlier wording — that a stale leash "would drag the body
toward a stale anchor" — was wrong and is retracted in this same
commit: ConstraintManager.ConstraintPos is write-only in both retail
and the port (never read by AdjustOffset), and
ConstraintManager::adjust_offset @0x00556180 only tapers or zeroes an
already-composed per-tick offset while InContact — a leash brakes
motion the interp/sticky chain already produced, it cannot pull
anything toward the anchor. The real residual is one tick of un-reset
brake accumulator, contact-gated, and it cannot move an airborne
far-snapped missile at all (the clamp branch does not run while
airborne).
NO CONNECTED GATE EXISTS for this route, by design: ACE never sends a
missile UpdatePosition (see above), so retail's own server never
exercises this code path in play. Every proof obligation here is
test-gated only — Runtime and App-level fixtures constructing the
packet directly — never a live client/server capture.
Three review rounds closed 8 MAJOR findings before this landed:
round 1 (A1 App discarded the seam's status; A2/R1 silent swallow on
an unbound missile; A3/R2 the adopted-body teleport_hook never
wired; A4/A5 zero Runtime/App test coverage); round 2 (a
ParentCellId regression introduced by round 1's own R6 finding,
which the retail reviewer retracted the following round as factually
wrong — the fix here is the REVERT to record.FullCellId, not the
relocation round 1 shipped; B2 the far-branch StopInterpolating skip
never extended to the adopted-body case; residual App/Runtime store-
path coverage; a per-packet closure contradicting the file's own
#315 cached-delegate pattern). Round 3 closed on coverage alone (no
defect): the Advance() retry arm's projectile branch — added at
round 2, semantically reordered at round 2's B5 fix (skip prediction
invalidation on a re-parked Contention, since it writes nothing) —
had never been executed by any test; two new tests drive it directly
and are sabotage-verified against both the reordering and the
retry-arm's own SyncProjectilePresentation call site. The one
recorded defect this campaign produced (the ParentCellId regression)
was caused by complying with a review finding that its own author
later retracted — the standing lesson recorded for future rounds is
that review findings are evidence to re-verify against the code, not
commands to obey unconditionally.
Complete Release suite: 11,063 passed / 4 skipped / 0 failed
(baseline 11,036 at 30d3d114, +27 new tests across this campaign).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
30d3d114b0
commit
36255af0f6
19 changed files with 5390 additions and 393 deletions
|
|
@ -516,9 +516,25 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
/// POSITION-only route; the first-entry conductor owns every Create)
|
||||
/// without excluding either remote Position shape.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// C4 route 5 (D-P3): widened to admit
|
||||
/// <c>ProjectileAuthoritative</c> alongside <c>RemoteAuthoritative</c> —
|
||||
/// a live missile's teleport/cell-less and far accepted-Position
|
||||
/// dispositions are disposition-identical to a remote's (the classifier
|
||||
/// never branches on kind past <c>LocalPlayer</c>), and route 5's
|
||||
/// <see cref="ApplyAcceptedProjectilePosition"/> is the sibling seam over
|
||||
/// this SAME shared core (<see cref="TryExecuteAcceptedRemotePosition"/> +
|
||||
/// <see cref="StoreAcceptedDestinationPose"/>) — never a second pending
|
||||
/// map, never a sibling controller. <c>OwnsFarSnap</c> and
|
||||
/// <c>OwnsTeleportPlacement</c> are deliberately NOT widened: both remote
|
||||
/// arm methods (<see cref="ApplyAcceptedRemoteFarSnap"/>/
|
||||
/// <see cref="ApplyAcceptedRemoteTeleport"/>) require and throw without a
|
||||
/// <c>RemoteMotion</c>, which a projectile never has.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static bool OwnsPlacement(RuntimeAuthoritativePositionRoute route) =>
|
||||
route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative
|
||||
or RuntimeSetPositionOperationKind.ProjectileAuthoritative
|
||||
&& route.Disposition is RuntimeAuthoritativePositionDisposition.SetPosition
|
||||
or RuntimeAuthoritativePositionDisposition.SetPositionSimple
|
||||
&& (route.SetPositionFlags & PhysicsSetPositionFlags.Teleport) != 0;
|
||||
|
|
@ -842,6 +858,234 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
return status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 5 (D-P2): the projectile arm over this SAME shared core. A
|
||||
/// live missile carries no <see cref="RemoteMotion"/>, so
|
||||
/// <see cref="ApplyAcceptedRemoteFarSnap"/>/<see cref="ApplyAcceptedRemoteTeleport"/>
|
||||
/// are not reusable — both require one and throw without it. This is the
|
||||
/// sibling seam the D-P2 design pins: same
|
||||
/// <see cref="TryExecuteAcceptedRemotePosition"/> +
|
||||
/// <see cref="StoreAcceptedDestinationPose"/> core, same
|
||||
/// <see cref="_pending"/>/<see cref="_awaitingAcknowledgement"/> ledgers,
|
||||
/// no second pending map, no sibling controller (trap T9).
|
||||
///
|
||||
/// <para>
|
||||
/// Returns <see langword="null"/> for every disposition this route does
|
||||
/// not own: <c>Interpolate</c> (near, in contact) and
|
||||
/// <c>NoPositionOperation</c> (airborne) are pinned NO-OPS — retail would
|
||||
/// lazily build interpolation/leash machinery for a manager-less missile
|
||||
/// (@0x005163AF / @0x00454272-@0x00510523), which acdream deliberately
|
||||
/// does not construct for a ballistic body (the register row this route
|
||||
/// adds); <c>RejectedAuthority</c>/<c>RejectedData</c> and an ownership
|
||||
/// mismatch are SWALLOWED — write nothing, never fall through to the
|
||||
/// remote tail (trap T5). A caller must not fall back to any remote arm
|
||||
/// when this returns <see langword="null"/>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>No velocity write (D-P5).</b> Retail's <c>MoveOrTeleport</c>
|
||||
/// @0x00516330 never references its velocity argument in the decompiled
|
||||
/// body, and a byte-level disassembly of the PDB-paired binary
|
||||
/// (@0x00516330-@0x00516438) confirms no instruction anywhere in the
|
||||
/// function reads that argument's stack slot. This seam commits no
|
||||
/// velocity from the Position packet at all — the Vector channel
|
||||
/// (<see cref="RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector"/>)
|
||||
/// remains the sole velocity authority for a missile.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>No constraint leash armed (D-P4).</b> Unlike the remote arms, this
|
||||
/// method never calls <c>TryArmConstraintAfterOperation</c> — the
|
||||
/// classifier's projectile routes still carry
|
||||
/// <c>ConstrainPhase.AfterPositionOperation</c> (kind-blind), but this arm
|
||||
/// deliberately does not consume it, matching the pinned divergence.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Teleport hook reduction (D-P4).</b> Of retail's six
|
||||
/// <c>teleport_hook</c> @0x00514ED0 actions, five are structurally absent
|
||||
/// for a manager-less missile (no <c>MovementManager</c>/
|
||||
/// <c>PositionManager</c>/<c>TargetManager</c>). The sixth,
|
||||
/// <c>report_collision_end(this, 1)</c> @0x00514F31-@0x00514620, applies
|
||||
/// to any object with a collision table and runs BEFORE the placement —
|
||||
/// ported here as <c>RuntimeCollisionReportingState.LeaveWorld</c> (the
|
||||
/// exact force-end seam the 4b-3 round-2 review validated against the
|
||||
/// same retail address).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Prediction is invalidated once per packet, before any body write on
|
||||
/// this route (placement or the store fallback) — mirroring
|
||||
/// <c>RuntimeProjectilePhysicsUpdater</c>'s existing invalidate-before-
|
||||
/// write ordering — so an in-flight split quantum straddling this packet
|
||||
/// aborts at <c>Complete</c> rather than clobbering a canonical
|
||||
/// placement. The no-op dispositions invalidate nothing: the body is
|
||||
/// untouched, so a straddling quantum completing over them is correct.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal RuntimeRemotePlacementExecutionStatus? ApplyAcceptedProjectilePosition(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeAuthoritativePositionRoute route)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (route.OperationKind
|
||||
is not RuntimeSetPositionOperationKind.ProjectileAuthoritative
|
||||
|| record.Projectile is not RuntimeProjectile projectile
|
||||
|| record.PhysicsBody is not { } body
|
||||
|| !ReferenceEquals(body, projectile.Body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// A6 fix (review round): captured BEFORE the placement dispatch,
|
||||
// mirroring the deleted tail's `bool wasInWorld = body.InWorld;`
|
||||
// ordering — TryExecuteAcceptedRemotePosition's canonical commit
|
||||
// calls body.SnapToCell, which sets InWorld = true, so reading this
|
||||
// AFTER the dispatch (as the first cut of this seam did) makes the
|
||||
// re-activation branch below permanently dead on every committed
|
||||
// outcome.
|
||||
bool wasInWorld = body.InWorld;
|
||||
|
||||
RuntimeRemotePlacementExecutionStatus status;
|
||||
switch (route.Disposition)
|
||||
{
|
||||
case RuntimeAuthoritativePositionDisposition.SetPosition:
|
||||
_entityObjects.Physics.CollisionReports.LeaveWorld(record);
|
||||
projectile.InvalidatePrediction();
|
||||
status = TryExecuteAcceptedRemotePosition(record, route);
|
||||
break;
|
||||
|
||||
case RuntimeAuthoritativePositionDisposition.SetPositionSimple:
|
||||
// B1/B2 fix (round-2 review): retail's far branch runs
|
||||
// `StopInterpolating` @0x005163C9-@0x005163CB whenever
|
||||
// `position_manager != 0` — the SAME guard the remote far
|
||||
// arm ports as `ApplyAcceptedRemoteFarSnap`'s
|
||||
// `if (route.StopInterpolating) remote.Interp.Clear();`. A
|
||||
// bare missile has no RemoteMotion so this is structurally
|
||||
// inert, but the ADOPTED-BODY case (TryBind's shared-body
|
||||
// branch: an ordinary remote whose Missile bit was set by a
|
||||
// later State packet) carries a live Interp queue the far
|
||||
// branch must clear too — the teleport hook only covers the
|
||||
// SetPosition disposition.
|
||||
if (route.StopInterpolating
|
||||
&& record.RemoteMotion is RemoteMotion adoptedFar)
|
||||
{
|
||||
adoptedFar.Interp.Clear();
|
||||
}
|
||||
projectile.InvalidatePrediction();
|
||||
status = TryExecuteAcceptedRemotePosition(record, route);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Interpolate / NoPositionOperation: pinned no-op (D-P4).
|
||||
// RejectedAuthority / RejectedData: swallow (T5) — the
|
||||
// shared authority gate already rejected an invalid payload
|
||||
// upstream; there is nothing left to route.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status.StoresAcceptedDestination())
|
||||
StoreAcceptedDestinationPose(record);
|
||||
if (status is not RuntimeRemotePlacementExecutionStatus.Deferred
|
||||
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
|
||||
{
|
||||
// Invariant 2: presentation advances on every committed/stored
|
||||
// outcome only — Deferred/RejectedByPlacement leave the body at
|
||||
// its prior (already-synced) pose.
|
||||
SyncProjectilePresentation(record, projectile, body, wasInWorld);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 5 (REVISED after the review round — A6/A7/A8): the
|
||||
/// J5.6-owned post-commit lifecycle tail (InWorld/Activate/shadow-sync
|
||||
/// on spatial+visible, suspend on spatial+hidden, deactivate+suspend on
|
||||
/// non-spatial), reduced from the deleted
|
||||
/// <c>RuntimeProjectilePhysicsUpdater.ApplyAuthoritativePosition</c>
|
||||
/// tail (former <c>:390-422</c>) to this controller's own seam —
|
||||
/// <paramref name="wasInWorld"/> is the caller's pre-dispatch capture
|
||||
/// (A6: reading <c>body.InWorld</c> here, after the canonical commit's
|
||||
/// own <c>SnapToCell</c> already forced it true, made the re-activation
|
||||
/// branch permanently dead); <see cref="_clock"/> supplies the same
|
||||
/// clock source the deleted method took as an explicit
|
||||
/// <c>currentTime</c> parameter; the world-frame offset comes from
|
||||
/// <see cref="RuntimePhysicsState.TryGetWorldFrameOffset"/> (the same
|
||||
/// source <see cref="StoreAcceptedDestinationPose"/> uses) instead of an
|
||||
/// App-supplied live-center pair.
|
||||
/// </summary>
|
||||
private void SyncProjectilePresentation(
|
||||
RuntimeEntityRecord record,
|
||||
RuntimeProjectile projectile,
|
||||
PhysicsBody body,
|
||||
bool wasInWorld)
|
||||
{
|
||||
if (!_entityObjects.Entities.IsCurrent(record)
|
||||
|| !ReferenceEquals(record.Projectile, projectile)
|
||||
|| !ReferenceEquals(record.PhysicsBody, body))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimePhysicsState physics = _entityObjects.Physics;
|
||||
bool spatial = physics.IsSpatialProjectile(record, projectile);
|
||||
bool hidden =
|
||||
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
|
||||
uint localId = record.LocalEntityId ?? 0u;
|
||||
if (spatial && !hidden)
|
||||
{
|
||||
if (!wasInWorld)
|
||||
{
|
||||
body.LastUpdateTime = _clock.SimulationTimeSeconds;
|
||||
if ((body.State & PhysicsStateFlags.Static) == 0)
|
||||
body.TransientState |= TransientStateFlags.Active;
|
||||
}
|
||||
body.InWorld = true;
|
||||
// A8 fix: #284's policy ("a frame that can never arrive is
|
||||
// terminal, never silent") applies here exactly as it does to
|
||||
// StoreAcceptedDestinationPose. A false result during the
|
||||
// legitimate pre-local-player-Create window silently skips the
|
||||
// publish (self-heals once the frame arrives); a false result
|
||||
// AFTER that window is a genuinely stuck frame, and
|
||||
// ThrowIfWorldFrameUnreachable escalates it instead of leaving
|
||||
// the shadow silently stale forever.
|
||||
if (physics.TryGetWorldFrameOffset(
|
||||
record.FullCellId,
|
||||
out float offsetX,
|
||||
out float offsetY))
|
||||
{
|
||||
physics.Engine.ShadowObjects.UpdatePosition(
|
||||
localId,
|
||||
body.Position,
|
||||
body.Orientation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
record.FullCellId,
|
||||
seedCellId: record.FullCellId);
|
||||
}
|
||||
else
|
||||
{
|
||||
physics.ThrowIfWorldFrameUnreachable(record.FullCellId);
|
||||
}
|
||||
}
|
||||
else if (spatial)
|
||||
{
|
||||
body.InWorld = true;
|
||||
// A7 fix: retail's hidden-branch clock consumption — restored,
|
||||
// matching ProjectileController.TryBind's equivalent branch
|
||||
// ("consume the hidden clock so UnHide cannot replay a time
|
||||
// backlog").
|
||||
body.LastUpdateTime = _clock.SimulationTimeSeconds;
|
||||
physics.Engine.ShadowObjects.Suspend(localId);
|
||||
}
|
||||
else
|
||||
{
|
||||
body.InWorld = false;
|
||||
body.TransientState &= ~TransientStateFlags.Active;
|
||||
physics.Engine.ShadowObjects.Suspend(localId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::store_position</c> @0x00515CE2, reached from
|
||||
/// <c>SetPositionInternal</c>'s no-resolvable-cell branch @0x00515C1D.
|
||||
|
|
@ -975,6 +1219,29 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
}
|
||||
_pending.Remove(key);
|
||||
|
||||
// R3 fix (review round): a retained retry can belong to a
|
||||
// projectile operation exactly as it can belong to a
|
||||
// remote's — this is the SAME shared _pending map (trap T9:
|
||||
// no second map), and pending.Route carries the OperationKind
|
||||
// that was classified when the retry was first parked.
|
||||
// Neither invariant this route pins (prediction invalidated
|
||||
// before every body write; presentation advances on every
|
||||
// committed/stored outcome) may hold on the direct arm only.
|
||||
// Real nullable locals (not a stored bool) so the compiler
|
||||
// can track definite assignment through the branches below.
|
||||
RuntimeProjectile? pendingProjectile = null;
|
||||
PhysicsBody? pendingBody = null;
|
||||
if (pending.Route.OperationKind
|
||||
is RuntimeSetPositionOperationKind.ProjectileAuthoritative
|
||||
&& pending.Record.Projectile is RuntimeProjectile candidateProjectile
|
||||
&& pending.Record.PhysicsBody is { } candidateBody
|
||||
&& ReferenceEquals(candidateBody, candidateProjectile.Body))
|
||||
{
|
||||
pendingProjectile = candidateProjectile;
|
||||
pendingBody = candidateBody;
|
||||
}
|
||||
bool pendingWasInWorld = pendingBody?.InWorld ?? false;
|
||||
|
||||
// B3 review fix: a retry can sit retained across many host
|
||||
// cadence pumps (bounded only by how long the asset stayed
|
||||
// unavailable) while its destination's collision publication
|
||||
|
|
@ -1000,17 +1267,55 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
CreateObject.ServerPosition? destination =
|
||||
pending.Record.Snapshot.Physics?.Position
|
||||
?? pending.Record.Snapshot.Position;
|
||||
RuntimeRemotePlacementExecutionStatus retryStatus;
|
||||
if (destination is not { } accepted
|
||||
|| !CanAttemptDestination(
|
||||
setPosition,
|
||||
accepted.LandblockId))
|
||||
{
|
||||
CancelToken(setPosition, pending.Token);
|
||||
pendingProjectile?.InvalidatePrediction();
|
||||
StoreAcceptedDestinationPose(pending.Record);
|
||||
continue;
|
||||
retryStatus = RuntimeRemotePlacementExecutionStatus.Refused;
|
||||
}
|
||||
else
|
||||
{
|
||||
// B5 fix (round-2 review): invalidating BEFORE this call
|
||||
// unconditionally was wrong when SubmitAndResolve itself
|
||||
// re-parks (returns Contention) — that outcome writes
|
||||
// NOTHING (no store, no commit), so invalidating for it
|
||||
// violates invariant 4's "the no-op dispositions
|
||||
// invalidate nothing" on this arm specifically (unlike
|
||||
// the entry point, where StoresAcceptedDestination()
|
||||
// treats Contention as a storing outcome via the
|
||||
// caller's own StoreAcceptedDestinationPose — this retry
|
||||
// arm does not store on a re-parked Contention, matching
|
||||
// the pre-existing residual A3/round-1 already named).
|
||||
// Invalidating AFTER the call instead of before is safe
|
||||
// here: this method is single-threaded and synchronous,
|
||||
// so a write performed inside SubmitAndResolve and the
|
||||
// very next statement's invalidate are never observably
|
||||
// separated by a quantum's Complete call.
|
||||
retryStatus = SubmitAndResolve(
|
||||
pending.Record, pending.Token, pending.Route);
|
||||
if (retryStatus
|
||||
is not RuntimeRemotePlacementExecutionStatus.Contention)
|
||||
{
|
||||
pendingProjectile?.InvalidatePrediction();
|
||||
}
|
||||
}
|
||||
|
||||
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
|
||||
if (pendingProjectile is { } confirmedProjectile
|
||||
&& pendingBody is { } confirmedBody
|
||||
&& retryStatus is not RuntimeRemotePlacementExecutionStatus.Deferred
|
||||
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
|
||||
{
|
||||
SyncProjectilePresentation(
|
||||
pending.Record,
|
||||
confirmedProjectile,
|
||||
confirmedBody,
|
||||
pendingWasInWorld);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue