fix #203: preserve animation on appearance updates
This commit is contained in:
parent
ff06aa3107
commit
c7607f019c
9 changed files with 237 additions and 22 deletions
|
|
@ -318,6 +318,10 @@ public sealed class GameWindow : IDisposable
|
|||
public readonly List<AcDream.Core.World.MeshRef> MeshRefsScratch = new();
|
||||
}
|
||||
|
||||
private sealed record AppearanceUpdateState(
|
||||
AcDream.Core.World.WorldEntity Entity,
|
||||
AnimatedEntity? Animation);
|
||||
|
||||
private AcDream.Core.Physics.DatCollectionLoader? _animLoader;
|
||||
|
||||
// Phase E.1: central fan-out for animation hooks. Audio (E.2),
|
||||
|
|
@ -2850,7 +2854,9 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private static bool IsDoorName(string? name) => name == "Door";
|
||||
|
||||
private void OnLiveEntitySpawnedLocked(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
|
||||
private void OnLiveEntitySpawnedLocked(
|
||||
AcDream.Core.Net.WorldSession.EntitySpawn spawn,
|
||||
AppearanceUpdateState? appearanceUpdate = null)
|
||||
{
|
||||
_liveSpawnReceived++;
|
||||
|
||||
|
|
@ -2877,7 +2883,8 @@ public sealed class GameWindow : IDisposable
|
|||
// For a respawn, drop the previous rendering state here before we
|
||||
// build the new one. `_entitiesByServerGuid` is the canonical map,
|
||||
// its value is the live WorldEntity we need to dispose.
|
||||
RemoveLiveEntityByServerGuid(spawn.Guid);
|
||||
if (appearanceUpdate is null)
|
||||
RemoveLiveEntityByServerGuid(spawn.Guid);
|
||||
|
||||
// When requested, log every spawn that arrives so we can inventory what the server
|
||||
// sends (including the ones we can't render yet). The Name field
|
||||
|
|
@ -3430,6 +3437,19 @@ public sealed class GameWindow : IDisposable
|
|||
animPartChanges[i].PartIndex, animPartChanges[i].NewModelId);
|
||||
}
|
||||
|
||||
if (appearanceUpdate is { } visualUpdate)
|
||||
{
|
||||
AcDream.Core.World.WorldEntity existing = visualUpdate.Entity;
|
||||
existing.ApplyAppearance(meshRefs, paletteOverride, entityPartOverrides);
|
||||
if (liveBounds.TryGet(out var appearanceMin, out var appearanceMax))
|
||||
existing.SetLocalBounds(appearanceMin, appearanceMax);
|
||||
if (visualUpdate.Animation is { } animation)
|
||||
RebindAnimatedEntityForAppearance(animation, existing, setup, scale, meshRefs);
|
||||
_classificationCache.InvalidateEntity(existing.Id);
|
||||
_lastSpawnByGuid[spawn.Guid] = spawn;
|
||||
return;
|
||||
}
|
||||
|
||||
var entity = new AcDream.Core.World.WorldEntity
|
||||
{
|
||||
Id = _liveEntityIdCounter++,
|
||||
|
|
@ -3541,8 +3561,6 @@ public sealed class GameWindow : IDisposable
|
|||
else if (idleCycle.Animation.PartFrames.Count <= 1)
|
||||
_liveAnimRejectPartFrames++;
|
||||
|
||||
|
||||
|
||||
if (idleCycle is not null && idleCycle.Framerate != 0f
|
||||
&& idleCycle.HighFrame > idleCycle.LowFrame
|
||||
&& idleCycle.Animation.PartFrames.Count > 1)
|
||||
|
|
@ -3770,11 +3788,11 @@ public sealed class GameWindow : IDisposable
|
|||
/// appearance changed (equip / unequip / tailoring / recipe result /
|
||||
/// character option toggle). The wire payload only carries the new
|
||||
/// ModelData (palette + texture + animpart changes), not position or
|
||||
/// motion, so we splice it onto the cached spawn and replay through
|
||||
/// <see cref="OnLiveEntitySpawned"/>. The dedup at the start of
|
||||
/// <see cref="OnLiveEntitySpawnedLocked"/> tears down the previous
|
||||
/// rendering state (GpuWorldState entry, animated entity, collision
|
||||
/// registration) before rebuilding.
|
||||
/// motion, so we splice it onto the cached spawn and rehydrate only the
|
||||
/// visual projection. Retail <c>SmartBox::UpdateVisualDesc</c> calls
|
||||
/// <c>CPhysicsObj::DoObjDescChangesFromDefault</c> on the existing object;
|
||||
/// accordingly, entity identity and all animation/movement/physics state
|
||||
/// survive while MeshRefs, palette, and part overrides are replaced.
|
||||
/// </summary>
|
||||
private void OnLiveAppearanceUpdated(AcDream.Core.Net.Messages.ObjDescEvent.Parsed update)
|
||||
{
|
||||
|
|
@ -3796,15 +3814,49 @@ public sealed class GameWindow : IDisposable
|
|||
SubPalettes = md.SubPalettes,
|
||||
BasePaletteId = md.BasePaletteId,
|
||||
};
|
||||
OnLiveEntitySpawned(newSpawn);
|
||||
lock (_datLock)
|
||||
{
|
||||
AppearanceUpdateState? appearanceState = CaptureLiveAppearanceState(update.Guid);
|
||||
OnLiveEntitySpawnedLocked(newSpawn, appearanceState);
|
||||
}
|
||||
|
||||
// Slice 2: a player appearance change (equip / unequip) rebuilt _entitiesByServerGuid[player]
|
||||
// above; flag the paperdoll doll to re-clone from it on the next doll pass (the C# analog of
|
||||
// RedressCreature). Cheap flag — the rebuild is deferred to the pre-UI hook when visible.
|
||||
// Slice 2: flag the paperdoll doll to re-clone the player's newly
|
||||
// mutated appearance on the next doll pass (the C# analog of
|
||||
// RedressCreature). The rebuild is deferred until the doll is visible.
|
||||
if (update.Guid == _playerServerGuid)
|
||||
_paperdollDollDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture the runtime identity that retail preserves across
|
||||
/// <c>DoObjDescChangesFromDefault</c>. Replacement visual data is hydrated
|
||||
/// first and then assigned to this same entity instance.
|
||||
/// </summary>
|
||||
private AppearanceUpdateState? CaptureLiveAppearanceState(uint serverGuid)
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var entity))
|
||||
return null;
|
||||
|
||||
_animatedEntities.TryGetValue(entity.Id, out var animation);
|
||||
return new AppearanceUpdateState(entity, animation);
|
||||
}
|
||||
|
||||
internal static void RebindAnimatedEntityForAppearance(
|
||||
AnimatedEntity animation,
|
||||
AcDream.Core.World.WorldEntity entity,
|
||||
DatReaderWriter.DBObjs.Setup setup,
|
||||
float scale,
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef> meshRefs)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the paperdoll doll from the live player entity (retail makeObject(player) +
|
||||
/// DoObjDescChangesFromDefault). Clones the player's already-resolved Setup / MeshRefs / palette /
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ namespace AcDream.Core.World;
|
|||
|
||||
public sealed class WorldEntity
|
||||
{
|
||||
private PaletteOverride? _paletteOverride;
|
||||
private IReadOnlyList<PartOverride> _partOverrides = Array.Empty<PartOverride>();
|
||||
|
||||
public required uint Id { get; init; }
|
||||
/// <summary>
|
||||
/// Server-assigned GUID (from CreateObject). Zero for dat-hydrated
|
||||
|
|
@ -34,7 +37,11 @@ public sealed class WorldEntity
|
|||
/// colors, creature recolors (e.g. stone-colored drudge statue),
|
||||
/// and team colors. Non-palette-indexed textures ignore this field.
|
||||
/// </summary>
|
||||
public PaletteOverride? PaletteOverride { get; init; }
|
||||
public PaletteOverride? PaletteOverride
|
||||
{
|
||||
get => _paletteOverride;
|
||||
init => _paletteOverride = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EnvCell or outdoor cell ID that owns this entity (room geometry, static
|
||||
|
|
@ -94,7 +101,30 @@ public sealed class WorldEntity
|
|||
/// <c>AnimatedEntityState</c>'s override map at spawn time. Empty for atlas-
|
||||
/// tier entities.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PartOverride> PartOverrides { get; init; } = Array.Empty<PartOverride>();
|
||||
public IReadOnlyList<PartOverride> PartOverrides
|
||||
{
|
||||
get => _partOverrides;
|
||||
init => _partOverrides = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the render-only appearance carried by an ObjDesc update while
|
||||
/// preserving this entity's identity, spatial state, and runtime owners.
|
||||
/// Retail performs the equivalent mutation through
|
||||
/// <c>CPhysicsObj::DoObjDescChangesFromDefault</c>.
|
||||
/// </summary>
|
||||
public void ApplyAppearance(
|
||||
IReadOnlyList<MeshRef> meshRefs,
|
||||
PaletteOverride? paletteOverride,
|
||||
IReadOnlyList<PartOverride> partOverrides)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(meshRefs);
|
||||
ArgumentNullException.ThrowIfNull(partOverrides);
|
||||
|
||||
MeshRefs = meshRefs;
|
||||
_paletteOverride = paletteOverride;
|
||||
_partOverrides = partOverrides;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bitmask of hidden Setup parts. Bit <c>i</c> set hides part <c>i</c> at
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue