refactor(world): extract live appearance and parenting
Move ObjDesc, Parent, Pickup, child-ready, and retained projection recovery into LiveEntityHydrationController while preserving retail timestamp and leave-world semantics. Pin ready publication to exact projection authority and restore attached descendant chains synchronously.
This commit is contained in:
parent
40352d5a7a
commit
fe5514967c
9 changed files with 1098 additions and 199 deletions
|
|
@ -749,8 +749,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
/// guid. Captured before the renderability gate so no-position inventory /
|
||||
/// parented children retain Setup and parent metadata; hydration rereads
|
||||
/// this canonical snapshot after every relationship callback.
|
||||
/// <see cref="OnLiveAppearanceUpdated"/> reuses the cached position/setup/motion
|
||||
/// fields when a 0xF625 ObjDescEvent arrives carrying only updated visuals.
|
||||
/// <see cref="LiveEntityHydrationController.OnAppearance"/> reuses the cached
|
||||
/// position/setup/motion fields when a 0xF625 ObjDescEvent carries only
|
||||
/// updated visuals.
|
||||
/// </summary>
|
||||
private IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> LastSpawns =>
|
||||
_liveEntities?.Snapshots ?? EmptyLiveSpawnMap;
|
||||
|
|
@ -1750,6 +1751,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
item, container, placement, amount),
|
||||
requestExternalContainer: guid => _externalContainers.RequestOpen(guid));
|
||||
|
||||
AcDream.App.World.DeferredLiveEntityParentAcceptance? parentAcceptance = null;
|
||||
|
||||
// Phase D.2b retail-look retained UI (ACDREAM_RETAIL_UI=1).
|
||||
if (_options.RetailUi)
|
||||
{
|
||||
|
|
@ -2270,13 +2273,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_lightingSink!,
|
||||
setupId => _dats!.Get<DatReaderWriter.DBObjs.Setup>(setupId));
|
||||
|
||||
parentAcceptance = new AcDream.App.World.DeferredLiveEntityParentAcceptance();
|
||||
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
||||
_dats!,
|
||||
_datLock,
|
||||
Objects,
|
||||
_liveEntities,
|
||||
_effectPoses,
|
||||
TryAcceptParentForRender,
|
||||
parentAcceptance.TryAccept,
|
||||
(childRecord, positionVersion, projectionVersion) =>
|
||||
_liveEntityProjectionWithdrawal!.WithdrawExact(
|
||||
childRecord,
|
||||
|
|
@ -2567,8 +2571,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
Objects,
|
||||
_datLock,
|
||||
projectionMaterializer,
|
||||
new AcDream.App.World.DelegateLiveEntitySpawnRelationshipSink(
|
||||
_equippedChildRenderer.OnSpawn),
|
||||
new AcDream.App.World.LiveEntityRelationshipProjection(
|
||||
_equippedChildRenderer),
|
||||
new AcDream.App.World.LiveEntityReadyPublisher(
|
||||
_liveEntities,
|
||||
_entityEffects!,
|
||||
|
|
@ -2578,11 +2582,17 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
PublishLocalPhysicsTimestamps,
|
||||
() => _playerServerGuid,
|
||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
|
||||
networkUpdateBridge.Bind(
|
||||
new AcDream.App.World.DelegateLiveEntityNetworkUpdateSink(
|
||||
RouteSameGenerationCreateObject));
|
||||
_equippedChildRenderer.EntityReady += candidate =>
|
||||
_liveEntityHydration.OnEntityReady(candidate);
|
||||
_liveEntityHydration.AppearanceApplied += guid =>
|
||||
{
|
||||
if (guid == _playerServerGuid)
|
||||
_paperdollDollDirty = true;
|
||||
};
|
||||
|
||||
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
|
||||
// enter the world as the first character on the account, and stream
|
||||
|
|
@ -2823,7 +2833,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
Deleted: deletion => _inboundEntityEvents.Run(
|
||||
this, deletion, static (window, value) => window.OnLiveEntityDeleted(value)),
|
||||
PickedUp: pickup => _inboundEntityEvents.Run(
|
||||
this, pickup, static (window, value) => window.OnLiveEntityPickedUp(value)),
|
||||
_liveEntityHydration!, pickup,
|
||||
static (hydration, value) => hydration.OnPickup(value)),
|
||||
MotionUpdated: motion => _inboundEntityEvents.Run(
|
||||
this, motion, static (window, value) => window.OnLiveMotionUpdated(value)),
|
||||
PositionUpdated: position => _inboundEntityEvents.Run(
|
||||
|
|
@ -2833,11 +2844,13 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
StateUpdated: state => _inboundEntityEvents.Run(
|
||||
this, state, static (window, value) => window.OnLiveStateUpdated(value)),
|
||||
ParentUpdated: parent => _inboundEntityEvents.Run(
|
||||
this, parent, static (window, value) => window.OnLiveParentUpdated(value)),
|
||||
_liveEntityHydration!, parent,
|
||||
static (hydration, value) => hydration.OnParent(value)),
|
||||
TeleportStarted: teleport => _inboundEntityEvents.Run(
|
||||
this, teleport, static (window, value) => window.OnTeleportStarted(value)),
|
||||
AppearanceUpdated: appearance => _inboundEntityEvents.Run(
|
||||
this, appearance, static (window, value) => window.OnLiveAppearanceUpdated(value)),
|
||||
_liveEntityHydration!, appearance,
|
||||
static (hydration, value) => hydration.OnAppearance(value)),
|
||||
PlayPhysicsScript: script => _inboundEntityEvents.Run(
|
||||
this, script, static (window, value) => window.OnPlayScriptReceived(value)),
|
||||
PlayPhysicsScriptType: message => _inboundEntityEvents.Run(
|
||||
|
|
@ -3037,46 +3050,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
candidate.Generation));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server broadcast a <c>0xF625 ObjDescEvent</c> — a creature/player's
|
||||
/// appearance changed (equip / unequip / tailoring / recipe result /
|
||||
/// character option toggle). The wire payload only carries the new
|
||||
/// ModelData (palette + texture + animpart changes), not position or
|
||||
/// motion, so we splice it onto the cached spawn and rehydrate only the
|
||||
/// visual projection. Retail <c>SmartBox::UpdateVisualDesc</c> calls
|
||||
/// <c>CPhysicsObj::DoObjDescChangesFromDefault</c> on the existing object;
|
||||
/// accordingly, entity identity and all animation/movement/physics state
|
||||
/// survive while MeshRefs, palette, and part overrides are replaced.
|
||||
/// </summary>
|
||||
private void OnLiveAppearanceUpdated(AcDream.Core.Net.Messages.ObjDescEvent.Parsed update)
|
||||
{
|
||||
if (!_liveEntities!.TryApplyObjDesc(update, out var newSpawn))
|
||||
{
|
||||
// Server can broadcast ObjDescEvent before we've seen a
|
||||
// CreateObject for this guid (race on landblock entry, or
|
||||
// if the entity is in a state we couldn't render). Drop —
|
||||
// when CreateObject lands, ACE includes the same ModelData
|
||||
// body inside it, so the appearance won't be lost.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_liveEntities.TryGetRecord(update.Guid, out LiveEntityRecord record))
|
||||
{
|
||||
LiveEntityAppearanceUpdateState? appearanceState =
|
||||
LiveEntityAppearanceBinding.Capture(_liveEntities, update.Guid);
|
||||
_liveEntityHydration!.ApplyAppearance(
|
||||
record,
|
||||
newSpawn,
|
||||
appearanceState);
|
||||
}
|
||||
|
||||
// Slice 2: flag the paperdoll doll to re-clone the player's newly
|
||||
// mutated appearance on the next doll pass (the C# analog of
|
||||
// RedressCreature). The rebuild is deferred until the doll is visible.
|
||||
if (update.Guid == _playerServerGuid)
|
||||
_paperdollDollDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the paperdoll doll from the live player entity (retail makeObject(player) +
|
||||
/// DoObjDescChangesFromDefault). Clones the player's already-resolved Setup / MeshRefs / palette /
|
||||
|
|
@ -3198,20 +3171,17 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_entityEffects?.OnLiveEntityDescriptionChanged(refresh.Appearance.Guid);
|
||||
}
|
||||
|
||||
OnLiveAppearanceUpdated(refresh.Appearance);
|
||||
_liveEntityHydration!.OnAppearance(refresh.Appearance);
|
||||
|
||||
if (refresh.Parent is { } parent
|
||||
&& _liveEntities!.TryApplyCreateParent(parent, out _))
|
||||
{
|
||||
_equippedChildRenderer?.OnCreateParentAccepted(parent);
|
||||
}
|
||||
if (refresh.Parent is { } parent)
|
||||
_liveEntityHydration.OnCreateParentAccepted(parent);
|
||||
else if (refresh.Position is { } position)
|
||||
{
|
||||
OnLivePositionUpdated(position);
|
||||
}
|
||||
else if (refresh.Pickup is { } pickup)
|
||||
{
|
||||
OnLiveEntityPickedUp(pickup);
|
||||
_liveEntityHydration.OnPickup(pickup);
|
||||
}
|
||||
|
||||
if (refresh.Movement is { } movement)
|
||||
|
|
@ -3220,49 +3190,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
OnLiveVectorUpdated(refresh.Vector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail PickupEvent is a POSITION_TS update, not object destruction.
|
||||
/// It leaves the world projection while retaining the weenie, generation,
|
||||
/// and every other timestamp for later inventory/parent events.
|
||||
/// </summary>
|
||||
private void OnLiveEntityPickedUp(AcDream.Core.Net.Messages.PickupEvent.Parsed pickup)
|
||||
{
|
||||
if (!_liveEntities!.TryApplyPickup(pickup, out _))
|
||||
return;
|
||||
|
||||
AcDream.App.Rendering.ChildUnparentDisposition childDisposition =
|
||||
_equippedChildRenderer?.OnChildBecameUnparented(pickup.Guid)
|
||||
?? AcDream.App.Rendering.ChildUnparentDisposition.NotAttached;
|
||||
bool removed = childDisposition switch
|
||||
{
|
||||
AcDream.App.Rendering.ChildUnparentDisposition.Completed => true,
|
||||
AcDream.App.Rendering.ChildUnparentDisposition.NotAttached => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (removed
|
||||
&& Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: pickup guid=0x{pickup.Guid:X8} instSeq={pickup.InstanceSequence} posSeq={pickup.PositionSequence}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ParentEvent may precede either CreateObject. The focused child renderer
|
||||
/// retains it; its realization callback performs the canonical timestamp
|
||||
/// acceptance immediately before changing the projection.
|
||||
/// </summary>
|
||||
private void OnLiveParentUpdated(AcDream.Core.Net.Messages.ParentEvent.Parsed update)
|
||||
{
|
||||
_equippedChildRenderer?.OnParentEvent(update);
|
||||
}
|
||||
|
||||
private bool TryAcceptParentForRender(AcDream.Core.Net.Messages.ParentEvent.Parsed update)
|
||||
{
|
||||
return _liveEntities!.TryApplyParent(update, out _);
|
||||
}
|
||||
|
||||
private void PublishLocalPhysicsTimestamps(
|
||||
uint guid,
|
||||
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue