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;
///
/// 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
/// and recomposes it after the parent's animation
/// advances each frame.
///
public sealed class EquippedChildRenderController : IDisposable
{
private readonly DatCollection _dats;
private readonly object _datLock;
private readonly ClientObjectTable _objects;
private readonly GpuWorldState _worldState;
private readonly Func _resolveEntity;
private readonly Func _resolveSpawn;
private readonly Func _nextEntityId;
private readonly Dictionary _pendingByChild = new();
private readonly Dictionary _lastRelationByChild = new();
private readonly Dictionary _attachedByChild = new();
private readonly Dictionary _positionSequenceByChild = new();
public IEnumerable AttachedEntityIds
{
get
{
foreach (AttachedChild child in _attachedByChild.Values)
yield return child.Entity.Id;
}
}
public EquippedChildRenderController(
DatCollection dats,
object datLock,
ClientObjectTable objects,
GpuWorldState worldState,
Func resolveEntity,
Func resolveSpawn,
Func 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;
}
///
/// 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.
///
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]);
}
}
///
/// Apply/queue a live ParentEvent using retail's two sequence gates:
/// parent instance must match, child position must advance strictly.
///
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);
}
/// Recompose every child after the parent's animation tick.
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 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(parentSetupId);
Setup? childSetup = _dats.Get(childSetupId);
if (parentSetup is null || childSetup is null)
return;
var parentLocation = (ParentLocation)pending.ParentLocation;
var placement = (Placement)pending.PlacementId;
IReadOnlyList 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 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 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 partChanges =
spawn.AnimPartChanges ?? Array.Empty();
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 textureChanges =
spawn.TextureChanges ?? Array.Empty();
for (int partIndex = 0; partIndex < result.Length; partIndex++)
{
Dictionary? oldToNew = null;
for (int t = 0; t < textureChanges.Count; t++)
{
CreateObject.TextureChange change = textureChanges[t];
if (change.PartIndex != partIndex) continue;
oldToNew ??= new Dictionary();
oldToNew[change.OldTexture] = change.NewTexture;
}
if (oldToNew is null) continue;
GfxObj? gfx = _dats.Get(result[partIndex].GfxObjId);
if (gfx is null) continue;
Dictionary? surfaceOverrides = null;
foreach (var surfaceQid in gfx.Surfaces)
{
uint surfaceId = (uint)surfaceQid;
Surface? surface = _dats.Get(surfaceId);
if (surface is null) continue;
uint oldTexture = (uint)surface.OrigTextureId;
if (!oldToNew.TryGetValue(oldTexture, out uint replacement)) continue;
surfaceOverrides ??= new Dictionary();
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 PartTemplate,
float Scale,
WorldEntity Entity);
}