This reverts ceec3bc4. Two independent reasons, either sufficient.
The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.
The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.
This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.
The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.
Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.
Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
585 lines
21 KiB
C#
585 lines
21 KiB
C#
using System;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Selection;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Retail <c>gmExternalContainerUI</c> controller for LayoutDesc 0x21000008.
|
|
/// Chests, corpses, and other world containers share this bottom-screen strip;
|
|
/// it is intentionally independent from the owned backpack window.
|
|
/// </summary>
|
|
public sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController
|
|
{
|
|
public const uint LayoutId = 0x21000008u;
|
|
public const uint RootId = 0x10000063u;
|
|
public const uint TopContainerId = 0x10000064u;
|
|
public const uint ContainerListId = 0x10000067u;
|
|
public const uint CloseButtonId = 0x10000068u;
|
|
public const uint ContentsListId = 0x1000006Au;
|
|
public const uint ContentsScrollbarId = 0x1000006Bu;
|
|
|
|
private const float ItemCellSize = 32f;
|
|
private const float ContainerCellSize = 36f;
|
|
// User-directed retained default: the raw 800-pixel LayoutDesc remains
|
|
// horizontally reachable by resize/persistence but opens less wide.
|
|
internal const float DefaultContentWidth = 700f;
|
|
internal const float MinimumContentWidth = 160f;
|
|
|
|
private readonly ExternalContainerState _state;
|
|
private readonly ClientObjectTable _objects;
|
|
private readonly SelectionState _selection;
|
|
private readonly ItemInteractionController _itemInteraction;
|
|
private readonly StackSplitQuantityState _stackSplitQuantity;
|
|
private readonly Func<ItemType, uint, uint, uint, uint, uint> _resolveIcon;
|
|
private readonly Func<ItemType, uint, uint, uint, uint, uint> _resolveDragIcon;
|
|
private readonly Action<uint> _sendUse;
|
|
private readonly Action<uint, uint, int> _sendPutItemInContainer;
|
|
private readonly Action<uint, uint, uint, uint> _sendSplitToContainer;
|
|
private readonly Func<uint, bool> _isWithinUseRange;
|
|
private readonly RetailWindowHandle _window;
|
|
private readonly UiItemList _topContainer;
|
|
private readonly UiItemList _containerList;
|
|
private readonly UiItemList _contentsList;
|
|
|
|
private uint _openContainer;
|
|
private bool _closeRequested;
|
|
private bool _disposed;
|
|
|
|
private ExternalContainerController(
|
|
ImportedLayout layout,
|
|
ExternalContainerState state,
|
|
ClientObjectTable objects,
|
|
SelectionState selection,
|
|
ItemInteractionController itemInteraction,
|
|
StackSplitQuantityState stackSplitQuantity,
|
|
Func<ItemType, uint, uint, uint, uint, uint> resolveIcon,
|
|
Func<ItemType, uint, uint, uint, uint, uint> resolveDragIcon,
|
|
Action<uint> sendUse,
|
|
Action<uint, uint, int> sendPutItemInContainer,
|
|
Action<uint, uint, uint, uint> sendSplitToContainer,
|
|
Func<uint, bool> isWithinUseRange,
|
|
RetailWindowHandle window,
|
|
uint contentsEmptySprite,
|
|
uint containerEmptySprite)
|
|
{
|
|
_state = state;
|
|
_objects = objects;
|
|
_selection = selection;
|
|
_itemInteraction = itemInteraction;
|
|
_stackSplitQuantity = stackSplitQuantity;
|
|
_resolveIcon = resolveIcon;
|
|
_resolveDragIcon = resolveDragIcon;
|
|
_sendUse = sendUse;
|
|
_sendPutItemInContainer = sendPutItemInContainer;
|
|
_sendSplitToContainer = sendSplitToContainer;
|
|
_isWithinUseRange = isWithinUseRange;
|
|
_window = window;
|
|
|
|
_topContainer = RequiredList(layout, TopContainerId);
|
|
_containerList = RequiredList(layout, ContainerListId);
|
|
_contentsList = RequiredList(layout, ContentsListId);
|
|
|
|
ConfigureList(_topContainer, ContainerCellSize, horizontalScroll: false, containerEmptySprite);
|
|
ConfigureList(_containerList, ContainerCellSize, horizontalScroll: true, containerEmptySprite);
|
|
ConfigureList(_contentsList, ItemCellSize, horizontalScroll: true, contentsEmptySprite);
|
|
_topContainer.PrimaryItemPressed = PressItem;
|
|
_containerList.PrimaryItemPressed = PressItem;
|
|
_contentsList.PrimaryItemPressed = PressItem;
|
|
_topContainer.ExamineItemRequested = ExamineItem;
|
|
_containerList.ExamineItemRequested = ExamineItem;
|
|
_contentsList.ExamineItemRequested = ExamineItem;
|
|
_contentsList.RegisterDragHandler(this);
|
|
|
|
if (layout.FindElement(ContentsScrollbarId) is UiScrollbar scrollbar)
|
|
{
|
|
scrollbar.Model = _contentsList.Scroll;
|
|
scrollbar.Horizontal = true;
|
|
scrollbar.SpriteResolve ??= _contentsList.SpriteResolve;
|
|
// Horizontal base LayoutDesc 0x2100003E media. The compatibility
|
|
// factory treats all horizontal bars as scalar controls; this panel
|
|
// binds the authored model sprites explicitly.
|
|
scrollbar.TrackSprite = 0x06004C7Fu;
|
|
scrollbar.ThumbTopSprite = 0x06004C80u;
|
|
scrollbar.ThumbSprite = 0x06004C83u;
|
|
scrollbar.ThumbBotSprite = 0x06004C86u;
|
|
scrollbar.DownSprite = 0x06004C89u;
|
|
scrollbar.UpSprite = 0x06004C8Cu;
|
|
}
|
|
|
|
BindClose(layout, RequestClose);
|
|
|
|
_state.Changed += OnExternalContainerChanged;
|
|
_objects.ObjectAdded += OnObjectChanged;
|
|
_objects.ObjectUpdated += OnObjectChanged;
|
|
_objects.ObjectMoved += OnObjectMoved;
|
|
_objects.ObjectRemoved += OnObjectRemoved;
|
|
_objects.ContainerContentsReplaced += OnContentsReplaced;
|
|
_objects.Cleared += OnObjectsCleared;
|
|
_selection.Changed += OnSelectionChanged;
|
|
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
|
ClearLists();
|
|
}
|
|
|
|
public static ExternalContainerController Bind(
|
|
ImportedLayout layout,
|
|
ExternalContainerState state,
|
|
ClientObjectTable objects,
|
|
SelectionState selection,
|
|
ItemInteractionController itemInteraction,
|
|
StackSplitQuantityState stackSplitQuantity,
|
|
Func<ItemType, uint, uint, uint, uint, uint> resolveIcon,
|
|
Func<ItemType, uint, uint, uint, uint, uint> resolveDragIcon,
|
|
Action<uint> sendUse,
|
|
Action<uint, uint, int> sendPutItemInContainer,
|
|
Action<uint, uint, uint, uint> sendSplitToContainer,
|
|
Func<uint, bool> isWithinUseRange,
|
|
RetailWindowHandle window,
|
|
uint contentsEmptySprite = 0u,
|
|
uint containerEmptySprite = 0u)
|
|
=> new(
|
|
layout,
|
|
state,
|
|
objects,
|
|
selection,
|
|
itemInteraction,
|
|
stackSplitQuantity,
|
|
resolveIcon,
|
|
resolveDragIcon,
|
|
sendUse,
|
|
sendPutItemInContainer,
|
|
sendSplitToContainer,
|
|
isWithinUseRange,
|
|
window,
|
|
contentsEmptySprite,
|
|
containerEmptySprite);
|
|
|
|
/// <summary>
|
|
/// Retail's root authors only the tiled center surface; the common floating-window
|
|
/// bevel surrounds it. Its Left/Right edge rules stretch every horizontal list and
|
|
/// pin the close button, so this strip resizes on X only.
|
|
/// </summary>
|
|
internal static RetailWindowFrame.Options CreateWindowOptions(UiElement root)
|
|
=> new()
|
|
{
|
|
WindowName = WindowNames.ExternalContainer,
|
|
Chrome = RetailWindowChrome.NineSlice,
|
|
Left = root.Left,
|
|
Top = root.Top,
|
|
ContentWidth = Math.Min(root.Width, DefaultContentWidth),
|
|
ContentHeight = root.Height,
|
|
MinWidth = MinimumContentWidth + 2f * RetailChromeSprites.Border,
|
|
Visible = false,
|
|
Draggable = true,
|
|
Resizable = true,
|
|
ResizeX = true,
|
|
ResizeY = false,
|
|
ResizableEdges = ResizeEdges.Left | ResizeEdges.Right,
|
|
ConstrainDragToParent = true,
|
|
ConstrainResizeToParent = true,
|
|
DrawChromeCenter = false,
|
|
};
|
|
|
|
public void Tick()
|
|
{
|
|
uint root = _state.CurrentContainerId;
|
|
if (root != 0u && _window.IsVisible && !_closeRequested && !_isWithinUseRange(root))
|
|
RequestClose();
|
|
}
|
|
|
|
public void RequestClose()
|
|
{
|
|
uint root = _state.CurrentContainerId;
|
|
if (root == 0u || _closeRequested)
|
|
return;
|
|
|
|
// gmExternalContainerUI::CloseCurrentContainer @ 0x004CBCB0 sends
|
|
// Use(root), detaches the lists, and waits for event 0x0052. It does not
|
|
// send NoLongerViewingContents on this path.
|
|
_closeRequested = true;
|
|
_window.Hide();
|
|
ClearLists();
|
|
_sendUse(root);
|
|
}
|
|
|
|
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
|
{
|
|
if (payload.ObjId != 0u)
|
|
_selection.Select(payload.ObjId, SelectionChangeSource.ExternalContainer);
|
|
}
|
|
|
|
public ItemDragAcceptance OnDragOver(
|
|
UiItemList targetList,
|
|
UiItemSlot targetCell,
|
|
ItemDragPayload payload)
|
|
{
|
|
if (!ReferenceEquals(targetList, _contentsList)
|
|
|| payload.SourceKind == ItemDragSource.ShortcutBar
|
|
|| payload.ObjId == 0u
|
|
|| _openContainer == 0u
|
|
|| payload.ObjId == _openContainer)
|
|
return ItemDragAcceptance.Reject;
|
|
return ItemDragAcceptance.Accept;
|
|
}
|
|
|
|
public void HandleDropRelease(
|
|
UiItemList targetList,
|
|
UiItemSlot targetCell,
|
|
ItemDragPayload payload)
|
|
{
|
|
if (OnDragOver(targetList, targetCell, payload) != ItemDragAcceptance.Accept)
|
|
return;
|
|
if (!_itemInteraction.EnsureInventoryRequestReady())
|
|
return;
|
|
if (_objects.Get(payload.ObjId) is not { } item)
|
|
return;
|
|
|
|
uint fullStack = (uint)Math.Max(1, item.StackSize);
|
|
uint amount = _stackSplitQuantity.GetObjectSplitSize(
|
|
item.ObjectId,
|
|
_selection.SelectedObjectId ?? 0u,
|
|
fullStack);
|
|
int placement = targetCell.ItemId != 0u
|
|
? Math.Max(0, targetCell.SlotIndex)
|
|
: _objects.GetContents(_openContainer).Count;
|
|
|
|
// ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 and
|
|
// UIAttemptSplitToContainer @ 0x0058D7D0 are request-only. Do not
|
|
// optimistically rewrite external ownership.
|
|
InventoryRequestKind kind = amount < fullStack
|
|
? InventoryRequestKind.SplitToContainer
|
|
: InventoryRequestKind.PutInContainer;
|
|
_itemInteraction.TryDispatchInventoryRequest(
|
|
kind,
|
|
item.ObjectId,
|
|
() =>
|
|
{
|
|
if (amount < fullStack)
|
|
_sendSplitToContainer(item.ObjectId, _openContainer, (uint)placement, amount);
|
|
else
|
|
_sendPutItemInContainer(item.ObjectId, _openContainer, placement);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
private void OnExternalContainerChanged(ExternalContainerTransition transition)
|
|
{
|
|
if (transition.Kind == ExternalContainerTransitionKind.ReplacementRequested)
|
|
{
|
|
// Retail SetGroundObject(new) first publishes SetGroundObject(0)
|
|
// to the panel and only repopulates it when the new root's
|
|
// ViewContents is accepted.
|
|
_openContainer = 0u;
|
|
_closeRequested = false;
|
|
_window.Hide();
|
|
ClearLists();
|
|
return;
|
|
}
|
|
|
|
if (transition.ContainerId == 0u)
|
|
{
|
|
_openContainer = 0u;
|
|
_closeRequested = false;
|
|
_window.Hide();
|
|
ClearLists();
|
|
return;
|
|
}
|
|
|
|
_openContainer = transition.ContainerId;
|
|
_closeRequested = false;
|
|
ScrollToHome();
|
|
Populate();
|
|
_window.Show();
|
|
}
|
|
|
|
private void Populate()
|
|
{
|
|
uint root = _state.CurrentContainerId;
|
|
if (root == 0u)
|
|
{
|
|
ClearLists();
|
|
return;
|
|
}
|
|
if (_openContainer == 0u)
|
|
_openContainer = root;
|
|
|
|
using IDisposable topLayout = _topContainer.DeferLayout();
|
|
using IDisposable containerLayout = _containerList.DeferLayout();
|
|
using IDisposable contentsLayout = _contentsList.DeferLayout();
|
|
_topContainer.Flush();
|
|
_containerList.Flush();
|
|
_contentsList.Flush();
|
|
|
|
AddRootCell(root);
|
|
foreach (uint guid in _objects.GetContents(root))
|
|
{
|
|
if (IsContainer(_objects.Get(guid)))
|
|
AddContainerCell(guid);
|
|
}
|
|
|
|
foreach (uint guid in _objects.GetContents(_openContainer))
|
|
{
|
|
if (!IsContainer(_objects.Get(guid)))
|
|
AddContentsCell(guid);
|
|
}
|
|
ApplyIndicators();
|
|
}
|
|
|
|
private void AddRootCell(uint guid)
|
|
{
|
|
UiItemSlot cell = CreateCell(_topContainer, guid, ItemDragSource.Ground);
|
|
cell.DoubleClicked = RequestClose;
|
|
cell.IsOpenContainer = _openContainer == guid;
|
|
SetCapacity(cell, guid);
|
|
_topContainer.AddItem(cell);
|
|
}
|
|
|
|
private void AddContainerCell(uint guid)
|
|
{
|
|
UiItemSlot cell = CreateCell(_containerList, guid, ItemDragSource.Ground);
|
|
cell.Clicked = () => OpenNestedContainer(guid);
|
|
SetCapacity(cell, guid);
|
|
_containerList.AddItem(cell);
|
|
}
|
|
|
|
private void AddContentsCell(uint guid)
|
|
{
|
|
UiItemSlot cell = CreateCell(_contentsList, guid, ItemDragSource.Ground);
|
|
cell.DoubleClicked = () => _itemInteraction.ActivateItem(guid);
|
|
cell.DragAcceptSprite = 0x060011F9u;
|
|
cell.DragRejectSprite = 0x060011F8u;
|
|
_contentsList.AddItem(cell);
|
|
}
|
|
|
|
private UiItemSlot CreateCell(UiItemList owner, uint guid, ItemDragSource source)
|
|
{
|
|
ClientObject? item = _objects.Get(guid);
|
|
uint icon = item is null ? 0u : _resolveIcon(
|
|
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
|
uint dragIcon = item is null ? 0u : _resolveDragIcon(
|
|
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
|
var cell = new UiItemSlot
|
|
{
|
|
SpriteResolve = owner.SpriteResolve,
|
|
SlotIndex = owner.GetNumUIItems(),
|
|
SourceKind = source,
|
|
};
|
|
cell.SetItem(guid, icon, dragIconTexture: dragIcon);
|
|
return cell;
|
|
}
|
|
|
|
private void OpenNestedContainer(uint guid)
|
|
{
|
|
Select(guid);
|
|
_openContainer = guid;
|
|
_contentsList.Scroll.SetScrollY(0);
|
|
Populate();
|
|
}
|
|
|
|
private void Select(uint guid)
|
|
=> _selection.Select(guid, SelectionChangeSource.ExternalContainer);
|
|
|
|
private void ExamineItem(uint guid)
|
|
{
|
|
Select(guid);
|
|
_itemInteraction.ExamineSelectedOrEnterMode(guid);
|
|
}
|
|
|
|
private bool PressItem(uint guid)
|
|
{
|
|
if (_itemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive)
|
|
return true;
|
|
Select(guid);
|
|
return false;
|
|
}
|
|
|
|
private void ApplyIndicators()
|
|
{
|
|
ApplyIndicators(_topContainer);
|
|
ApplyIndicators(_containerList);
|
|
ApplyIndicators(_contentsList);
|
|
}
|
|
|
|
private void ApplyIndicators(UiItemList list)
|
|
{
|
|
for (int i = 0; i < list.GetNumUIItems(); i++)
|
|
{
|
|
if (list.GetItem(i) is not { } cell) continue;
|
|
bool pendingSource = _itemInteraction.IsPendingSource(cell.ItemId);
|
|
cell.Selected = cell.ItemId != 0u
|
|
&& cell.ItemId == _selection.SelectedObjectId
|
|
&& !pendingSource;
|
|
cell.IsOpenContainer = cell.ItemId != 0u && cell.ItemId == _openContainer;
|
|
}
|
|
}
|
|
|
|
private void SetCapacity(UiItemSlot cell, uint containerId)
|
|
{
|
|
int capacity = _objects.Get(containerId)?.ItemsCapacity ?? 0;
|
|
cell.CapacityFill = capacity <= 0
|
|
? -1f
|
|
: Math.Clamp(_objects.GetContents(containerId).Count / (float)capacity, 0f, 1f);
|
|
}
|
|
|
|
private void ClearLists()
|
|
{
|
|
_topContainer.Flush();
|
|
_containerList.Flush();
|
|
_contentsList.Flush();
|
|
ScrollToHome();
|
|
}
|
|
|
|
private void ScrollToHome()
|
|
{
|
|
_topContainer.Scroll.SetScrollY(0);
|
|
_containerList.Scroll.SetScrollY(0);
|
|
_contentsList.Scroll.SetScrollY(0);
|
|
}
|
|
|
|
private void OnObjectChanged(ClientObject item)
|
|
{
|
|
if (_window.IsVisible && Concerns(item))
|
|
Populate();
|
|
}
|
|
|
|
private void OnObjectMoved(ClientObjectMove move)
|
|
{
|
|
if (!_window.IsVisible) return;
|
|
uint root = _state.CurrentContainerId;
|
|
if ((move.Item is { } item && Concerns(item))
|
|
|| move.Previous.ContainerId == root
|
|
|| move.Current.ContainerId == root
|
|
|| move.Previous.ContainerId == _openContainer
|
|
|| move.Current.ContainerId == _openContainer)
|
|
{
|
|
Populate();
|
|
}
|
|
}
|
|
private void OnContentsReplaced(uint containerId)
|
|
{
|
|
if (_window.IsVisible
|
|
&& (containerId == _state.CurrentContainerId
|
|
|| containerId == _openContainer
|
|
|| ProjectionContains(_state.CurrentContainerId, containerId)))
|
|
Populate();
|
|
}
|
|
|
|
private void OnObjectRemoved(ClientObject item)
|
|
{
|
|
if (_selection.SelectedObjectId == item.ObjectId)
|
|
{
|
|
_selection.Clear(
|
|
SelectionChangeSource.System,
|
|
SelectionChangeReason.SelectedObjectRemoved);
|
|
}
|
|
|
|
if (item.ObjectId == _state.CurrentContainerId)
|
|
_state.ApplyClose(item.ObjectId);
|
|
else if (_window.IsVisible && Concerns(item))
|
|
Populate();
|
|
}
|
|
|
|
private void OnObjectsCleared()
|
|
{
|
|
_openContainer = 0u;
|
|
_closeRequested = false;
|
|
_window.Hide();
|
|
ClearLists();
|
|
}
|
|
|
|
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
|
|
private void OnInteractionStateChanged() => ApplyIndicators();
|
|
|
|
private static bool IsContainer(ClientObject? item)
|
|
=> item is not null
|
|
&& (item.ContainerTypeHint != 0u
|
|
|| item.Type.HasFlag(ItemType.Container)
|
|
|| item.ItemsCapacity > 0);
|
|
|
|
private bool Concerns(ClientObject item)
|
|
{
|
|
uint root = _state.CurrentContainerId;
|
|
return item.ObjectId == root
|
|
|| item.ObjectId == _openContainer
|
|
|| item.ContainerId == root
|
|
|| item.ContainerId == _openContainer
|
|
|| ProjectionContains(root, item.ObjectId)
|
|
|| ProjectionContains(_openContainer, item.ObjectId);
|
|
}
|
|
|
|
private bool ProjectionContains(uint containerId, uint itemId)
|
|
{
|
|
if (containerId == 0u || itemId == 0u) return false;
|
|
foreach (uint candidate in _objects.GetContents(containerId))
|
|
{
|
|
if (candidate == itemId) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static void ConfigureList(
|
|
UiItemList list,
|
|
float cellSize,
|
|
bool horizontalScroll,
|
|
uint emptySprite)
|
|
{
|
|
list.Columns = 1;
|
|
list.CellWidth = cellSize;
|
|
list.CellHeight = cellSize;
|
|
list.SingleRow = true;
|
|
list.HorizontalScroll = horizontalScroll;
|
|
list.FillVisibleEmptySlots = true;
|
|
if (emptySprite != 0u)
|
|
list.CellEmptySprite = emptySprite;
|
|
list.EmptySlotFactory = () => new UiItemSlot
|
|
{
|
|
SpriteResolve = list.SpriteResolve,
|
|
SourceKind = ItemDragSource.Ground,
|
|
DragAcceptSprite = 0x060011F9u,
|
|
DragRejectSprite = 0x060011F8u,
|
|
};
|
|
}
|
|
|
|
private static UiItemList RequiredList(ImportedLayout layout, uint id)
|
|
=> layout.FindElement(id) as UiItemList
|
|
?? throw new InvalidOperationException(
|
|
$"External-container LayoutDesc is missing ItemList 0x{id:X8}.");
|
|
|
|
private static void BindClose(ImportedLayout layout, Action close)
|
|
{
|
|
switch (layout.FindElement(CloseButtonId))
|
|
{
|
|
case UiButton button:
|
|
button.OnClick = close;
|
|
break;
|
|
case UiDatElement element:
|
|
element.ClickThrough = false;
|
|
element.OnClick = close;
|
|
break;
|
|
default:
|
|
throw new InvalidOperationException(
|
|
"External-container LayoutDesc is missing its close button.");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
_state.Changed -= OnExternalContainerChanged;
|
|
_objects.ObjectAdded -= OnObjectChanged;
|
|
_objects.ObjectUpdated -= OnObjectChanged;
|
|
_objects.ObjectMoved -= OnObjectMoved;
|
|
_objects.ObjectRemoved -= OnObjectRemoved;
|
|
_objects.ContainerContentsReplaced -= OnContentsReplaced;
|
|
_objects.Cleared -= OnObjectsCleared;
|
|
_selection.Changed -= OnSelectionChanged;
|
|
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
|
_topContainer.PrimaryItemPressed = null;
|
|
_containerList.PrimaryItemPressed = null;
|
|
_contentsList.PrimaryItemPressed = null;
|
|
_topContainer.ExamineItemRequested = null;
|
|
_containerList.ExamineItemRequested = null;
|
|
_contentsList.ExamineItemRequested = null;
|
|
}
|
|
}
|