diff --git a/src/AcDream.App/UI/Layout/IndicatorBarController.cs b/src/AcDream.App/UI/Layout/IndicatorBarController.cs
index 6f4becf7..f0e4d4fe 100644
--- a/src/AcDream.App/UI/Layout/IndicatorBarController.cs
+++ b/src/AcDream.App/UI/Layout/IndicatorBarController.cs
@@ -30,6 +30,14 @@ public sealed class IndicatorBarController : IRetainedPanelController
public const uint LinkButtonId = 0x100000F8u;
public const uint EndCharacterSessionButtonId = 0x100000FAu;
+ ///
+ /// The press-highlight overlay every indicator button authors as its own
+ /// child: a full-size (20x20) Type-3 element whose DirectState carries a
+ /// draw-nothing File=0 image and whose only other state, Normal_pressed,
+ /// carries the green selector sprite 0x06004CE8.
+ ///
+ public const uint PressHighlightId = 0x100000F2u;
+
public const uint UnencumberedState = 14u;
public const uint EncumberedState = 15u;
public const uint HeavilyEncumberedState = 16u;
@@ -64,6 +72,65 @@ public sealed class IndicatorBarController : IRetainedPanelController
Disconnected = 4,
}
+ ///
+ /// Re-attaches the green press-highlight each indicator button authors as a
+ /// child.
+ ///
+ ///
+ ///
+ /// drops a button's dat children
+ /// at import — correct for caption/face art, which the button draws itself,
+ /// but this child is neither. It is a real overlay carrying its own media,
+ /// so swallowing it loses the press feedback entirely: the buttons author
+ /// Normal_pressed with PassToChildren, and the cascade then
+ /// has nothing left to reach.
+ ///
+ ///
+ /// Same class of loss, and the same repair, as the map hotspot's
+ /// rollover-highlight child — see 's
+ /// BuildTownMarkers. Attaching is safe because the child's DirectState
+ /// authors a File=0 draw-nothing image, so it is invisible until the
+ /// button's own press state cascades into it.
+ ///
+ ///
+ public void AttachPressHighlights(
+ ElementInfo layoutRoot, Func build)
+ {
+ foreach ((UiButton button, uint id) in Indicators())
+ {
+ if (FindInfo(layoutRoot, id) is not { } info)
+ continue;
+ foreach (ElementInfo child in info.Children)
+ {
+ if (child.Id != PressHighlightId)
+ continue;
+ if (build(child) is { } highlight)
+ button.AddChild(highlight);
+ }
+ }
+ }
+
+ private (UiButton Button, uint Id)[] Indicators() =>
+ [
+ (_link, LinkButtonId),
+ (_helpful, HelpfulButtonId),
+ (_harmful, HarmfulButtonId),
+ (_vitae, VitaeButtonId),
+ (_burden, BurdenButtonId),
+ (_miniGame, MiniGameButtonId),
+ (_endCharacterSession, EndCharacterSessionButtonId),
+ ];
+
+ private static ElementInfo? FindInfo(ElementInfo root, uint id)
+ {
+ if (root.Id == id)
+ return root;
+ foreach (ElementInfo child in root.Children)
+ if (FindInfo(child, id) is { } found)
+ return found;
+ return null;
+ }
+
private IndicatorBarController(
IndicatorBarBindings bindings,
UiButton link,
diff --git a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs
index 7a408d8d..a4f52b15 100644
--- a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs
+++ b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs
@@ -57,6 +57,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
private readonly Action _addFavorite;
private readonly Action _sendFilter;
private readonly Action _removeSpell;
+ private readonly Action? _examineSpell;
private readonly Action> _showConfirmation;
private readonly Action _setDesiredComponent;
private readonly Action _close;
@@ -93,6 +94,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
Action addFavorite,
Action sendFilter,
Action removeSpell,
+ Action? examineSpell,
Action> showConfirmation,
Action setDesiredComponent,
Action close,
@@ -120,6 +122,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
_addFavorite = addFavorite;
_sendFilter = sendFilter;
_removeSpell = removeSpell;
+ _examineSpell = examineSpell;
_showConfirmation = showConfirmation;
_setDesiredComponent = setDesiredComponent;
_close = close;
@@ -177,6 +180,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
Action addFavorite,
Action sendFilter,
Action removeSpell,
+ Action? examineSpell,
Action> showConfirmation,
Action setDesiredComponent,
Action close,
@@ -213,7 +217,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
layout, spellbook, objects, playerGuid, components, selection,
resolveSpellIcon, resolveComponentIcon,
spellLevel, selectObject,
- addFavorite, sendFilter, removeSpell, showConfirmation,
+ addFavorite, sendFilter, removeSpell, examineSpell, showConfirmation,
setDesiredComponent, close,
spellPage, componentPage, spellTab, componentTab, closeButton,
deleteButton, spellList, componentList, componentTemplates,
@@ -231,6 +235,10 @@ public sealed class SpellbookWindowController : IRetainedPanelController
private void ConfigureSpellList(ImportedLayout layout)
{
+ // Right-click a spell row to assess it, into the same examination window
+ // an item or creature uses: retail's UIElement_ItemList right-click
+ // branch @ 0x004E4F1F -> ClientUISystem::ExamineSpell @ 0x00564A70.
+ _spellList.ExamineCatalogEntryRequested = _examineSpell;
_spellList.Columns = 1;
_spellList.CellWidth = Math.Max(1f, _spellList.Width);
_spellList.CellHeight = _rowStyle.Height;
diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs
index c7ffb0ff..9e7e9db9 100644
--- a/src/AcDream.App/UI/RetailUiRuntime.cs
+++ b/src/AcDream.App/UI/RetailUiRuntime.cs
@@ -1267,6 +1267,22 @@ public sealed class RetailUiRuntime : IDisposable
return windowId != 0;
}
+ ///
+ /// Builds one dat child that a behavioral widget's
+ /// dropped at import, so a
+ /// controller can re-attach it. Same seam as the map hotspots' AD-108
+ /// icon/highlight rebuild.
+ ///
+ private UiElement? BuildSwallowedChild(ElementInfo info)
+ {
+ lock (_bindings.Assets.DatLock)
+ return LayoutImporter.Build(
+ info,
+ _bindings.Assets.ResolveSprite,
+ _bindings.Assets.DefaultFont,
+ _bindings.Assets.ResolveFont).Root;
+ }
+
private ImportedLayout? Import(uint layoutId)
{
lock (_bindings.Assets.DatLock)
@@ -1921,6 +1937,7 @@ public sealed class RetailUiRuntime : IDisposable
spellId => SpellcastingUiController?.AddFavorite(spellId),
_bindings.Magic.SendSpellbookFilter,
_bindings.Magic.RemoveSpell,
+ spellId => AppraisalController?.ExamineSpell(spellId),
(message, completed) => ShowConfirmation(message, completed),
_bindings.Magic.SetDesiredComponent,
() => CloseWindow(WindowNames.Spellbook),
@@ -2434,6 +2451,17 @@ public sealed class RetailUiRuntime : IDisposable
}
IndicatorBarController = controller;
+
+ // Restore the green press selector the buttons author as swallowed
+ // children (see IndicatorBarController.AttachPressHighlights).
+ lock (_bindings.Assets.DatLock)
+ {
+ ElementInfo? infos = LayoutImporter.ImportInfos(
+ _bindings.Assets.Dats, IndicatorBarController.LayoutId);
+ if (infos is not null)
+ controller.AttachPressHighlights(infos, BuildSwallowedChild);
+ }
+
RetailWindowFrame.Mount(
Host.Root,
layout.Root,
diff --git a/src/AcDream.App/UI/UiCatalogSlot.cs b/src/AcDream.App/UI/UiCatalogSlot.cs
index 61a5befd..0220af8b 100644
--- a/src/AcDream.App/UI/UiCatalogSlot.cs
+++ b/src/AcDream.App/UI/UiCatalogSlot.cs
@@ -93,9 +93,18 @@ public sealed class UiCatalogSlot : UiItemSlot
DoubleClicked?.Invoke();
return true;
case UiEventType.RightClick:
- if (EntryId != 0u
- && FindList() is { ExamineCatalogEntryRequested: { } examine })
- examine(EntryId);
+ // Retail examines from the LIST, generically:
+ // UIElement_ItemList::ListenToElementMessage @ 0x004E4ECA takes
+ // the right-click branch and calls ExamineObject for a row with
+ // an itemID, ExamineSpell for one with a spellID.
+ //
+ // Returning true unconditionally here swallowed the event on any
+ // list that had not wired the seam -- the spellbook among them --
+ // so report it unhandled instead and let UiRoot keep bubbling.
+ if (EntryId == 0u
+ || FindList() is not { ExamineCatalogEntryRequested: { } examine })
+ return false;
+ examine(EntryId);
return true;
case UiEventType.DragBegin:
if (e.Payload is not null) DragBegan?.Invoke(e.Payload);
diff --git a/src/AcDream.Plugins.MossTank/BuffPlan.cs b/src/AcDream.Plugins.MossTank/BuffPlan.cs
index 235428f3..e9f7b13d 100644
--- a/src/AcDream.Plugins.MossTank/BuffPlan.cs
+++ b/src/AcDream.Plugins.MossTank/BuffPlan.cs
@@ -40,9 +40,15 @@ public sealed class BuffSettings
public bool BuffBanes { get; set; } = true;
///
- /// Anything else self-targeted with a duration (regeneration and friends).
- /// Off by default: useful to some characters, wasted mana for others, and
- /// it is the bucket anything unrecognised falls into.
+ /// The vital regeneration rates — Regeneration (health), Rejuvenation
+ /// (stamina), Mana Renewal (mana). Cast last, as the tail of a pass.
+ ///
+ public bool BuffRegeneration { get; set; } = true;
+
+ ///
+ /// Anything else self-targeted with a duration. Off by default: useful to
+ /// some characters, wasted mana for others, and it is the bucket anything
+ /// unrecognised falls into.
///
public bool BuffOther { get; set; }
@@ -122,6 +128,7 @@ public static class BuffPlan
BuffTargetKind.Protection => settings.BuffProtections,
BuffTargetKind.Aura => settings.BuffAuras,
BuffTargetKind.Bane => settings.BuffBanes,
+ BuffTargetKind.Regeneration => settings.BuffRegeneration,
BuffTargetKind.Other => settings.BuffOther,
_ => false,
};
@@ -187,7 +194,8 @@ public static class BuffPlan
///
///
/// - Item Enchantment — the banes and weapon auras.
- /// - Life Magic last — the protections and Armor Self.
+ /// - Life Magic last — the protections and Armor Self, then
+ /// the vital regeneration rates to finish.
///
///
/// Willpower is matched as "Self". Retail's spell is named Willpower
@@ -206,11 +214,23 @@ public static class BuffPlan
_ => 3, // war/void and anything unschooled trail the rest
};
- // Only the creature group has an internal order; the other groups are
- // cast in whatever order the tiebreak gives.
- return (school * 10) + (school == 0 ? CreatureOrder(line) : 0);
+ int within = school switch
+ {
+ 0 => CreatureOrder(line),
+ 2 => LifeOrder(line),
+ _ => 0, // the item group has no internal order
+ };
+ return (school * 10) + within;
}
+ ///
+ /// Inside Life Magic the protections go first and the vital regeneration
+ /// rates finish the pass — they are the buffs that matter least if mana
+ /// runs out, and the ones a character most often wants topped up last.
+ ///
+ private static int LifeOrder(BuffLine line) =>
+ line.Kind == BuffTargetKind.Regeneration ? 1 : 0;
+
private static int CreatureOrder(BuffLine line)
{
if (line.Kind == BuffTargetKind.Skill
diff --git a/src/AcDream.Plugins.MossTank/BuffProfile.cs b/src/AcDream.Plugins.MossTank/BuffProfile.cs
index b13f3712..b8e7d326 100644
--- a/src/AcDream.Plugins.MossTank/BuffProfile.cs
+++ b/src/AcDream.Plugins.MossTank/BuffProfile.cs
@@ -27,7 +27,13 @@ public enum BuffTargetKind
/// though it is, in effect, a self buff.
///
Bane,
- /// Any other self-targeted duration buff (regeneration and friends).
+ ///
+ /// A vital regeneration rate buff: Regeneration (health), Rejuvenation
+ /// (stamina), Mana Renewal (mana), and their Empyrean/Prodigal kin. All
+ /// Life Magic, and cast at the very end of a pass.
+ ///
+ Regeneration,
+ /// Any other self-targeted duration buff.
Other,
}
@@ -84,9 +90,13 @@ public static partial class BuffProfile
/// Retail's own wording for the weapon/caster auras. Matched on the
/// description rather than the "Aura of" name prefix so the older
/// non-aura phrasings classify the same way.
+ ///
+ /// "magic casting implement's" is the wand buff Aura of Hermetic Link, and
+ /// it is the ONLY one of retail's six aura lines that none of the other
+ /// alternatives reach -- it was silently landing in Other.
///
[GeneratedRegex(
- @"\b(a weapon's|weapon or magic caster|magic caster|missile weapon's)\b",
+ @"\b(a weapon's|weapon or magic caster|magic caster|missile weapon's|magic casting implement's)\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex AuraPattern();
@@ -101,6 +111,36 @@ public static partial class BuffProfile
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex BanePattern();
+ ///
+ /// The three vital-rate lines, matched one vital at a time. Retail words
+ /// each of them differently and two of the six phrasings do not even begin
+ /// with "Increases the caster's":
+ ///
+ /// Increase caster's natural healing rate by 10%. (Regeneration)
+ /// Increases your Health Regeneration Rate by 50%. (Empyrean)
+ /// Increases the rate at which the caster regains Stamina by 10%. (Rejuvenation)
+ /// Increases your Stamina Regeneration Rate by 50%. (Empyrean)
+ /// Increases the caster's natural mana rate by 10%. (Mana Renewal)
+ /// Increases your Mana Regeneration Rate by 50%. (Empyrean)
+ ///
+ /// Note "Increase", not "Increases", in the health line — retail's own typo,
+ /// which is exactly why matching a strict sentence shape lost these.
+ ///
+ [GeneratedRegex(
+ "natural healing rate|Health Regeneration Rate",
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
+ private static partial Regex HealthRegenPattern();
+
+ [GeneratedRegex(
+ "rate at which the caster regains Stamina|Stamina Regeneration Rate",
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
+ private static partial Regex StaminaRegenPattern();
+
+ [GeneratedRegex(
+ "natural mana rate|Mana Regeneration Rate",
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
+ private static partial Regex ManaRegenPattern();
+
///
/// Retail's spell text says "Assess Monster" where the skill table says
/// "Assess Creature". Without this the skill silently never matches and its
@@ -178,6 +218,30 @@ public static partial class BuffProfile
return;
}
+ // Regeneration before the generic "Increases the caster's X by N" match
+ // further down, which would otherwise read Mana Renewal as a buff to a
+ // stat named "natural mana rate".
+ if (HealthRegenPattern().IsMatch(description))
+ {
+ kind = BuffTargetKind.Regeneration;
+ target = "Health";
+ return;
+ }
+
+ if (StaminaRegenPattern().IsMatch(description))
+ {
+ kind = BuffTargetKind.Regeneration;
+ target = "Stamina";
+ return;
+ }
+
+ if (ManaRegenPattern().IsMatch(description))
+ {
+ kind = BuffTargetKind.Regeneration;
+ target = "Mana";
+ return;
+ }
+
// Auras next: "Increases a weapon's damage value" would otherwise be
// read as raising something on the caster.
if (AuraPattern().IsMatch(description))
diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs
index 532e5321..30b39259 100644
--- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs
+++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs
@@ -128,6 +128,8 @@ internal sealed class MossTankPanel
$"Buff weapon auras: {OnOff(_buffSettings.BuffAuras)}";
public string BanesText =>
$"Buff banes (armor): {OnOff(_buffSettings.BuffBanes)}";
+ public string RegenerationText =>
+ $"Buff regen rates: {OnOff(_buffSettings.BuffRegeneration)}";
public string OtherText =>
$"Buff other self-spells: {OnOff(_buffSettings.BuffOther)}";
@@ -159,6 +161,8 @@ internal sealed class MossTankPanel
_buffSettings.BuffProtections = !_buffSettings.BuffProtections;
public Action ToggleAuras => () => _buffSettings.BuffAuras = !_buffSettings.BuffAuras;
public Action ToggleBanes => () => _buffSettings.BuffBanes = !_buffSettings.BuffBanes;
+ public Action ToggleRegeneration =>
+ () => _buffSettings.BuffRegeneration = !_buffSettings.BuffRegeneration;
public Action ToggleOther => () => _buffSettings.BuffOther = !_buffSettings.BuffOther;
private static double Step(double value, int direction) =>
diff --git a/src/AcDream.Plugins.MossTank/mosstank-settings.xml b/src/AcDream.Plugins.MossTank/mosstank-settings.xml
index 0e2a2317..47bf4a27 100644
--- a/src/AcDream.Plugins.MossTank/mosstank-settings.xml
+++ b/src/AcDream.Plugins.MossTank/mosstank-settings.xml
@@ -4,7 +4,7 @@
vocabulary, and the toggle is just another Action.
Adjuster buttons rather than typed fields, because editable text in a
plugin panel needs keyboard routing plumbed through first. -->
-
+
@@ -43,8 +43,11 @@
-
-
+
+
-
+
+
+
+
diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs
index 0c431349..2b637e1e 100644
--- a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs
@@ -109,6 +109,48 @@ public sealed class SpellbookWindowControllerTests
(delete.Left, delete.Top, delete.FaceWidth, delete.FaceHeight));
}
+ [Fact]
+ public void RightClickingASpellRow_AssessesIt()
+ {
+ // Retail examines from the list itself:
+ // UIElement_ItemList::ListenToElementMessage @ 0x004E4F1F takes the
+ // right-click branch and calls ClientUISystem::ExamineSpell for a row
+ // carrying a spellID, the same examination window an item or creature
+ // opens. The spellbook never wired that seam.
+ ImportedLayout layout = FixtureLoader.LoadSpellbook();
+ Spellbook book = CreateSpellbook();
+ book.OnSpellLearned(101u);
+ var examined = new List();
+ using SpellbookWindowController controller = Bind(
+ layout, book, examineSpell: examined.Add)!;
+
+ UiItemList list = Assert.IsType(
+ layout.FindElement(SpellbookWindowController.SpellListId));
+ UiCatalogSlot row = Assert.IsType(list.GetItem(0));
+
+ Assert.True(row.OnEvent(new UiEvent(0, row, UiEventType.RightClick)));
+ Assert.Equal([101u], examined);
+ }
+
+ [Fact]
+ public void RightClickWithNoExamineSeam_BubblesInsteadOfBeingSwallowed()
+ {
+ // The regression that lost the spellbook's assess: the catalog row
+ // reported EVERY right-click handled, so on any list that had not wired
+ // the seam UiRoot stopped bubbling and nothing ran. Reporting it
+ // unhandled is what lets an ancestor still act on it.
+ ImportedLayout layout = FixtureLoader.LoadSpellbook();
+ Spellbook book = CreateSpellbook();
+ book.OnSpellLearned(101u);
+ using SpellbookWindowController controller = Bind(layout, book)!;
+
+ UiItemList list = Assert.IsType(
+ layout.FindElement(SpellbookWindowController.SpellListId));
+ UiCatalogSlot row = Assert.IsType(list.GetItem(0));
+
+ Assert.False(row.OnEvent(new UiEvent(0, row, UiEventType.RightClick)));
+ }
+
[Fact]
public void LearnedSpells_AreAuthoredRows_InDisplayOrder_WithSelectionAndDragPayload()
{
@@ -394,6 +436,7 @@ public sealed class SpellbookWindowControllerTests
Action? addFavorite = null,
Action? sendFilter = null,
Action? removeSpell = null,
+ Action? examineSpell = null,
Action>? showConfirmation = null,
Action? close = null,
ClientObjectTable? objects = null,
@@ -420,6 +463,7 @@ public sealed class SpellbookWindowControllerTests
sendFilter?.Invoke(filters);
},
removeSpell ?? (_ => { }),
+ examineSpell,
showConfirmation ?? ((_, _) => { }),
(componentId, amount) =>
{
diff --git a/tests/AcDream.Plugins.MossTank.Tests/BuffPlanTests.cs b/tests/AcDream.Plugins.MossTank.Tests/BuffPlanTests.cs
index 67caab9f..7b6d68f8 100644
--- a/tests/AcDream.Plugins.MossTank.Tests/BuffPlanTests.cs
+++ b/tests/AcDream.Plugins.MossTank.Tests/BuffPlanTests.cs
@@ -433,4 +433,94 @@ public class BuffPlanTests
Assert.True(item[0].ManaCost <= item[1].ManaCost);
Assert.Equal(8u, item[0].SpellId); // the 20-mana aura before the 30-mana bane
}
+
+ // ── The vital regeneration rates ─────────────────────────────────────
+
+ [Theory]
+ // Retail writes "Increase", not "Increases", and omits "the" -- a typo in
+ // shipped data, and the reason the strict sentence shape lost this line.
+ [InlineData("Increase caster's natural healing rate by 10%.", "Health")]
+ [InlineData("Increases your Health Regeneration Rate by 50%. "
+ + "This effect can be layered with normal spell effects.", "Health")]
+ // This one does not begin with "Increases the caster's" at all.
+ [InlineData("Increases the rate at which the caster regains Stamina by 10%.", "Stamina")]
+ [InlineData("Increases your Stamina Regeneration Rate by 50%.", "Stamina")]
+ // This one DOES match the generic shape, and would have been read as a buff
+ // to a stat called "natural mana rate" had it been checked in that order.
+ [InlineData("Increases the caster's natural mana rate by 10%.", "Mana")]
+ [InlineData("Increases your Mana Regeneration Rate by 50%.", "Mana")]
+ public void RegenerationRatesAreClassifiedPerVital(string description, string vital)
+ {
+ BuffProfile.Classify(description, out var kind, out string target);
+ Assert.Equal(BuffTargetKind.Regeneration, kind);
+ Assert.Equal(vital, target);
+ }
+
+ [Fact]
+ public void HermeticLinkIsAWandAura()
+ {
+ // The only one of retail's six aura lines that none of the other
+ // alternatives reach; it was landing in Other and never being cast.
+ BuffProfile.Classify(
+ "Increases a magic casting implement's mana conversion bonus by 10%.",
+ out var kind, out _);
+ Assert.Equal(BuffTargetKind.Aura, kind);
+ }
+
+ [Fact]
+ public void RegenerationRatesFinishThePass()
+ {
+ List book = Lines(
+ Spell(1, 201, 1, "Increases the caster's Focus by 10 points.",
+ mana: 10, school: CreatureEnchantmentSkill),
+ Spell(2, 202, 1, "Reduces damage the caster takes from Fire by 9%.",
+ mana: 10, school: LifeMagicSkill),
+ Spell(3, 203, 1, "Increases the caster's natural armor by 20 points.",
+ mana: 10, school: LifeMagicSkill),
+ // The three regen lines, deliberately the CHEAPEST in the book.
+ Spell(4, 204, 1, "Increase caster's natural healing rate by 10%.",
+ mana: 1, school: LifeMagicSkill),
+ Spell(5, 205, 1, "Increases the rate at which the caster regains Stamina by 10%.",
+ mana: 1, school: LifeMagicSkill),
+ Spell(6, 206, 1, "Increases the caster's natural mana rate by 10%.",
+ mana: 1, school: LifeMagicSkill));
+
+ List plan = BuffPlan.Build(
+ book,
+ new[]
+ {
+ Skill(CreatureEnchantmentSkill, "Creature Enchantment",
+ PluginSkillTraining.Specialized),
+ Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained),
+ },
+ new[] { Attribute(4, "Focus") },
+ Array.Empty(),
+ Default,
+ force: true);
+
+ // All six are cast -- regen is on by default...
+ Assert.Equal(6, plan.Count);
+ // ...the protections precede them...
+ Assert.Equal(new uint[] { 2, 3 }, plan.Skip(1).Take(2).Select(s => s.SpellId));
+ // ...and the three regen lines are the tail, despite being cheapest.
+ Assert.Equal(
+ new uint[] { 4, 5, 6 },
+ plan.TakeLast(3).Select(s => s.SpellId).OrderBy(id => id));
+ }
+
+ [Fact]
+ public void RegenerationRatesCanBeSwitchedOff()
+ {
+ List book = Lines(
+ Spell(1, 301, 1, "Increase caster's natural healing rate by 10%.",
+ school: LifeMagicSkill));
+ var off = new BuffSettings { BuffRegeneration = false };
+
+ Assert.Single(BuffPlan.Build(book, Array.Empty(),
+ Array.Empty(),
+ Array.Empty(), Default, force: true));
+ Assert.Empty(BuffPlan.Build(book, Array.Empty(),
+ Array.Empty(),
+ Array.Empty(), off, force: true));
+ }
}
diff --git a/tools/LayoutDump/LayoutDump.csproj b/tools/LayoutDump/LayoutDump.csproj
new file mode 100644
index 00000000..37d9a6a1
--- /dev/null
+++ b/tools/LayoutDump/LayoutDump.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ LayoutDump
+
+
+
+
+
+
+
diff --git a/tools/LayoutDump/Program.cs b/tools/LayoutDump/Program.cs
new file mode 100644
index 00000000..fe7a2a1b
--- /dev/null
+++ b/tools/LayoutDump/Program.cs
@@ -0,0 +1,70 @@
+// Print the AUTHORED geometry and state set of a retail LayoutDesc element
+// tree, straight from the installed DATs.
+//
+// Layout questions -- "is this scrollbar where retail put it?", "does this
+// button even have a pressed state?" -- were being answered by reading our own
+// importer and inferring. This reads the authored truth instead, which is the
+// only thing either question is actually about.
+//
+// dotnet run --project tools/LayoutDump -- 0x21000071
+// dotnet run --project tools/LayoutDump -- 0x2100002F 0x1000018E --states
+using AcDream.App.UI.Layout;
+using AcDream.Content;
+using DatReaderWriter;
+using DatReaderWriter.Options;
+using SysEnv = System.Environment;
+
+if (args.Length == 0)
+{
+ Console.WriteLine("usage: LayoutDump [rootElementId] [--states]");
+ return 1;
+}
+
+bool showStates = args.Contains("--states");
+uint[] ids = args.Where(a => !a.StartsWith("--"))
+ .Select(a => Convert.ToUInt32(a, a.StartsWith("0x") ? 16 : 10))
+ .ToArray();
+
+string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
+ ?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
+ "Documents", "Asheron's Call");
+using var dats = new DatCollection(datDir, DatAccessType.Read);
+using var adapter = new DatCollectionAdapter(dats);
+
+ElementInfo? root = ids.Length > 1
+ ? LayoutImporter.ImportInfos(adapter, ids[0], ids[1])
+ : LayoutImporter.ImportInfos(adapter, ids[0]);
+if (root is null)
+{
+ Console.WriteLine($"layout 0x{ids[0]:X8} not found (or root 0x{(ids.Length > 1 ? ids[1] : 0):X8} missing)");
+ return 2;
+}
+
+Console.WriteLine($"layout 0x{ids[0]:X8}");
+Print(root, 0);
+return 0;
+
+void Print(ElementInfo e, int depth)
+{
+ string pad = new(' ', depth * 2);
+ Console.WriteLine(
+ $"{pad}0x{e.Id:X8} type={e.Type,-10} "
+ + $"x={e.X,6:0.#} y={e.Y,6:0.#} w={e.Width,6:0.#} h={e.Height,6:0.#} "
+ + $"edges=L{e.Left}/T{e.Top}/R{e.Right}/B{e.Bottom} "
+ + $"parent={(e.HasOriginalParentSize ? $"{e.OriginalParentWidth:0.#}x{e.OriginalParentHeight:0.#}" : "-")} "
+ + $"z={e.ZLevel} order={e.ReadOrder}");
+
+ if (showStates && e.States.Count != 0)
+ {
+ string names = string.Join(", ", e.States
+ .OrderBy(kv => kv.Key)
+ .Select(kv => $"{kv.Key}{(kv.Value.Name.Length != 0 ? $":{kv.Value.Name}" : "")}"
+ + $"[pass={kv.Value.PassToChildren}"
+ + $" img={(kv.Value.Image is { } m ? $"0x{m.File:X8}" : "-")}"
+ + $" media={kv.Value.MediaCount}/{kv.Value.ImageMediaCount}]"));
+ Console.WriteLine($"{pad} states(default={e.DefaultStateId}): {names}");
+ }
+
+ foreach (ElementInfo child in e.Children)
+ Print(child, depth + 1);
+}
diff --git a/tools/LayoutDump/packages.neutral.lock.json b/tools/LayoutDump/packages.neutral.lock.json
new file mode 100644
index 00000000..1be3a088
--- /dev/null
+++ b/tools/LayoutDump/packages.neutral.lock.json
@@ -0,0 +1,560 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Chorizite.DatReaderWriter": {
+ "type": "Direct",
+ "requested": "[2.1.7, )",
+ "resolved": "2.1.7",
+ "contentHash": "6CpUhfHDV/O+lNx0xUZUcXK7wGkEkHYCnGHFtD8BBeAv4i1BuaNfTID+VQoSzx2EtmOVM0A2O/9wRFibApz6rQ==",
+ "dependencies": {
+ "DotNet.Standard.Common": "2.0.1",
+ "ZLibDotNet": "0.1.1"
+ }
+ },
+ "Arch.LowLevel": {
+ "type": "Transitive",
+ "resolved": "1.1.5",
+ "contentHash": "kc6T2qAsgzdb5RV+RIqVQ33LROhgXmA5fZRUhXRK+AJ1J58NML1i2M6XTKIj1ISirCwuqgYnM4qYkhqGcngG1g==",
+ "dependencies": {
+ "CommunityToolkit.HighPerformance": "8.2.2"
+ }
+ },
+ "Autofac": {
+ "type": "Transitive",
+ "resolved": "8.4.0",
+ "contentHash": "XMWHyO6fXTv8rwCfhm6+64mQS6CyL0rve/hWSODsUrVuEGtq1fjxSOlVTBqCRsW6L8K3OQDskJaPB1boVMI2eQ=="
+ },
+ "Chorizite.ACProtocol": {
+ "type": "Transitive",
+ "resolved": "1.0.1",
+ "contentHash": "PVDw/KRu4WPxT+2MzHwOQ9UFqYlOgpIRswSnco/EgHSHnbQyxOqxwiOwSK0Il+cI6dTSXTW34QNmL7iH0lXLKw==",
+ "dependencies": {
+ "Chorizite.Common": "1.0.0",
+ "Medo.PcapRW": "1.2.0",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
+ "System.CodeDom": "9.0.0"
+ }
+ },
+ "Chorizite.Common": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "KqI0su7UY2diiSQuq11gF/NztqR6orZLr/e5UKTQ91XM8OsZGgBGHmhf2/jopX9VhwpXOTTLIeFBYTBc30cK8w==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0"
+ }
+ },
+ "Collections.Pooled": {
+ "type": "Transitive",
+ "resolved": "2.0.0-preview.27",
+ "contentHash": "VS3uHc1GNamanS1i1hQ3PoZUddIagCswVMWvucAgqWwY2KVwgL2Q7raGu0hTqP/CWuROoq0RiNbIvu4ST1bMzg=="
+ },
+ "CommunityToolkit.HighPerformance": {
+ "type": "Transitive",
+ "resolved": "8.4.0",
+ "contentHash": "flxspiBs0G/0GMp7IK2J2ijV9bTG6hEwFc/z6ekHqB6nwRJ4Ry2yLdx+TkbCUYFCl4XhABkAwomeKbT6zM2Zlg=="
+ },
+ "Cyotek.Drawing.BitmapFont": {
+ "type": "Transitive",
+ "resolved": "2.0.4",
+ "contentHash": "iA6WehGVdMUuNbfsQQDq/Bt+mMd/OqHjiMUtKFLIQd/0pyYh4ehT7FEjTxN9/4OXNKQZsp9bAJgltP2nnswUJg=="
+ },
+ "DotNet.Standard.Common": {
+ "type": "Transitive",
+ "resolved": "2.0.1",
+ "contentHash": "zW0m0ytHi43ccbEOTNDa10cDDnT7BAzY1R1Rb1dlhbdkiyglsALjrsyTPSEjbdnTmCOAvAvl4kkbvBLoYhC6dQ=="
+ },
+ "FontStashSharp": {
+ "type": "Transitive",
+ "resolved": "1.3.10",
+ "contentHash": "7JTrihTt3DR8LYbb4L1eZcnbwOUOu/mvY+PJoZ3WVWiKjA6xNUk93GSW3OC/kZBD3iYzrXK8QlmKyYY5Lef/Rg==",
+ "dependencies": {
+ "Cyotek.Drawing.BitmapFont": "2.0.4",
+ "FontStashSharp.Base": "1.1.9",
+ "FontStashSharp.Rasterizers.StbTrueTypeSharp": "1.1.9",
+ "StbImageSharp": "2.30.15"
+ }
+ },
+ "FontStashSharp.Base": {
+ "type": "Transitive",
+ "resolved": "1.1.9",
+ "contentHash": "/AjkOcPNijs8vyNgcCj3FfBJbVWmsSH744hqkhLfBt8qspDz/tEoD+U09my5u9eRBX6zX+RLQ/gdCvwy+ZBOtg=="
+ },
+ "FontStashSharp.Rasterizers.StbTrueTypeSharp": {
+ "type": "Transitive",
+ "resolved": "1.1.9",
+ "contentHash": "yi5iuTERem46uyHC5p+jRi3Jh8dKWzgNWLqcvHciGlyVHD1cWFdERgnxshZU4xWB2hRGnogxaCudCirbMpg4eQ==",
+ "dependencies": {
+ "FontStashSharp.Base": "1.1.9",
+ "StbTrueTypeSharp": "1.26.12"
+ }
+ },
+ "Medo.PcapRW": {
+ "type": "Transitive",
+ "resolved": "1.2.0",
+ "contentHash": "vgwcHDg60Q9LJfry7twA78pUFio1P4EypI2IIlk+7mEuySsIRInm+Gx2OINDICyocbuyQZdS/zABjbmejAObeg=="
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
+ },
+ "Microsoft.Diagnostics.NETCore.Client": {
+ "type": "Transitive",
+ "resolved": "0.2.410101",
+ "contentHash": "I4hMjlbPcM5R+M4ThD2Zt1z58M8uZnWkDbFLXHntOOAajajEucrw4XYNSaoi5rgoqksgxQ3g388Vof4QzUNwdQ==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "1.1.0",
+ "Microsoft.Extensions.Logging": "2.1.1"
+ }
+ },
+ "Microsoft.Diagnostics.Runtime": {
+ "type": "Transitive",
+ "resolved": "3.1.512801",
+ "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==",
+ "dependencies": {
+ "Microsoft.Diagnostics.NETCore.Client": "0.2.410101"
+ }
+ },
+ "Microsoft.DotNet.PlatformAbstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.6",
+ "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "LjVKO6P2y52c5ZhTLX/w8zc5H4Y3J/LJsgqTBj49TtFq/hAtVNue/WA0F6/7GMY90xhD7K0MDZ4qpOeWXbLvzg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "VfuZJNa0WUshZ/+8BFZAhwFKiKuu/qOUCFntfdLpHj7vcRnsGHqd3G2Hse78DM+pgozczGM63lGPRLmy+uhUOA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "fcLCTS03poWE4v9tSNBr3pWn0QwGgAn1vzqHXlXgvqZeOc7LvQNzaWcKRQZTdEc3+YhQKwMsOtm3VKSA2aWQ8w==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.9",
+ "contentHash": "/hymojfWbE9AlDOa0mczR44m00Jj+T3+HZO0ZnVTI032fVycI0ZbNOVFP6kqZMcXiLSYXzR2ilcwaRi6dzeGyA=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "Transitive",
+ "resolved": "9.0.9",
+ "contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "hh+mkOAQDTp6XH80xJt3+wwYVzkbwYQl9XZRCz4Um0JjP/o7N9vHM3rZ6wwwtr+BBe/L6iBO2sz0px6OWBzqZQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "2.1.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
+ "Microsoft.Extensions.Logging.Abstractions": "2.1.1",
+ "Microsoft.Extensions.Options": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "7.0.0",
+ "contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA=="
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "V7lXCU78lAbzaulCGFKojcCyG8RTJicEbiBkPJjFqiqXwndEBBIehdXRMWEVU3UtzQ1yDvphiWUL9th6/4gJ7w==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
+ "Microsoft.Extensions.Primitives": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "scJ1GZNIxMmjpENh0UZ8XCQ6vzr/LzeF9WvEA51Ix2OQGAs9WPgPu8ABVUdvpKPLuor/t05gm6menJK3PwqOXg=="
+ },
+ "Namotion.Reflection": {
+ "type": "Transitive",
+ "resolved": "3.4.3",
+ "contentHash": "KLk2gLR9f8scM82EiL+p9TONXXPy9+IAZVMzJOA/Wsa7soZD7UJGG6j0fq0D9ZoVnBRRnSeEC7kShhRo3Olgaw=="
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.3",
+ "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
+ },
+ "NJsonSchema": {
+ "type": "Transitive",
+ "resolved": "11.5.1",
+ "contentHash": "3a7ntoBncSKkLgpIhT3uQ8BiyDzYKOHIzpzNF4o1vtKc+Re4vWxBcDXFDarOWcr/UkxZ8nxRXbbWk05j6bXFzQ==",
+ "dependencies": {
+ "NJsonSchema.Annotations": "11.5.1",
+ "Namotion.Reflection": "3.4.3",
+ "Newtonsoft.Json": "13.0.3"
+ }
+ },
+ "NJsonSchema.Annotations": {
+ "type": "Transitive",
+ "resolved": "11.5.1",
+ "contentHash": "xiqZ2DBJM1HuV+EhXgueb5ZUBlWFN3kVfLTKdtpTSxvtyQCO/vit8lqZiUiejnReUMRMIUhtS9m0GbieHZlSow=="
+ },
+ "Silk.NET.Core": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
+ "dependencies": {
+ "Microsoft.DotNet.PlatformAbstractions": "3.1.6",
+ "Microsoft.Extensions.DependencyModel": "9.0.9"
+ }
+ },
+ "Silk.NET.GLFW": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0",
+ "Ultz.Native.GLFW": "3.4.0"
+ }
+ },
+ "Silk.NET.Input.Common": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
+ "dependencies": {
+ "Silk.NET.Windowing.Common": "2.23.0"
+ }
+ },
+ "Silk.NET.Input.Glfw": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
+ "dependencies": {
+ "Silk.NET.Input.Common": "2.23.0",
+ "Silk.NET.Windowing.Glfw": "2.23.0"
+ }
+ },
+ "Silk.NET.Maths": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
+ },
+ "Silk.NET.Windowing.Common": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0",
+ "Silk.NET.Maths": "2.23.0"
+ }
+ },
+ "Silk.NET.Windowing.Glfw": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
+ "dependencies": {
+ "Silk.NET.GLFW": "2.23.0",
+ "Silk.NET.Windowing.Common": "2.23.0"
+ }
+ },
+ "SixLabors.Fonts": {
+ "type": "Transitive",
+ "resolved": "2.1.3",
+ "contentHash": "ORWbZ5BHrC/LZvo+Y09MnoJq5VUKD85LsYALk+YI7CHFra+m5arCkz00IntDM6SrAiB22bvSdKtKmuCyHOKlqg=="
+ },
+ "SixLabors.ImageSharp.Drawing": {
+ "type": "Transitive",
+ "resolved": "2.1.7",
+ "contentHash": "9KwCo9Fa350cx6ckpsy8NqXQZKwir4RQ8Kj0sdCmJA7wsK9FMyfgC527Sn4l/D6bj2ditSHlhS7dGzcgGszvSQ==",
+ "dependencies": {
+ "SixLabors.Fonts": "2.1.3",
+ "SixLabors.ImageSharp": "3.1.11"
+ }
+ },
+ "System.CodeDom": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA=="
+ },
+ "Ultz.Native.GLFW": {
+ "type": "Transitive",
+ "resolved": "3.4.0",
+ "contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
+ },
+ "ZeroAllocJobScheduler": {
+ "type": "Transitive",
+ "resolved": "1.1.2",
+ "contentHash": "PKu/zSvwV1fWxt8+CEW15O9OlQ2zB9UoeAaor/aJJ0QeTKxxSjigczYLoVPKjYLAQu3ddZQd7Z3zplVTtUgigQ=="
+ },
+ "ZLibDotNet": {
+ "type": "Transitive",
+ "resolved": "0.1.1",
+ "contentHash": "QEti4O7dwRcOb9zbnLuudSrt2IT61OYjq0R7lcJb+EzUm5N6djOVGU/cp+FZIZzJUnaFIntwrpwiuRvhpS7ZHg=="
+ },
+ "acdream.app": {
+ "type": "Project",
+ "dependencies": {
+ "AcDream.Content": "[1.0.0, )",
+ "AcDream.Core": "[1.0.0, )",
+ "AcDream.Core.Net": "[1.0.0, )",
+ "AcDream.Platform": "[1.0.0, )",
+ "AcDream.Runtime": "[1.0.0, )",
+ "AcDream.UI.Abstractions": "[1.0.0, )",
+ "Arch": "[2.1.0, )",
+ "BCnEncoder.Net.ImageSharp": "[1.1.2, )",
+ "Chorizite.Core": "[0.0.18, )",
+ "Serilog": "[4.0.2, )",
+ "Serilog.Sinks.Console": "[6.0.0, )",
+ "Silk.NET.Input": "[2.23.0, )",
+ "Silk.NET.OpenAL": "[2.23.0, )",
+ "Silk.NET.OpenAL.Extensions.Creative": "[2.23.0, )",
+ "Silk.NET.OpenAL.Extensions.EXT": "[2.23.0, )",
+ "Silk.NET.OpenAL.Soft.Native": "[1.23.1, )",
+ "Silk.NET.Vulkan": "[2.23.0, )",
+ "Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
+ "Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
+ "Silk.NET.Windowing": "[2.23.0, )",
+ "SixLabors.ImageSharp": "[3.1.12, )",
+ "StbTrueTypeSharp": "[1.26.12, )"
+ }
+ },
+ "acdream.content": {
+ "type": "Project",
+ "dependencies": {
+ "AcDream.Core": "[1.0.0, )",
+ "BCnEncoder.Net.ImageSharp": "[1.1.2, )",
+ "SixLabors.ImageSharp": "[3.1.12, )"
+ }
+ },
+ "acdream.core": {
+ "type": "Project",
+ "dependencies": {
+ "AcDream.Plugin.Abstractions": "[1.0.0, )",
+ "BCnEncoder.Net": "[2.2.1, )",
+ "Chorizite.Core": "[0.0.18, )",
+ "Chorizite.DatReaderWriter": "[2.1.7, )",
+ "Serilog": "[4.0.2, )",
+ "StbImageSharp": "[2.30.16, )"
+ }
+ },
+ "acdream.core.net": {
+ "type": "Project",
+ "dependencies": {
+ "AcDream.Core": "[1.0.0, )"
+ }
+ },
+ "acdream.platform": {
+ "type": "Project"
+ },
+ "acdream.plugin.abstractions": {
+ "type": "Project"
+ },
+ "acdream.runtime": {
+ "type": "Project",
+ "dependencies": {
+ "AcDream.Content": "[1.0.0, )",
+ "AcDream.Core": "[1.0.0, )",
+ "AcDream.Core.Net": "[1.0.0, )",
+ "AcDream.Platform": "[1.0.0, )",
+ "AcDream.Plugin.Abstractions": "[1.0.0, )"
+ }
+ },
+ "acdream.ui.abstractions": {
+ "type": "Project",
+ "dependencies": {
+ "AcDream.Core": "[1.0.0, )",
+ "AcDream.Runtime": "[1.0.0, )",
+ "Silk.NET.Input": "[2.23.0, )"
+ }
+ },
+ "Arch": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.0, )",
+ "resolved": "2.1.0",
+ "contentHash": "Z9zxQztEvD/c2tsdW+LDm5E3Rd04JKeWRa6S0JOBfEFE7800+JLa7pgMAL6I/hvOY6q93QXvQ6Y+5E/TPc5Tfw==",
+ "dependencies": {
+ "Arch.LowLevel": "1.1.5",
+ "Collections.Pooled": "2.0.0-preview.27",
+ "CommunityToolkit.HighPerformance": "8.2.2",
+ "Microsoft.Extensions.ObjectPool": "7.0.0",
+ "ZeroAllocJobScheduler": "1.1.2"
+ }
+ },
+ "BCnEncoder.Net": {
+ "type": "CentralTransitive",
+ "requested": "[2.2.1, )",
+ "resolved": "2.2.1",
+ "contentHash": "tI5+/OQo0kciLqWrViRjpOH+IL3FjexYnoWZajiGV41g/EM9CGbWsxsPzBDmpoxNkrV9uox/EtIhCIi9chBSFw==",
+ "dependencies": {
+ "CommunityToolkit.HighPerformance": "8.4.0"
+ }
+ },
+ "BCnEncoder.Net.ImageSharp": {
+ "type": "CentralTransitive",
+ "requested": "[1.1.2, )",
+ "resolved": "1.1.2",
+ "contentHash": "qUi8L+bNfHJii95BMBcV6MhBchkKU2VV6sd6D1yyzgm77YhMt+aFT0keh5uf70bvTsRrq/ZKQnE4UQScNU6XAA==",
+ "dependencies": {
+ "BCnEncoder.Net": "2.2.0",
+ "CommunityToolkit.HighPerformance": "8.4.0",
+ "SixLabors.ImageSharp": "3.1.7"
+ }
+ },
+ "Chorizite.Core": {
+ "type": "CentralTransitive",
+ "requested": "[0.0.18, )",
+ "resolved": "0.0.18",
+ "contentHash": "Pvf5idSsN0NfhZspJhrpo7QiNTmHx3mnKFkmAvHFpEFx+RnqfXh96Q+Pxam+hrqXbimmKGfk1ooIl5kpQcMo6w==",
+ "dependencies": {
+ "Autofac": "8.4.0",
+ "Chorizite.ACProtocol": "1.0.1",
+ "Chorizite.Common": "1.0.3",
+ "Chorizite.DatReaderWriter": "1.0.0",
+ "FontStashSharp": "1.3.10",
+ "Microsoft.Diagnostics.Runtime": "3.1.512801",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.9",
+ "NJsonSchema": "11.5.1",
+ "SixLabors.ImageSharp": "3.1.11",
+ "SixLabors.ImageSharp.Drawing": "2.1.7"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.9, )",
+ "resolved": "9.0.9",
+ "contentHash": "FEgpSF+Z9StMvrsSViaybOBwR0f0ZZxDm8xV5cSOFiXN/t+ys+rwAlTd/6yG7Ld1gfppgvLcMasZry3GsI9lGA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.9"
+ }
+ },
+ "Serilog": {
+ "type": "CentralTransitive",
+ "requested": "[4.0.2, )",
+ "resolved": "4.0.2",
+ "contentHash": "Vehq4uNYtURe/OnHEpWGvMgrvr5Vou7oZLdn3BuEH5FSCeHXDpNJtpzWoqywXsSvCTuiv0I65mZDRnJSeUvisA=="
+ },
+ "Serilog.Sinks.Console": {
+ "type": "CentralTransitive",
+ "requested": "[6.0.0, )",
+ "resolved": "6.0.0",
+ "contentHash": "fQGWqVMClCP2yEyTXPIinSr5c+CBGUvBybPxjAGcf7ctDhadFhrQw03Mv8rJ07/wR5PDfFjewf2LimvXCDzpbA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Silk.NET.Input": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
+ "dependencies": {
+ "Silk.NET.Input.Common": "2.23.0",
+ "Silk.NET.Input.Glfw": "2.23.0"
+ }
+ },
+ "Silk.NET.OpenAL": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "O8EaDRWGOcKniZ2VxKD/xOmugzUJz747KpqGRw3cezDsz8nD66OZW217o1rT06cG76f8eYigy/yY6eRdhRqHHQ==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0"
+ }
+ },
+ "Silk.NET.OpenAL.Extensions.Creative": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "++3+P3G2CuV4ZbD9EG81XAxsu2enofp6jFhz6GmqEf96ZrC+3COMJPgzFtjDnPswuNOFK8FWzTCucdbw1jGXBQ==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0",
+ "Silk.NET.OpenAL": "2.23.0"
+ }
+ },
+ "Silk.NET.OpenAL.Extensions.EXT": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "nhn0wv7o+RFqcxv5QTbHyEcZAf4ojqyK5aE4xoIyBubIlHBPXvV46O7w7ybo0z0ChZCBBS/teP42vaxx5YP+tQ==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0",
+ "Silk.NET.OpenAL": "2.23.0"
+ }
+ },
+ "Silk.NET.OpenAL.Soft.Native": {
+ "type": "CentralTransitive",
+ "requested": "[1.23.1, )",
+ "resolved": "1.23.1",
+ "contentHash": "gZIbksInoiXbJZmepYnTs5O6Edg5O5k86c1+Uus0zSaVnQx5WRbInGz2VTlvudbs0qAqXs/asL1YvJzco/nfAQ=="
+ },
+ "Silk.NET.Vulkan": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0"
+ }
+ },
+ "Silk.NET.Vulkan.Extensions.EXT": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0",
+ "Silk.NET.Vulkan": "2.23.0"
+ }
+ },
+ "Silk.NET.Vulkan.Extensions.KHR": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
+ "dependencies": {
+ "Silk.NET.Core": "2.23.0",
+ "Silk.NET.Vulkan": "2.23.0"
+ }
+ },
+ "Silk.NET.Windowing": {
+ "type": "CentralTransitive",
+ "requested": "[2.23.0, )",
+ "resolved": "2.23.0",
+ "contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
+ "dependencies": {
+ "Silk.NET.Windowing.Common": "2.23.0",
+ "Silk.NET.Windowing.Glfw": "2.23.0"
+ }
+ },
+ "SixLabors.ImageSharp": {
+ "type": "CentralTransitive",
+ "requested": "[3.1.12, )",
+ "resolved": "3.1.12",
+ "contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
+ },
+ "StbImageSharp": {
+ "type": "CentralTransitive",
+ "requested": "[2.30.16, )",
+ "resolved": "2.30.16",
+ "contentHash": "qg1i+NHihXVKLKYacGKrauhSQIGL31eBWcTC4Vc7jnGmBFj87LkRuCdXB5aDZisWMRnB6x2mcfwYXQxMWEG/lw=="
+ },
+ "StbTrueTypeSharp": {
+ "type": "CentralTransitive",
+ "requested": "[1.26.12, )",
+ "resolved": "1.26.12",
+ "contentHash": "hCc6/OsfcPa5VsLECcEU2m78WOshBrKwK42nAodSm9Z5wH68f7n66SoiRLCdGCkDaqbWz2TlX4zYHIjogj1HJA=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/SpellDump/Program.cs b/tools/SpellDump/Program.cs
index d6b4c6fd..355b57c6 100644
--- a/tools/SpellDump/Program.cs
+++ b/tools/SpellDump/Program.cs
@@ -55,7 +55,11 @@ if (args.Length > 0 && args[0] == "--desc")
{
if (!table.TryGet(id, out var m)) continue;
if (!m.IsSelfTargeted || !m.IsBeneficial || m.IsDebuff) continue;
- if (m.Generation != 1) continue; // one representative per line
+ // One representative per line by default; a name filter lifts that so
+ // the higher tiers' wording can be checked (they do not always match).
+ string descFilter = args.Length > 1 ? args[1].ToLowerInvariant() : "";
+ if (descFilter.Length == 0 && m.Generation != 1) continue;
+ if (descFilter.Length != 0 && !m.Name.ToLowerInvariant().Contains(descFilter)) continue;
Console.WriteLine($"fam {m.Family,-5} 0x{m.SpellId:X4} {m.Name,-40} | {m.Description}");
}
return;