feat(vfx): port retail hidden and teleport presentation
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
parent
a51ebc66e9
commit
1e98d81448
46 changed files with 4883 additions and 127 deletions
|
|
@ -297,4 +297,22 @@ public sealed class TargetManager
|
|||
foreach (var voyeur in _voyeurTable.Values.ToList())
|
||||
SendVoyeurUpdate(voyeur, _host.Position, status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts a terminal availability event and drops the watched-role
|
||||
/// subscriptions after delivery. Retail normally removes the object from
|
||||
/// cell lookup before its watchers process the event; clearing the local
|
||||
/// table explicitly preserves that lifecycle boundary when the App uses
|
||||
/// the target fan-out as the temporary bridge for an unavailable object.
|
||||
/// The object's own watcher role (<see cref="TargetInfo"/>) is untouched.
|
||||
/// </summary>
|
||||
public void NotifyVoyeurOfEventAndClear(TargetStatus status)
|
||||
{
|
||||
if (_voyeurTable == null)
|
||||
return;
|
||||
|
||||
foreach (var voyeur in _voyeurTable.Values.ToList())
|
||||
SendVoyeurUpdate(voyeur, _host.Position, status);
|
||||
_voyeurTable.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1388,7 +1388,11 @@ public sealed class PhysicsEngine
|
|||
bool ok = transition.FindPlacementPos(this);
|
||||
var sp = transition.SpherePath;
|
||||
var ci = transition.CollisionInfo;
|
||||
bool onGround = ci.ContactPlaneValid
|
||||
bool inContact = ci.ContactPlaneValid;
|
||||
bool onWalkable = PhysicsObjUpdate.IsWalkableContact(
|
||||
inContact,
|
||||
ci.ContactPlane.Normal);
|
||||
bool onGround = inContact
|
||||
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable);
|
||||
|
||||
return new ResolveResult(
|
||||
|
|
@ -1397,6 +1401,12 @@ public sealed class PhysicsEngine
|
|||
onGround,
|
||||
ci.CollisionNormalValid,
|
||||
ci.CollisionNormal,
|
||||
ok);
|
||||
ok,
|
||||
Orientation: sp.CurOrientation,
|
||||
InContact: inContact,
|
||||
OnWalkable: onWalkable,
|
||||
ContactPlane: ci.ContactPlane,
|
||||
ContactPlaneCellId: ci.ContactPlaneCellId,
|
||||
ContactPlaneIsWater: ci.ContactPlaneIsWater);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,70 @@ public static class PhysicsObjUpdate
|
|||
body.calc_acceleration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits the contact/walkable/collision-response tail of retail
|
||||
/// <c>CPhysicsObj::SetPositionInternal</c> (<c>0x00515330</c>) in its
|
||||
/// original order. The pre-transition flags are explicit because a
|
||||
/// deferred placement may temporarily park the body without contact while
|
||||
/// still needing the source edge when its destination cell becomes ready.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail writes Contact and recalculates acceleration, calls
|
||||
/// <c>set_on_walkable</c> (which invokes HitGround/LeaveGround), then calls
|
||||
/// <c>handle_all_collisions</c> at <c>0x005154FE</c>. Collision response
|
||||
/// must therefore observe any velocity change made by the movement
|
||||
/// callback; moving that callback after reflection changes the result.
|
||||
/// </remarks>
|
||||
public static void CommitSetPositionTransition(
|
||||
PhysicsBody body,
|
||||
bool inContact,
|
||||
bool onWalkable,
|
||||
bool collisionNormalValid,
|
||||
Vector3 collisionNormal,
|
||||
bool previousContact,
|
||||
bool previousOnWalkable,
|
||||
Action? hitGround = null,
|
||||
Action? leaveGround = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
|
||||
// SetPositionInternal replaces Contact first but retains the source
|
||||
// OnWalkable bit through its first calc_acceleration call. A deferred
|
||||
// teleport may have parked the live body with both bits cleared, so
|
||||
// restore the captured source bit explicitly before reproducing that
|
||||
// ordering.
|
||||
if (previousOnWalkable)
|
||||
body.TransientState |= TransientStateFlags.OnWalkable;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
||||
|
||||
if (inContact)
|
||||
body.TransientState |= TransientStateFlags.Contact;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.Contact;
|
||||
body.calc_acceleration();
|
||||
|
||||
bool finalOnWalkable = inContact && onWalkable;
|
||||
if (finalOnWalkable)
|
||||
body.TransientState |= TransientStateFlags.OnWalkable;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
||||
|
||||
if (!previousOnWalkable && finalOnWalkable)
|
||||
hitGround?.Invoke();
|
||||
else if (previousOnWalkable && !finalOnWalkable)
|
||||
leaveGround?.Invoke();
|
||||
body.calc_acceleration();
|
||||
|
||||
HandleAllCollisions(
|
||||
body,
|
||||
collisionNormalValid,
|
||||
collisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable,
|
||||
finalOnWalkable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// retail <c>handle_all_collisions</c> (0x00514780). Reflects or zeros the body's
|
||||
/// <see cref="PhysicsBody.Velocity"/> (retail m_velocityVector) based on
|
||||
|
|
|
|||
|
|
@ -51,4 +51,10 @@ public readonly record struct ResolveResult(
|
|||
/// Exact retail walkability result after testing the contact-plane normal
|
||||
/// against <see cref="PhysicsGlobals.FloorZ"/>.
|
||||
/// </summary>
|
||||
bool OnWalkable = false);
|
||||
bool OnWalkable = false,
|
||||
/// <summary>The accepted SetPosition contact plane, when present.</summary>
|
||||
Plane ContactPlane = default,
|
||||
/// <summary>Full cell that owns <see cref="ContactPlane"/>.</summary>
|
||||
uint ContactPlaneCellId = 0,
|
||||
/// <summary>Whether the accepted contact plane is water.</summary>
|
||||
bool ContactPlaneIsWater = false);
|
||||
|
|
|
|||
89
src/AcDream.Core/Physics/RetailPhysicsStateTransition.cs
Normal file
89
src/AcDream.Core/Physics/RetailPhysicsStateTransition.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Result of retail <c>CPhysicsObj::set_state</c> (<c>0x00514DD0</c>) and
|
||||
/// the nested <c>set_hidden</c> call (<c>0x00514C60</c>).
|
||||
/// </summary>
|
||||
public readonly record struct RetailPhysicsStateTransition(
|
||||
PhysicsStateFlags PreviousState,
|
||||
PhysicsStateFlags RequestedState,
|
||||
PhysicsStateFlags FinalState,
|
||||
bool LightingChanged,
|
||||
bool NoDrawChanged,
|
||||
RetailHiddenTransition HiddenTransition)
|
||||
{
|
||||
public bool HasChanges => PreviousState != FinalState;
|
||||
}
|
||||
|
||||
public enum RetailHiddenTransition
|
||||
{
|
||||
None,
|
||||
BecameHidden,
|
||||
BecameVisible,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure port of retail's PhysicsState side-effect ordering. The server's raw
|
||||
/// state is installed first; Lighting and NoDraw are processed next; Hidden is
|
||||
/// processed last and may rewrite ReportCollisions/IgnoreCollisions.
|
||||
/// </summary>
|
||||
public static class RetailPhysicsStateTransitions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj</c> constructor state at <c>0x00512508</c>.
|
||||
/// This deliberately does not contain Hidden: a normal visible
|
||||
/// CreateObject must not manufacture an UnHide script.
|
||||
/// </summary>
|
||||
public const PhysicsStateFlags ConstructorState =
|
||||
PhysicsStateFlags.EdgeSlide
|
||||
| PhysicsStateFlags.Lighting
|
||||
| PhysicsStateFlags.Gravity
|
||||
| PhysicsStateFlags.ReportCollisions;
|
||||
|
||||
public static RetailPhysicsStateTransition Apply(
|
||||
PhysicsStateFlags previousState,
|
||||
PhysicsStateFlags requestedState)
|
||||
{
|
||||
// CPhysicsObj::set_state compares only the low 16 bits. Every state
|
||||
// with a set_state side effect (Lighting, NoDraw, Hidden) lives there.
|
||||
uint changedLow = ((uint)previousState ^ (uint)requestedState) & 0xFFFFu;
|
||||
bool lightingChanged =
|
||||
(changedLow & (uint)PhysicsStateFlags.Lighting) != 0;
|
||||
bool noDrawChanged =
|
||||
(changedLow & (uint)PhysicsStateFlags.NoDraw) != 0;
|
||||
bool hiddenChanged =
|
||||
(changedLow & (uint)PhysicsStateFlags.Hidden) != 0;
|
||||
|
||||
PhysicsStateFlags finalState = requestedState;
|
||||
RetailHiddenTransition hiddenTransition = RetailHiddenTransition.None;
|
||||
if (hiddenChanged)
|
||||
{
|
||||
if ((requestedState & PhysicsStateFlags.Hidden) != 0)
|
||||
{
|
||||
// set_hidden(true): report_collision_end, clear reporting,
|
||||
// then enable collision-ignore before hiding from the cell.
|
||||
finalState &= ~PhysicsStateFlags.ReportCollisions;
|
||||
finalState |= PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions;
|
||||
hiddenTransition = RetailHiddenTransition.BecameHidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
// set_hidden(false): clear ignore and restore collision
|
||||
// reporting even when the incoming raw state omitted it.
|
||||
finalState &= ~(PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions);
|
||||
finalState |= PhysicsStateFlags.ReportCollisions;
|
||||
hiddenTransition = RetailHiddenTransition.BecameVisible;
|
||||
}
|
||||
}
|
||||
|
||||
return new RetailPhysicsStateTransition(
|
||||
previousState,
|
||||
requestedState,
|
||||
finalState,
|
||||
lightingChanged,
|
||||
noDrawChanged,
|
||||
hiddenTransition);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue