feat(vfx): bind effects to live animated poses
This commit is contained in:
parent
96ddfdf175
commit
542dcfc384
41 changed files with 3246 additions and 741 deletions
|
|
@ -286,7 +286,8 @@ public sealed class GameWindow : IDisposable
|
|||
/// from the current animation frame; only these two fields are
|
||||
/// reused unchanged.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<(uint GfxObjId, IReadOnlyDictionary<uint, uint>? SurfaceOverrides)> PartTemplate;
|
||||
public required IReadOnlyList<AnimatedPartTemplate> PartTemplate;
|
||||
public required IReadOnlyList<bool> PartAvailability;
|
||||
public float CurrFrame; // monotonically increasing within [LowFrame, HighFrame]
|
||||
public AcDream.Core.Physics.AnimationSequencer? Sequencer;
|
||||
|
||||
|
|
@ -307,8 +308,15 @@ public sealed class GameWindow : IDisposable
|
|||
/// in-place mutation of this list.
|
||||
/// </summary>
|
||||
public readonly List<AcDream.Core.World.MeshRef> MeshRefsScratch = new();
|
||||
/// <summary>Indexed final part poses, including debug-hidden parts.</summary>
|
||||
public readonly List<System.Numerics.Matrix4x4> EffectPartPosesScratch = new();
|
||||
}
|
||||
|
||||
internal readonly record struct AnimatedPartTemplate(
|
||||
uint GfxObjId,
|
||||
IReadOnlyDictionary<uint, uint>? SurfaceOverrides,
|
||||
bool IsDrawable);
|
||||
|
||||
private sealed record AppearanceUpdateState(
|
||||
AcDream.Core.World.WorldEntity Entity,
|
||||
AnimatedEntity? Animation);
|
||||
|
|
@ -332,6 +340,8 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.Core.Vfx.EmitterDescRegistry? _emitterRegistry;
|
||||
private AcDream.Core.Vfx.ParticleSystem? _particleSystem;
|
||||
private AcDream.Core.Vfx.ParticleHookSink? _particleSink;
|
||||
private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new();
|
||||
private AcDream.App.Rendering.Vfx.AnimationHookFrameQueue? _animationHookFrames;
|
||||
// Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754)
|
||||
// and typed PlayScriptType (0xF755) events through one effect owner, then
|
||||
// fans every dat-defined hook to particles, audio, lights, translucency,
|
||||
|
|
@ -815,6 +825,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Wired into the hook router in OnLoad so SetLightHook fires
|
||||
// from the animation pipeline flip the matching LightSource.IsLit.
|
||||
private AcDream.Core.Lighting.LightingHookSink? _lightingSink;
|
||||
private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights;
|
||||
|
||||
// #188 — TransparentPartHook fires from the animation pipeline drive
|
||||
// a per-(entity,part) translucency ramp; WbDrawDispatcher reads it
|
||||
|
|
@ -1444,7 +1455,12 @@ public sealed class GameWindow : IDisposable
|
|||
// StopParticle hooks fired from motion tables produce visible
|
||||
// spawns. The Tick call is driven from OnRender.
|
||||
_particleSystem = new AcDream.Core.Vfx.ParticleSystem(_emitterRegistry!);
|
||||
_particleSink = new AcDream.Core.Vfx.ParticleHookSink(_particleSystem);
|
||||
_particleSink = new AcDream.Core.Vfx.ParticleHookSink(_particleSystem, _effectPoses);
|
||||
_particleSink.DiagnosticSink = message =>
|
||||
Console.Error.WriteLine($"vfx: {message}");
|
||||
_animationHookFrames = new AcDream.App.Rendering.Vfx.AnimationHookFrameQueue(
|
||||
_hookRouter,
|
||||
_effectPoses);
|
||||
_hookRouter.Register(_particleSink);
|
||||
|
||||
// Phase 6c — PhysicsScript runner. Uses the DatCollection to
|
||||
|
|
@ -1460,7 +1476,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase G.2 lighting hooks: SetLightHook flips IsLit on
|
||||
// owner-tagged lights so ignite-torch animations light up,
|
||||
// extinguish-torch animations go dark.
|
||||
_lightingSink = new AcDream.Core.Lighting.LightingHookSink(Lighting);
|
||||
_lightingSink = new AcDream.Core.Lighting.LightingHookSink(Lighting, _effectPoses);
|
||||
_hookRouter.Register(_lightingSink);
|
||||
|
||||
// #188 — TransparentPartHook (per-part translucency fade, e.g.
|
||||
|
|
@ -2294,8 +2310,10 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase C.1.5a/b: construct EntityScriptActivator so static entities
|
||||
// (server-spawned AND dat-hydrated) fire Setup.DefaultScript through
|
||||
// the PhysicsScriptRunner on enter-world. C.1.5b adds per-part
|
||||
// transforms via SetupPartTransforms.Compute so multi-emitter scripts
|
||||
// distribute across mesh parts (closes #56). _scriptRunner and
|
||||
// transforms from the entity's exact current MeshRefs so
|
||||
// multi-emitter scripts distribute across mesh parts (closes #56).
|
||||
// Animated frames replace this snapshot through EntityEffectPoseRegistry.
|
||||
// _scriptRunner and
|
||||
// _particleSink are initialised earlier in OnLoad (line ~1083); both
|
||||
// are non-null here. The resolver lambda captures _dats and swallows
|
||||
// dat-lookup throws — see C.1.5a spec §6 (error handling) for rationale.
|
||||
|
|
@ -2309,12 +2327,21 @@ public sealed class GameWindow : IDisposable
|
|||
e.SourceGfxObjOrSetupId);
|
||||
if (setup is null) return null;
|
||||
uint scriptId = setup.DefaultScript.DataId;
|
||||
var parts = AcDream.Core.Meshing.SetupPartTransforms.Compute(setup);
|
||||
if (e.IndexedPartTransforms.Count == 0)
|
||||
{
|
||||
var indexed = AcDream.App.Rendering.Vfx.IndexedSetupPartPoseBuilder.Build(
|
||||
setup,
|
||||
e);
|
||||
e.SetIndexedPartPoses(indexed.Poses, indexed.Available);
|
||||
}
|
||||
IReadOnlyList<System.Numerics.Matrix4x4> parts = e.IndexedPartTransforms;
|
||||
IReadOnlyList<bool> availability = e.IndexedPartAvailable;
|
||||
var profile = AcDream.App.Rendering.Vfx.EntityEffectProfile.CreateDatStatic(setup);
|
||||
return new AcDream.App.Rendering.Vfx.ScriptActivationInfo(
|
||||
scriptId,
|
||||
parts,
|
||||
profile);
|
||||
profile,
|
||||
availability);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -2324,6 +2351,7 @@ public sealed class GameWindow : IDisposable
|
|||
var entityScriptActivator = new AcDream.App.Rendering.Vfx.EntityScriptActivator(
|
||||
_scriptRunner!,
|
||||
_particleSink!,
|
||||
_effectPoses,
|
||||
ResolveActivation,
|
||||
(ownerId, entity, profile) =>
|
||||
entityEffects?.OnDatStaticEntityReady(ownerId, entity, profile),
|
||||
|
|
@ -2364,12 +2392,24 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}),
|
||||
TearDownLiveEntityRuntimeComponents);
|
||||
_liveEntities.ProjectionVisibilityChanged += (record, visible) =>
|
||||
{
|
||||
if (record.WorldEntity is { } entity)
|
||||
_particleSink!.SetEntityPresentationVisible(entity.Id, visible);
|
||||
};
|
||||
|
||||
_liveEntityLights = new AcDream.App.Rendering.Vfx.LiveEntityLightController(
|
||||
_liveEntities,
|
||||
_effectPoses,
|
||||
_lightingSink!,
|
||||
setupId => _dats!.Get<DatReaderWriter.DBObjs.Setup>(setupId));
|
||||
|
||||
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
||||
_dats!,
|
||||
_datLock,
|
||||
Objects,
|
||||
_liveEntities,
|
||||
_effectPoses,
|
||||
TryAcceptParentForRender);
|
||||
|
||||
var tableResolver = new AcDream.Core.Vfx.PhysicsScriptTableResolver(
|
||||
|
|
@ -2378,6 +2418,7 @@ public sealed class GameWindow : IDisposable
|
|||
_liveEntities,
|
||||
_scriptRunner!,
|
||||
tableResolver,
|
||||
_effectPoses,
|
||||
(parentLocalId, partIndex) =>
|
||||
_equippedChildRenderer.FindChildLocalIdAtPart(parentLocalId, partIndex),
|
||||
childLocalId => _equippedChildRenderer.FindParentLocalId(childLocalId),
|
||||
|
|
@ -2389,8 +2430,14 @@ public sealed class GameWindow : IDisposable
|
|||
_entitySoundTables?.Set(ownerId, did);
|
||||
});
|
||||
_entityEffects = entityEffects;
|
||||
entityEffects.DiagnosticSink = message =>
|
||||
Console.Error.WriteLine($"vfx: {message}");
|
||||
_equippedChildRenderer.EntityReady += guid =>
|
||||
{
|
||||
entityEffects.OnLiveEntityReady(guid);
|
||||
};
|
||||
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
||||
_liveEntityLights.OnAttachedPoseReady(guid);
|
||||
_hookRouter.Register(entityEffects);
|
||||
|
||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||
|
|
@ -2619,6 +2666,7 @@ public sealed class GameWindow : IDisposable
|
|||
// F754/F755 can precede CreateObject, so the mixed pending FIFO
|
||||
// may contain owners with no LiveEntityRecord to tear down.
|
||||
_entityEffects?.ClearNetworkState();
|
||||
_animationHookFrames?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3626,14 +3674,39 @@ public sealed class GameWindow : IDisposable
|
|||
// creatures/characters whose size is intrinsic to the mesh).
|
||||
float scale = spawn.ObjScale ?? 1.0f;
|
||||
var scaleMat = System.Numerics.Matrix4x4.CreateScale(scale);
|
||||
IReadOnlyList<System.Numerics.Matrix4x4> rigidPartTransforms =
|
||||
AcDream.Core.Meshing.SetupPartTransforms.Compute(
|
||||
setup,
|
||||
idleFrame,
|
||||
scale);
|
||||
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
var indexedPartTransforms = new System.Numerics.Matrix4x4[parts.Count];
|
||||
var indexedPartAvailable = new bool[parts.Count];
|
||||
var animatedPartTemplate = new AnimatedPartTemplate[parts.Count];
|
||||
var liveBounds = new AcDream.Core.Meshing.LocalBoundsAccumulator();
|
||||
int dumpClothingTotalTris = 0;
|
||||
for (int partIdx = 0; partIdx < parts.Count; partIdx++)
|
||||
{
|
||||
var mr = parts[partIdx];
|
||||
IReadOnlyDictionary<uint, uint>? surfaceOverrides = null;
|
||||
if (resolvedOverridesByPart is not null && resolvedOverridesByPart.TryGetValue(partIdx, out var partOverrides))
|
||||
surfaceOverrides = partOverrides;
|
||||
|
||||
// Keep the Setup index even when the visual GfxObj is absent.
|
||||
// MeshRefs is a drawable-only list, while hooks and holding
|
||||
// locations address the stable CPartArray slot number.
|
||||
var transform = scale == 1.0f ? mr.PartTransform : mr.PartTransform * scaleMat;
|
||||
indexedPartTransforms[partIdx] = partIdx < rigidPartTransforms.Count
|
||||
? rigidPartTransforms[partIdx]
|
||||
: System.Numerics.Matrix4x4.Identity;
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
bool isDrawable = gfx is not null;
|
||||
indexedPartAvailable[partIdx] = isDrawable;
|
||||
animatedPartTemplate[partIdx] = new AnimatedPartTemplate(
|
||||
mr.GfxObjId,
|
||||
surfaceOverrides,
|
||||
isDrawable);
|
||||
if (gfx is null)
|
||||
{
|
||||
if (dumpClothing)
|
||||
|
|
@ -3650,10 +3723,6 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine($" EMIT part={partIdx:D2} gfx=0x{mr.GfxObjId:X8} subMeshes={subs} tris={tris}");
|
||||
}
|
||||
|
||||
IReadOnlyDictionary<uint, uint>? surfaceOverrides = null;
|
||||
if (resolvedOverridesByPart is not null && resolvedOverridesByPart.TryGetValue(partIdx, out var partOverrides))
|
||||
surfaceOverrides = partOverrides;
|
||||
|
||||
// Multiplication order matches offline scenery hydration:
|
||||
// `PartTransform * scaleMat`. In row-vector semantics this means
|
||||
// "apply PartTransform first (which includes the part-attachment
|
||||
|
|
@ -3663,8 +3732,6 @@ public sealed class GameWindow : IDisposable
|
|||
// for multi-part entities like the Nullified Statue that causes
|
||||
// the parts to drift relative to each other ("distorted") and the
|
||||
// base anchor to end up below the ground ("sinks into foundry").
|
||||
var transform = scale == 1.0f ? mr.PartTransform : mr.PartTransform * scaleMat;
|
||||
|
||||
// #119 follow-up: vertex-derived root-local bounds (see WorldEntity.RefreshAabb).
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) liveBounds.Add(transform, pb.Value);
|
||||
|
|
@ -3719,10 +3786,17 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
AcDream.Core.World.WorldEntity existing = visualUpdate.Entity;
|
||||
existing.ApplyAppearance(meshRefs, paletteOverride, entityPartOverrides);
|
||||
existing.SetIndexedPartPoses(indexedPartTransforms, indexedPartAvailable);
|
||||
if (liveBounds.TryGet(out var appearanceMin, out var appearanceMax))
|
||||
existing.SetLocalBounds(appearanceMin, appearanceMax);
|
||||
if (visualUpdate.Animation is { } animation)
|
||||
RebindAnimatedEntityForAppearance(animation, existing, setup, scale, meshRefs);
|
||||
RebindAnimatedEntityForAppearance(
|
||||
animation,
|
||||
existing,
|
||||
setup,
|
||||
scale,
|
||||
animatedPartTemplate,
|
||||
indexedPartAvailable);
|
||||
_classificationCache.InvalidateEntity(existing.Id);
|
||||
if (_liveEntities!.TryGetRecord(spawn.Guid, out LiveEntityRecord record)
|
||||
&& record.ProjectionKind is LiveEntityProjectionKind.Attached)
|
||||
|
|
@ -3732,6 +3806,14 @@ public sealed class GameWindow : IDisposable
|
|||
// after replacing the child's visual description.
|
||||
_equippedChildRenderer?.OnSpawn(spawn);
|
||||
}
|
||||
else
|
||||
{
|
||||
// SmartBox::UpdateVisualDesc mutates the existing PartArray.
|
||||
// Publish those exact replacement part frames before another
|
||||
// packet in this update can execute a part-attached hook.
|
||||
_effectPoses.PublishMeshRefs(existing);
|
||||
_equippedChildRenderer?.OnPosePublished(spawn.Guid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -3762,6 +3844,7 @@ public sealed class GameWindow : IDisposable
|
|||
PartOverrides = entityPartOverrides,
|
||||
ParentCellId = spawn.Position.Value.LandblockId,
|
||||
};
|
||||
created.SetIndexedPartPoses(indexedPartTransforms, indexedPartAvailable);
|
||||
if (liveBounds.TryGet(out var createdMin, out var createdMax))
|
||||
created.SetLocalBounds(createdMin, createdMax);
|
||||
return created;
|
||||
|
|
@ -3777,8 +3860,10 @@ public sealed class GameWindow : IDisposable
|
|||
entity.Rotation = rot;
|
||||
entity.ParentCellId = spawn.Position.Value.LandblockId;
|
||||
entity.ApplyAppearance(meshRefs, paletteOverride, entityPartOverrides);
|
||||
entity.SetIndexedPartPoses(indexedPartTransforms, indexedPartAvailable);
|
||||
if (liveBounds.TryGet(out var retainedMin, out var retainedMax))
|
||||
entity.SetLocalBounds(retainedMin, retainedMax);
|
||||
_effectPoses.PublishMeshRefs(entity);
|
||||
}
|
||||
|
||||
// Retail CPhysicsObj::leave_world removes cell/shadow membership but
|
||||
|
|
@ -3788,47 +3873,6 @@ public sealed class GameWindow : IDisposable
|
|||
bool retainedAnimationRuntime = !createdProjection
|
||||
&& _animatedEntities.TryGetValue(entity.Id, out _);
|
||||
|
||||
// A7 indoor lighting: server-spawned weenie fixtures (lanterns,
|
||||
// braziers, glowing items) carry their light in Setup.Lights exactly
|
||||
// like dat-baked statics, but the static registration in
|
||||
// ApplyLoadedTerrainLocked never sees CreateObject entities — so an
|
||||
// interior's lanterns cast no light and the room reads dark. Register
|
||||
// them here, mirroring that path (GameWindow.cs ~6742). Owned by
|
||||
// entity.Id, so leave-world and logical teardown both remove their
|
||||
// cell-scoped presentation. Retail registers object-borne lights
|
||||
// regardless of static-vs-dynamic origin (insert_light 0x0054d1b0).
|
||||
// The light is placed at the spawn frame and does NOT follow a moving
|
||||
// light-bearer yet (register row AP-44) — fine for stationary fixtures,
|
||||
// which is the common case. _dats is non-null here (checked above).
|
||||
if (_lightingSink is not null
|
||||
&& (entity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
var liteSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(entity.SourceGfxObjOrSetupId);
|
||||
if (liteSetup is not null && liteSetup.Lights.Count > 0)
|
||||
{
|
||||
var loaded = AcDream.Core.Lighting.LightInfoLoader.Load(
|
||||
liteSetup,
|
||||
ownerId: entity.Id,
|
||||
entityPosition: entity.Position,
|
||||
entityRotation: entity.Rotation,
|
||||
// A7 fix #2 (#176/#181): isDynamic is decided by whether the light
|
||||
// MOVES, not by weenie-vs-dat origin. Server-spawned FIXTURES
|
||||
// (lanterns, braziers) are stationary — they take retail's STATIC
|
||||
// curve (calc_point_light 0x0059c8b0: 1/d³ beyond 1 m, range×1.3,
|
||||
// per-channel colour clamp), not the D3D dynamic path (1/d,
|
||||
// range×1.5) the former #143 flag forced — which burned every
|
||||
// fixture ~10× hotter than retail at 3 m: the magenta wash that
|
||||
// zebra-striped the Facility Hub walls. Site-A lights are all
|
||||
// stationary today (AP-44: they don't follow bearers); genuinely
|
||||
// moving lights (projectiles, portal swirls) re-earn isDynamic
|
||||
// when they exist.
|
||||
isDynamic: false,
|
||||
cellId: entity.ParentCellId ?? 0u); // A7 #176/#177: scope to the owning cell's visibility
|
||||
foreach (var ls in loaded)
|
||||
_lightingSink.RegisterOwnedLight(ls);
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
|
||||
Id: entity.Id,
|
||||
SourceId: entity.SourceGfxObjOrSetupId,
|
||||
|
|
@ -3895,9 +3939,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Snapshot per-part identity from the hydrated meshRefs so the
|
||||
// tick can rebuild MeshRefs without redoing AnimPartChanges or
|
||||
// texture-override resolution every frame.
|
||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[meshRefs.Count];
|
||||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
||||
AnimatedPartTemplate[] template = animatedPartTemplate;
|
||||
|
||||
// Create an AnimationSequencer if we can load the MotionTable.
|
||||
AcDream.Core.Physics.AnimationSequencer? sequencer = null;
|
||||
|
|
@ -3925,6 +3967,7 @@ public sealed class GameWindow : IDisposable
|
|||
Framerate = idleCycle.Framerate,
|
||||
Scale = scale,
|
||||
PartTemplate = template,
|
||||
PartAvailability = indexedPartAvailable,
|
||||
CurrFrame = idleCycle.LowFrame,
|
||||
Sequencer = sequencer,
|
||||
};
|
||||
|
|
@ -3969,9 +4012,7 @@ public sealed class GameWindow : IDisposable
|
|||
var sequencer = SpawnMotionInitializer.Create(
|
||||
setup, mtable, _animLoader, spawn.MotionState);
|
||||
|
||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[meshRefs.Count];
|
||||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
||||
AnimatedPartTemplate[] template = animatedPartTemplate;
|
||||
|
||||
_animatedEntities[entity.Id] = new AnimatedEntity
|
||||
{
|
||||
|
|
@ -3983,6 +4024,7 @@ public sealed class GameWindow : IDisposable
|
|||
Framerate = 0f,
|
||||
Scale = scale,
|
||||
PartTemplate = template,
|
||||
PartAvailability = indexedPartAvailable,
|
||||
CurrFrame = 0,
|
||||
Sequencer = sequencer,
|
||||
};
|
||||
|
|
@ -4124,15 +4166,14 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.World.WorldEntity entity,
|
||||
DatReaderWriter.DBObjs.Setup setup,
|
||||
float scale,
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef> meshRefs)
|
||||
IReadOnlyList<AnimatedPartTemplate> partTemplate,
|
||||
IReadOnlyList<bool> partAvailability)
|
||||
{
|
||||
animation.Entity = entity;
|
||||
animation.Setup = setup;
|
||||
animation.Scale = scale;
|
||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[meshRefs.Count];
|
||||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
||||
animation.PartTemplate = template;
|
||||
animation.PartTemplate = partTemplate;
|
||||
animation.PartAvailability = partAvailability;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -4878,6 +4919,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_translucencyFades.ClearEntity(existingEntity.Id); // #188
|
||||
LeaveWorldLiveEntityRuntimeComponents(record);
|
||||
_liveEntityLights?.Forget(existingEntity.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -4897,10 +4939,10 @@ public sealed class GameWindow : IDisposable
|
|||
if (record.ServerGuid == _playerServerGuid)
|
||||
_lastLocalPlayerShadow = null;
|
||||
|
||||
// Object-borne lights are cell-scoped presentation. The owning Setup
|
||||
// and logical light capability remain on the live record; re-entry
|
||||
// registers the light in the new cell without duplicating it.
|
||||
_lightingSink?.UnregisterOwner(entity.Id);
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -5625,6 +5667,8 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (!_liveEntities!.TryApplyState(parsed, out _)) return;
|
||||
|
||||
_liveEntityLights?.OnStateChanged(parsed.Guid);
|
||||
|
||||
if (!_entitiesByServerGuid.TryGetValue(parsed.Guid, out var entity)) return;
|
||||
|
||||
// L.2g slice 1c (2026-05-13): the server addresses entities by
|
||||
|
|
@ -6628,29 +6672,25 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
var key = new SkyPesKey(i, obj.PesObjectId, obj.IsPostScene);
|
||||
seen.Add(key);
|
||||
|
||||
if (_activeSkyPes.Contains(key) || _missingSkyPes.Contains(key))
|
||||
continue;
|
||||
|
||||
uint skyEntityId = SkyPesEntityId(key);
|
||||
_entityEffects?.RegisterSyntheticOwner(skyEntityId);
|
||||
var renderPass = obj.IsPostScene
|
||||
? AcDream.Core.Vfx.ParticleRenderPass.SkyPostScene
|
||||
: AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene;
|
||||
_particleSink.SetEntityRenderPass(skyEntityId, renderPass);
|
||||
var anchor = SkyPesAnchor(obj, cameraWorldPos);
|
||||
var rotation = SkyPesRotation(obj, dayFraction);
|
||||
// Refresh anchor + rotation every frame so AttachLocal
|
||||
// (is_parent_local=1) particles track the camera. Retail
|
||||
// ParticleEmitter::UpdateParticles at 0x0051d2d4 reads the
|
||||
// live parent frame each tick; for sky-PES the parent IS
|
||||
// the camera. UpdateEntityAnchor is a no-op when no
|
||||
// emitters yet exist (script just spawned this frame).
|
||||
_particleSink.UpdateEntityAnchor(skyEntityId, anchor, rotation);
|
||||
_effectPoses.Publish(
|
||||
skyEntityId,
|
||||
System.Numerics.Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* System.Numerics.Matrix4x4.CreateTranslation(anchor),
|
||||
System.Array.Empty<System.Numerics.Matrix4x4>(),
|
||||
cellId: 0u);
|
||||
|
||||
if (_activeSkyPes.Contains(key) || _missingSkyPes.Contains(key))
|
||||
continue;
|
||||
|
||||
_entityEffects?.RegisterSyntheticOwner(skyEntityId);
|
||||
|
||||
if (_scriptRunner.Play(obj.PesObjectId, skyEntityId, anchor))
|
||||
{
|
||||
_activeSkyPes.Add(key);
|
||||
|
|
@ -6660,6 +6700,7 @@ public sealed class GameWindow : IDisposable
|
|||
_missingSkyPes.Add(key);
|
||||
_entityEffects?.UnregisterSyntheticOwner(skyEntityId);
|
||||
_particleSink.ClearEntityRenderPass(skyEntityId);
|
||||
_effectPoses.Remove(skyEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6673,6 +6714,7 @@ public sealed class GameWindow : IDisposable
|
|||
_scriptRunner.StopAllForEntity(skyEntityId);
|
||||
_entityEffects?.UnregisterSyntheticOwner(skyEntityId);
|
||||
_particleSink.StopAllForEntity(skyEntityId, fadeOut: true);
|
||||
_effectPoses.Remove(skyEntityId);
|
||||
_activeSkyPes.Remove(key);
|
||||
}
|
||||
|
||||
|
|
@ -9110,6 +9152,7 @@ public sealed class GameWindow : IDisposable
|
|||
if (_animatedEntities.Count > 0)
|
||||
TickAnimations((float)deltaSeconds);
|
||||
_equippedChildRenderer?.Tick();
|
||||
_animationHookFrames?.Drain();
|
||||
|
||||
// #188 — advance translucency fades UNCONDITIONALLY (not gated on
|
||||
// _animatedEntities.Count): a one-shot open-cycle animation can
|
||||
|
|
@ -9276,8 +9319,10 @@ public sealed class GameWindow : IDisposable
|
|||
// debug-only and disabled for normal retail rendering.
|
||||
if (_options.EnableSkyPesDebug)
|
||||
UpdateSkyPes((float)WorldTime.DayFraction, _activeDayGroup, camPos, cameraInsideCell);
|
||||
_entityEffects?.RefreshLiveOwnerAnchors();
|
||||
_entityEffects?.RefreshLiveOwnerPoses();
|
||||
_scriptRunner?.Tick(_physicsScriptGameTime);
|
||||
_particleSink?.RefreshAttachedEmitters();
|
||||
_liveEntityLights?.Refresh();
|
||||
_particleSystem?.Tick((float)deltaSeconds);
|
||||
|
||||
// Phase G.1/G.2: feed the sun, tick LightManager, build + upload
|
||||
|
|
@ -10369,33 +10414,13 @@ public sealed class GameWindow : IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
// Phase E.1: drain animation hooks (footstep sounds, attack
|
||||
// damage frames, particle spawns, part swaps, etc.) and fan
|
||||
// them out to registered subsystems via the hook router.
|
||||
// Mirrors ACE's PhysicsObj.add_anim_hook dispatch path.
|
||||
//
|
||||
// R2-Q4 queue-drain wiring (r2-port-plan.md §4, the G6 seam;
|
||||
// per-tick PLACEMENT provisional until R6 installs retail's
|
||||
// UpdateObjectInternal order): each drained AnimDone hook
|
||||
// advances the entity's MotionTableManager countdown
|
||||
// (retail AnimDoneHook::Execute 0x00526c20 → Hook_AnimDone
|
||||
// 0x0050fda0 → CPartArray::AnimationDone(1)), and UseTime
|
||||
// runs once per tick to sweep zero-tick queue entries
|
||||
// (retail call sites 0x00517d57/0x00517d67).
|
||||
var hooks = ae.Sequencer.ConsumePendingHooks();
|
||||
if (hooks.Count > 0)
|
||||
{
|
||||
System.Numerics.Vector3 worldPos = ae.Entity.Position;
|
||||
for (int hi = 0; hi < hooks.Count; hi++)
|
||||
{
|
||||
var hook = hooks[hi];
|
||||
if (hook is null) continue;
|
||||
_hookRouter.OnHook(ae.Entity.Id, worldPos, hook);
|
||||
if (hook is DatReaderWriter.Types.AnimationDoneHook)
|
||||
ae.Sequencer.Manager.AnimationDone(success: true);
|
||||
}
|
||||
}
|
||||
ae.Sequencer.Manager.UseTime();
|
||||
// Capture hooks now, but deliver them only after every final
|
||||
// part transform and equipped-child root has been published.
|
||||
// This matches CPhysicsObj::UpdateObjectInternal followed by
|
||||
// CPhysicsObj::UpdateChild (0x00512D50): a hand/weapon effect
|
||||
// reads this frame's pose, never the previous frame's pose.
|
||||
// AnimationDone/UseTime remain paired with the deferred drain.
|
||||
_animationHookFrames?.Capture(ae.Entity.Id, ae.Sequencer);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -10475,6 +10500,8 @@ public sealed class GameWindow : IDisposable
|
|||
// consumer caches a stale reference or diffs list identity).
|
||||
var newMeshRefs = ae.MeshRefsScratch;
|
||||
newMeshRefs.Clear();
|
||||
var effectPartPoses = ae.EffectPartPosesScratch;
|
||||
effectPartPoses.Clear();
|
||||
var scaleMat = ae.Scale == 1.0f
|
||||
? System.Numerics.Matrix4x4.Identity
|
||||
: System.Numerics.Matrix4x4.CreateScale(ae.Scale);
|
||||
|
|
@ -10532,7 +10559,7 @@ public sealed class GameWindow : IDisposable
|
|||
? ae.Setup.DefaultScale[i]
|
||||
: System.Numerics.Vector3.One;
|
||||
|
||||
var partTransform =
|
||||
var visualPartTransform =
|
||||
System.Numerics.Matrix4x4.CreateScale(defaultScale) *
|
||||
System.Numerics.Matrix4x4.CreateFromQuaternion(orientation) *
|
||||
System.Numerics.Matrix4x4.CreateTranslation(origin);
|
||||
|
|
@ -10540,14 +10567,23 @@ public sealed class GameWindow : IDisposable
|
|||
// Bake the entity's ObjScale on top, matching the hydration
|
||||
// order (PartTransform * scaleMat) — see comment in OnLiveEntitySpawned.
|
||||
if (ae.Scale != 1.0f)
|
||||
partTransform = partTransform * scaleMat;
|
||||
visualPartTransform *= scaleMat;
|
||||
|
||||
// Retail CPartArray::UpdateParts (0x005190F0) scales only the
|
||||
// frame origin. Setup DefaultScale is visual gfxobj_scale,
|
||||
// not part-frame state (SetScaleInternal 0x00518A00).
|
||||
var rigidPartTransform =
|
||||
System.Numerics.Matrix4x4.CreateFromQuaternion(orientation) *
|
||||
System.Numerics.Matrix4x4.CreateTranslation(origin * ae.Scale);
|
||||
effectPartPoses.Add(rigidPartTransform);
|
||||
|
||||
var template = ae.PartTemplate[i];
|
||||
if (_options.HidePartIndex >= 0 && i == _options.HidePartIndex && partCount >= 10)
|
||||
if (!template.IsDrawable
|
||||
|| (_options.HidePartIndex >= 0 && i == _options.HidePartIndex && partCount >= 10))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
|
||||
newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, visualPartTransform)
|
||||
{
|
||||
SurfaceOverrides = template.SurfaceOverrides,
|
||||
});
|
||||
|
|
@ -10557,6 +10593,8 @@ public sealed class GameWindow : IDisposable
|
|||
// property store) and keeps this line's shape identical to the
|
||||
// pre-MP-Alloc code for anyone grepping the history.
|
||||
ae.Entity.MeshRefs = newMeshRefs;
|
||||
ae.Entity.SetIndexedPartPoses(effectPartPoses, ae.PartAvailability);
|
||||
_effectPoses.Publish(ae.Entity, effectPartPoses, ae.PartAvailability);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14019,7 +14057,11 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
_liveEntityLights?.Dispose();
|
||||
_liveEntityLights = null;
|
||||
_entityEffects?.ClearNetworkState();
|
||||
_animationHookFrames?.Clear();
|
||||
_effectPoses.Clear();
|
||||
}
|
||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue