using System.Numerics; using AcDream.App.Physics; using AcDream.App.Rendering.Vfx; using AcDream.App.Rendering.Wb; using AcDream.App.Update; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Meshing; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Plugins; using AcDream.Core.World; using AcDream.Runtime.Entities; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.App.Rendering; /// /// DAT-backed CPhysicsObj::set_description @ 0x00514F40 projection /// materializer. It owns no identity map and may mutate only the exact /// supplied by . /// Logical registration and spatial rebucketing remain owned by /// . /// internal sealed class DatLiveEntityProjectionMaterializer : ILiveEntityProjectionMaterializer { private readonly RuntimeOptions _options; private readonly IDatReaderWriter _dats; private readonly LiveEntityRuntime _runtime; private readonly LiveCollisionAssetPublisher _collisionAssets; private readonly IAnimationLoader _animationLoader; private readonly EntitySpawnAdapter _spawnAdapter; private readonly TextureCache _textures; private readonly EntityClassificationCache _classification; private readonly EntityEffectPoseRegistry _effectPoses; private readonly EquippedChildRenderController _equippedChildren; private readonly WorldGameState _worldState; private readonly WorldEvents _worldEvents; private readonly ShadowObjectRegistry _shadows; private readonly LiveEntityCollisionBuilder _collisionBuilder; private readonly ProjectileController _projectiles; private readonly LiveEntityAnimationPresenter _animationPresenter; private readonly RetailStaticAnimatingObjectScheduler _staticAnimations; private readonly LiveWorldOriginState _origin; private readonly IPhysicsScriptTimeSource _gameTime; private int _received; private int _hydrated; private int _noPosition; private int _noSetup; private int _missingSetup; private int _noMesh; private int _noCycle; private int _zeroFramerate; private int _singleFrame; private int _missingPartFrames; public DatLiveEntityProjectionMaterializer( RuntimeOptions options, IDatReaderWriter dats, LiveEntityRuntime runtime, LiveCollisionAssetPublisher collisionAssets, IAnimationLoader animationLoader, EntitySpawnAdapter spawnAdapter, TextureCache textures, EntityClassificationCache classification, EntityEffectPoseRegistry effectPoses, EquippedChildRenderController equippedChildren, WorldGameState worldState, WorldEvents worldEvents, ShadowObjectRegistry shadows, LiveEntityCollisionBuilder collisionBuilder, ProjectileController projectiles, LiveEntityAnimationPresenter animationPresenter, RetailStaticAnimatingObjectScheduler staticAnimations, LiveWorldOriginState origin, IPhysicsScriptTimeSource gameTime) { _options = options ?? throw new ArgumentNullException(nameof(options)); _dats = dats ?? throw new ArgumentNullException(nameof(dats)); _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _collisionAssets = collisionAssets ?? throw new ArgumentNullException(nameof(collisionAssets)); _animationLoader = animationLoader ?? throw new ArgumentNullException(nameof(animationLoader)); _spawnAdapter = spawnAdapter ?? throw new ArgumentNullException(nameof(spawnAdapter)); _textures = textures ?? throw new ArgumentNullException(nameof(textures)); _classification = classification ?? throw new ArgumentNullException(nameof(classification)); _effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses)); _equippedChildren = equippedChildren ?? throw new ArgumentNullException(nameof(equippedChildren)); _worldState = worldState ?? throw new ArgumentNullException(nameof(worldState)); _worldEvents = worldEvents ?? throw new ArgumentNullException(nameof(worldEvents)); _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); _collisionBuilder = collisionBuilder ?? throw new ArgumentNullException(nameof(collisionBuilder)); _projectiles = projectiles ?? throw new ArgumentNullException(nameof(projectiles)); _animationPresenter = animationPresenter ?? throw new ArgumentNullException(nameof(animationPresenter)); _staticAnimations = staticAnimations ?? throw new ArgumentNullException(nameof(staticAnimations)); _origin = origin ?? throw new ArgumentNullException(nameof(origin)); _gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime)); } public void ResetSessionState() { _received = 0; _hydrated = 0; _noPosition = 0; _noSetup = 0; _missingSetup = 0; _noMesh = 0; _noCycle = 0; _zeroFramerate = 0; _singleFrame = 0; _missingPartFrames = 0; } public bool TryMaterialize( RuntimeEntityRecord expectedCanonical, WorldSession.EntitySpawn canonicalSpawn, LiveProjectionPurpose purpose, ulong expectedCreateIntegrationVersion, LiveEntityAppearanceUpdateState? appearanceUpdate = null) { ArgumentNullException.ThrowIfNull(expectedCanonical); if (purpose is LiveProjectionPurpose.AppearanceMutation != (appearanceUpdate is not null)) { throw new ArgumentException( "Appearance mutation requires the exact captured visual owner.", nameof(appearanceUpdate)); } if (!_runtime.IsCurrentCreateIntegration( expectedCanonical, expectedCreateIntegrationVersion) || canonicalSpawn.Guid != expectedCanonical.ServerGuid || canonicalSpawn.InstanceSequence != expectedCanonical.Incarnation) { return false; } _runtime.TryGetProjection( expectedCanonical, out LiveEntityRecord? expectedRecord); if (appearanceUpdate is not null && expectedRecord is null) return false; _received++; bool dumpLiveSpawns = _options.DumpLiveSpawns; DumpSpawn(canonicalSpawn, dumpLiveSpawns); if (!_origin.IsKnown) return false; if (canonicalSpawn.Position is null || canonicalSpawn.SetupTableId is null) { if (canonicalSpawn.Position is null) _noPosition++; else _noSetup++; return false; } CreateObject.ServerPosition position = canonicalSpawn.Position.Value; int lbX = (int)((position.LandblockId >> 24) & 0xFFu); int lbY = (int)((position.LandblockId >> 16) & 0xFFu); if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeWorldFrameEnabled) ProbeWorldFrameAgreement(position.LandblockId); var worldOrigin = new Vector3( (lbX - _origin.CenterX) * 192f, (lbY - _origin.CenterY) * 192f, 0f); Vector3 worldPosition = new( position.PositionX + worldOrigin.X, position.PositionY + worldOrigin.Y, position.PositionZ); var rotation = new Quaternion( position.RotationX, position.RotationY, position.RotationZ, position.RotationW); Setup? setup = _dats.Get(canonicalSpawn.SetupTableId.Value); if (setup is not null) _collisionAssets.CacheSetup(canonicalSpawn.SetupTableId.Value, setup); if (setup is null) { _missingSetup++; if (dumpLiveSpawns) { Console.WriteLine( $"live: DROP setup dat 0x{canonicalSpawn.SetupTableId.Value:X8} missing " + $"(guid=0x{canonicalSpawn.Guid:X8})"); } return false; } _runtime.SetHasPartArray(expectedCanonical, true); ushort? stanceOverride = canonicalSpawn.MotionState?.Stance; ushort? commandOverride = canonicalSpawn.MotionState?.ForwardCommand; var idleCycle = MotionResolver.GetIdleCycle( setup, _dats, _animationLoader, motionTableIdOverride: canonicalSpawn.MotionTableId, stanceOverride: stanceOverride, commandOverride: commandOverride); AnimationFrame? idleFrame = null; if (idleCycle is not null) { int startIndex = idleCycle.LowFrame; if (startIndex < 0 || startIndex >= idleCycle.Animation.PartFrames.Count) startIndex = 0; idleFrame = idleCycle.Animation.PartFrames[startIndex]; } List flattened = [.. SetupMesh.Flatten(setup, idleFrame)]; IReadOnlyList animPartChanges = canonicalSpawn.AnimPartChanges ?? Array.Empty(); bool dumpClothing = _options.DumpClothing && setup.Parts.Count >= 10; DumpClothingHeader( canonicalSpawn, setup, flattened, idleFrame, animPartChanges, dumpClothing); foreach (CreateObject.AnimPartChange change in animPartChanges) { if (change.PartIndex < flattened.Count) { flattened[change.PartIndex] = new MeshRef( change.NewModelId, flattened[change.PartIndex].PartTransform); } } uint[] collisionPartGfxObjIds = LiveEntityCollisionBuilder.ResolveEffectivePartIdentities( flattened.Select(static part => part.GfxObjId).ToArray(), baseId => ResolveCollisionPart(baseId)); if (_options.RetailCloseDegrades && IsIssue47HumanoidSetup(setup)) ApplyRetailCloseDegrades(flattened, dumpClothing); IReadOnlyList textureChanges = canonicalSpawn.TextureChanges ?? Array.Empty(); Dictionary>? surfaceOverrides = ResolveSurfaceOverrides( canonicalSpawn, flattened, textureChanges, dumpClothing, dumpLiveSpawns); float scale = canonicalSpawn.ObjScale ?? 1f; Matrix4x4 scaleMatrix = Matrix4x4.CreateScale(scale); IReadOnlyList rigidPartTransforms = SetupPartTransforms.Compute(setup, idleFrame, scale); var meshRefs = new List(); var indexedPartTransforms = new Matrix4x4[flattened.Count]; var indexedPartAvailable = new bool[flattened.Count]; var animatedPartTemplate = new LiveAnimationPartTemplate[flattened.Count]; var bounds = new LocalBoundsAccumulator(); int clothingTriangles = 0; for (int partIndex = 0; partIndex < flattened.Count; partIndex++) { MeshRef part = flattened[partIndex]; IReadOnlyDictionary? overrides = null; if (surfaceOverrides?.TryGetValue(partIndex, out var found) == true) overrides = found; Matrix4x4 transform = scale == 1f ? part.PartTransform : part.PartTransform * scaleMatrix; indexedPartTransforms[partIndex] = partIndex < rigidPartTransforms.Count ? rigidPartTransforms[partIndex] : Matrix4x4.Identity; GfxObj? gfx = _dats.Get(part.GfxObjId); bool drawable = gfx is not null; indexedPartAvailable[partIndex] = drawable; animatedPartTemplate[partIndex] = new LiveAnimationPartTemplate( part.GfxObjId, overrides, drawable); if (gfx is null) { if (dumpClothing) Console.WriteLine($" EMIT part={partIndex:D2} gfx=0x{part.GfxObjId:X8} GFXOBJ_DAT_MISSING -> 0 tris"); continue; } _collisionAssets.CacheGfxObj(part.GfxObjId, gfx); if (dumpClothing) { var subMeshes = GfxObjMesh.Build(gfx, _dats); int triangles = 0; foreach (var subMesh in subMeshes) triangles += subMesh.Indices.Length / 3; clothingTriangles += triangles; Console.WriteLine( $" EMIT part={partIndex:D2} gfx=0x{part.GfxObjId:X8} " + $"subMeshes={subMeshes.Count} tris={triangles}"); } if (GfxObjBounds.Get(gfx) is { } partBounds) bounds.Add(transform, partBounds); meshRefs.Add(new MeshRef(part.GfxObjId, transform) { SurfaceOverrides = overrides, }); } if (meshRefs.Count == 0) { _noMesh++; if (dumpLiveSpawns) { Console.WriteLine( $"live: DROP no mesh refs from setup 0x{canonicalSpawn.SetupTableId.Value:X8} " + $"(guid=0x{canonicalSpawn.Guid:X8})"); } return false; } if (dumpClothing) { Console.WriteLine( $" TOTAL tris={clothingTriangles} meshRefs={meshRefs.Count} " + $"(parts.Count={flattened.Count})"); } PaletteOverride? paletteOverride = CreatePaletteOverride(canonicalSpawn); PartOverride[] partOverrides = CreatePartOverrides(animPartChanges); bool supersessionRecovery = purpose is LiveProjectionPurpose.CreateSupersessionRecovery; if (supersessionRecovery && expectedRecord?.WorldEntity is not null) { return LiveEntityCreateSupersessionRecovery.TryApply( _runtime, expectedRecord, expectedCreateIntegrationVersion, captureAppearance: () => appearanceUpdate ?? LiveEntityAppearanceBinding.Capture( _runtime, expectedRecord.ServerGuid), publishAppearance: visualUpdate => ApplyAppearance( expectedRecord, canonicalSpawn, visualUpdate, setup, scale, worldOrigin, collisionPartGfxObjIds, meshRefs, paletteOverride, partOverrides, indexedPartTransforms, indexedPartAvailable, animatedPartTemplate, bounds, expectedCreateIntegrationVersion), publishCurrentSnapshot: visualUpdate => { var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot( visualUpdate.Entity.Id, visualUpdate.Entity.SourceGfxObjOrSetupId, visualUpdate.Entity.Position, visualUpdate.Entity.Rotation); _worldState.Add(snapshot); _worldEvents.UpsertCurrent(snapshot); }, synchronizeAnimation: visualUpdate => RegisterAnimation( expectedRecord, visualUpdate.Entity, setup, canonicalSpawn, idleCycle, scale, animatedPartTemplate, indexedPartAvailable, retainedAnimation: expectedRecord.AnimationRuntime is LiveEntityAnimationState, synchronizeAnimation: true)); } if (appearanceUpdate is { } visualUpdate) { return ApplyAppearance( expectedRecord!, canonicalSpawn, visualUpdate, setup, scale, worldOrigin, collisionPartGfxObjIds, meshRefs, paletteOverride, partOverrides, indexedPartTransforms, indexedPartAvailable, animatedPartTemplate, bounds, expectedCreateIntegrationVersion); } return MaterializeProjection( expectedCanonical, expectedRecord, canonicalSpawn, setup, idleCycle, worldOrigin, worldPosition, rotation, scale, collisionPartGfxObjIds, meshRefs, paletteOverride, partOverrides, indexedPartTransforms, indexedPartAvailable, animatedPartTemplate, bounds, dumpLiveSpawns, expectedCreateIntegrationVersion, synchronizeAnimation: supersessionRecovery); } private uint ResolveCollisionPart(uint baseId) { if (!GfxObjDegradeResolver.TryResolveCloseGfxObj( _dats, baseId, out uint slotZeroId, out GfxObj? slotZeroGfx)) { return baseId; } if (slotZeroGfx is not null) _collisionAssets.CacheGfxObj(slotZeroId, slotZeroGfx); return slotZeroId; } private void ApplyRetailCloseDegrades( List parts, bool dumpClothing) { for (int partIndex = 0; partIndex < parts.Count; partIndex++) { MeshRef part = parts[partIndex]; if (!GfxObjDegradeResolver.TryResolveCloseGfxObj( _dats, part.GfxObjId, out uint resolvedId, out _) || resolvedId == part.GfxObjId) { continue; } parts[partIndex] = new MeshRef(resolvedId, part.PartTransform); if (dumpClothing) { Console.WriteLine( $" DEGRADE part={partIndex:D2} gfx=0x{part.GfxObjId:X8} " + $"-> close=0x{resolvedId:X8}"); } } } private Dictionary>? ResolveSurfaceOverrides( WorldSession.EntitySpawn spawn, IReadOnlyList parts, IReadOnlyList textureChanges, bool dumpClothing, bool dumpLiveSpawns) { if (dumpClothing) { Console.WriteLine($" TextureChanges count={textureChanges.Count}"); foreach (CreateObject.TextureChange change in textureChanges) { Console.WriteLine( $" TC part={change.PartIndex:D2} oldTex=0x{change.OldTexture:X8} " + $"-> newTex=0x{change.NewTexture:X8}"); } } if (textureChanges.Count == 0) return null; var oldToNewByPart = new Dictionary>(); foreach (CreateObject.TextureChange change in textureChanges) { if (!oldToNewByPart.TryGetValue(change.PartIndex, out var oldToNew)) { oldToNew = []; oldToNewByPart.Add(change.PartIndex, oldToNew); } oldToNew[change.OldTexture] = change.NewTexture; } bool statueDiagnostic = dumpLiveSpawns && spawn.Name?.Contains("Statue", StringComparison.OrdinalIgnoreCase) == true; var result = new Dictionary>(); for (int partIndex = 0; partIndex < parts.Count; partIndex++) { if (!oldToNewByPart.TryGetValue(partIndex, out var oldToNew)) continue; GfxObj? gfx = _dats.Get(parts[partIndex].GfxObjId); if (gfx is null) { if (statueDiagnostic) { Console.WriteLine( $"live: [STATUE] resolve part={partIndex} " + $"GfxObj 0x{parts[partIndex].GfxObjId:X8} missing"); } continue; } _collisionAssets.CacheGfxObj(parts[partIndex].GfxObjId, gfx); Dictionary? resolved = null; foreach (var surfaceQid in gfx.Surfaces) { uint surfaceId = (uint)surfaceQid; Surface? surface = _dats.Get(surfaceId); if (surface is null) continue; uint originalTexture = (uint)surface.OrigTextureId; if (originalTexture == 0 || !oldToNew.TryGetValue(originalTexture, out uint newTexture)) { continue; } (resolved ??= [])[surfaceId] = newTexture; } if (resolved is not null) result[partIndex] = resolved; } return result.Count == 0 ? null : result; } private static PaletteOverride? CreatePaletteOverride( WorldSession.EntitySpawn spawn) { if (spawn.SubPalettes is not { Count: > 0 } subPalettes) return null; var ranges = new PaletteOverride.SubPaletteRange[subPalettes.Count]; for (int i = 0; i < subPalettes.Count; i++) { ranges[i] = new PaletteOverride.SubPaletteRange( subPalettes[i].SubPaletteId, subPalettes[i].Offset, subPalettes[i].Length); } return new PaletteOverride(spawn.BasePaletteId ?? 0u, ranges); } private static PartOverride[] CreatePartOverrides( IReadOnlyList changes) { if (changes.Count == 0) return []; var result = new PartOverride[changes.Count]; for (int i = 0; i < changes.Count; i++) result[i] = new PartOverride(changes[i].PartIndex, changes[i].NewModelId); return result; } private bool ApplyAppearance( LiveEntityRecord expectedRecord, WorldSession.EntitySpawn spawn, LiveEntityAppearanceUpdateState visualUpdate, Setup setup, float scale, Vector3 worldOrigin, IReadOnlyList collisionPartGfxObjIds, IReadOnlyList meshRefs, PaletteOverride? paletteOverride, IReadOnlyList partOverrides, IReadOnlyList indexedPartTransforms, IReadOnlyList indexedPartAvailable, IReadOnlyList animatedPartTemplate, LocalBoundsAccumulator bounds, ulong expectedCreateIntegrationVersion) { WorldEntity entity = visualUpdate.Entity; if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } LiveEntityAppearanceCollisionUpdate? appearanceCollision = LiveEntityAppearanceBinding.PrepareCollision( _runtime, _collisionBuilder, entity, setup, collisionPartGfxObjIds, spawn, worldOrigin); bool published = _spawnAdapter.OnAppearanceChanged( entity, meshRefs, partOverrides, () => { _textures.ReleaseOwner(entity.Id); entity.ApplyAppearance(meshRefs, paletteOverride, partOverrides); }, () => { if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return; } if (appearanceCollision is not null) { LiveEntityAppearanceBinding.CommitCollision( _runtime, _shadows, appearanceCollision); } entity.SetIndexedPartPoses(indexedPartTransforms, indexedPartAvailable); if (bounds.TryGet(out Vector3 minimum, out Vector3 maximum)) entity.SetLocalBounds(minimum, maximum); if (visualUpdate.Animation is { } animation) { LiveEntityAppearanceBinding.RebindAnimation( animation, entity, setup, scale, animatedPartTemplate, indexedPartAvailable); } _classification.InvalidateEntity(entity.Id); if (_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) && expectedRecord.ProjectionKind is LiveEntityProjectionKind.Attached) { _equippedChildren.OnSpawn(expectedRecord.Snapshot); } else { _effectPoses.PublishMeshRefs(entity); if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion)) { return; } _equippedChildren.OnPosePublished(spawn.Guid); } }); return published && _runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) && ReferenceEquals(expectedRecord.WorldEntity, entity); } private bool MaterializeProjection( RuntimeEntityRecord expectedCanonical, LiveEntityRecord? retainedRecord, WorldSession.EntitySpawn spawn, Setup setup, MotionResolver.IdleCycle? idleCycle, Vector3 worldOrigin, Vector3 worldPosition, Quaternion rotation, float scale, IReadOnlyList collisionPartGfxObjIds, IReadOnlyList meshRefs, PaletteOverride? paletteOverride, IReadOnlyList partOverrides, IReadOnlyList indexedPartTransforms, IReadOnlyList indexedPartAvailable, IReadOnlyList animatedPartTemplate, LocalBoundsAccumulator bounds, bool dumpLiveSpawns, ulong expectedCreateIntegrationVersion, bool synchronizeAnimation) { EntityEffectProfile profile = spawn.Physics is { } physics ? EntityEffectProfile.CreateLive(setup, physics) : EntityEffectProfile.CreateDatStatic(setup); if (retainedRecord is not null && !_runtime.TryGetEffectProfile(spawn.Guid, out _)) { _runtime.SetEffectProfile(spawn.Guid, profile); } bool createdProjection = false; // C3c route-1 flip: a fresh world Create materializes presentation- // only — the sidecar exists but stays non-spatial until the // residence-driven Runtime placement's completion receipt (or, for a // record whose residence already completed before its sidecar could // exist, the self-projection below). An already-materialized record // keeps its sticky residence so unflipped same-incarnation // transitions (an equipped child dropping to world) stay on their // legacy path by construction. LiveEntityMaterializationResidence residence = retainedRecord?.MaterializationResidence ?? LiveEntityMaterializationResidence.AwaitRuntimePlacement; WorldEntity? entity = _runtime.MaterializeLiveEntity( expectedCanonical, spawn.Position!.Value.LandblockId, localId => { createdProjection = true; var created = new WorldEntity { Id = localId, ServerGuid = spawn.Guid, SourceGfxObjOrSetupId = spawn.SetupTableId!.Value, Position = worldPosition, Rotation = rotation, MeshRefs = meshRefs, PaletteOverride = paletteOverride, PartOverrides = partOverrides, ParentCellId = spawn.Position.Value.LandblockId, }; created.SetIndexedPartPoses(indexedPartTransforms, indexedPartAvailable); if (bounds.TryGet(out Vector3 minimum, out Vector3 maximum)) created.SetLocalBounds(minimum, maximum); return created; }, LiveEntityProjectionKind.World, initializeProjection: record => record.EffectProfile = profile, out LiveEntityRecord? expectedRecord, residence); if (entity is null || expectedRecord is null || !_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } if (residence is LiveEntityMaterializationResidence.AwaitRuntimePlacement && expectedCanonical.FullCellId != 0u && !_runtime.HasActiveInitialCreateResidence(expectedCanonical)) { // The residence-driven placement already committed before this // sidecar existed (a deferred-parent child replayed during its // parent's drain, or a recovery re-materialization) — its // completion receipt is gone, so presentation self-projects from // the committed canonical state through the presentation-only // bucket path. if (!_runtime.RebucketLiveEntity( spawn.Guid, expectedCanonical.FullCellId) || !_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } } if (!createdProjection) { entity.SetPosition(worldPosition); entity.Rotation = rotation; entity.ParentCellId = spawn.Position.Value.LandblockId; entity.ApplyAppearance(meshRefs, paletteOverride, partOverrides); entity.SetIndexedPartPoses(indexedPartTransforms, indexedPartAvailable); if (bounds.TryGet(out Vector3 minimum, out Vector3 maximum)) entity.SetLocalBounds(minimum, maximum); _effectPoses.PublishMeshRefs(entity); } bool retainedAnimation = !createdProjection && expectedRecord.AnimationRuntime is LiveEntityAnimationState; var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot( entity.Id, entity.SourceGfxObjOrSetupId, entity.Position, entity.Rotation); _worldState.Add(snapshot); _worldEvents.UpsertCurrent(snapshot); if (_runtime.TryMarkWorldSpawnPublished(spawn.Guid)) _worldEvents.FireEntitySpawned(snapshot); _hydrated++; if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } _equippedChildren.OnWorldEntityRegistered(spawn.Guid); if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } if (_collisionBuilder.Build( entity, setup, collisionPartGfxObjIds, spawn, expectedRecord, worldOrigin) is { } collision) { LiveEntityCollisionBuilder.Register(_shadows, collision); } if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) return false; bool initialResidenceActive = _runtime.HasActiveInitialCreateResidence(expectedCanonical); // C3c first entry owns CPhysicsObj construction and SetPosition for a // residence-managed Create. Eager projectile classification used to // acquire that same body here, before the conductor ran; the // conductor then correctly rejected the unexpected owner and the // missile remained permanently cell-less. The committed projection // visibility edge retries TryBind after Runtime has constructed and // placed the one canonical body. if (!initialResidenceActive) { _projectiles.TryBind( expectedRecord, setup, _gameTime.CurrentScriptTime, _origin.CenterX, _origin.CenterY); } if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } RegisterAnimation( expectedRecord, entity, setup, spawn, idleCycle, scale, animatedPartTemplate, indexedPartAvailable, retainedAnimation, synchronizeAnimation); if (!_runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion) || !ReferenceEquals(expectedRecord.WorldEntity, entity)) { return false; } if (dumpLiveSpawns && _received % 20 == 0) { Console.WriteLine( $"live: animated={_runtime.AnimationRuntimeCount} " + $"animReject: noCycle={_noCycle} fr0={_zeroFramerate} " + $"1frame={_singleFrame} partFrames={_missingPartFrames}"); Console.WriteLine( $"live: summary recv={_received} hydrated={_hydrated} " + $"drops: noPos={_noPosition} noSetup={_noSetup} " + $"setupMissing={_missingSetup} noMesh={_noMesh}"); } return _runtime.IsCurrentCreateIntegration( expectedRecord, expectedCreateIntegrationVersion); } private void RegisterAnimation( LiveEntityRecord expectedRecord, WorldEntity entity, Setup setup, WorldSession.EntitySpawn spawn, MotionResolver.IdleCycle? idleCycle, float scale, IReadOnlyList partTemplate, IReadOnlyList partAvailability, bool retainedAnimation, bool synchronizeAnimation) { if (retainedAnimation && synchronizeAnimation && expectedRecord.AnimationRuntime is LiveEntityAnimationState retained) { SynchronizeRetainedAnimation( expectedRecord, retained, setup, spawn, idleCycle); } if (!retainedAnimation) { if (idleCycle is null) _noCycle++; else if (idleCycle.Framerate == 0f) _zeroFramerate++; else if (idleCycle.HighFrame <= idleCycle.LowFrame) _singleFrame++; else if (idleCycle.Animation.PartFrames.Count <= 1) _missingPartFrames++; } if (!retainedAnimation && idleCycle is not null && idleCycle.Framerate != 0f && idleCycle.HighFrame > idleCycle.LowFrame && idleCycle.Animation.PartFrames.Count > 1) { AnimationSequencer? sequencer = CreateMotionSequencer(setup, spawn); _runtime.SetAnimationRuntime( spawn.Guid, new LiveEntityAnimationState { Entity = entity, Setup = setup, Animation = idleCycle.Animation, LowFrame = Math.Max(0, idleCycle.LowFrame), HighFrame = Math.Min( idleCycle.HighFrame, idleCycle.Animation.PartFrames.Count - 1), Framerate = idleCycle.Framerate, Scale = scale, PartTemplate = partTemplate, PartAvailability = partAvailability, CurrFrame = idleCycle.LowFrame, Sequencer = sequencer, }); } else if (!retainedAnimation) { uint motionTableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable; if (motionTableId != 0 && _dats.Get(motionTableId) is { } motionTable) { AnimationSequencer sequencer = SpawnMotionInitializer.Create( setup, motionTable, _animationLoader, spawn.MotionState); _runtime.SetAnimationRuntime( spawn.Guid, new LiveEntityAnimationState { Entity = entity, Setup = setup, Animation = null!, LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = scale, PartTemplate = partTemplate, PartAvailability = partAvailability, CurrFrame = 0, Sequencer = sequencer, }); if (PhysicsDiagnostics.ProbeBuildingEnabled) { var initial = SpawnMotionInitializer.ResolvePlan( motionTable, spawn.MotionState); Console.WriteLine( $"[reactive-anim] registered guid=0x{spawn.Guid:X8} " + $"entityId=0x{entity.Id:X8} mtable=0x{motionTableId:X8} " + $"initialStyle=0x{initial.Style:X8} initialCycle=0x{initial.Motion:X8}"); } } } bool physicsStatic = (expectedRecord.FinalPhysicsState & PhysicsStateFlags.Static) != 0; if (!retainedAnimation && expectedRecord.AnimationRuntime is null && (uint)setup.DefaultAnimation != 0) { var sequencer = new AnimationSequencer( setup, new MotionTable(), _animationLoader); if (sequencer.HasCurrentNode) { _runtime.SetAnimationRuntime( spawn.Guid, new LiveEntityAnimationState { Entity = entity, Setup = setup, Animation = null!, LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = scale, PartTemplate = partTemplate, PartAvailability = partAvailability, CurrFrame = 0, Sequencer = sequencer, }); } } if (expectedRecord.AnimationRuntime is not LiveEntityAnimationState animation) return; _animationPresenter.PrepareAnimation(expectedRecord, animation); if (!physicsStatic || animation.Sequencer is not { } staticSequencer || spawn.Position is not { } staticPosition) { return; } if (_runtime.HasActiveInitialCreateResidence(expectedRecord.Canonical)) { // RuntimeRemoteFirstEntryState owns CPhysicsObj construction and // SetPosition until the Create residence completes. The static // scheduler retains this pending animation owner and binds the // canonical body after projection becomes spatially visible. return; } PhysicsBody body = _runtime.GetOrCreatePhysicsBody( spawn.Guid, incarnation => { var created = new PhysicsBody { Orientation = entity.Rotation }; created.SnapToCell( staticPosition.LandblockId, entity.Position, new Vector3( staticPosition.PositionX, staticPosition.PositionY, staticPosition.PositionZ)); return created; }); _staticAnimations.BindLiveOwner( entity, animation, body); } private void SynchronizeRetainedAnimation( LiveEntityRecord expectedRecord, LiveEntityAnimationState animation, Setup setup, WorldSession.EntitySpawn spawn, MotionResolver.IdleCycle? idleCycle) { // Retail's same-instance HandleCreateObject branch never constructs a // second CPhysicsObj/CPartArray/MovementManager. A completed object has // already consumed the newer MovementData through SetObjectMovement, // so its sequencer and every MotionInterpreter binding stay intact. // Only an interrupted *initial* hydration needs the retained, not-yet- // published sequencer reset to the newest canonical spawn state. uint motionTableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable; MotionTable? motionTable = motionTableId == 0 ? null : _dats.Get(motionTableId); LiveEntityCreateAnimationSynchronization .TrySynchronizeInterruptedInitialOwner( expectedRecord, animation, idleCycle?.Animation, idleCycle is null ? 0 : Math.Max(0, idleCycle.LowFrame), idleCycle is null ? 0 : Math.Min( idleCycle.HighFrame, idleCycle.Animation.PartFrames.Count - 1), idleCycle?.Framerate ?? 0f, motionTable, spawn.MotionState); } private AnimationSequencer? CreateMotionSequencer( Setup setup, WorldSession.EntitySpawn spawn) { uint motionTableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable; return motionTableId != 0 && _dats.Get(motionTableId) is { } motionTable ? SpawnMotionInitializer.Create( setup, motionTable, _animationLoader, spawn.MotionState) : null; } private void DumpSpawn(WorldSession.EntitySpawn spawn, bool enabled) { if (!enabled) return; string position = spawn.Position is { } p ? $"({p.PositionX:F1},{p.PositionY:F1},{p.PositionZ:F1})@0x{p.LandblockId:X8}" : "no-pos"; string setup = spawn.SetupTableId is { } setupId ? $"0x{setupId:X8}" : "no-setup"; string physicsTable = spawn.Physics?.PhysicsScriptTableId is { } tableId ? $"0x{tableId:X8}" : "no-petable"; string name = spawn.Name is { Length: > 0 } foundName ? $"\"{foundName}\"" : "no-name"; string itemType = spawn.ItemType is { } foundItemType ? $"0x{foundItemType:X8}" : "no-itemtype"; Console.WriteLine( $"live: spawn guid=0x{spawn.Guid:X8} name={name} setup={setup} pos={position} " + $"petable={physicsTable} itemType={itemType} " + $"animParts={spawn.AnimPartChanges?.Count ?? 0} " + $"texChanges={spawn.TextureChanges?.Count ?? 0} " + $"subPalettes={spawn.SubPalettes?.Count ?? 0}"); } private void DumpClothingHeader( WorldSession.EntitySpawn spawn, Setup setup, IReadOnlyList flattened, AnimationFrame? idleFrame, IReadOnlyList changes, bool enabled) { if (!enabled) return; Console.WriteLine( $"\n=== DUMP_CLOTHING: guid=0x{spawn.Guid:X8} name='{spawn.Name}' " + $"setup=0x{setup.Id:X8} setup.Parts.Count={setup.Parts.Count} " + $"flatten.Count={flattened.Count} APC={changes.Count} ==="); foreach (CreateObject.AnimPartChange change in changes) Console.WriteLine($" APC part={change.PartIndex:D2} -> gfx=0x{change.NewModelId:X8}"); Console.WriteLine( $" basePalette=0x{spawn.BasePaletteId ?? 0:X8} " + $"subPalettes={spawn.SubPalettes?.Count ?? 0}"); } private static bool IsIssue47HumanoidSetup(Setup setup) { if (setup.Parts.Count != 34) return false; const uint nullPartGfx = 0x010001ECu; int nullSlots = 0; for (int i = 17; i < setup.Parts.Count; i++) { if ((uint)setup.Parts[i] == nullPartGfx) nullSlots++; } return nullSlots >= 8; } /// /// #283 reachability probe. This method is the App-side counterpart of /// Runtime's TryGetWorldFrameOffset: both convert a /// landblock-local network origin into the streamed world frame, and both /// must use the SAME centre landblock or the entity lands a multiple of /// 192 m from the geometry around it. /// /// Runtime rebases on the accepted teleport Position; App's /// LiveWorldOriginState rebases only after old-window retirement /// completes. Emits one line per DISAGREEMENT — silence across a portal /// run is the evidence that the window is unreachable in practice, and /// that #283 can be closed as a permanent invariant rather than an /// ownership restructure. Measurement only; it never gates placement. /// private void ProbeWorldFrameAgreement(uint landblockId) { uint runtimeCenter = _runtime.Physics.WorldFrameCenterLandblockId; if (runtimeCenter == 0u || !_origin.IsKnown) return; int runtimeCenterX = (int)((runtimeCenter >> 24) & 0xFFu); int runtimeCenterY = (int)((runtimeCenter >> 16) & 0xFFu); if (runtimeCenterX == _origin.CenterX && runtimeCenterY == _origin.CenterY) { return; } float deltaX = (runtimeCenterX - _origin.CenterX) * 192f; float deltaY = (runtimeCenterY - _origin.CenterY) * 192f; Console.WriteLine(System.FormattableString.Invariant( $"[world-frame] DISAGREE runtime=({runtimeCenterX},{runtimeCenterY}) app=({_origin.CenterX},{_origin.CenterY}) offsetDelta=({deltaX:F0}m,{deltaY:F0}m) projecting=0x{landblockId:X8}")); } }