fix(ui): restore radar, retail wield switching, and protection meshes

Late-bind radar snapshots to the canonical LiveEntityRuntime maps so the retained radar survives bootstrap and session replacement instead of capturing empty sentinels.

Route paperdoll drops through the retail AutoWield blocker transaction. Move conflicting held weapons, shields, explicit jewelry destinations, and mismatched ammo to the backpack one authoritative confirmation at a time; preserve compatible arrows when switching to melee.

Render mode-1 and no-degrade particle GfxObjs as their authored modern-pipeline meshes, retain Always2D billboards, interleave both paths back-to-front, balance emitter mesh ownership, and fail safely on corrupt DAT material metadata. This restores the closed apex on Armor Self/protection effects.

Retain Studio fixture controller lifetimes, add installed-DAT and adversarial regression coverage, synchronize retail research/divergence bookkeeping, and pass all three review tracks plus the full Release suite.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 20:52:45 +02:00
parent 8d63e5c28a
commit b26f84cc69
26 changed files with 1361 additions and 141 deletions

View file

@ -48,8 +48,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
private readonly Func<uint> _playerGuid;
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
private readonly Action<uint, uint>? _sendWield; // (itemGuid, equipMask) → GetAndWieldItem 0x001A
private readonly ItemInteractionController? _itemInteraction;
private readonly ItemInteractionController _itemInteraction;
private readonly bool _ownsItemInteraction;
private readonly SelectionState _selection;
private readonly PaperdollClickMap? _clickMap;
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
@ -65,15 +65,17 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
Action<uint, uint>? sendWield,
uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction,
ItemInteractionController itemInteraction,
uint emptySlotSprite, UiDatFont? datFont,
PaperdollClickMap? clickMap,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
IReadOnlyDictionary<uint, uint>? emptySlotSprites)
IReadOnlyDictionary<uint, uint>? emptySlotSprites,
bool ownsItemInteraction)
{
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds;
_dragIconIds = dragIconIds;
_itemInteraction = itemInteraction;
_itemInteraction = itemInteraction ?? throw new ArgumentNullException(nameof(itemInteraction));
_ownsItemInteraction = ownsItemInteraction;
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
_clickMap = clickMap;
@ -172,15 +174,15 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
public static PaperdollController Bind(
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
Action<uint, uint>? sendWield = null,
ItemInteractionController itemInteraction,
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
ItemInteractionController? itemInteraction = null,
PaperdollClickMap? clickMap = null,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null,
IReadOnlyDictionary<uint, uint>? emptySlotSprites = null)
IReadOnlyDictionary<uint, uint>? emptySlotSprites = null,
bool ownsItemInteraction = false)
=> new PaperdollController(
layout, objects, playerGuid, iconIds, selection, sendWield, emptySlotSprite,
datFont, itemInteraction, clickMap, dragIconIds, emptySlotSprites);
layout, objects, playerGuid, iconIds, selection, itemInteraction, emptySlotSprite,
datFont, clickMap, dragIconIds, emptySlotSprites, ownsItemInteraction);
private void HandleDollClick(int x, int y)
{
@ -311,9 +313,10 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
: ItemDragAcceptance.Reject;
}
/// <summary>Wield the dropped item: optimistic local equip (instant) + GetAndWieldItem 0x001A.
/// <summary>Wield the dropped item through the shared retail AutoWield transaction.
/// wieldMask = ValidLocations & slotMask resolves the specific bit (the composite weapon slot, the
/// chosen finger). Retail: gmPaperDollUI HandleDropRelease → GetAndWieldItem.</summary>
/// chosen finger). Retail: gmPaperDollUI::HandleDropRelease @ 0x004A4D80 to
/// AcceptDragObject @ 0x004A3B10 to CPlayerSystem::AutoWield @ 0x00560A60.</summary>
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
// gmPaperDollUI::HandleDropRelease @ 0x004A4D80 applies the same flag
@ -324,8 +327,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
if (item is null) return;
EquipMask wieldMask = ItemEquipRules.ResolvePaperdollDropWieldMask(item, MaskFor(targetList));
if (wieldMask == EquipMask.None) return; // not wieldable here (defensive; OnDragOver already rejected)
_objects.WieldItemOptimistic(payload.ObjId, _playerGuid(), wieldMask);
_sendWield?.Invoke(payload.ObjId, (uint)wieldMask);
_itemInteraction.WieldFromPaperdoll(payload.ObjId, wieldMask);
}
/// <summary>
@ -362,5 +364,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
element.OnClickAt = null;
break;
}
if (_ownsItemInteraction)
_itemInteraction.Dispose();
}
}

View file

@ -19,9 +19,9 @@ public sealed class RadarSnapshotProvider
private static readonly Vector2 ProductionCenter = new(60f, 60f);
private readonly ClientObjectTable _objects;
private readonly IReadOnlyDictionary<uint, WorldEntity> _worldEntities;
private readonly IReadOnlyDictionary<uint, WorldEntity> _playerEntities;
private readonly IReadOnlyDictionary<uint, WorldSession.EntitySpawn> _spawns;
private readonly Func<IReadOnlyDictionary<uint, WorldEntity>> _worldEntities;
private readonly Func<IReadOnlyDictionary<uint, WorldEntity>> _playerEntities;
private readonly Func<IReadOnlyDictionary<uint, WorldSession.EntitySpawn>> _spawns;
private readonly Func<uint> _playerGuid;
private readonly Func<float> _playerYawRadians;
private readonly Func<uint> _playerCellId;
@ -32,8 +32,8 @@ public sealed class RadarSnapshotProvider
public RadarSnapshotProvider(
ClientObjectTable objects,
IReadOnlyDictionary<uint, WorldEntity> worldEntities,
IReadOnlyDictionary<uint, WorldSession.EntitySpawn> spawns,
Func<IReadOnlyDictionary<uint, WorldEntity>> worldEntities,
Func<IReadOnlyDictionary<uint, WorldSession.EntitySpawn>> spawns,
Func<uint> playerGuid,
Func<float> playerYawRadians,
Func<uint> playerCellId,
@ -41,7 +41,7 @@ public sealed class RadarSnapshotProvider
Func<bool> coordinatesOnRadar,
Func<bool> uiLocked,
Func<uint, RadarRelationshipTraits>? relationshipFor = null,
IReadOnlyDictionary<uint, WorldEntity>? playerEntities = null)
Func<IReadOnlyDictionary<uint, WorldEntity>>? playerEntities = null)
{
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_worldEntities = worldEntities ?? throw new ArgumentNullException(nameof(worldEntities));
@ -58,9 +58,17 @@ public sealed class RadarSnapshotProvider
public UiRadarSnapshot BuildSnapshot()
{
// The retained UI mounts before LiveEntityRuntime during GameWindow
// startup. Resolve these canonical projections per snapshot so the
// provider observes that runtime once it is installed, rather than
// retaining the bootstrap empty-map sentinels forever.
IReadOnlyDictionary<uint, WorldEntity> worldEntities = _worldEntities();
IReadOnlyDictionary<uint, WorldEntity> playerEntities = _playerEntities();
IReadOnlyDictionary<uint, WorldSession.EntitySpawn> spawns = _spawns();
bool uiLocked = _uiLocked();
uint playerGuid = _playerGuid();
if (playerGuid == 0u || !_playerEntities.TryGetValue(playerGuid, out var playerEntity))
if (playerGuid == 0u || !playerEntities.TryGetValue(playerGuid, out var playerEntity))
return UiRadarSnapshot.Empty with { UiLocked = uiLocked };
float heading = MoveToMath.HeadingFromYaw(_playerYawRadians());
@ -70,15 +78,15 @@ public sealed class RadarSnapshotProvider
? playerCoordinates.CombinedText
: null;
uint playerPwd = _spawns.TryGetValue(playerGuid, out var playerSpawn)
uint playerPwd = spawns.TryGetValue(playerGuid, out var playerSpawn)
? playerSpawn.ObjectDescriptionFlags ?? 0u
: 0u;
var playerTraits = RadarObjectTraits.FromPublicWeenieDescription(
(uint)(_objects.Get(playerGuid)?.Type ?? ItemType.None), playerPwd);
float range = RetailRadar.GetRangeMeters(isOutside);
var blips = new List<UiRadarBlip>(_worldEntities.Count);
foreach (var pair in _worldEntities)
var blips = new List<UiRadarBlip>(worldEntities.Count);
foreach (var pair in worldEntities)
{
uint guid = pair.Key;
if (guid == playerGuid)
@ -86,7 +94,7 @@ public sealed class RadarSnapshotProvider
var entity = pair.Value;
var clientObject = _objects.Get(guid);
_spawns.TryGetValue(guid, out var spawn);
spawns.TryGetValue(guid, out var spawn);
byte? rawBehavior = clientObject?.RadarBehavior ?? spawn.RadarBehavior;
if (rawBehavior is null