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
|
|
@ -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