perf(vtank): slice 7 fix round B item 12 — materialize grid columns once per mutation, not once per frame
Every retained-UI draw re-evaluates every markup-bound column property. Several of these did real per-row work on EVERY frame instead of returning a cached field: - ExcludedComponentIcons (Consumables) called ResolveComponentIcon per row, which itself calls IAutomation.Items.CaptureOwnedItems() — a live inventory snapshot — per row per frame. - All 23 Monsters-grid columns (14 flag columns via MonsterFlagColumn, 7 text columns, 2 move-icon columns) re-ran a LINQ .Select(...).ToArray() over _combatSettings.Rules every frame; Weapon/Offhand additionally called ItemDisplayName (another CaptureOwnedItems() scan) per row. - Meta's MetaStateColumn/MetaConditionColumn/MetaActionColumn/ MetaDeleteColumn/MetaMoveUpIcons/MetaMoveDownIcons and Route's RouteWaypointCountColumn allocated a fresh array with LINQ every frame. Fixed by materializing each into a field, computed once at the point of actual mutation: - ExcludedComponentIcons: computed in RefreshItemEditors (already the sole owner of _excludedComponentRows). - RouteWaypointCountColumn: computed in RefreshRouteEditor (already called at 13 real mutation points, including profile load). - Meta's 6 columns: computed in RefreshMetaEditor (already called at every real mutation point, including profile load). - Monsters' 23 columns: new RefreshMonsterEditor, wired into the 4 actual Rules-mutating methods (AddMonsterRuleCore, DeleteMonsterRuleAtCore, MoveMonsterRuleAtCore, UpdateMonsterActionsAt — the last already covers every flag toggle and value-cycle action) plus panel construction and ResetProfileConsumers (profile load/switch). The now-unused MonsterFlagColumn helper is removed. New tests (FakeAutomation gained a CaptureOwnedItemsCallCount counter): ExcludedComponentIconsDoesNotScanLiveInventoryOnEveryRead, MonsterGridColumnsDoNotScanLiveInventoryOrAllocateOnEveryRead (also asserts Assert.Same across reads), MetaAndRouteGridColumnsDoNotReallocateOnEveryRead. All three mutation-checked by temporarily reverting to the old per-read computation: each turned red (call-count mismatch or Assert.Same failure) against the reverted code; restoring the cached-field getters turns each green. tests/AcDream.Plugins.MossTank.Tests: 674/674 (was 671/671, +3 new tests). tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (unchanged — App-layer surface untouched by this item). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
da42c1fce6
commit
169a6c6e32
2 changed files with 312 additions and 63 deletions
|
|
@ -117,6 +117,37 @@ internal sealed partial class MossTankPanel
|
|||
private string _profileLifecycleNotice = "Macro settings are stored by character.";
|
||||
private string _monsterExpressionDraft = string.Empty;
|
||||
private string _monsterEditorNotice = "Add a monster name or expression, or select one in the world.";
|
||||
// Fix round B item 12: the Monsters grid's 23 columns, materialized once
|
||||
// per RefreshMonsterEditor (called at every real mutation point below) —
|
||||
// not once per row per frame in each column's own getter. The 14 flag
|
||||
// columns previously re-ran a LINQ .Select(...).ToArray() over
|
||||
// _combatSettings.Rules on every draw; Weapon/Offhand additionally
|
||||
// called ItemDisplayName (which itself calls
|
||||
// IAutomation.Items.CaptureOwnedItems(), a live inventory snapshot) once
|
||||
// per row per frame.
|
||||
private IReadOnlyList<bool> _monsterFesterColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterBroadsideColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterGravityWellColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterImperilColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterYieldColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterVulnerabilityColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterAttackColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterRingColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterStreakColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterWeakeningColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterFesteringColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterCorruptionColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterDestructiveColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<bool> _monsterCorrosionColumn = Array.Empty<bool>();
|
||||
private IReadOnlyList<string> _monsterNameColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _monsterPriorityColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _monsterDamageColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _monsterExtraVulnColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _monsterWeaponColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _monsterOffhandColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _monsterPetDamageColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<uint> _monsterMoveUpIconsColumn = Array.Empty<uint>();
|
||||
private IReadOnlyList<uint> _monsterMoveDownIconsColumn = Array.Empty<uint>();
|
||||
// VVS's own HudList always has a "selected" backing int even though
|
||||
// VTank's Monsters grid never reads it for anything (every real cell in
|
||||
// that row is an action target, not a select-then-edit target — see
|
||||
|
|
@ -174,6 +205,12 @@ internal sealed partial class MossTankPanel
|
|||
private static readonly string[] HandednessCycle =
|
||||
["Auto", "1-Handed", "2-Handed"];
|
||||
private IReadOnlyList<string> _excludedComponentRows = Array.Empty<string>();
|
||||
// Fix round B item 12: resolved once per RefreshItemEditors (a real
|
||||
// state-change event), not once per row per frame — the old
|
||||
// ExcludedComponentIcons getter called ResolveComponentIcon (which
|
||||
// itself calls IAutomation.Items.CaptureOwnedItems(), a live inventory
|
||||
// snapshot) for every row on every draw.
|
||||
private IReadOnlyList<uint> _excludedComponentIcons = Array.Empty<uint>();
|
||||
private int _selectedExcludedComponentRow;
|
||||
private bool _buffPickerVisible;
|
||||
private bool _buffPickerForBlacklist;
|
||||
|
|
@ -202,6 +239,9 @@ internal sealed partial class MossTankPanel
|
|||
private string _lootEditorNotice = "Add a rule or select one to edit.";
|
||||
private string _lootProfileNameDraft = string.Empty;
|
||||
private IReadOnlyList<string> _routeRows = Array.Empty<string>();
|
||||
// Fix round B item 12: materialized in RefreshRouteEditor, not once per
|
||||
// frame in the RouteWaypointCountColumn getter.
|
||||
private IReadOnlyList<string> _routeWaypointCounts = Array.Empty<string>();
|
||||
private int _selectedRouteWaypoint;
|
||||
private string _routeProfileNameDraft = string.Empty;
|
||||
private string _routeNotice = "Add the current position or a selected object.";
|
||||
|
|
@ -210,6 +250,15 @@ internal sealed partial class MossTankPanel
|
|||
private RouteRecallKind _routeRecallKind = RouteRecallKind.PrimaryPortal;
|
||||
private RouteInsertMode _routeInsertMode = RouteInsertMode.AddToEnd;
|
||||
private IReadOnlyList<string> _metaRows = Array.Empty<string>();
|
||||
// Fix round B item 12: the Meta grid's 6 columns, materialized once per
|
||||
// RefreshMetaEditor (already called at every real mutation point) —
|
||||
// not once per row per frame in each column's own getter.
|
||||
private IReadOnlyList<string> _metaDeleteColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<uint> _metaMoveUpIconsColumn = Array.Empty<uint>();
|
||||
private IReadOnlyList<uint> _metaMoveDownIconsColumn = Array.Empty<uint>();
|
||||
private IReadOnlyList<string> _metaStateColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _metaConditionColumn = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _metaActionColumn = Array.Empty<string>();
|
||||
private int _selectedMetaRule;
|
||||
private string _metaProfileNameDraft = string.Empty;
|
||||
private string _metaStateDraft = MetaEngine.DefaultState;
|
||||
|
|
@ -325,6 +374,7 @@ internal sealed partial class MossTankPanel
|
|||
_initialized = true;
|
||||
ApplyPersistedOptionOverrides();
|
||||
EnsureDefaultMonsterRule();
|
||||
RefreshMonsterEditor();
|
||||
RefreshItemEditors();
|
||||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
|
|
@ -597,9 +647,7 @@ internal sealed partial class MossTankPanel
|
|||
// Selected" requires a selected spell-component-class object,
|
||||
// per-cell click deletes) ────────────────────────────────────────────
|
||||
public IReadOnlyList<string> ExcludedComponentRows => _excludedComponentRows;
|
||||
public IReadOnlyList<uint> ExcludedComponentIcons => _excludedComponentRows
|
||||
.Select(ResolveComponentIcon)
|
||||
.ToArray();
|
||||
public IReadOnlyList<uint> ExcludedComponentIcons => _excludedComponentIcons;
|
||||
public int SelectedExcludedComponentIndex => _selectedExcludedComponentRow;
|
||||
public Action<int> SelectExcludedComponentRow => index =>
|
||||
_selectedExcludedComponentRow = ClampRow(index, _excludedComponentRows.Count);
|
||||
|
|
@ -844,10 +892,7 @@ internal sealed partial class MossTankPanel
|
|||
// (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Route";
|
||||
// PluginCore.cs:3576-3599 — any cell click deletes the waypoint).
|
||||
public IReadOnlyList<string> RouteWaypointTextColumn => _routeRows;
|
||||
public IReadOnlyList<string> RouteWaypointCountColumn => Enumerable
|
||||
.Range(1, _routeRows.Count)
|
||||
.Select(static n => n.ToString(CultureInfo.InvariantCulture))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> RouteWaypointCountColumn => _routeWaypointCounts;
|
||||
public Action<int> DeleteRouteWaypointAt => DeleteRouteWaypointAtCore;
|
||||
public Action SelectNearestRouteWaypoint => SelectNearestRouteWaypointCore;
|
||||
// VTank's own cmbNavType captions read Circular/Linear/Follow/Once
|
||||
|
|
@ -1017,18 +1062,12 @@ internal sealed partial class MossTankPanel
|
|||
// Monsters/Route grids), and "X" is plain ASCII the default retail
|
||||
// font already renders — see NoButtonAnywhereUsesTheUnrenderableArrowGlyphs
|
||||
// for why an unconfirmed glyph id is worse than a text fallback. ──
|
||||
public IReadOnlyList<string> MetaDeleteColumn =>
|
||||
Enumerable.Repeat("X", _metaProfile.Rules.Count).ToArray();
|
||||
public IReadOnlyList<uint> MetaMoveUpIcons =>
|
||||
Enumerable.Repeat(0x060028FCu, _metaProfile.Rules.Count).ToArray();
|
||||
public IReadOnlyList<uint> MetaMoveDownIcons =>
|
||||
Enumerable.Repeat(0x060028FDu, _metaProfile.Rules.Count).ToArray();
|
||||
public IReadOnlyList<string> MetaStateColumn =>
|
||||
_metaProfile.Rules.Select(static rule => rule.State).ToArray();
|
||||
public IReadOnlyList<string> MetaConditionColumn =>
|
||||
_metaProfile.Rules.Select(static rule => DescribeMetaCondition(rule.Condition)).ToArray();
|
||||
public IReadOnlyList<string> MetaActionColumn =>
|
||||
_metaProfile.Rules.Select(static rule => DescribeMetaAction(rule.Action)).ToArray();
|
||||
public IReadOnlyList<string> MetaDeleteColumn => _metaDeleteColumn;
|
||||
public IReadOnlyList<uint> MetaMoveUpIcons => _metaMoveUpIconsColumn;
|
||||
public IReadOnlyList<uint> MetaMoveDownIcons => _metaMoveDownIconsColumn;
|
||||
public IReadOnlyList<string> MetaStateColumn => _metaStateColumn;
|
||||
public IReadOnlyList<string> MetaConditionColumn => _metaConditionColumn;
|
||||
public IReadOnlyList<string> MetaActionColumn => _metaActionColumn;
|
||||
public Action<int> DeleteMetaRuleAt => row =>
|
||||
{
|
||||
SelectMetaRuleCore(row);
|
||||
|
|
@ -1226,12 +1265,12 @@ internal sealed partial class MossTankPanel
|
|||
|
||||
// ── the 14 check columns (decompile cases 0-13; order F B G I Y V A R S
|
||||
// WC FC Cp DC Cs matches MonsterActionFlags's own declared bit order) ──
|
||||
public IReadOnlyList<bool> MonsterFesterColumn => MonsterFlagColumn(MonsterActionFlags.Fester);
|
||||
public IReadOnlyList<bool> MonsterBroadsideColumn => MonsterFlagColumn(MonsterActionFlags.Broadside);
|
||||
public IReadOnlyList<bool> MonsterGravityWellColumn => MonsterFlagColumn(MonsterActionFlags.GravityWell);
|
||||
public IReadOnlyList<bool> MonsterImperilColumn => MonsterFlagColumn(MonsterActionFlags.Imperil);
|
||||
public IReadOnlyList<bool> MonsterYieldColumn => MonsterFlagColumn(MonsterActionFlags.Yield);
|
||||
public IReadOnlyList<bool> MonsterVulnerabilityColumn => MonsterFlagColumn(MonsterActionFlags.Vulnerability);
|
||||
public IReadOnlyList<bool> MonsterFesterColumn => _monsterFesterColumn;
|
||||
public IReadOnlyList<bool> MonsterBroadsideColumn => _monsterBroadsideColumn;
|
||||
public IReadOnlyList<bool> MonsterGravityWellColumn => _monsterGravityWellColumn;
|
||||
public IReadOnlyList<bool> MonsterImperilColumn => _monsterImperilColumn;
|
||||
public IReadOnlyList<bool> MonsterYieldColumn => _monsterYieldColumn;
|
||||
public IReadOnlyList<bool> MonsterVulnerabilityColumn => _monsterVulnerabilityColumn;
|
||||
// Case 6 ("A") assigns inverted in VTank's own obfuscated field
|
||||
// (a10.t = !checkbox), but that inversion is purely an artifact of
|
||||
// VTank's internal storage — the CHECKBOX ITSELF still shows "checked
|
||||
|
|
@ -1239,14 +1278,14 @@ internal sealed partial class MossTankPanel
|
|||
// the field back). Our MonsterActionFlags.Attack is already declared in
|
||||
// the un-inverted, on-screen sense, so no inversion is needed here: this
|
||||
// column reads/writes Attack directly and shows exactly what VTank shows.
|
||||
public IReadOnlyList<bool> MonsterAttackColumn => MonsterFlagColumn(MonsterActionFlags.Attack);
|
||||
public IReadOnlyList<bool> MonsterRingColumn => MonsterFlagColumn(MonsterActionFlags.Ring);
|
||||
public IReadOnlyList<bool> MonsterStreakColumn => MonsterFlagColumn(MonsterActionFlags.Streak);
|
||||
public IReadOnlyList<bool> MonsterWeakeningColumn => MonsterFlagColumn(MonsterActionFlags.WeakeningCurse);
|
||||
public IReadOnlyList<bool> MonsterFesteringColumn => MonsterFlagColumn(MonsterActionFlags.FesteringCurse);
|
||||
public IReadOnlyList<bool> MonsterCorruptionColumn => MonsterFlagColumn(MonsterActionFlags.Corruption);
|
||||
public IReadOnlyList<bool> MonsterDestructiveColumn => MonsterFlagColumn(MonsterActionFlags.DestructiveCurse);
|
||||
public IReadOnlyList<bool> MonsterCorrosionColumn => MonsterFlagColumn(MonsterActionFlags.Corrosion);
|
||||
public IReadOnlyList<bool> MonsterAttackColumn => _monsterAttackColumn;
|
||||
public IReadOnlyList<bool> MonsterRingColumn => _monsterRingColumn;
|
||||
public IReadOnlyList<bool> MonsterStreakColumn => _monsterStreakColumn;
|
||||
public IReadOnlyList<bool> MonsterWeakeningColumn => _monsterWeakeningColumn;
|
||||
public IReadOnlyList<bool> MonsterFesteringColumn => _monsterFesteringColumn;
|
||||
public IReadOnlyList<bool> MonsterCorruptionColumn => _monsterCorruptionColumn;
|
||||
public IReadOnlyList<bool> MonsterDestructiveColumn => _monsterDestructiveColumn;
|
||||
public IReadOnlyList<bool> MonsterCorrosionColumn => _monsterCorrosionColumn;
|
||||
|
||||
public Action<int> ToggleMonsterFesterAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Fester);
|
||||
public Action<int> ToggleMonsterBroadsideAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Broadside);
|
||||
|
|
@ -1264,26 +1303,13 @@ internal sealed partial class MossTankPanel
|
|||
public Action<int> ToggleMonsterCorrosionAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Corrosion);
|
||||
|
||||
// ── the 7 text columns (decompile cases 14-20) ──────────────────────────
|
||||
public IReadOnlyList<string> MonsterNameColumn =>
|
||||
_combatSettings.Rules.Select(static r => r.Expression).ToArray();
|
||||
public IReadOnlyList<string> MonsterPriorityColumn =>
|
||||
_combatSettings.Rules.Select(
|
||||
static r => r.Actions.BoundedPriority.ToString(CultureInfo.InvariantCulture))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> MonsterDamageColumn =>
|
||||
_combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.DamageType)).ToArray();
|
||||
public IReadOnlyList<string> MonsterExtraVulnColumn =>
|
||||
_combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.ExtraVulnerability)).ToArray();
|
||||
public IReadOnlyList<string> MonsterWeaponColumn =>
|
||||
_combatSettings.Rules.Select(
|
||||
r => ItemDisplayName(r.Actions.WeaponObjectId, r.Actions.WeaponName))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> MonsterOffhandColumn =>
|
||||
_combatSettings.Rules.Select(
|
||||
r => ItemDisplayName(r.Actions.OffhandObjectId, r.Actions.OffhandName))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> MonsterPetDamageColumn =>
|
||||
_combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.PetDamageType)).ToArray();
|
||||
public IReadOnlyList<string> MonsterNameColumn => _monsterNameColumn;
|
||||
public IReadOnlyList<string> MonsterPriorityColumn => _monsterPriorityColumn;
|
||||
public IReadOnlyList<string> MonsterDamageColumn => _monsterDamageColumn;
|
||||
public IReadOnlyList<string> MonsterExtraVulnColumn => _monsterExtraVulnColumn;
|
||||
public IReadOnlyList<string> MonsterWeaponColumn => _monsterWeaponColumn;
|
||||
public IReadOnlyList<string> MonsterOffhandColumn => _monsterOffhandColumn;
|
||||
public IReadOnlyList<string> MonsterPetDamageColumn => _monsterPetDamageColumn;
|
||||
|
||||
// Case 14: clicking the Name cell of any non-DEFAULT row deletes it
|
||||
// (PluginCore.cs:7948-7955's `if (A_1 != 0) { ...; b(A_1); }`, where
|
||||
|
|
@ -1325,10 +1351,8 @@ internal sealed partial class MossTankPanel
|
|||
// ── the 2 icon columns (decompile cases 21/22 — move up/down; DAT ids
|
||||
// 0x060028FC/0x060028FD, docs/plans/2026-09-07-campaign-vt-slice7-tabs.md
|
||||
// S7.3) ──────────────────────────────────────────────────────────────
|
||||
public IReadOnlyList<uint> MonsterMoveUpIcons =>
|
||||
Enumerable.Repeat(0x060028FCu, _combatSettings.Rules.Count).ToArray();
|
||||
public IReadOnlyList<uint> MonsterMoveDownIcons =>
|
||||
Enumerable.Repeat(0x060028FDu, _combatSettings.Rules.Count).ToArray();
|
||||
public IReadOnlyList<uint> MonsterMoveUpIcons => _monsterMoveUpIconsColumn;
|
||||
public IReadOnlyList<uint> MonsterMoveDownIcons => _monsterMoveDownIconsColumn;
|
||||
public Action<int> MoveMonsterRuleUpAt => row => MoveMonsterRuleAtCore(row, -1);
|
||||
public Action<int> MoveMonsterRuleDownAt => row => MoveMonsterRuleAtCore(row, 1);
|
||||
|
||||
|
|
@ -1735,6 +1759,10 @@ internal sealed partial class MossTankPanel
|
|||
_consumableRows.Count);
|
||||
_excludedComponentRows = ParseExcludedComponents(
|
||||
_buffSettings.BlacklistedSpellComponents);
|
||||
var excludedIcons = new uint[_excludedComponentRows.Count];
|
||||
for (int i = 0; i < excludedIcons.Length; i++)
|
||||
excludedIcons[i] = ResolveComponentIcon(_excludedComponentRows[i]);
|
||||
_excludedComponentIcons = excludedIcons;
|
||||
_selectedExcludedComponentRow = ClampRow(
|
||||
_selectedExcludedComponentRow,
|
||||
_excludedComponentRows.Count);
|
||||
|
|
@ -2143,6 +2171,10 @@ internal sealed partial class MossTankPanel
|
|||
$"{(index == active && _navigationSettings.Enabled ? "<<" : " ")} "
|
||||
+ waypoint.DisplayText)
|
||||
.ToArray();
|
||||
var counts = new string[_routeRows.Count];
|
||||
for (int i = 0; i < counts.Length; i++)
|
||||
counts[i] = (i + 1).ToString(CultureInfo.InvariantCulture);
|
||||
_routeWaypointCounts = counts;
|
||||
}
|
||||
|
||||
private void AddRoutePointCore()
|
||||
|
|
@ -2502,8 +2534,95 @@ internal sealed partial class MossTankPanel
|
|||
_combatSettings.Rules.Add(new MonsterRule("DEFAULT", 0));
|
||||
}
|
||||
|
||||
private IReadOnlyList<bool> MonsterFlagColumn(MonsterActionFlags flag) =>
|
||||
_combatSettings.Rules.Select(r => (r.Actions.Flags & flag) != 0).ToArray();
|
||||
/// <summary>
|
||||
/// Fix round B item 12: materializes every Monsters-grid column
|
||||
/// (14 flags + 7 text + 2 move icons = 23) from
|
||||
/// <see cref="_combatSettings"/>'s <c>Rules</c> in one pass. Called from
|
||||
/// every real mutation point (Add/Delete/Move/UpdateMonsterActionsAt)
|
||||
/// plus panel construction and profile reload — never from a column's
|
||||
/// own getter, which previously re-ran this same work (plus, for
|
||||
/// Weapon/Offhand, a live <c>CaptureOwnedItems()</c> scan) on every
|
||||
/// single frame.
|
||||
/// </summary>
|
||||
private void RefreshMonsterEditor()
|
||||
{
|
||||
int count = _combatSettings.Rules.Count;
|
||||
var fester = new bool[count];
|
||||
var broadside = new bool[count];
|
||||
var gravityWell = new bool[count];
|
||||
var imperil = new bool[count];
|
||||
var yield = new bool[count];
|
||||
var vulnerability = new bool[count];
|
||||
var attack = new bool[count];
|
||||
var ring = new bool[count];
|
||||
var streak = new bool[count];
|
||||
var weakening = new bool[count];
|
||||
var festering = new bool[count];
|
||||
var corruption = new bool[count];
|
||||
var destructive = new bool[count];
|
||||
var corrosion = new bool[count];
|
||||
var names = new string[count];
|
||||
var priorities = new string[count];
|
||||
var damage = new string[count];
|
||||
var extraVuln = new string[count];
|
||||
var weapon = new string[count];
|
||||
var offhand = new string[count];
|
||||
var petDamage = new string[count];
|
||||
var moveUpIcons = new uint[count];
|
||||
var moveDownIcons = new uint[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
MonsterRule rule = _combatSettings.Rules[i];
|
||||
MonsterRuleActions actions = rule.Actions;
|
||||
MonsterActionFlags flags = actions.Flags;
|
||||
fester[i] = (flags & MonsterActionFlags.Fester) != 0;
|
||||
broadside[i] = (flags & MonsterActionFlags.Broadside) != 0;
|
||||
gravityWell[i] = (flags & MonsterActionFlags.GravityWell) != 0;
|
||||
imperil[i] = (flags & MonsterActionFlags.Imperil) != 0;
|
||||
yield[i] = (flags & MonsterActionFlags.Yield) != 0;
|
||||
vulnerability[i] = (flags & MonsterActionFlags.Vulnerability) != 0;
|
||||
attack[i] = (flags & MonsterActionFlags.Attack) != 0;
|
||||
ring[i] = (flags & MonsterActionFlags.Ring) != 0;
|
||||
streak[i] = (flags & MonsterActionFlags.Streak) != 0;
|
||||
weakening[i] = (flags & MonsterActionFlags.WeakeningCurse) != 0;
|
||||
festering[i] = (flags & MonsterActionFlags.FesteringCurse) != 0;
|
||||
corruption[i] = (flags & MonsterActionFlags.Corruption) != 0;
|
||||
destructive[i] = (flags & MonsterActionFlags.DestructiveCurse) != 0;
|
||||
corrosion[i] = (flags & MonsterActionFlags.Corrosion) != 0;
|
||||
names[i] = rule.Expression;
|
||||
priorities[i] = actions.BoundedPriority.ToString(CultureInfo.InvariantCulture);
|
||||
damage[i] = DamageTypeDisplay(actions.DamageType);
|
||||
extraVuln[i] = DamageTypeDisplay(actions.ExtraVulnerability);
|
||||
weapon[i] = ItemDisplayName(actions.WeaponObjectId, actions.WeaponName);
|
||||
offhand[i] = ItemDisplayName(actions.OffhandObjectId, actions.OffhandName);
|
||||
petDamage[i] = DamageTypeDisplay(actions.PetDamageType);
|
||||
moveUpIcons[i] = 0x060028FCu;
|
||||
moveDownIcons[i] = 0x060028FDu;
|
||||
}
|
||||
_monsterFesterColumn = fester;
|
||||
_monsterBroadsideColumn = broadside;
|
||||
_monsterGravityWellColumn = gravityWell;
|
||||
_monsterImperilColumn = imperil;
|
||||
_monsterYieldColumn = yield;
|
||||
_monsterVulnerabilityColumn = vulnerability;
|
||||
_monsterAttackColumn = attack;
|
||||
_monsterRingColumn = ring;
|
||||
_monsterStreakColumn = streak;
|
||||
_monsterWeakeningColumn = weakening;
|
||||
_monsterFesteringColumn = festering;
|
||||
_monsterCorruptionColumn = corruption;
|
||||
_monsterDestructiveColumn = destructive;
|
||||
_monsterCorrosionColumn = corrosion;
|
||||
_monsterNameColumn = names;
|
||||
_monsterPriorityColumn = priorities;
|
||||
_monsterDamageColumn = damage;
|
||||
_monsterExtraVulnColumn = extraVuln;
|
||||
_monsterWeaponColumn = weapon;
|
||||
_monsterOffhandColumn = offhand;
|
||||
_monsterPetDamageColumn = petDamage;
|
||||
_monsterMoveUpIconsColumn = moveUpIcons;
|
||||
_monsterMoveDownIconsColumn = moveDownIcons;
|
||||
}
|
||||
|
||||
private void ToggleMonsterFlagAt(int row, MonsterActionFlags flag) =>
|
||||
UpdateMonsterActionsAt(row, actions => actions with { Flags = actions.Flags ^ flag });
|
||||
|
|
@ -2515,6 +2634,7 @@ internal sealed partial class MossTankPanel
|
|||
var rule = new MonsterRule(expression, new MonsterRuleActions());
|
||||
_combatSettings.Rules.Add(rule);
|
||||
_monsterEditorNotice = $"Added {expression}.";
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
catch (FormatException error)
|
||||
|
|
@ -2575,6 +2695,7 @@ internal sealed partial class MossTankPanel
|
|||
_combatSettings.Rules.RemoveAt(row);
|
||||
_monsterEditorNotice = $"Removed {rule.Expression}.";
|
||||
EnsureDefaultMonsterRule();
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
|
|
@ -2690,6 +2811,7 @@ internal sealed partial class MossTankPanel
|
|||
_combatSettings.Rules.RemoveAt(row);
|
||||
_combatSettings.Rules.Insert(destination, current);
|
||||
_monsterEditorNotice = $"Moved {current.Expression}.";
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
|
|
@ -2700,6 +2822,7 @@ internal sealed partial class MossTankPanel
|
|||
return;
|
||||
MonsterRule current = _combatSettings.Rules[row];
|
||||
_combatSettings.Rules[row] = new MonsterRule(current.Expression, update(current.Actions));
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
|
|
@ -2807,9 +2930,34 @@ internal sealed partial class MossTankPanel
|
|||
|
||||
private void RefreshMetaEditor()
|
||||
{
|
||||
_metaRows = _metaProfile.Rules.Select(static rule =>
|
||||
$"{rule.State,-16} {DescribeMetaCondition(rule.Condition),-34} "
|
||||
+ DescribeMetaAction(rule.Action)).ToArray();
|
||||
int count = _metaProfile.Rules.Count;
|
||||
var rows = new string[count];
|
||||
var deleteColumn = new string[count];
|
||||
var moveUpIcons = new uint[count];
|
||||
var moveDownIcons = new uint[count];
|
||||
var stateColumn = new string[count];
|
||||
var conditionColumn = new string[count];
|
||||
var actionColumn = new string[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
MetaRule rule = _metaProfile.Rules[i];
|
||||
string conditionText = DescribeMetaCondition(rule.Condition);
|
||||
string actionText = DescribeMetaAction(rule.Action);
|
||||
rows[i] = $"{rule.State,-16} {conditionText,-34} {actionText}";
|
||||
deleteColumn[i] = "X";
|
||||
moveUpIcons[i] = 0x060028FCu;
|
||||
moveDownIcons[i] = 0x060028FDu;
|
||||
stateColumn[i] = rule.State;
|
||||
conditionColumn[i] = conditionText;
|
||||
actionColumn[i] = actionText;
|
||||
}
|
||||
_metaRows = rows;
|
||||
_metaDeleteColumn = deleteColumn;
|
||||
_metaMoveUpIconsColumn = moveUpIcons;
|
||||
_metaMoveDownIconsColumn = moveDownIcons;
|
||||
_metaStateColumn = stateColumn;
|
||||
_metaConditionColumn = conditionColumn;
|
||||
_metaActionColumn = actionColumn;
|
||||
_selectedMetaRule = ClampRow(_selectedMetaRule, _metaProfile.Rules.Count);
|
||||
if (SelectedMetaRule is not MetaRule selected)
|
||||
return;
|
||||
|
|
@ -4111,6 +4259,7 @@ internal sealed partial class MossTankPanel
|
|||
_coverageSpellSnapshot = null;
|
||||
_coverageRefreshRemaining = 0d;
|
||||
EnsureDefaultMonsterRule();
|
||||
RefreshMonsterEditor();
|
||||
RefreshItemEditors();
|
||||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,34 @@ public sealed class MossTankPanelTests
|
|||
Assert.Empty(panel.ExcludedComponentRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExcludedComponentIconsDoesNotScanLiveInventoryOnEveryRead()
|
||||
{
|
||||
// Fix round B item 12: ExcludedComponentIcons used to call
|
||||
// ResolveComponentIcon (CaptureOwnedItems(), a live inventory scan)
|
||||
// once per row on every retained-UI draw. It must now be a plain
|
||||
// field materialized once in RefreshItemEditors.
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
ItemEntries =
|
||||
[
|
||||
Item(20, "Charged Yellow Scarab", 0x20) with { IconId = 0x06001234u },
|
||||
],
|
||||
};
|
||||
var host = new FakeHost(automation);
|
||||
var panel = new MossTankPanel(host);
|
||||
host.Selection.Select(20);
|
||||
panel.AddSelectedComponent();
|
||||
Assert.Equal(0x06001234u, panel.ExcludedComponentIcons[0]);
|
||||
|
||||
int callsAfterAdd = automation.CaptureOwnedItemsCallCount;
|
||||
// Simulate several retained-UI draw frames reading the same column.
|
||||
for (int i = 0; i < 5; i++)
|
||||
_ = panel.ExcludedComponentIcons;
|
||||
|
||||
Assert.Equal(callsAfterAdd, automation.CaptureOwnedItemsCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuffPickerAddsToTheRequestedListAndAnyCellClickDeletes()
|
||||
{
|
||||
|
|
@ -2087,6 +2115,43 @@ public sealed class MossTankPanelTests
|
|||
Assert.Equal("Fire Sword", second.MonsterWeaponColumn[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonsterGridColumnsDoNotScanLiveInventoryOrAllocateOnEveryRead()
|
||||
{
|
||||
// Fix round B item 12: every Monsters-grid column getter used to
|
||||
// re-run a LINQ .Select(...).ToArray() over _combatSettings.Rules
|
||||
// on every retained-UI draw; Weapon/Offhand additionally called
|
||||
// ItemDisplayName (CaptureOwnedItems(), a live inventory scan) once
|
||||
// per row per frame. All 23 columns are now materialized once in
|
||||
// RefreshMonsterEditor: repeated reads must not re-scan inventory,
|
||||
// and must return the SAME array instance (proof of no fresh
|
||||
// allocation per read).
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
Name = "Perf Check",
|
||||
ItemEntries = [Item(10, "Fire Sword", 1)],
|
||||
};
|
||||
var host = new FakeHost(automation);
|
||||
var panel = new MossTankPanel(host);
|
||||
host.Selection.Select(10);
|
||||
panel.AddSelectedItem();
|
||||
panel.CycleMonsterWeaponAt(0); // populates the Weapon column with a real name
|
||||
|
||||
int callsAfterCycle = automation.CaptureOwnedItemsCallCount;
|
||||
IReadOnlyList<string> weaponFirstRead = panel.MonsterWeaponColumn;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_ = panel.MonsterFesterColumn;
|
||||
_ = panel.MonsterNameColumn;
|
||||
_ = panel.MonsterWeaponColumn;
|
||||
_ = panel.MonsterOffhandColumn;
|
||||
_ = panel.MonsterMoveUpIcons;
|
||||
}
|
||||
|
||||
Assert.Equal(callsAfterCycle, automation.CaptureOwnedItemsCallCount);
|
||||
Assert.Same(weaponFirstRead, panel.MonsterWeaponColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RouteGridDeletesByAnyCellClickAndSelectsNearestByDistance()
|
||||
{
|
||||
|
|
@ -2230,6 +2295,33 @@ public sealed class MossTankPanelTests
|
|||
Assert.Equal(["Hunt"], panel.MetaStateColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetaAndRouteGridColumnsDoNotReallocateOnEveryRead()
|
||||
{
|
||||
// Fix round B item 12: MetaStateColumn/MetaConditionColumn/
|
||||
// MetaActionColumn/MetaDeleteColumn/MetaMoveUpIcons/
|
||||
// MetaMoveDownIcons and RouteWaypointCountColumn used to rebuild a
|
||||
// fresh array with LINQ on every retained-UI draw. All are now
|
||||
// materialized once in RefreshMetaEditor/RefreshRouteEditor:
|
||||
// repeated reads must return the SAME array instance.
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = NavigationAt(0f),
|
||||
}));
|
||||
panel.SelectMetaAction(nameof(MetaActionKind.ChatCommand));
|
||||
panel.SetMetaActionTextDraft("/mt one");
|
||||
panel.AddMetaRule();
|
||||
panel.AddRoutePoint();
|
||||
|
||||
Assert.Same(panel.MetaStateColumn, panel.MetaStateColumn);
|
||||
Assert.Same(panel.MetaConditionColumn, panel.MetaConditionColumn);
|
||||
Assert.Same(panel.MetaActionColumn, panel.MetaActionColumn);
|
||||
Assert.Same(panel.MetaDeleteColumn, panel.MetaDeleteColumn);
|
||||
Assert.Same(panel.MetaMoveUpIcons, panel.MetaMoveUpIcons);
|
||||
Assert.Same(panel.MetaMoveDownIcons, panel.MetaMoveDownIcons);
|
||||
Assert.Same(panel.RouteWaypointCountColumn, panel.RouteWaypointCountColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetaEditorPopupOpensOnCellClickOrCreateAndClosesOnApplyOrCancel()
|
||||
{
|
||||
|
|
@ -2992,7 +3084,15 @@ public sealed class MossTankPanelTests
|
|||
public int BusyReferences { get; set; }
|
||||
public PluginNavigationSnapshot NavigationSnapshot { get; set; }
|
||||
public PluginNavigationSnapshot Snapshot => NavigationSnapshot;
|
||||
public IReadOnlyList<PluginInventoryItem> CaptureOwnedItems() => ItemEntries;
|
||||
// Fix round B item 12: counts CaptureOwnedItems calls so a test can
|
||||
// prove a column getter is a plain field read (no live inventory
|
||||
// scan) rather than per-row/per-frame work.
|
||||
public int CaptureOwnedItemsCallCount { get; private set; }
|
||||
public IReadOnlyList<PluginInventoryItem> CaptureOwnedItems()
|
||||
{
|
||||
CaptureOwnedItemsCallCount++;
|
||||
return ItemEntries;
|
||||
}
|
||||
public IReadOnlyList<PluginWorldObject> CaptureObjects() => WorldObjects;
|
||||
|
||||
bool IWorldObjectAutomation.TryGet(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue