feat: regen buffs, wand aura, spellbook assess, indicator press flash

Four reports from one gate round. Three were mine; the fourth I first
mis-explained, and the correction is the useful part.

**The vital regeneration rates were never cast.** Regeneration (health),
Rejuvenation (stamina) and Mana Renewal (mana) all landed in the catch-all
Other bucket, which is off by default. Retail words each of the three
differently and two of the six phrasings do not begin with "Increases the
caster's" at all:

    Increase caster's natural healing rate by 10%.                 <- and note "Increase"
    Increases your Health Regeneration Rate by 50%.                (Empyrean)
    Increases the rate at which the caster regains Stamina by 10%.
    Increases the caster's natural mana rate by 10%.

They are matched per vital, on by default, and ranked at the very tail of the
Life group so they finish the pass. The mana line had to be checked BEFORE the
generic "Increases the caster's X by N" match, which would otherwise read it as
a buff to a stat named "natural mana rate".

**Aura of Hermetic Link was the sixth aura line and the only one missed.**
"a magic casting implement's" is reached by none of the other alternatives, so
the wand's mana-conversion buff was silently in Other too.

**Right-clicking a spell in the spellbook did nothing.** I claimed this had
never worked; the user said it used to, and they were right -- I had checked
one file's history and concluded from it. The regression is 3e31b0ac, which
gave UiCatalogSlot its own RightClick case returning true unconditionally. On
any list that had not wired the examine seam -- the spellbook among them -- the
event was reported handled and UiRoot stopped bubbling. Two fixes: the row now
reports an unwired right-click UNHANDLED so bubbling continues, and the
spellbook wires the seam to the same appraisal window the spell bar uses.

Retail does this generically in the list rather than per window
(UIElement_ItemList::ListenToElementMessage @ 0x004E4F1F -> ExamineSpell
@ 0x00564A70), which is exactly why a per-controller seam could be forgotten
for one window and not another.

**No green flash when pressing an indicator.** Every indicator button authors
a full-size 0x100000F2 child whose DirectState is a draw-nothing File=0 image
and whose only other state, Normal_pressed, carries the green selector sprite
0x06004CE8 -- and the buttons author Normal_pressed with PassToChildren. But
UiButton.ConsumesDatChildren drops dat children at import, so the cascade had
nothing left to reach. The child is re-attached through the same repair the map
hotspot's rollover highlight already uses.

**tools/LayoutDump** is new, and is why the last two are diagnoses rather than
guesses: it prints an authored LayoutDesc tree -- geometry, edge modes, state
sets, PassToChildren, per-state media -- straight from the installed DATs.
"Does this button even have a pressed state?" was being answered by reading our
own importer and inferring; now it is read from the data.

Solution builds clean; 14,464 tests pass on the standard hermetic lane filter,
0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-20 20:38:28 +02:00
parent fe1f124cd8
commit 46ce6f238c
14 changed files with 1003 additions and 18 deletions

View file

@ -30,6 +30,14 @@ public sealed class IndicatorBarController : IRetainedPanelController
public const uint LinkButtonId = 0x100000F8u;
public const uint EndCharacterSessionButtonId = 0x100000FAu;
/// <summary>
/// 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.
/// </summary>
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,
}
/// <summary>
/// Re-attaches the green press-highlight each indicator button authors as a
/// <see cref="PressHighlightId"/> child.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="UiButton.ConsumesDatChildren"/> 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
/// <c>Normal_pressed</c> with <c>PassToChildren</c>, and the cascade then
/// has nothing left to reach.
/// </para>
/// <para>
/// Same class of loss, and the same repair, as the map hotspot's
/// rollover-highlight child — see <see cref="MapPageController"/>'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.
/// </para>
/// </remarks>
public void AttachPressHighlights(
ElementInfo layoutRoot, Func<ElementInfo, UiElement?> 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,

View file

@ -57,6 +57,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
private readonly Action<uint> _addFavorite;
private readonly Action<uint> _sendFilter;
private readonly Action<uint> _removeSpell;
private readonly Action<uint>? _examineSpell;
private readonly Action<string, Action<bool>> _showConfirmation;
private readonly Action<uint, uint> _setDesiredComponent;
private readonly Action _close;
@ -93,6 +94,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
Action<uint> addFavorite,
Action<uint> sendFilter,
Action<uint> removeSpell,
Action<uint>? examineSpell,
Action<string, Action<bool>> showConfirmation,
Action<uint, uint> 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<uint> addFavorite,
Action<uint> sendFilter,
Action<uint> removeSpell,
Action<uint>? examineSpell,
Action<string, Action<bool>> showConfirmation,
Action<uint, uint> 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;

View file

@ -1267,6 +1267,22 @@ public sealed class RetailUiRuntime : IDisposable
return windowId != 0;
}
/// <summary>
/// Builds one dat child that a behavioral widget's
/// <see cref="UiElement.ConsumesDatChildren"/> dropped at import, so a
/// controller can re-attach it. Same seam as the map hotspots' AD-108
/// icon/highlight rebuild.
/// </summary>
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,

View file

@ -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);

View file

