perf(ui): retain shaped report text
This commit is contained in:
parent
c0bec56dfe
commit
f2a015be8e
11 changed files with 525 additions and 38 deletions
|
|
@ -87,6 +87,9 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
private readonly UiElement _spellFormulaHost;
|
||||
private readonly UiTextureElement _spellIcon;
|
||||
private readonly List<UiElement> _spellFormulaCells = [];
|
||||
private readonly Dictionary<UiText, UiTextLayoutCache<string>>
|
||||
_textLayouts = new();
|
||||
private UiTextLayoutCache<ItemAppraisalReport> _itemTextLayout = null!;
|
||||
private readonly float _spellFormulaOriginX;
|
||||
private readonly float _spellFormulaCellWidth;
|
||||
private AppraisalView _activeView;
|
||||
|
|
@ -173,8 +176,11 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
layout.FindElement(InscriptionBackgroundId) as UiDatElement;
|
||||
_close = layout.FindElement(CloseId) as UiButton;
|
||||
|
||||
_title.LinesProvider = () =>
|
||||
[new UiText.Line(_titleValue, _title.DefaultColor)];
|
||||
BindTextSource(
|
||||
_title,
|
||||
() => _titleValue,
|
||||
static (target, value) =>
|
||||
[new UiText.Line(value, target.DefaultColor)]);
|
||||
// ItemExamineUI::AddItemInfo @ 0x004AC050 appends the freshly
|
||||
// rebuilt report from the beginning of the authored text surface.
|
||||
// LayoutDesc 0x2100006B carries Bottom here, which is appropriate for
|
||||
|
|
@ -200,8 +206,13 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
scrollbar.Model = _inscriptionField.Scroll;
|
||||
}
|
||||
if (_signature is not null)
|
||||
_signature.LinesProvider = () =>
|
||||
[new UiText.Line(_signatureValue, _signature.DefaultColor)];
|
||||
{
|
||||
BindTextSource(
|
||||
_signature,
|
||||
() => _signatureValue,
|
||||
static (target, value) =>
|
||||
[new UiText.Line(value, target.DefaultColor)]);
|
||||
}
|
||||
if (_close is not null)
|
||||
_close.OnClick = close;
|
||||
if (_inscriptionBackground is not null)
|
||||
|
|
@ -717,11 +728,49 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
text.PreserveEndOnLayout = false;
|
||||
text.WheelScrollEnabled = true;
|
||||
text.ClickThrough = false;
|
||||
text.LinesProvider = () => IndicatorDetailText.Shape(text, value());
|
||||
BindTextSource(
|
||||
text,
|
||||
value,
|
||||
static (target, content) =>
|
||||
IndicatorDetailText.Shape(target, content));
|
||||
if (_layout.FindElement(scrollbarId) is UiScrollbar scrollbar)
|
||||
scrollbar.Model = text.Scroll;
|
||||
}
|
||||
|
||||
private void BindTextSource(
|
||||
UiText text,
|
||||
Func<string> source,
|
||||
Func<UiText, string, IReadOnlyList<UiText.Line>> shape)
|
||||
{
|
||||
var cache = new UiTextLayoutCache<string>(
|
||||
text,
|
||||
shape,
|
||||
source,
|
||||
StringComparer.Ordinal);
|
||||
_textLayouts.Add(text, cache);
|
||||
text.LinesProvider = cache.Provider;
|
||||
}
|
||||
|
||||
private UiTextLayoutCache<string> GetTextLayout(UiText text)
|
||||
{
|
||||
if (_textLayouts.TryGetValue(
|
||||
text,
|
||||
out UiTextLayoutCache<string>? cache))
|
||||
{
|
||||
return cache;
|
||||
}
|
||||
|
||||
cache = new UiTextLayoutCache<string>(
|
||||
text,
|
||||
static (target, value) =>
|
||||
IndicatorDetailText.Shape(target, value),
|
||||
string.Empty,
|
||||
StringComparer.Ordinal);
|
||||
_textLayouts.Add(text, cache);
|
||||
text.LinesProvider = cache.Provider;
|
||||
return cache;
|
||||
}
|
||||
|
||||
private void ConfigureScrollableItemText(
|
||||
UiText text,
|
||||
uint scrollbarId)
|
||||
|
|
@ -729,8 +778,13 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
text.PreserveEndOnLayout = false;
|
||||
text.WheelScrollEnabled = true;
|
||||
text.ClickThrough = false;
|
||||
text.LinesProvider = () =>
|
||||
ItemAppraisalTextLayout.Shape(text, _itemReport);
|
||||
_itemTextLayout = new UiTextLayoutCache<ItemAppraisalReport>(
|
||||
text,
|
||||
static (target, report) =>
|
||||
ItemAppraisalTextLayout.Shape(target, report),
|
||||
() => _itemReport,
|
||||
ReferenceEqualityComparer.Instance);
|
||||
text.LinesProvider = _itemTextLayout.Provider;
|
||||
if (_layout.FindElement(scrollbarId) is UiScrollbar scrollbar)
|
||||
scrollbar.Model = text.Scroll;
|
||||
}
|
||||
|
|
@ -746,8 +800,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
private static void SetSpellText(UiText text, string value)
|
||||
=> text.LinesProvider = () => IndicatorDetailText.Shape(text, value);
|
||||
private void SetSpellText(UiText text, string value)
|
||||
=> GetTextLayout(text).SetValue(value);
|
||||
|
||||
private static string BuildSpellDisplay(
|
||||
SpellMetadata metadata,
|
||||
|
|
@ -811,7 +865,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
text.PreserveEndOnLayout = false;
|
||||
text.WheelScrollEnabled = scrollable;
|
||||
text.ClickThrough = !scrollable;
|
||||
text.LinesProvider = () => IndicatorDetailText.Shape(text, value);
|
||||
GetTextLayout(text).SetValue(value);
|
||||
if (scrollable)
|
||||
{
|
||||
UiScrollbar? scrollbar = Descendants(text.Parent)
|
||||
|
|
@ -830,7 +884,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
0x10000150u, 0x10000151u, 0x10000152u, 0x1000053Au,
|
||||
})
|
||||
if (_layout.FindElement(id) is UiText text)
|
||||
text.LinesProvider = static () => Array.Empty<UiText.Line>();
|
||||
GetTextLayout(text).SetValue(string.Empty);
|
||||
_creatureStats?.Flush();
|
||||
_creatureExtra?.Flush();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,12 +67,13 @@ public static class CharacterController
|
|||
internal static IReadOnlyList<string> AugmentationStringKeys
|
||||
=> Augmentations.Select(descriptor => descriptor.StringKey).ToArray();
|
||||
|
||||
public static void Bind(
|
||||
public static CharacterInformationUiController Bind(
|
||||
ImportedLayout layout,
|
||||
Func<CharacterSheet> data,
|
||||
UiDatFont? datFont = null,
|
||||
Action? close = null,
|
||||
CharacterInfoStrings? strings = null)
|
||||
CharacterInfoStrings? strings = null,
|
||||
Func<Action, IDisposable>? subscribeChanged = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
|
@ -96,11 +97,19 @@ public static class CharacterController
|
|||
|
||||
CharacterInfoStrings copy = strings ?? CharacterInfoStrings.English;
|
||||
text.PreserveEndOnLayout = false;
|
||||
text.LinesProvider = () => IndicatorDetailText.Shape(text, BuildReport(data(), copy));
|
||||
if (layout.FindElement(ScrollbarId) is UiScrollbar scrollbar)
|
||||
UiScrollbar? scrollbar = layout.FindElement(ScrollbarId) as UiScrollbar;
|
||||
if (scrollbar is not null)
|
||||
scrollbar.Model = text.Scroll;
|
||||
if (layout.FindElement(CloseId) is UiButton closeButton)
|
||||
UiButton? closeButton = layout.FindElement(CloseId) as UiButton;
|
||||
if (closeButton is not null)
|
||||
closeButton.OnClick = close;
|
||||
return new CharacterInformationUiController(
|
||||
text,
|
||||
data,
|
||||
copy,
|
||||
subscribeChanged,
|
||||
scrollbar,
|
||||
closeButton);
|
||||
}
|
||||
|
||||
internal static string BuildReport(CharacterSheet sheet, CharacterInfoStrings strings)
|
||||
|
|
@ -330,6 +339,69 @@ public static class CharacterController
|
|||
bool IncludeValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event-invalidated report owner for retail's character-information text.
|
||||
/// Stable frames borrow the same shaped line collection.
|
||||
/// </summary>
|
||||
public sealed class CharacterInformationUiController : IRetainedPanelController
|
||||
{
|
||||
private readonly Func<CharacterSheet> _data;
|
||||
private readonly CharacterInfoStrings _strings;
|
||||
private readonly UiTextLayoutCache<string> _layout;
|
||||
private readonly IDisposable? _changeBinding;
|
||||
private readonly UiScrollbar? _scrollbar;
|
||||
private readonly UiButton? _close;
|
||||
private bool _contentDirty = true;
|
||||
private bool _disposed;
|
||||
|
||||
internal CharacterInformationUiController(
|
||||
UiText text,
|
||||
Func<CharacterSheet> data,
|
||||
CharacterInfoStrings strings,
|
||||
Func<Action, IDisposable>? subscribeChanged,
|
||||
UiScrollbar? scrollbar,
|
||||
UiButton? close)
|
||||
{
|
||||
_data = data;
|
||||
_strings = strings;
|
||||
_scrollbar = scrollbar;
|
||||
_close = close;
|
||||
_layout = new UiTextLayoutCache<string>(
|
||||
text,
|
||||
static (target, value) => IndicatorDetailText.Shape(target, value),
|
||||
string.Empty,
|
||||
StringComparer.Ordinal);
|
||||
text.LinesProvider = GetLines;
|
||||
_changeBinding = subscribeChanged?.Invoke(InvalidateContent);
|
||||
}
|
||||
|
||||
private IReadOnlyList<UiText.Line> GetLines()
|
||||
{
|
||||
if (_contentDirty)
|
||||
{
|
||||
_layout.SetValue(CharacterController.BuildReport(_data(), _strings));
|
||||
_contentDirty = false;
|
||||
}
|
||||
return _layout.GetLines();
|
||||
}
|
||||
|
||||
private void InvalidateContent() => _contentDirty = true;
|
||||
|
||||
public void OnShown() => InvalidateContent();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_changeBinding?.Dispose();
|
||||
if (_scrollbar is not null)
|
||||
_scrollbar.Model = null;
|
||||
if (_close is not null)
|
||||
_close.OnClick = null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CharacterInfoStrings(
|
||||
string[] Birth,
|
||||
string[] Played,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,17 @@ public sealed class CharacterSheetProvider
|
|||
_sendTrainSkill = sendTrainSkill;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to semantic inputs used by <see cref="BuildSheet"/>. The
|
||||
/// returned binding owns every event registration and filters object-table
|
||||
/// notices to the current player GUID.
|
||||
/// </summary>
|
||||
public IDisposable SubscribeChanged(Action changed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(changed);
|
||||
return new ChangeBinding(this, changed);
|
||||
}
|
||||
|
||||
// ── Sheet assembly ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Best display name: active toon key, else the live object's name, else "Player".</summary>
|
||||
|
|
@ -181,6 +192,52 @@ public sealed class CharacterSheetProvider
|
|||
: _localPlayer.Properties;
|
||||
}
|
||||
|
||||
private sealed class ChangeBinding : IDisposable
|
||||
{
|
||||
private CharacterSheetProvider? _owner;
|
||||
private readonly Action _changed;
|
||||
|
||||
public ChangeBinding(CharacterSheetProvider owner, Action changed)
|
||||
{
|
||||
_owner = owner;
|
||||
_changed = changed;
|
||||
owner._objects.ObjectAdded += OnObjectChanged;
|
||||
owner._objects.ObjectUpdated += OnObjectChanged;
|
||||
owner._objects.ObjectRemoved += OnObjectChanged;
|
||||
owner._objects.Cleared += OnCleared;
|
||||
owner._localPlayer.AttributeChanged += OnAttributeChanged;
|
||||
owner._localPlayer.CharacterChanged += OnCharacterChanged;
|
||||
}
|
||||
|
||||
private void OnObjectChanged(ClientObject value)
|
||||
{
|
||||
CharacterSheetProvider? owner = _owner;
|
||||
if (owner is not null && value.ObjectId == owner._playerGuid())
|
||||
_changed();
|
||||
}
|
||||
|
||||
private void OnCleared() => _changed();
|
||||
|
||||
private void OnAttributeChanged(LocalPlayerState.AttributeKind _) =>
|
||||
_changed();
|
||||
|
||||
private void OnCharacterChanged() => _changed();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CharacterSheetProvider? owner = Interlocked.Exchange(ref _owner, null);
|
||||
if (owner is null)
|
||||
return;
|
||||
|
||||
owner._objects.ObjectAdded -= OnObjectChanged;
|
||||
owner._objects.ObjectUpdated -= OnObjectChanged;
|
||||
owner._objects.ObjectRemoved -= OnObjectChanged;
|
||||
owner._objects.Cleared -= OnCleared;
|
||||
owner._localPlayer.AttributeChanged -= OnAttributeChanged;
|
||||
owner._localPlayer.CharacterChanged -= OnCharacterChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the portal ExperienceTable (0x0E000018), falling back to a
|
||||
/// type scan for older or odd dat collections. Failures are logged —
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
private readonly string _selectPrompt;
|
||||
private readonly UiItemList _list;
|
||||
private readonly UiText? _info;
|
||||
private readonly UiTextLayoutCache<string>? _infoLayout;
|
||||
private readonly UiButton? _close;
|
||||
private readonly UiScrollbar? _listScrollbar;
|
||||
private readonly UiScrollbar? _infoScrollbar;
|
||||
|
|
@ -70,7 +71,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
_list.CellHeight = templates.Height;
|
||||
if (_listScrollbar is not null)
|
||||
_listScrollbar.Model = _list.Scroll;
|
||||
ConfigureInfo();
|
||||
_infoLayout = ConfigureInfo();
|
||||
_spellbook.EnchantmentsChanged += Rebuild;
|
||||
Rebuild();
|
||||
}
|
||||
|
|
@ -155,6 +156,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
&& !_rows.Values.Any(row => row.Slot.EntryId == selected))
|
||||
_selectedSpellId = null;
|
||||
SyncSelection();
|
||||
UpdateInfoText();
|
||||
}
|
||||
|
||||
private IEnumerable<ActiveEnchantmentRecord> VisibleEnchantments()
|
||||
|
|
@ -189,6 +191,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
// token's spell stat, not its enchantment layer identity.
|
||||
_selectedSpellId = _selectedSpellId == spellId ? null : spellId;
|
||||
SyncSelection();
|
||||
UpdateInfoText();
|
||||
}
|
||||
|
||||
private void SyncSelection()
|
||||
|
|
@ -197,30 +200,39 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
row.Slot.SetSelected(row.Slot.EntryId == _selectedSpellId);
|
||||
}
|
||||
|
||||
private void ConfigureInfo()
|
||||
private UiTextLayoutCache<string>? ConfigureInfo()
|
||||
{
|
||||
if (_info is null) return;
|
||||
if (_info is null) return null;
|
||||
_info.PreserveEndOnLayout = false;
|
||||
_info.WheelScrollEnabled = true;
|
||||
_info.ClickThrough = false;
|
||||
if (_infoScrollbar is not null)
|
||||
_infoScrollbar.Model = _info.Scroll;
|
||||
_info.LinesProvider = () =>
|
||||
var cache = new UiTextLayoutCache<string>(
|
||||
_info,
|
||||
static (target, value) => IndicatorDetailText.Shape(target, value),
|
||||
_selectPrompt,
|
||||
StringComparer.Ordinal);
|
||||
_info.LinesProvider = cache.Provider;
|
||||
return cache;
|
||||
}
|
||||
|
||||
private void UpdateInfoText()
|
||||
{
|
||||
if (_infoLayout is null)
|
||||
return;
|
||||
|
||||
string value = _selectPrompt;
|
||||
if (_selectedSpellId is uint spellId
|
||||
&& _spellbook.ActiveEnchantmentSnapshot.Any(
|
||||
record => record.SpellId == spellId)
|
||||
&& _spellbook.TryGetMetadata(spellId, out SpellMetadata metadata))
|
||||
{
|
||||
if (_selectedSpellId is not uint spellId)
|
||||
return IndicatorDetailText.Shape(_info, _selectPrompt);
|
||||
ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot
|
||||
.Cast<ActiveEnchantmentRecord?>()
|
||||
.FirstOrDefault(record => record?.SpellId == spellId);
|
||||
if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata))
|
||||
return IndicatorDetailText.Shape(_info, _selectPrompt);
|
||||
// gmEffectsUI::UpdateSelection @ 0x004B7F90 supplies one literal
|
||||
// name + blank line + description to UIElement_Text::SetText. The
|
||||
// text widget reflows it to the authored information width.
|
||||
return IndicatorDetailText.Shape(
|
||||
_info,
|
||||
metadata.Name + "\n\n" + metadata.Description);
|
||||
};
|
||||
// name + blank line + description to UIElement_Text::SetText.
|
||||
value = metadata.Name + "\n\n" + metadata.Description;
|
||||
}
|
||||
_infoLayout.SetValue(value);
|
||||
}
|
||||
|
||||
public void OnShown() => Rebuild();
|
||||
|
|
|
|||
115
src/AcDream.App/UI/Layout/UiTextLayoutCache.cs
Normal file
115
src/AcDream.App/UI/Layout/UiTextLayoutCache.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Retains the shaped line projection of one <see cref="UiText"/> value.
|
||||
/// Controllers publish semantic content; the cache reshapes only when that
|
||||
/// content or the text widget's layout/font inputs change.
|
||||
/// </summary>
|
||||
internal sealed class UiTextLayoutCache<T>
|
||||
{
|
||||
private readonly UiText _target;
|
||||
private readonly Func<UiText, T, IReadOnlyList<UiText.Line>> _shape;
|
||||
private readonly IEqualityComparer<T> _comparer;
|
||||
private readonly Func<T>? _source;
|
||||
private T _value = default!;
|
||||
private bool _hasValue;
|
||||
private bool _shaped;
|
||||
private IReadOnlyList<UiText.Line> _lines = Array.Empty<UiText.Line>();
|
||||
private float _width;
|
||||
private float _padding;
|
||||
private Vector4 _defaultColor;
|
||||
private UiDatFont? _datFont;
|
||||
private BitmapFont? _bitmapFont;
|
||||
private IReadOnlyList<Vector4>? _fontColorPalette;
|
||||
|
||||
public UiTextLayoutCache(
|
||||
UiText target,
|
||||
Func<UiText, T, IReadOnlyList<UiText.Line>> shape,
|
||||
T initialValue,
|
||||
IEqualityComparer<T>? comparer = null)
|
||||
: this(target, shape, source: null, comparer, initialize: true)
|
||||
{
|
||||
SetValue(initialValue);
|
||||
}
|
||||
|
||||
public UiTextLayoutCache(
|
||||
UiText target,
|
||||
Func<UiText, T, IReadOnlyList<UiText.Line>> shape,
|
||||
Func<T> source,
|
||||
IEqualityComparer<T>? comparer = null)
|
||||
: this(
|
||||
target,
|
||||
shape,
|
||||
source ?? throw new ArgumentNullException(nameof(source)),
|
||||
comparer,
|
||||
initialize: true)
|
||||
{
|
||||
}
|
||||
|
||||
private UiTextLayoutCache(
|
||||
UiText target,
|
||||
Func<UiText, T, IReadOnlyList<UiText.Line>> shape,
|
||||
Func<T>? source,
|
||||
IEqualityComparer<T>? comparer,
|
||||
bool initialize)
|
||||
{
|
||||
_ = initialize;
|
||||
_target = target ?? throw new ArgumentNullException(nameof(target));
|
||||
_shape = shape ?? throw new ArgumentNullException(nameof(shape));
|
||||
_source = source;
|
||||
_comparer = comparer ?? EqualityComparer<T>.Default;
|
||||
}
|
||||
|
||||
public Func<IReadOnlyList<UiText.Line>> Provider => GetLines;
|
||||
|
||||
public void SetValue(T value)
|
||||
{
|
||||
if (_hasValue && _comparer.Equals(_value, value))
|
||||
return;
|
||||
|
||||
_value = value;
|
||||
_hasValue = true;
|
||||
_shaped = false;
|
||||
}
|
||||
|
||||
public void Invalidate() => _shaped = false;
|
||||
|
||||
public IReadOnlyList<UiText.Line> GetLines()
|
||||
{
|
||||
if (_source is not null)
|
||||
SetValue(_source());
|
||||
|
||||
if (!_hasValue)
|
||||
return Array.Empty<UiText.Line>();
|
||||
|
||||
if (!_shaped || LayoutChanged())
|
||||
{
|
||||
CaptureLayout();
|
||||
_lines = _shape(_target, _value);
|
||||
_shaped = true;
|
||||
}
|
||||
|
||||
return _lines;
|
||||
}
|
||||
|
||||
private bool LayoutChanged()
|
||||
=> _width != _target.Width
|
||||
|| _padding != _target.Padding
|
||||
|| _defaultColor != _target.DefaultColor
|
||||
|| !ReferenceEquals(_datFont, _target.DatFont)
|
||||
|| !ReferenceEquals(_bitmapFont, _target.Font)
|
||||
|| !ReferenceEquals(_fontColorPalette, _target.FontColorPalette);
|
||||
|
||||
private void CaptureLayout()
|
||||
{
|
||||
_width = _target.Width;
|
||||
_padding = _target.Padding;
|
||||
_defaultColor = _target.DefaultColor;
|
||||
_datFont = _target.DatFont;
|
||||
_bitmapFont = _target.Font;
|
||||
_fontColorPalette = _target.FontColorPalette;
|
||||
}
|
||||
}
|
||||
|
|
@ -1193,18 +1193,19 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
}
|
||||
if (rootInfo is null || layout is null) return;
|
||||
|
||||
CharacterController.Bind(
|
||||
CharacterInformationUiController controller = CharacterController.Bind(
|
||||
layout,
|
||||
_bindings.Character.Provider.BuildSheet,
|
||||
_bindings.Assets.DefaultFont,
|
||||
() => CloseWindow(WindowNames.CharacterInformation),
|
||||
strings);
|
||||
strings,
|
||||
_bindings.Character.Provider.SubscribeChanged);
|
||||
RegisterIndicatorDetailPanel(
|
||||
RetailPanelCatalog.CharacterInformation,
|
||||
WindowNames.CharacterInformation,
|
||||
rootInfo,
|
||||
layout.Root,
|
||||
controller: null);
|
||||
controller);
|
||||
}
|
||||
|
||||
private void MountLinkStatusPanel()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue