using System.Globalization; using System.Numerics; using AcDream.Content; using AcDream.Core.Net.Messages; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.App.UI.Layout; public enum CreatureAppraisalValueStyle { Normal, Positive, Negative, Incomplete, } public readonly record struct CreatureAppraisalRow( string Label, string Value, CreatureAppraisalValueStyle Style); /// /// Pure projection of retail's six AttributeInfoRegion and three /// Attribute2ndInfoRegion tokens. Ordering and formatting come from /// BasicCreatureExamineUI and the two Update overloads at /// 0x004F1D90/0x004F1E80. /// public static class CreatureAppraisalRows { private const string Unknown = "???"; public static IReadOnlyList Build( AppraiseInfoParser.CreatureProfile profile, bool success) { return [ Primary("Strength", profile.Strength, 0, profile, success), Primary("Endurance", profile.Endurance, 1, profile, success), Primary("Coordination", profile.Coordination, 3, profile, success), Primary("Quickness", profile.Quickness, 2, profile, success), Primary("Focus", profile.Focus, 4, profile, success), Primary("Self", profile.Self, 5, profile, success), Secondary( "Health", profile.Health, profile.HealthMax, showPercent: true, enchantmentBit: 6, profile, success), Secondary( "Stamina", profile.Stamina, profile.StaminaMax, showPercent: false, enchantmentBit: 7, profile, success), Secondary( "Mana", profile.Mana, profile.ManaMax, showPercent: false, enchantmentBit: 8, profile, success), ]; } private static CreatureAppraisalRow Primary( string label, uint? value, int enchantmentBit, AppraiseInfoParser.CreatureProfile profile, bool success) => new( label, value is > 0 ? value.Value.ToString(CultureInfo.CurrentCulture) : Unknown, Style(enchantmentBit, profile, success)); private static CreatureAppraisalRow Secondary( string label, uint? current, uint? maximum, bool showPercent, int enchantmentBit, AppraiseInfoParser.CreatureProfile profile, bool success) { string value = Unknown; if (current is > 0 && maximum is > 0) { int percent = RoundedPercent(current.Value, maximum.Value); if (success) { value = showPercent ? $"{current.Value.ToString(CultureInfo.CurrentCulture)}/{maximum.Value.ToString(CultureInfo.CurrentCulture)} ({percent.ToString(CultureInfo.CurrentCulture)} %)" : $"{current.Value.ToString(CultureInfo.CurrentCulture)}/{maximum.Value.ToString(CultureInfo.CurrentCulture)}"; } else if (showPercent) { value = $"{percent.ToString(CultureInfo.CurrentCulture)} %"; } } return new CreatureAppraisalRow( label, value, Style(enchantmentBit, profile, success)); } private static int RoundedPercent(uint numerator, uint denominator) => denominator == 0u ? 0 : (int)Math.Min( int.MaxValue, ((100L * numerator) + denominator / 2L) / denominator); private static CreatureAppraisalValueStyle Style( int bit, AppraiseInfoParser.CreatureProfile profile, bool success) { if (!success) return CreatureAppraisalValueStyle.Incomplete; ushort highlight = profile.AttributeHighlights ?? 0; if ((highlight & (1 << bit)) == 0) return CreatureAppraisalValueStyle.Normal; ushort color = profile.AttributeColors ?? 0; return (color & (1 << bit)) != 0 ? CreatureAppraisalValueStyle.Positive : CreatureAppraisalValueStyle.Negative; } } /// /// Instantiates LayoutDesc 0x2100006B's InfoRegion token template /// 0x10000166, matching UIElement_ListBox::AddItemFromTemplateList. /// public sealed class CreatureAppraisalRowTemplateFactory { public const uint TemplateId = 0x10000166u; public const uint LabelId = 0x1000012Au; public const uint ValueId = 0x1000012Bu; private readonly ElementInfo _template; private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public CreatureAppraisalRowTemplateFactory( ElementInfo template, Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { _template = template ?? throw new ArgumentNullException(nameof(template)); _resolveSprite = resolveSprite ?? throw new ArgumentNullException(nameof(resolveSprite)); _defaultFont = defaultFont; _fonts = fonts ?? new Dictionary(); } public float Width => _template.Width; public float Height => _template.Height; public static CreatureAppraisalRowTemplateFactory? TryLoad( IDatReaderWriter dats, Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { ElementInfo? template = LayoutImporter.ImportInfos( dats, AppraisalUiController.LayoutId, TemplateId); if (template is null || Find(template, LabelId) is null || Find(template, ValueId) is null || template.Width <= 0f || template.Height <= 0f) { return null; } var fonts = new Dictionary(); CaptureFonts(template, resolveFont, fonts); return new CreatureAppraisalRowTemplateFactory( template, resolveSprite, defaultFont, fonts); } public UiTemplateListSlot Create(CreatureAppraisalRow row) { ImportedLayout content = LayoutImporter.Build( _template, _resolveSprite, _defaultFont, did => _fonts.TryGetValue(did, out UiDatFont? font) ? font : _defaultFont); UiText label = Required(content, LabelId); UiText value = Required(content, ValueId); label.LinesProvider = () => [new UiText.Line(row.Label, label.DefaultColor)]; value.LinesProvider = () => [new UiText.Line(row.Value, ResolveColor(row.Style, value.DefaultColor))]; return new UiTemplateListSlot( content, entryId: 0u, content.Root is IUiDatStateful stateful ? stateful.ActiveRetailStateId : UiStateInfo.DirectStateId, selectedState: null); } private static Vector4 ResolveColor( CreatureAppraisalValueStyle style, Vector4 normal) { // Retail selects one of four authored FontInfo entries here. Keep the // semantic state in the row model, but do not invent substitute RGB // values while that FontInfo-list binding remains unported (AP-110). _ = style; return normal; } private static T Required(ImportedLayout content, uint id) where T : UiElement => content.FindElement(id) as T ?? throw new InvalidOperationException( $"Retail creature appraisal template element 0x{id:X8} did not resolve to {typeof(T).Name}."); private static ElementInfo? Find(ElementInfo root, uint id) { if (root.Id == id) return root; foreach (ElementInfo child in root.Children) if (Find(child, id) is { } found) return found; return null; } 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); } } /// /// Retail AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0 resolves /// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces /// underscores with spaces. /// public sealed class CreatureDisplayNameResolver { public const uint MapperDid = 0x2200000Eu; private readonly IReadOnlyDictionary _names; public CreatureDisplayNameResolver( IReadOnlyDictionary names) { _names = names ?? throw new ArgumentNullException(nameof(names)); } public static CreatureDisplayNameResolver Load(IDatReaderWriter dats) { ArgumentNullException.ThrowIfNull(dats); var names = new Dictionary(); var visited = new HashSet(); uint did = MapperDid; while (did != 0u && visited.Add(did)) { EnumMapper? mapper = dats.Get(did); if (mapper is null) break; foreach ((uint id, PStringBase text) in mapper.IdToStringMap) names.TryAdd(id, text.Value.Replace('_', ' ')); did = mapper.BaseEnumMap; } return new CreatureDisplayNameResolver(names); } public string Resolve(int creatureType) => creatureType > 0 && _names.TryGetValue((uint)creatureType, out string? name) ? name : string.Empty; }