feat(combat): port retail held weapon parenting
Select the default combat mode from ordered equipped objects so bows request missile stance. Parse CreateObject parent metadata and ParentEvent, then render held objects as separate children composed from setup holding locations and placement frames each animation tick.
This commit is contained in:
parent
564d39dfea
commit
ab6d96d113
18 changed files with 1152 additions and 17 deletions
423
src/AcDream.App/Rendering/EquippedChildRenderController.cs
Normal file
423
src/AcDream.App/Rendering/EquippedChildRenderController.cs
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Render-side owner of retail parented physics objects (held weapons,
|
||||
/// shields, and visible ammunition). Network/state parsing remains in Core;
|
||||
/// this controller projects an accepted parent relation into a dynamic child
|
||||
/// <see cref="WorldEntity"/> and recomposes it after the parent's animation
|
||||
/// advances each frame.
|
||||
/// </summary>
|
||||
public sealed class EquippedChildRenderController : IDisposable
|
||||
{
|
||||
private readonly DatCollection _dats;
|
||||
private readonly object _datLock;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly GpuWorldState _worldState;
|
||||
private readonly Func<uint, WorldEntity?> _resolveEntity;
|
||||
private readonly Func<uint, WorldSession.EntitySpawn?> _resolveSpawn;
|
||||
private readonly Func<uint> _nextEntityId;
|
||||
|
||||
private readonly Dictionary<uint, PendingAttachment> _pendingByChild = new();
|
||||
private readonly Dictionary<uint, PendingAttachment> _lastRelationByChild = new();
|
||||
private readonly Dictionary<uint, AttachedChild> _attachedByChild = new();
|
||||
private readonly Dictionary<uint, ushort> _positionSequenceByChild = new();
|
||||
|
||||
public IEnumerable<uint> AttachedEntityIds
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (AttachedChild child in _attachedByChild.Values)
|
||||
yield return child.Entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public EquippedChildRenderController(
|
||||
DatCollection dats,
|
||||
object datLock,
|
||||
ClientObjectTable objects,
|
||||
GpuWorldState worldState,
|
||||
Func<uint, WorldEntity?> resolveEntity,
|
||||
Func<uint, WorldSession.EntitySpawn?> resolveSpawn,
|
||||
Func<uint> nextEntityId)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||
_resolveEntity = resolveEntity ?? throw new ArgumentNullException(nameof(resolveEntity));
|
||||
_resolveSpawn = resolveSpawn ?? throw new ArgumentNullException(nameof(resolveSpawn));
|
||||
_nextEntityId = nextEntityId ?? throw new ArgumentNullException(nameof(nextEntityId));
|
||||
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.MoveRolledBack += OnMoveRolledBack;
|
||||
_objects.ObjectRemoved += OnObjectRemoved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed/refresh an object from CreateObject. Equipped child CreateObjects
|
||||
/// carry Placement + Parent directly; this is retail's login/first-observe
|
||||
/// bootstrap and does not wait for a separate ParentEvent.
|
||||
/// </summary>
|
||||
public void OnSpawn(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
_positionSequenceByChild[spawn.Guid] = spawn.PositionSequence;
|
||||
|
||||
if (spawn.ParentGuid is { } parentGuid and not 0
|
||||
&& spawn.ParentLocation is { } parentLocation
|
||||
&& spawn.PlacementId is { } placementId)
|
||||
{
|
||||
var relation = new PendingAttachment(
|
||||
parentGuid,
|
||||
spawn.Guid,
|
||||
parentLocation,
|
||||
placementId,
|
||||
spawn.InstanceSequence,
|
||||
spawn.PositionSequence,
|
||||
FromCreateObject: true);
|
||||
_pendingByChild[spawn.Guid] = relation;
|
||||
_lastRelationByChild[spawn.Guid] = relation;
|
||||
}
|
||||
|
||||
lock (_datLock)
|
||||
{
|
||||
TryRealize(spawn.Guid);
|
||||
|
||||
// ParentEvent can precede the parent's CreateObject. Revisit every
|
||||
// child waiting specifically on the object that just arrived.
|
||||
uint[] waiting = _pendingByChild.Values
|
||||
.Where(p => p.ParentGuid == spawn.Guid)
|
||||
.Select(p => p.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < waiting.Length; i++)
|
||||
TryRealize(waiting[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply/queue a live ParentEvent using retail's two sequence gates:
|
||||
/// parent instance must match, child position must advance strictly.
|
||||
/// </summary>
|
||||
public void OnParentEvent(ParentEvent.Parsed update)
|
||||
{
|
||||
WorldSession.EntitySpawn? parentSpawn = _resolveSpawn(update.ParentGuid);
|
||||
if (parentSpawn is { } knownParent
|
||||
&& knownParent.InstanceSequence != update.ParentInstanceSequence)
|
||||
{
|
||||
// Known parent newer than the event: stale. Event newer than the
|
||||
// known parent: retain until its CreateObject arrives.
|
||||
if (MotionSequenceGate.IsNewer(
|
||||
update.ParentInstanceSequence,
|
||||
knownParent.InstanceSequence))
|
||||
return;
|
||||
}
|
||||
|
||||
if (_positionSequenceByChild.TryGetValue(update.ChildGuid, out ushort current)
|
||||
&& !MotionSequenceGate.IsNewer(current, update.ChildPositionSequence))
|
||||
return;
|
||||
|
||||
var relation = new PendingAttachment(
|
||||
update.ParentGuid,
|
||||
update.ChildGuid,
|
||||
update.ParentLocation,
|
||||
update.PlacementId,
|
||||
update.ParentInstanceSequence,
|
||||
update.ChildPositionSequence,
|
||||
FromCreateObject: false);
|
||||
_pendingByChild[update.ChildGuid] = relation;
|
||||
_lastRelationByChild[update.ChildGuid] = relation;
|
||||
|
||||
lock (_datLock)
|
||||
TryRealize(update.ChildGuid);
|
||||
}
|
||||
|
||||
public void OnObjectDeleted(uint guid)
|
||||
{
|
||||
Remove(guid);
|
||||
|
||||
uint[] children = _attachedByChild.Values
|
||||
.Where(c => c.ParentGuid == guid)
|
||||
.Select(c => c.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
Remove(children[i]);
|
||||
|
||||
uint[] pendingChildren = _lastRelationByChild.Values
|
||||
.Where(c => c.ParentGuid == guid)
|
||||
.Select(c => c.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < pendingChildren.Length; i++)
|
||||
{
|
||||
_pendingByChild.Remove(pendingChildren[i]);
|
||||
_lastRelationByChild.Remove(pendingChildren[i]);
|
||||
}
|
||||
|
||||
_pendingByChild.Remove(guid);
|
||||
_lastRelationByChild.Remove(guid);
|
||||
_positionSequenceByChild.Remove(guid);
|
||||
}
|
||||
|
||||
/// <summary>Recompose every child after the parent's animation tick.</summary>
|
||||
public void Tick()
|
||||
{
|
||||
foreach (AttachedChild child in _attachedByChild.Values)
|
||||
{
|
||||
WorldEntity? parent = _resolveEntity(child.ParentGuid);
|
||||
if (parent is null)
|
||||
continue;
|
||||
|
||||
if (!EquippedChildAttachment.TryCompose(
|
||||
child.ParentSetup,
|
||||
parent.MeshRefs,
|
||||
child.ChildSetup,
|
||||
child.ParentLocation,
|
||||
child.Placement,
|
||||
child.PartTemplate,
|
||||
child.Scale,
|
||||
out IReadOnlyList<MeshRef> parts))
|
||||
continue;
|
||||
|
||||
child.Entity.MeshRefs = parts;
|
||||
child.Entity.SetPosition(parent.Position);
|
||||
child.Entity.Rotation = parent.Rotation;
|
||||
child.Entity.ParentCellId = parent.ParentCellId;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRealize(uint childGuid)
|
||||
{
|
||||
if (!_pendingByChild.TryGetValue(childGuid, out PendingAttachment pending))
|
||||
return;
|
||||
|
||||
// A ParentEvent queued before the child existed is replayed only if
|
||||
// its position stamp still advances the child's CreateObject seed.
|
||||
// Retail reaches the same check inside DoParentEvent after its blob
|
||||
// queue resolves both objects.
|
||||
if (!pending.FromCreateObject
|
||||
&& _positionSequenceByChild.TryGetValue(childGuid, out ushort childPosition)
|
||||
&& !MotionSequenceGate.IsNewer(childPosition, pending.ChildPositionSequence))
|
||||
{
|
||||
_pendingByChild.Remove(childGuid);
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEntity? parentEntity = _resolveEntity(pending.ParentGuid);
|
||||
WorldSession.EntitySpawn? parentSpawn = _resolveSpawn(pending.ParentGuid);
|
||||
WorldSession.EntitySpawn? childSpawn = _resolveSpawn(childGuid);
|
||||
if (parentEntity is null || parentSpawn is null || childSpawn is null)
|
||||
return;
|
||||
|
||||
if (!pending.FromCreateObject
|
||||
&& parentSpawn.Value.InstanceSequence != pending.ParentInstanceSequence)
|
||||
{
|
||||
if (MotionSequenceGate.IsNewer(
|
||||
pending.ParentInstanceSequence,
|
||||
parentSpawn.Value.InstanceSequence))
|
||||
_pendingByChild.Remove(childGuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentSpawn.Value.SetupTableId is not { } parentSetupId
|
||||
|| childSpawn.Value.SetupTableId is not { } childSetupId
|
||||
|| parentEntity.ParentCellId is not { } parentCellId)
|
||||
return;
|
||||
|
||||
Setup? parentSetup = _dats.Get<Setup>(parentSetupId);
|
||||
Setup? childSetup = _dats.Get<Setup>(childSetupId);
|
||||
if (parentSetup is null || childSetup is null)
|
||||
return;
|
||||
|
||||
var parentLocation = (ParentLocation)pending.ParentLocation;
|
||||
var placement = (Placement)pending.PlacementId;
|
||||
IReadOnlyList<MeshRef> template = BuildPartTemplate(childSetup, childSpawn.Value);
|
||||
float scale = childSpawn.Value.ObjScale is { } objScale && objScale > 0f
|
||||
? objScale
|
||||
: 1.0f;
|
||||
|
||||
if (!EquippedChildAttachment.TryCompose(
|
||||
parentSetup,
|
||||
parentEntity.MeshRefs,
|
||||
childSetup,
|
||||
parentLocation,
|
||||
placement,
|
||||
template,
|
||||
scale,
|
||||
out IReadOnlyList<MeshRef> parts))
|
||||
return;
|
||||
|
||||
Remove(childGuid);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = _nextEntityId(),
|
||||
ServerGuid = childGuid,
|
||||
SourceGfxObjOrSetupId = childSetupId,
|
||||
Position = parentEntity.Position,
|
||||
Rotation = parentEntity.Rotation,
|
||||
MeshRefs = parts,
|
||||
PaletteOverride = BuildPaletteOverride(childSpawn.Value),
|
||||
ParentCellId = parentCellId,
|
||||
};
|
||||
|
||||
_worldState.AppendLiveEntity(parentCellId, entity);
|
||||
_attachedByChild[childGuid] = new AttachedChild(
|
||||
pending.ParentGuid,
|
||||
childGuid,
|
||||
parentLocation,
|
||||
placement,
|
||||
parentSetup,
|
||||
childSetup,
|
||||
template,
|
||||
scale,
|
||||
entity);
|
||||
Console.WriteLine(
|
||||
$"equipment: attached child=0x{childGuid:X8} parent=0x{pending.ParentGuid:X8} " +
|
||||
$"location={parentLocation} placement={placement}");
|
||||
_positionSequenceByChild[childGuid] = pending.ChildPositionSequence;
|
||||
_pendingByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
private IReadOnlyList<MeshRef> BuildPartTemplate(
|
||||
Setup setup,
|
||||
WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
var result = new MeshRef[setup.Parts.Count];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
result[i] = new MeshRef((uint)setup.Parts[i], Matrix4x4.Identity);
|
||||
|
||||
IReadOnlyList<CreateObject.AnimPartChange> partChanges =
|
||||
spawn.AnimPartChanges ?? Array.Empty<CreateObject.AnimPartChange>();
|
||||
for (int i = 0; i < partChanges.Count; i++)
|
||||
{
|
||||
CreateObject.AnimPartChange change = partChanges[i];
|
||||
if (change.PartIndex < result.Length)
|
||||
result[change.PartIndex] = new MeshRef(change.NewModelId, Matrix4x4.Identity);
|
||||
}
|
||||
|
||||
IReadOnlyList<CreateObject.TextureChange> textureChanges =
|
||||
spawn.TextureChanges ?? Array.Empty<CreateObject.TextureChange>();
|
||||
for (int partIndex = 0; partIndex < result.Length; partIndex++)
|
||||
{
|
||||
Dictionary<uint, uint>? oldToNew = null;
|
||||
for (int t = 0; t < textureChanges.Count; t++)
|
||||
{
|
||||
CreateObject.TextureChange change = textureChanges[t];
|
||||
if (change.PartIndex != partIndex) continue;
|
||||
oldToNew ??= new Dictionary<uint, uint>();
|
||||
oldToNew[change.OldTexture] = change.NewTexture;
|
||||
}
|
||||
if (oldToNew is null) continue;
|
||||
|
||||
GfxObj? gfx = _dats.Get<GfxObj>(result[partIndex].GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
Dictionary<uint, uint>? surfaceOverrides = null;
|
||||
foreach (var surfaceQid in gfx.Surfaces)
|
||||
{
|
||||
uint surfaceId = (uint)surfaceQid;
|
||||
Surface? surface = _dats.Get<Surface>(surfaceId);
|
||||
if (surface is null) continue;
|
||||
uint oldTexture = (uint)surface.OrigTextureId;
|
||||
if (!oldToNew.TryGetValue(oldTexture, out uint replacement)) continue;
|
||||
surfaceOverrides ??= new Dictionary<uint, uint>();
|
||||
surfaceOverrides[surfaceId] = replacement;
|
||||
}
|
||||
|
||||
if (surfaceOverrides is not null)
|
||||
{
|
||||
result[partIndex] = new MeshRef(
|
||||
result[partIndex].GfxObjId,
|
||||
Matrix4x4.Identity)
|
||||
{
|
||||
SurfaceOverrides = surfaceOverrides,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PaletteOverride? BuildPaletteOverride(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++)
|
||||
{
|
||||
CreateObject.SubPaletteSwap swap = subPalettes[i];
|
||||
ranges[i] = new PaletteOverride.SubPaletteRange(
|
||||
swap.SubPaletteId,
|
||||
swap.Offset,
|
||||
swap.Length);
|
||||
}
|
||||
return new PaletteOverride(spawn.BasePaletteId ?? 0, ranges);
|
||||
}
|
||||
|
||||
private void OnObjectMoved(ClientObject item, uint _, uint __)
|
||||
{
|
||||
if (item.CurrentlyEquippedLocation == EquipMask.None)
|
||||
Remove(item.ObjectId);
|
||||
}
|
||||
|
||||
private void OnMoveRolledBack(ClientObject item)
|
||||
{
|
||||
if (item.CurrentlyEquippedLocation == EquipMask.None)
|
||||
return;
|
||||
|
||||
// A rejected unwield restores the equipped location without a fresh
|
||||
// wire ParentEvent; reinstall the last accepted relationship only for
|
||||
// this explicit rollback signal.
|
||||
if (_lastRelationByChild.TryGetValue(item.ObjectId, out PendingAttachment relation))
|
||||
{
|
||||
_pendingByChild[item.ObjectId] = relation;
|
||||
lock (_datLock)
|
||||
TryRealize(item.ObjectId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnObjectRemoved(ClientObject item) => OnObjectDeleted(item.ObjectId);
|
||||
|
||||
private void Remove(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.Remove(childGuid))
|
||||
return;
|
||||
_worldState.RemoveEntityByServerGuid(childGuid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
_objects.MoveRolledBack -= OnMoveRolledBack;
|
||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||
}
|
||||
|
||||
private readonly record struct PendingAttachment(
|
||||
uint ParentGuid,
|
||||
uint ChildGuid,
|
||||
uint ParentLocation,
|
||||
uint PlacementId,
|
||||
ushort ParentInstanceSequence,
|
||||
ushort ChildPositionSequence,
|
||||
bool FromCreateObject);
|
||||
|
||||
private sealed record AttachedChild(
|
||||
uint ParentGuid,
|
||||
uint ChildGuid,
|
||||
ParentLocation ParentLocation,
|
||||
Placement Placement,
|
||||
Setup ParentSetup,
|
||||
Setup ChildSetup,
|
||||
IReadOnlyList<MeshRef> PartTemplate,
|
||||
float Scale,
|
||||
WorldEntity Entity);
|
||||
}
|
||||
|
|
@ -131,6 +131,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
||||
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
||||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||
private int _streamingRadius = 2; // default 5×5 (kept for debug overlay getStreamingRadius callback)
|
||||
private int _nearRadius = 4; // Phase A.5 T16: two-tier near ring (default 4 → 9×9)
|
||||
|
|
@ -1030,8 +1031,10 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly Dictionary<uint, AcDream.Core.World.WorldEntity> _entitiesByServerGuid = new();
|
||||
/// <summary>
|
||||
/// Latest <see cref="AcDream.Core.Net.WorldSession.EntitySpawn"/> for each
|
||||
/// guid. Captured at the end of <see cref="OnLiveEntitySpawnedLocked"/> so
|
||||
/// <see cref="OnLiveAppearanceUpdated"/> can reuse the position/setup/motion
|
||||
/// guid. Captured before the renderability gate so no-position inventory /
|
||||
/// parented children retain Setup and parent metadata; hydrated world objects
|
||||
/// refresh it again at the end of <see cref="OnLiveEntitySpawnedLocked"/>.
|
||||
/// <see cref="OnLiveAppearanceUpdated"/> reuses the cached position/setup/motion
|
||||
/// fields when a 0xF625 ObjDescEvent arrives carrying only updated visuals.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> _lastSpawnByGuid = new();
|
||||
|
|
@ -2269,6 +2272,17 @@ public sealed class GameWindow : IDisposable
|
|||
onLandblockUnloaded: _classificationCache.InvalidateLandblock,
|
||||
entityScriptActivator: entityScriptActivator);
|
||||
|
||||
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
||||
_dats!,
|
||||
_datLock,
|
||||
Objects,
|
||||
_worldState,
|
||||
guid => _entitiesByServerGuid.TryGetValue(guid, out var entity) ? entity : null,
|
||||
guid => _lastSpawnByGuid.TryGetValue(guid, out var spawn)
|
||||
? spawn
|
||||
: (AcDream.Core.Net.WorldSession.EntitySpawn?)null,
|
||||
() => _liveEntityIdCounter++);
|
||||
|
||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!,
|
||||
_classificationCache, _translucencyFades);
|
||||
|
|
@ -2494,6 +2508,7 @@ public sealed class GameWindow : IDisposable
|
|||
_liveSession.PositionUpdated += OnLivePositionUpdated;
|
||||
_liveSession.VectorUpdated += OnLiveVectorUpdated;
|
||||
_liveSession.StateUpdated += OnLiveStateUpdated;
|
||||
_liveSession.ParentUpdated += update => _equippedChildRenderer?.OnParentEvent(update);
|
||||
_liveSession.TeleportStarted += OnTeleportStarted;
|
||||
_liveSession.AppearanceUpdated += OnLiveAppearanceUpdated;
|
||||
|
||||
|
|
@ -2908,6 +2923,14 @@ public sealed class GameWindow : IDisposable
|
|||
if (appearanceUpdate is null)
|
||||
RemoveLiveEntityByServerGuid(spawn.Guid);
|
||||
|
||||
// Retail's weenie-object table retains CreateObject data for inventory
|
||||
// and parented children even when they have no world Position. Held
|
||||
// weapon CreateObjects are intentionally no-position and carry their
|
||||
// Parent + Placement in PhysicsData, so cache before the renderability
|
||||
// gate and offer the relationship to the focused child controller.
|
||||
_lastSpawnByGuid[spawn.Guid] = spawn;
|
||||
_equippedChildRenderer?.OnSpawn(spawn);
|
||||
|
||||
// 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
|
||||
// is the critical one — we can grep the log for "Nullified Statue
|
||||
|
|
@ -3549,6 +3572,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Cache the spawn so OnLiveAppearanceUpdated can replay it with new
|
||||
// appearance fields when a later 0xF625 ObjDescEvent arrives.
|
||||
_lastSpawnByGuid[spawn.Guid] = spawn;
|
||||
_equippedChildRenderer?.OnSpawn(spawn);
|
||||
|
||||
// Commit B 2026-04-29 — live-entity collision registration. The
|
||||
// local player is the simulator (its PhysicsBody is the source of
|
||||
|
|
@ -3796,8 +3820,23 @@ public sealed class GameWindow : IDisposable
|
|||
// re-create adopts fresh stamps from its CreateObject (retail's
|
||||
// update_times die with the CPhysicsObj).
|
||||
_motionSequenceGates.Remove(delete.Guid);
|
||||
_equippedChildRenderer?.OnObjectDeleted(delete.Guid);
|
||||
|
||||
if (RemoveLiveEntityByServerGuid(delete.Guid)
|
||||
// Snapshot before RemoveLiveEntityByServerGuid clears the render-side
|
||||
// cache. Pickup removes only the 3-D projection; the weenie persists
|
||||
// in inventory and may later become a parented weapon.
|
||||
AcDream.Core.Net.WorldSession.EntitySpawn? pickedUp =
|
||||
delete.FromPickup && _lastSpawnByGuid.TryGetValue(delete.Guid, out var cached)
|
||||
? cached with { Position = null }
|
||||
: null;
|
||||
|
||||
bool removed = RemoveLiveEntityByServerGuid(delete.Guid);
|
||||
if (pickedUp is { } retained)
|
||||
_lastSpawnByGuid[delete.Guid] = retained;
|
||||
else if (!delete.FromPickup)
|
||||
_lastSpawnByGuid.Remove(delete.Guid);
|
||||
|
||||
if (removed
|
||||
&& Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
|
|
@ -8889,6 +8928,7 @@ public sealed class GameWindow : IDisposable
|
|||
// so the renderer always sees the up-to-date per-part transforms.
|
||||
if (_animatedEntities.Count > 0)
|
||||
TickAnimations((float)deltaSeconds);
|
||||
_equippedChildRenderer?.Tick();
|
||||
|
||||
// #188 — advance translucency fades UNCONDITIONALLY (not gated on
|
||||
// _animatedEntities.Count): a one-shot open-cycle animation can
|
||||
|
|
@ -9186,6 +9226,11 @@ public sealed class GameWindow : IDisposable
|
|||
_animatedIdsScratch.Clear();
|
||||
foreach (var k in _animatedEntities.Keys)
|
||||
_animatedIdsScratch.Add(k);
|
||||
if (_equippedChildRenderer is not null)
|
||||
{
|
||||
foreach (uint id in _equippedChildRenderer.AttachedEntityIds)
|
||||
_animatedIdsScratch.Add(id);
|
||||
}
|
||||
HashSet<uint>? animatedIds = _animatedIdsScratch;
|
||||
|
||||
// Phase G.1: sky renderer — draws the far-plane-infinity
|
||||
|
|
@ -11971,7 +12016,18 @@ public sealed class GameWindow : IDisposable
|
|||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
return;
|
||||
|
||||
var nextMode = AcDream.Core.Combat.CombatInputPlanner.ToggleMode(Combat.CurrentMode);
|
||||
var orderedEquipmentIds = Objects.GetContents(_playerServerGuid);
|
||||
var orderedEquipment = new List<AcDream.Core.Items.ClientObject>(orderedEquipmentIds.Count);
|
||||
for (int i = 0; i < orderedEquipmentIds.Count; i++)
|
||||
{
|
||||
if (Objects.Get(orderedEquipmentIds[i]) is { } item)
|
||||
orderedEquipment.Add(item);
|
||||
}
|
||||
var defaultMode = AcDream.Core.Combat.CombatInputPlanner
|
||||
.GetDefaultCombatMode(orderedEquipment);
|
||||
var nextMode = AcDream.Core.Combat.CombatInputPlanner.ToggleMode(
|
||||
Combat.CurrentMode,
|
||||
defaultMode);
|
||||
_liveSession.SendChangeCombatMode(nextMode);
|
||||
Combat.SetCombatMode(nextMode);
|
||||
string text = $"Combat mode {nextMode}";
|
||||
|
|
@ -13618,6 +13674,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase I.7: unsubscribe combat → chat translator before the
|
||||
// session it depends on goes away.
|
||||
_combatChatTranslator?.Dispose();
|
||||
_equippedChildRenderer?.Dispose();
|
||||
_liveSessionController?.Dispose();
|
||||
_liveSession = null;
|
||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue