using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
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 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 PhysicsDataCache _physicsData;
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 Func _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,
PhysicsDataCache physicsData,
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,
Func gameTime)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData));
_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(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
LiveEntityAppearanceUpdateState? appearanceUpdate = null)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
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(
expectedRecord,
expectedCreateIntegrationVersion)
|| canonicalSpawn.Guid != expectedRecord.ServerGuid
|| canonicalSpawn.InstanceSequence != expectedRecord.Generation)
{
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);
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)
_physicsData.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;
}
expectedRecord.HasPartArray = 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;
}
_physicsData.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(
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)
_physicsData.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;
}
_physicsData.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(
LiveEntityRecord expectedRecord,
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)
{
if (!_runtime.TryGetEffectProfile(spawn.Guid, out _))
{
EntityEffectProfile profile = spawn.Physics is { } physics
? EntityEffectProfile.CreateLive(setup, physics)
: EntityEffectProfile.CreateDatStatic(setup);
_runtime.SetEffectProfile(spawn.Guid, profile);
}
bool createdProjection = false;
WorldEntity? entity = _runtime.MaterializeLiveEntity(
spawn.Guid,
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;
});
if (entity is null
|| !_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;
_projectiles.TryBind(
expectedRecord,
setup,
_gameTime(),
_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.AnimationRuntimes.Count} "
+ $"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;
}
PhysicsBody body = _runtime.GetOrCreatePhysicsBody(
spawn.Guid,
incarnation =>
{
var created = new PhysicsBody { Orientation = entity.Rotation };
RemotePhysicsBodyInitializer.Initialize(created, incarnation);
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;
}
}