Owner request: plugin panels (Decal/VirindiViewService-class, per the
MosswartMassacre reference usage) need to embed real DAT icons the way
FlagTrackerView.SafeSetListImage does — spell/skill art, raw portal
indices, and a window icon. This is Slice B of
docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md (Slice A, the
movable/collapsible shelf, landed in 01b98ca30/4fada238e/718005b21).
What shipped:
- AcDream.Plugin.Abstractions.PluginIcons.Normalize: the one Decal-style
bare-index -> 0x06xxxxxx RenderSurface DID grammar, applied at every
icon SINK (descriptor IconSurfaceId in PluginShelfButton, and markup
<icon did>/<button icon>/<list icons> did-kind ids) rather than on the
plugin-facing records, which already carry real DIDs read straight
from the client's tables.
- PluginSpellInfo.IconId / PluginSkillInfo.IconId /
PluginInventoryItem.IconId / PluginWorldObject.IconId: additive init
properties (default 0), filled in AppAutomationSurface from
SpellMetadata.IconId (already projected from SpellBase.Icon by
RetailSpellMetadataProjector — no gap there), a new BindSkillIcons
parallel to BindSkillNames (GameWindow reads
DatReaderWriter.Types.SkillBase.IconId — confirmed via reflection over
the installed Chorizite.DatReaderWriter package, since its XML docs
don't cover Pack/Unpack-generated public fields: Description, Name,
IconId (uint), TrainedCost, SpecializedCost, Category, ChargenUse,
MinLevel, Formula, UpperBound, LowerBound, LearnMod), and
ClientObject.IconId in CaptureOwnedItems/ProjectWorldObject.
- IMarkupIconResolver (AcDream.App.UI): ResolveDid/ResolveSpell/
ResolveItem. MarkupDocument.Build gains an optional parameter (null by
default -> every icon sink resolves to nothing rather than throwing,
so pre-Slice-B callers/tests are unaffected). RetailUiRuntime.
MountPlugins builds ONE RetailMarkupIconResolver per pass from
RetailUiAssets.ResolveSprite + RetailUiAssets.Icons (the shared
IconComposer) + Toolbar.Objects (the SAME ClientObjectTable
Magic/Toolbar bindings already borrow for their own icon resolution —
no second object lookup introduced).
- New UiMarkupIcon widget (<icon x y w h did|spell|item tooltip>):
exactly one source required (FormatException at Build otherwise,
matching every other malformed-attribute rule), aspect-preserved,
centered, click-through unless a tooltip makes it a real hit-test
target.
- UiSimpleButton.IconSource and UiMarkupList.IconIdsSource/IconResolve:
additive, default null/no-op, so every existing button/list caller
(including the plugin shelf's own toggle/minimize buttons) is
unaffected. Button icon draws flush left and shifts the caption's
centering region right; list icons reserve a leading RowHeight-2
column (Decal's IconColumn) and skip rows whose id is 0 or
unresolvable.
- MarkupDocument centralizes the did/spell/item dispatch (including
PluginIcons.Normalize for did) in two small helpers (BuildIconSource
for <icon>/<button>, BuildRowIconResolve for <list>) so all three
markup surfaces share one resolver call path.
- AcDream.Plugins.Smoke ships a RegisterPanelContent (in-memory KSML,
no plugin-side .xml file) proof panel exercising every new surface:
a bare-index <icon>, a literal-hex <icon>, a composited <icon
spell=...>, a <button icon=...>, and a <list icons=... iconkind=
spell> of the first five known self-buffs with their IconId printed
alongside. Descriptor IconSurfaceId reuses the same bare index to
prove the shelf button and the panel's own icon normalize identically.
- docs/plugin-ui-markup.md is the new SSOT for the full markup
vocabulary + icon grammar + the Slice A shelf; linked from
docs/README.md and docs/plans/2026-04-24-ui-framework.md.
Design decisions where the plan left room:
- Normalize runs inside the resolver dispatch (BuildIconSource/
BuildRowIconResolve), not scattered at each markup call site, so
every did-kind sink shares one choke point.
- did/spell/item all accept either a literal (decimal or 0x-hex) or a
{Binding}, via one BindUintLiteralOrBinding helper, for symmetry —
the plan only showed spell/item as bindings but didn't forbid a
literal.
- <icon> requires exactly one source INCLUDING zero (not just two);
an icon with no source is not a coherent element.
- The button/list icon draw math (icon column extent, padding) lives
in the widgets themselves (UiSimpleButton/UiMarkupList), not in
MarkupDocument, keeping the parser only responsible for wiring
Func<(tex,w,h)> sources.
Tests: PluginIconsTests (Normalize table), MarkupIconTests (icon/button/
list resolver dispatch via a fake IMarkupIconResolver, plus draw-level
pins via the RecordingGpuDevice/TextRenderer apparatus already used by
UiAncestorClipTests/UiRenderContextDrawStringDatOutlineTests — "draws
nothing when unresolvable" and "button/list icon shifts the text"),
and AppAutomationSurfaceIconInstalledDatTests (Lane=InstalledDat: a
known spell's IconId matches the real installed SpellTable's own Icon
field exactly). Verified every new test fails to COMPILE without this
change (git-stashed the src/ changes, rebuilt the test project: CS0246
on IMarkupIconResolver) before restoring. Full App suite: 7331 passed /
97 skipped / 36 failed (identical pre-existing failure set/count to the
7306/97/36 baseline; the +25 passes are exactly the new tests).
AcDream.Plugins.MossTank.Tests (the main consumer of the touched
Plugin.Abstractions records) passes 337/337 unchanged, confirming
API-v1 binary/source compatibility. Full solution builds green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
123 lines
3.4 KiB
C#
123 lines
3.4 KiB
C#
namespace AcDream.Plugin.Abstractions;
|
|
|
|
/// <summary>
|
|
/// Virindi/Decal's stable ObjectClass numbers. These are deliberately distinct
|
|
/// from retail's ItemType flags: expressions and imported metas commonly use
|
|
/// numeric ObjectClass values (for example 5 = Monster and 24 = Player).
|
|
/// </summary>
|
|
public enum PluginObjectClass
|
|
{
|
|
Unknown = 0,
|
|
MeleeWeapon = 1,
|
|
Armor = 2,
|
|
Clothing = 3,
|
|
Jewelry = 4,
|
|
Monster = 5,
|
|
Food = 6,
|
|
Money = 7,
|
|
Misc = 8,
|
|
MissileWeapon = 9,
|
|
Container = 10,
|
|
Gem = 11,
|
|
SpellComponent = 12,
|
|
Key = 13,
|
|
Portal = 14,
|
|
TradeNote = 15,
|
|
ManaStone = 16,
|
|
Plant = 17,
|
|
BaseCooking = 18,
|
|
BaseAlchemy = 19,
|
|
BaseFletching = 20,
|
|
CraftedCooking = 21,
|
|
CraftedAlchemy = 22,
|
|
CraftedFletching = 23,
|
|
Player = 24,
|
|
Vendor = 25,
|
|
Door = 26,
|
|
Corpse = 27,
|
|
Lifestone = 28,
|
|
HealingKit = 29,
|
|
Lockpick = 30,
|
|
WandStaffOrb = 31,
|
|
Bundle = 32,
|
|
Book = 33,
|
|
Journal = 34,
|
|
Sign = 35,
|
|
Housing = 36,
|
|
Npc = 37,
|
|
Foci = 38,
|
|
Salvage = 39,
|
|
Ust = 40,
|
|
Services = 41,
|
|
Scroll = 42,
|
|
CombatPet = 43,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detached canonical world-object projection for general plugins and
|
|
/// expression engines. The host reports facts; filtering and automation
|
|
/// policy remain in the plugin.
|
|
/// </summary>
|
|
public readonly record struct PluginWorldObject(
|
|
uint ObjectId,
|
|
uint WeenieClassId,
|
|
string Name,
|
|
PluginObjectClass ObjectClass,
|
|
uint ItemType,
|
|
uint ContainerObjectId,
|
|
uint WielderObjectId)
|
|
{
|
|
public bool IsOwned { get; init; }
|
|
public bool IsLandscape { get; init; }
|
|
public bool HasPosition { get; init; }
|
|
public PluginNavigationPosition Position { get; init; }
|
|
public bool HasAppraisalData { get; init; }
|
|
/// <summary>
|
|
/// Decal-compatible monotonic millisecond tick of the latest successful
|
|
/// identify response for this exact object lifetime.
|
|
/// </summary>
|
|
public int LastIdTime { get; init; }
|
|
public bool IsDoorOpen { get; init; }
|
|
public int StackSize { get; init; } = 1;
|
|
public int ItemsCapacity { get; init; }
|
|
public int ContainersCapacity { get; init; }
|
|
public IReadOnlyList<uint> SpellIds { get; init; } = Array.Empty<uint>();
|
|
public IReadOnlyList<uint> ActiveSpellIds { get; init; } = Array.Empty<uint>();
|
|
/// <summary>
|
|
/// Retail <c>ClientObject.IconId</c> RenderSurface DID (base icon, before
|
|
/// compositing). Same id a plugin markup <c>iconkind="item"</c> resolves
|
|
/// through <c>IconComposer.GetIcon</c>. 0 when unknown.
|
|
/// </summary>
|
|
public uint IconId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// General object discovery used by UtilityBelt expressions and third-party
|
|
/// plugins. It borrows the same Runtime entity directory and ClientObject table
|
|
/// as world rendering and inventory; no plugin-specific mirror is introduced.
|
|
/// </summary>
|
|
public interface IWorldObjectAutomation
|
|
{
|
|
bool IsAvailable => false;
|
|
uint OpenContainerObjectId => 0u;
|
|
|
|
IReadOnlyList<PluginWorldObject> CaptureObjects() =>
|
|
Array.Empty<PluginWorldObject>();
|
|
|
|
bool TryGet(uint objectId, out PluginWorldObject value)
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
bool TryCaptureProperties(
|
|
uint objectId,
|
|
out PluginItemProperties properties)
|
|
{
|
|
properties = default;
|
|
return false;
|
|
}
|
|
|
|
PluginItemCommandResult Identify(uint objectId) =>
|
|
new(PluginItemCommandStatus.Unavailable);
|
|
}
|