using System;
namespace AcDream.Core.Items;
public enum ExternalContainerTransitionKind
{
Opened,
ReplacementRequested,
Closed,
Reset,
}
public readonly record struct ExternalContainerTransition(
ExternalContainerTransitionKind Kind,
uint PreviousContainerId,
uint ContainerId);
///
/// Owns retail ClientUISystem::groundObject and the outstanding external
/// container request. ACE sends ViewContents for the requested root and then for
/// nested containers; only the expected root may become the visible ground object.
/// Retail: ClientUISystem::OnViewContents @ 0x00565870 and
/// SetGroundObject @ 0x00564510.
///
public sealed class ExternalContainerState
{
public uint RequestedContainerId { get; private set; }
public uint CurrentContainerId { get; private set; }
public event Action? Changed;
public bool RequestOpen(uint containerId)
{
if (containerId == 0u || RequestedContainerId == containerId)
return false;
uint previous = CurrentContainerId;
RequestedContainerId = containerId;
// ClientUISystem::SetGroundObject @ 0x00564510 retires the old
// ground-object presentation synchronously when a different root is
// requested. ViewContents for the new root arrives later. Keeping the
// old root current during that interval lets its range watcher issue a
// close Use, which ACE interprets as cancellation of the new root's
// MoveTo chain.
if (previous != 0u && previous != containerId)
{
CurrentContainerId = 0u;
Changed?.Invoke(new ExternalContainerTransition(
ExternalContainerTransitionKind.ReplacementRequested,
previous,
containerId));
}
return true;
}
public bool ApplyViewContents(uint containerId)
{
if (containerId == 0u || containerId != RequestedContainerId)
return false;
uint previous = CurrentContainerId;
CurrentContainerId = containerId;
RequestedContainerId = containerId;
if (previous == containerId)
return false;
Changed?.Invoke(new ExternalContainerTransition(
ExternalContainerTransitionKind.Opened,
previous,
containerId));
return true;
}
public bool ApplyClose(uint containerId)
{
if (containerId == 0u || containerId != CurrentContainerId)
return false;
uint previous = CurrentContainerId;
CurrentContainerId = 0u;
RequestedContainerId = 0u;
Changed?.Invoke(new ExternalContainerTransition(
ExternalContainerTransitionKind.Closed,
previous,
0u));
return true;
}
public bool ApplyUseDone(uint weenieError)
{
if (weenieError == 0u || RequestedContainerId == CurrentContainerId)
return false;
RequestedContainerId = CurrentContainerId;
return true;
}
public bool Reset()
{
uint previous = CurrentContainerId;
bool changed = previous != 0u || RequestedContainerId != 0u;
CurrentContainerId = 0u;
RequestedContainerId = 0u;
var transition = new ExternalContainerTransition(
ExternalContainerTransitionKind.Reset,
previous,
0u);
Action? listeners = Changed;
if (listeners is not null)
{
List? failures = null;
foreach (Action listener in listeners.GetInvocationList())
{
try { listener(transition); }
catch (Exception error) { (failures ??= []).Add(error); }
}
if (failures is not null)
throw new AggregateException(
"One or more external-container reset observers failed.",
failures);
}
return changed;
}
}