@ -40,9 +40,15 @@ public sealed class BuffSettings
public bool BuffBanes { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public bool BuffRegeneration { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
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
/// </list>
/// </item>
/// <item><b>Item Enchantment</b> — the banes and weapon auras.</item>
/// <item><b>Life Magic</b> last — the protections and Armor Self.</item>
/// <item><b>Life Magic</b> last — the protections and Armor Self, then
/// the vital regeneration rates to finish.</item>
/// </list>
/// <para>
/// <b>Willpower is matched as "Self".</b> 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;
}
/// <summary>
/// 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.
/// </summary>
private static int LifeOrder(BuffLine line) =>
line.Kind == BuffTargetKind.Regeneration ? 1 : 0;
private static int CreatureOrder(BuffLine line)
{
if (line.Kind == BuffTargetKind.Skill

View file

@ -27,7 +27,13 @@ public enum BuffTargetKind
/// though it is, in effect, a self buff.
/// </summary>
Bane,
/// <summary>Any other self-targeted duration buff (regeneration and friends).</summary>
/// <summary>
/// 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.
/// </summary>
Regeneration,
/// <summary>Any other self-targeted duration buff.</summary>
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.
/// </summary>
[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();
/// <summary>
/// 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":
/// <code>
/// 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)
/// </code>
/// Note "Increase", not "Increases", in the health line — retail's own typo,
/// which is exactly why matching a strict sentence shape lost these.
/// </summary>
[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();
/// <summary>
/// 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))

View file

@ -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) =>

View file

@ -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. -->
<panel x="40" y="120" w="360" h="404" title="MossTank — Settings" visible="{SettingsVisible}">
<panel x="40" y="120" w="360" h="432" title="MossTank — Settings" visible="{SettingsVisible}">
<label x="12" y="30" text="{DifficultyText}" color="#FFE8E4C8" />
<button x="292" y="26" w="26" h="22" text="-" onclick="{DifficultyDown}" />
<button x="322" y="26" w="26" h="22" text="+" onclick="{DifficultyUp}" />
@ -43,8 +43,11 @@
<label x="12" y="312" text="{BanesText}" color="#FF8F9C78" />
<button x="292" y="308" w="56" h="22" text="toggle" onclick="{ToggleBanes}" />
<label x="12" y="340" text="{OtherText}" color="#FF8F9C78" />
<button x="292" y="336" w="56" h="22" text="toggle" onclick="{ToggleOther}" />
<label x="12" y="340" text="{RegenerationText}" color="#FF8F9C78" />
<button x="292" y="336" w="56" h="22" text="toggle" onclick="{ToggleRegeneration}" />
<button x="12" y="368" w="108" h="26" text="Back" onclick="{CloseSettings}" />
<label x="12" y="368" text="{OtherText}" color="#FF8F9C78" />
<button x="292" y="364" w="56" h="22" text="toggle" onclick="{ToggleOther}" />
<button x="12" y="396" w="108" h="26" text="Back" onclick="{CloseSettings}" />
</panel>

View file

@ -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<uint>();
using SpellbookWindowController controller = Bind(
layout, book, examineSpell: examined.Add)!;
UiItemList list = Assert.IsType<UiItemList>(
layout.FindElement(SpellbookWindowController.SpellListId));
UiCatalogSlot row = Assert.IsType<UiCatalogSlot>(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<UiItemList>(
layout.FindElement(SpellbookWindowController.SpellListId));
UiCatalogSlot row = Assert.IsType<UiCatalogSlot>(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<uint>? addFavorite = null,
Action<uint>? sendFilter = null,
Action<uint>? removeSpell = null,
Action<uint>? examineSpell = null,
Action<string, Action<bool>>? showConfirmation = null,
Action? close = null,
ClientObjectTable? objects = null,
@ -420,6 +463,7 @@ public sealed class SpellbookWindowControllerTests
sendFilter?.Invoke(filters);
},
removeSpell ?? (_ => { }),
examineSpell,
showConfirmation ?? ((_, _) => { }),
(componentId, amount) =>
{

View file

@ -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<BuffLine> 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<PluginSpellInfo> plan = BuffPlan.Build(
book,
new[]
{
Skill(CreatureEnchantmentSkill, "Creature Enchantment",
PluginSkillTraining.Specialized),
Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained),
},
new[] { Attribute(4, "Focus") },
Array.Empty<PluginActiveEnchantment>(),
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<BuffLine> 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<PluginSkillInfo>(),
Array.Empty<PluginAttributeInfo>(),
Array.Empty<PluginActiveEnchantment>(), Default, force: true));
Assert.Empty(BuffPlan.Build(book, Array.Empty<PluginSkillInfo>(),
Array.Empty<PluginAttributeInfo>(),
Array.Empty<PluginActiveEnchantment>(), off, force: true));
}
}

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>LayoutDump</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chorizite.DatReaderWriter" />
<ProjectReference Include="..\..\src\AcDream.App\AcDream.App.csproj" />
<ProjectReference Include="..\..\src\AcDream.Content\AcDream.Content.csproj" />
</ItemGroup>
</Project>

View file

@ -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 <layoutId> [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);
}

View file

@ -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=="
}
}
}
}

View file

@ -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;