using DatReaderWriter;
using AcDream.Content;
namespace AcDream.App.UI.Layout;
///
/// Instantiates retail's category and component listbox templates from LayoutDesc
/// 0x21000033. This is the retained equivalent of
/// UIElement_ListBox::AddItemFromTemplateList used by
/// gmSpellComponentUI::UpdateComponents @ 0x0048A910.
///
public sealed class ComponentBookTemplateFactory
{
public const uint LayoutId = 0x21000033u;
public const uint CategoryTemplateId = 0x10000466u;
public const uint ComponentTemplateId = 0x10000467u;
public const uint IconId = 0x10000468u;
public const uint NameId = 0x10000469u;
public const uint OwnedCountId = 0x1000046Au;
public const uint DesiredCountId = 0x1000046Bu;
private const uint LocalStringTableId = 0x23000001u;
private const uint HighlightState = UiButtonStateMachine.Highlight;
private static readonly (string Key, string Fallback)[] CategoryStrings =
[
("ID_SpellComp_Category_Scarabs", "SCARABS"),
("ID_SpellComp_Category_Herbs", "HERBS"),
("ID_SpellComp_Category_Gems", "POWDERED GEMS"),
("ID_SpellComp_Category_Alchemical", "ALCHEMICAL SUBSTANCES"),
("ID_SpellComp_Category_Talismans", "TALISMANS"),
("ID_SpellComp_Category_Tapers", "TAPERS"),
("ID_SpellComp_Category_Peas", "PEAS"),
];
private readonly ElementInfo _categoryTemplate;
private readonly ElementInfo _componentTemplate;
private readonly Func _resolveSprite;
private readonly UiDatFont? _defaultFont;
private readonly IReadOnlyDictionary _fonts;
private readonly string[] _categoryNames;
public ComponentBookTemplateFactory(
ElementInfo categoryTemplate,
ElementInfo componentTemplate,
Func resolveSprite,
UiDatFont? defaultFont,
IReadOnlyDictionary? fonts = null,
IReadOnlyList? categoryNames = null)
{
_categoryTemplate = categoryTemplate;
_componentTemplate = componentTemplate;
_resolveSprite = resolveSprite;
_defaultFont = defaultFont;
_fonts = fonts ?? new Dictionary();
_categoryNames = categoryNames?.ToArray()
?? CategoryStrings.Select(value => value.Fallback).ToArray();
}
///
/// Loads and resolves the two template definitions while the caller holds the
/// shared DAT lock. Fonts and localized category strings are captured eagerly,
/// so later inventory refreshes instantiate rows without reading DAT files.
///
public static ComponentBookTemplateFactory? TryLoad(
IDatReaderWriter dats,
Func resolveSprite,
UiDatFont? defaultFont,
Func? resolveFont)
{
ElementInfo? category = LayoutImporter.ImportInfos(
dats, LayoutId, CategoryTemplateId);
ElementInfo? component = LayoutImporter.ImportInfos(
dats, LayoutId, ComponentTemplateId);
if (category is null || component is null)
return null;
var fonts = new Dictionary();
CaptureFonts(category, resolveFont, fonts);
CaptureFonts(component, resolveFont, fonts);
var strings = new DatStringResolver(dats);
string[] categoryNames = CategoryStrings
.Select(value => strings.Resolve(
LocalStringTableId,
DatStringResolver.ComputeHash(value.Key))
?? value.Fallback)
.ToArray();
return new ComponentBookTemplateFactory(
category, component, resolveSprite, defaultFont, fonts, categoryNames);
}
public UiTemplateListSlot CreateCategoryRow(uint category)
{
ImportedLayout content = Build(_categoryTemplate);
if (content.Root is not UiText title)
throw new InvalidOperationException(
"Retail component category template did not resolve to UIElement_Text.");
string label = category < _categoryNames.Length
? _categoryNames[category]
: "OTHER COMPONENTS";
title.LinesProvider = () => [new UiText.Line(label, title.DefaultColor)];
return new UiTemplateListSlot(
content, uint.MaxValue, content.Root is IUiDatStateful stateful
? stateful.ActiveRetailStateId
: UiStateInfo.DirectStateId, selectedState: null);
}
public ComponentRow CreateComponentRow(
uint componentId,
uint iconTexture,
string name,
int ownedCount,
uint desiredCount)
{
ImportedLayout content = Build(_componentTemplate);
UiElement iconHost = Required(content, IconId);
UiText nameText = Required(content, NameId);
UiText ownedText = Required(content, OwnedCountId);
UiField desiredField = Required(content, DesiredCountId);
var icon = new UiTextureElement
{
Width = iconHost.Width,
Height = iconHost.Height,
Anchors = AnchorEdges.Left | AnchorEdges.Top
| AnchorEdges.Right | AnchorEdges.Bottom,
Texture = iconTexture,
};
iconHost.AddChild(icon);
nameText.LinesProvider = () => [new UiText.Line(name, nameText.DefaultColor)];
ownedText.LinesProvider = () =>
[new UiText.Line(ownedCount.ToString(System.Globalization.CultureInfo.InvariantCulture),
ownedText.DefaultColor)];
// NumberInputFilter + the 0..5000 validation are installed by the controller,
// matching gmSpellComponentUI::ListenToElementMessage @ 0x0048A420.
desiredField.ClearOnSubmit = false;
desiredField.RecordHistory = false;
desiredField.SelectAllOnFocus = true;
desiredField.CharacterFilter = char.IsAsciiDigit;
desiredField.SetText(desiredCount.ToString(
System.Globalization.CultureInfo.InvariantCulture));
uint normalState = content.Root is IUiDatStateful stateful
? stateful.ActiveRetailStateId
: UiStateInfo.DirectStateId;
var slot = new UiTemplateListSlot(
content, componentId, normalState, HighlightState);
return new ComponentRow(slot, desiredField);
}
private ImportedLayout Build(ElementInfo template)
=> LayoutImporter.Build(
template,
_resolveSprite,
_defaultFont,
did => _fonts.TryGetValue(did, out UiDatFont? font) ? font : _defaultFont);
private static T Required(ImportedLayout content, uint id)
where T : UiElement
=> content.FindElement(id) as T
?? throw new InvalidOperationException(
$"Retail component template element 0x{id:X8} did not resolve to {typeof(T).Name}.");
private static UiElement Required(ImportedLayout content, uint id)
=> content.FindElement(id)
?? throw new InvalidOperationException(
$"Retail component template element 0x{id:X8} is missing.");
private static void CaptureFonts(
ElementInfo info,
Func? resolveFont,
Dictionary fonts)
{
if (info.FontDid != 0u && !fonts.ContainsKey(info.FontDid))
fonts[info.FontDid] = resolveFont?.Invoke(info.FontDid);
foreach (ElementInfo child in info.Children)
CaptureFonts(child, resolveFont, fonts);
}
public readonly record struct ComponentRow(
UiTemplateListSlot Slot,
UiField DesiredField);
}