feat(mosstank): add VTank-style automation PoC
This commit is contained in:
parent
f6fe0f2a4f
commit
4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions
|
|
@ -53,7 +53,38 @@ public readonly record struct PluginSpellInfo(
|
|||
uint School,
|
||||
string Description,
|
||||
bool IsSelfTargeted,
|
||||
bool IsBeneficial);
|
||||
bool IsBeneficial)
|
||||
{
|
||||
/// <summary>Retail spell-table classification, projected without policy.</summary>
|
||||
public bool IsDebuff { get; init; }
|
||||
public bool IsOffensive { get; init; }
|
||||
public bool IsFellowship { get; init; }
|
||||
public bool IsUntargeted { get; init; }
|
||||
/// <summary>
|
||||
/// VTank's spell-facing rule: targeted spells require facing except the
|
||||
/// authored family range 222..235.
|
||||
/// </summary>
|
||||
public bool RequiresTurnTo { get; init; }
|
||||
public bool IsProjectile { get; init; }
|
||||
public bool IsDamageOverTime { get; init; }
|
||||
public uint RawFlags { get; init; }
|
||||
public int SpellType { get; init; }
|
||||
public uint TargetMask { get; init; }
|
||||
public float BaseRangeConstant { get; init; }
|
||||
public float BaseRangeModifier { get; init; }
|
||||
/// <summary>
|
||||
/// Retail formula component ids in authored order. Plugins can inspect
|
||||
/// requirements without importing client/Core spell types.
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> FormulaComponentIds { get; init; } =
|
||||
Array.Empty<uint>();
|
||||
/// <summary>
|
||||
/// VTank's spell quality. It is the portal spell difficulty unless its
|
||||
/// official GameInfoDB override supplies a replacement.
|
||||
/// </summary>
|
||||
public int? QualityOverride { get; init; }
|
||||
public int Quality => QualityOverride ?? Difficulty;
|
||||
}
|
||||
|
||||
/// <summary>One enchantment currently in force on the local player.</summary>
|
||||
public readonly record struct PluginActiveEnchantment(
|
||||
|
|
@ -62,18 +93,39 @@ public readonly record struct PluginActiveEnchantment(
|
|||
int Tier,
|
||||
double SecondsRemaining);
|
||||
|
||||
/// <summary>One immutable entry from retail SpellComponentTable 0x0E00000F.</summary>
|
||||
public readonly record struct PluginSpellComponentInfo(
|
||||
uint ComponentId,
|
||||
uint WeenieClassId,
|
||||
string Name,
|
||||
double BurnRate,
|
||||
uint GestureId,
|
||||
double GestureSpeed,
|
||||
uint IconId,
|
||||
uint SortKey,
|
||||
string Type,
|
||||
string Word);
|
||||
|
||||
/// <summary>One of the character's skills, named from the retail skill table.</summary>
|
||||
public readonly record struct PluginSkillInfo(
|
||||
uint SkillId,
|
||||
string Name,
|
||||
PluginSkillTraining Training,
|
||||
uint Current);
|
||||
uint Current)
|
||||
{
|
||||
/// <summary>Unenchanted retail skill level before vitae and spell mods.</summary>
|
||||
public uint Base { get; init; } = Current;
|
||||
}
|
||||
|
||||
/// <summary>One primary attribute. <paramref name="Kind"/> is 0..5.</summary>
|
||||
public readonly record struct PluginAttributeInfo(
|
||||
int Kind,
|
||||
string Name,
|
||||
uint Current);
|
||||
uint Current)
|
||||
{
|
||||
/// <summary>Unenchanted primary-attribute value.</summary>
|
||||
public uint Base { get; init; } = Current;
|
||||
}
|
||||
|
||||
/// <summary>Why a cast would or would not be accepted right now.</summary>
|
||||
public enum PluginCastGate
|
||||
|
|
@ -93,6 +145,28 @@ public interface ICharacterInfo
|
|||
{
|
||||
bool IsInWorld { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stable in-world character name. Empty when unavailable. Plugins use it
|
||||
/// for VTank-compatible "By char" profile scoping; it is identity data,
|
||||
/// not a presentation-owned label.
|
||||
/// </summary>
|
||||
string Name => string.Empty;
|
||||
|
||||
/// <summary>Server-advertised world name used to scope global variables.</summary>
|
||||
string WorldName => string.Empty;
|
||||
|
||||
/// <summary>Authenticated account name; expression surfaces expose only its hash.</summary>
|
||||
string AccountName => string.Empty;
|
||||
|
||||
/// <summary>Retail roster slot for this character, or -1 when unavailable.</summary>
|
||||
int CharacterIndex => -1;
|
||||
|
||||
/// <summary>Current character level.</summary>
|
||||
int Level => 0;
|
||||
|
||||
/// <summary>Unused ordinary slots in the main pack.</summary>
|
||||
int MainPackFreeSlots => 0;
|
||||
|
||||
/// <summary>
|
||||
/// The local player's own object id, or 0 when not in world. Needed to
|
||||
/// target yourself: retail's banes are Item Enchantments whose description
|
||||
|
|
@ -108,6 +182,12 @@ public interface ICharacterInfo
|
|||
uint CurrentMana { get; }
|
||||
uint MaxMana { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail PropertyInt.SummoningMastery: 0 undef/geomancer, 1 primalist,
|
||||
/// 2 necromancer, 3 naturalist.
|
||||
/// </summary>
|
||||
int SummoningMastery => 0;
|
||||
|
||||
/// <summary>Skills the character has, with training state and current level.</summary>
|
||||
IReadOnlyList<PluginSkillInfo> Skills { get; }
|
||||
|
||||
|
|
@ -141,18 +221,75 @@ public interface ISpellCatalog
|
|||
/// </remarks>
|
||||
IReadOnlyList<PluginSpellInfo> KnownSelfBuffs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Learned direct offensive spells. Debuffs and beneficial spells are
|
||||
/// excluded; the plugin owns which attack spell to choose.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginSpellInfo> KnownAttackSpells =>
|
||||
Array.Empty<PluginSpellInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Every learned offensive or debuff spell, including untargeted rings,
|
||||
/// streaks and damage-over-time lines. The host supplies data; the plugin
|
||||
/// decides which names/families implement its combat policy.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginSpellInfo> KnownCombatSpells =>
|
||||
Array.Empty<PluginSpellInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the character has learned this exact spell id. <see cref="TryGet"/>
|
||||
/// answers a different question: it can resolve metadata for spells that
|
||||
/// are not in the character's spellbook, such as a scroll being appraised.
|
||||
/// </summary>
|
||||
bool IsKnown(uint spellId) => false;
|
||||
|
||||
bool TryGet(uint spellId, out PluginSpellInfo info);
|
||||
|
||||
/// <summary>Resolve retail's spell-component id, not its inventory WCID.</summary>
|
||||
bool TryGetComponent(uint componentId, out PluginSpellComponentInfo info)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Seconds remaining for one retail shared cooldown id.</summary>
|
||||
double GetCooldownRemaining(uint cooldownId) => 0d;
|
||||
}
|
||||
|
||||
/// <summary>Writing to the player's chat window.</summary>
|
||||
public readonly record struct PluginChatMessage(
|
||||
ulong Sequence,
|
||||
uint SenderObjectId,
|
||||
int Kind,
|
||||
string Sender,
|
||||
string Text,
|
||||
string ChannelName);
|
||||
|
||||
/// <summary>Reading confirmed chat and writing client-local notices.</summary>
|
||||
public interface IPluginChat
|
||||
{
|
||||
/// <summary>
|
||||
/// Ordered transcript messages newer than <paramref name="afterSequence"/>.
|
||||
/// The cursor is host-session independent and monotonically increases for
|
||||
/// the lifetime of this automation surface. VTank uses actual combat lines
|
||||
/// such as "You cast ... on ..." to confirm item and weapon procs.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginChatMessage> CaptureMessages(ulong afterSequence) =>
|
||||
Array.Empty<PluginChatMessage>();
|
||||
|
||||
/// <summary>
|
||||
/// Post a client-local system line, the channel retail uses for the
|
||||
/// client's own notices. It is local to this client: nothing is sent to the
|
||||
/// server and no other player sees it.
|
||||
/// </summary>
|
||||
void PostSystemMessage(string text);
|
||||
|
||||
/// <summary>
|
||||
/// Submit text through the client's normal retail chat-command parser.
|
||||
/// Commands, emotes, tells, and ordinary speech therefore use the same
|
||||
/// route as text entered in the main chat field.
|
||||
/// </summary>
|
||||
bool Submit(string text) => false;
|
||||
}
|
||||
|
||||
/// <summary>Casting, with a preflight so a plugin need not guess.</summary>
|
||||
|
|
@ -160,6 +297,13 @@ public interface IMagicCommands
|
|||
{
|
||||
bool IsCasting { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Last server-completed cast request. Revision changes exactly once when
|
||||
/// the matching UseDone arrives; zero means the host cannot supply cast
|
||||
/// receipts. A dispatched request is not reported as success early.
|
||||
/// </summary>
|
||||
PluginCastCompletion LastCompletion => default;
|
||||
|
||||
PluginCastGate EvaluateGate(uint spellId);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -167,6 +311,13 @@ public interface IMagicCommands
|
|||
/// not whether the spell ultimately lands, which the server decides.
|
||||
/// </summary>
|
||||
bool Cast(uint spellId);
|
||||
|
||||
/// <summary>Evaluate a cast against an explicit target atomically.</summary>
|
||||
PluginCastGate EvaluateGate(uint spellId, uint targetObjectId) =>
|
||||
PluginCastGate.Refused;
|
||||
|
||||
/// <summary>Select and cast on one explicit target in the same host call.</summary>
|
||||
bool Cast(uint spellId, uint targetObjectId) => false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -187,6 +338,52 @@ public interface IAutomationSurface
|
|||
ISpellCatalog Spells { get; }
|
||||
IMagicCommands Magic { get; }
|
||||
IPluginChat Chat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Target queries and physical-combat attempts. The default keeps plugins
|
||||
/// compiled against API v1 binary-compatible with hosts that do not yet
|
||||
/// provide combat automation.
|
||||
/// </summary>
|
||||
ICombatAutomation Combat => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Owned equipment reads and confirmed AutoWield attempts.</summary>
|
||||
IEquipmentAutomation Equipment => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Carried-item reads and canonical use/apply attempts.</summary>
|
||||
IItemAutomation Items => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>External-container discovery and canonical corpse looting.</summary>
|
||||
ILootAutomation Loot => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Authoritative fellowship vitals for helper spell policy.</summary>
|
||||
IFellowshipAutomation Fellowship => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Shared confirmed duration-spell observations by target.</summary>
|
||||
IEnchantmentAutomation Enchantments => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Canonical position reads and command-interpreter movement.</summary>
|
||||
INavigationAutomation Navigation => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>General canonical object discovery and raw property access.</summary>
|
||||
IWorldObjectAutomation Objects => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Runtime-owned Dereth calendar and day/night projection.</summary>
|
||||
IWorldTimeAutomation WorldTime => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Account roster and one-shot post-logout character entry.</summary>
|
||||
ILoginAutomation Login => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Other local acdream clients discovered by the host.</summary>
|
||||
INetworkAutomation Network => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Explicit VTank-compatible stuck-action recovery.</summary>
|
||||
IRecoveryAutomation Recovery => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Bounded projectile collision probes over the live world.</summary>
|
||||
IProjectileAutomation Projectiles => NoOpAutomationSurface.Instance;
|
||||
|
||||
/// <summary>Canonical retail previous/next selection actions.</summary>
|
||||
ISelectionAutomation Selection => NoOpAutomationSurface.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -194,7 +391,13 @@ public interface IAutomationSurface
|
|||
/// and every command refuses, so a plugin can keep one code path.
|
||||
/// </summary>
|
||||
public sealed class NoOpAutomationSurface
|
||||
: IAutomationSurface, ICharacterInfo, ISpellCatalog, IMagicCommands, IPluginChat
|
||||
: IAutomationSurface, ICharacterInfo, ISpellCatalog, IMagicCommands,
|
||||
IPluginChat, ICombatAutomation
|
||||
, IEquipmentAutomation, IItemAutomation, ILootAutomation,
|
||||
IFellowshipAutomation, IEnchantmentAutomation, INavigationAutomation
|
||||
, IWorldObjectAutomation, IWorldTimeAutomation, ILoginAutomation,
|
||||
INetworkAutomation, IRecoveryAutomation, IProjectileAutomation
|
||||
, ISelectionAutomation
|
||||
{
|
||||
public static NoOpAutomationSurface Instance { get; } = new();
|
||||
|
||||
|
|
@ -207,11 +410,41 @@ public sealed class NoOpAutomationSurface
|
|||
public ISpellCatalog Spells => this;
|
||||
public IMagicCommands Magic => this;
|
||||
public IPluginChat Chat => this;
|
||||
public ICombatAutomation Combat => this;
|
||||
public IEquipmentAutomation Equipment => this;
|
||||
public IItemAutomation Items => this;
|
||||
public ILootAutomation Loot => this;
|
||||
public IFellowshipAutomation Fellowship => this;
|
||||
public IEnchantmentAutomation Enchantments => this;
|
||||
public INavigationAutomation Navigation => this;
|
||||
public IWorldObjectAutomation Objects => this;
|
||||
public IWorldTimeAutomation WorldTime => this;
|
||||
public ILoginAutomation Login => this;
|
||||
public INetworkAutomation Network => this;
|
||||
public IRecoveryAutomation Recovery => this;
|
||||
public IProjectileAutomation Projectiles => this;
|
||||
public ISelectionAutomation Selection => this;
|
||||
|
||||
public void PostSystemMessage(string text)
|
||||
{
|
||||
}
|
||||
|
||||
public bool Submit(string text) => false;
|
||||
|
||||
PluginNavigationSnapshot INavigationAutomation.Snapshot => default;
|
||||
public bool TryGetObject(
|
||||
uint objectId,
|
||||
out PluginNavigationObject value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
public PluginNavigationCommandStatus SetMovementIntent(
|
||||
in PluginMovementIntent intent) =>
|
||||
PluginNavigationCommandStatus.Unavailable;
|
||||
public PluginNavigationCommandStatus ClearMovementIntent() =>
|
||||
PluginNavigationCommandStatus.Unavailable;
|
||||
|
||||
public bool IsInWorld => false;
|
||||
public uint ObjectId => 0;
|
||||
public uint CurrentHealth => 0;
|
||||
|
|
@ -220,6 +453,7 @@ public sealed class NoOpAutomationSurface
|
|||
public uint MaxStamina => 0;
|
||||
public uint CurrentMana => 0;
|
||||
public uint MaxMana => 0;
|
||||
public int SummoningMastery => 0;
|
||||
|
||||
public IReadOnlyList<PluginSkillInfo> Skills { get; } = Array.Empty<PluginSkillInfo>();
|
||||
public IReadOnlyList<PluginAttributeInfo> Attributes { get; } =
|
||||
|
|
@ -228,6 +462,10 @@ public sealed class NoOpAutomationSurface
|
|||
Array.Empty<PluginActiveEnchantment>();
|
||||
public IReadOnlyList<PluginSpellInfo> KnownSelfBuffs { get; } =
|
||||
Array.Empty<PluginSpellInfo>();
|
||||
public IReadOnlyList<PluginSpellInfo> KnownAttackSpells { get; } =
|
||||
Array.Empty<PluginSpellInfo>();
|
||||
public IReadOnlyList<PluginSpellInfo> KnownCombatSpells { get; } =
|
||||
Array.Empty<PluginSpellInfo>();
|
||||
|
||||
public bool TryGetSkill(uint skillId, out PluginSkillInfo skill)
|
||||
{
|
||||
|
|
@ -242,6 +480,64 @@ public sealed class NoOpAutomationSurface
|
|||
}
|
||||
|
||||
public bool IsCasting => false;
|
||||
public PluginCastCompletion LastCompletion => default;
|
||||
public PluginCastGate EvaluateGate(uint spellId) => PluginCastGate.Unavailable;
|
||||
public bool Cast(uint spellId) => false;
|
||||
public PluginCastGate EvaluateGate(uint spellId, uint targetObjectId) =>
|
||||
PluginCastGate.Unavailable;
|
||||
public bool Cast(uint spellId, uint targetObjectId) => false;
|
||||
|
||||
public PluginCombatSnapshot Snapshot => default;
|
||||
public IReadOnlyList<PluginCombatTarget> CaptureHostileTargets(
|
||||
float maximumDistance) => Array.Empty<PluginCombatTarget>();
|
||||
public PluginCombatCommandResult EnterDefaultMode() => new(
|
||||
PluginCombatCommandStatus.Unavailable);
|
||||
bool IEquipmentAutomation.IsAvailable => false;
|
||||
bool IEquipmentAutomation.IsBusy => false;
|
||||
public IReadOnlyList<PluginEquipmentItem> CaptureOwnedEquipment() =>
|
||||
Array.Empty<PluginEquipmentItem>();
|
||||
public PluginEquipmentCommandResult Equip(
|
||||
uint objectId,
|
||||
uint requestedLocation = 0u) =>
|
||||
new(PluginEquipmentCommandStatus.Unavailable);
|
||||
bool IItemAutomation.IsAvailable => false;
|
||||
bool IItemAutomation.IsBusy => false;
|
||||
int IItemAutomation.ActiveOwnedPetCount => 0;
|
||||
PluginItemUseCompletion IItemAutomation.LastCompletion => default;
|
||||
PluginItemUseCompletion ILootAutomation.LastItemUseCompletion => default;
|
||||
PluginInventoryCompletion ILootAutomation.LastInventoryCompletion => default;
|
||||
PluginAppraisalState ILootAutomation.Appraisal => default;
|
||||
public IReadOnlyList<PluginInventoryItem> CaptureOwnedItems() =>
|
||||
Array.Empty<PluginInventoryItem>();
|
||||
public PluginItemCommandResult Use(uint objectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
public PluginItemCommandResult Apply(uint objectId, uint targetObjectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
public IReadOnlyList<PluginLootContainer> CaptureCorpses(
|
||||
float maximumDistance) => Array.Empty<PluginLootContainer>();
|
||||
public IReadOnlyList<PluginInventoryItem> CaptureCurrentContents() =>
|
||||
Array.Empty<PluginInventoryItem>();
|
||||
public PluginItemCommandResult Open(uint containerObjectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
public PluginItemCommandResult Identify(uint objectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
public PluginItemCommandResult Pickup(uint objectId, bool mainPack = false) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
public bool IsInFellowship => false;
|
||||
public IReadOnlyList<PluginFellowMember> CaptureMembers() =>
|
||||
Array.Empty<PluginFellowMember>();
|
||||
public IReadOnlyList<PluginTrackedEnchantment> Capture(
|
||||
uint targetObjectId) => Array.Empty<PluginTrackedEnchantment>();
|
||||
public bool ReportCast(
|
||||
uint targetObjectId,
|
||||
uint spellId,
|
||||
double durationSeconds) => false;
|
||||
PluginWorldTimeSnapshot IWorldTimeAutomation.Snapshot => default;
|
||||
public PluginCombatCommandResult BeginPhysicalAttack(
|
||||
uint targetObjectId, PluginAttackHeight height, float power) => new(
|
||||
PluginCombatCommandStatus.Unavailable);
|
||||
public PluginCombatCommandResult ReleasePhysicalAttack() => new(
|
||||
PluginCombatCommandStatus.Unavailable);
|
||||
public PluginCombatCommandResult AbortPhysicalAttack() => new(
|
||||
PluginCombatCommandStatus.Unavailable);
|
||||
}
|
||||
|
|
|
|||
152
src/AcDream.Plugin.Abstractions/CombatAutomation.cs
Normal file
152
src/AcDream.Plugin.Abstractions/CombatAutomation.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>Presentation-independent combat mode projected to a plugin.</summary>
|
||||
public enum PluginCombatMode
|
||||
{
|
||||
Unknown = 0,
|
||||
Peace,
|
||||
Melee,
|
||||
Missile,
|
||||
Magic,
|
||||
}
|
||||
|
||||
/// <summary>Retail's three physical attack heights.</summary>
|
||||
public enum PluginAttackHeight
|
||||
{
|
||||
High = 1,
|
||||
Medium = 2,
|
||||
Low = 3,
|
||||
}
|
||||
|
||||
/// <summary>One canonical hostile candidate at the instant it was captured.</summary>
|
||||
public readonly record struct PluginCombatTarget(
|
||||
uint ObjectId,
|
||||
string Name,
|
||||
uint WeenieClassId,
|
||||
float Distance,
|
||||
float RelativeAngleDegrees,
|
||||
bool IsHealthKnown,
|
||||
float HealthFraction)
|
||||
{
|
||||
/// <summary>Retail PropertyInt CreatureType (2), or zero when unknown.</summary>
|
||||
public int SpeciesId { get; init; }
|
||||
|
||||
/// <summary>Retail creature-enum display name used by VTank's species variable.</summary>
|
||||
public string SpeciesName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Spawn/appraisal maximum HP, or zero until the host knows it.</summary>
|
||||
public int MaximumHealth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VTank's dynamic hasshield value: true when the target currently has an
|
||||
/// equipped object whose object class is Armor.
|
||||
/// </summary>
|
||||
public bool HasShield { get; init; }
|
||||
public ushort Incarnation { get; init; }
|
||||
|
||||
/// <summary>Monotonic revision of the last server health update.</summary>
|
||||
public long HealthRevision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Seconds since the last server health update at capture time, or
|
||||
/// positive infinity when health has never been reported.
|
||||
/// </summary>
|
||||
public double SecondsSinceHealthUpdate { get; init; } =
|
||||
double.PositiveInfinity;
|
||||
}
|
||||
|
||||
/// <summary>The canonical local combat/attack state visible to a plugin.</summary>
|
||||
public readonly record struct PluginCombatSnapshot(
|
||||
uint SelectedObjectId,
|
||||
PluginCombatMode Mode,
|
||||
PluginAttackHeight AttackHeight,
|
||||
float DesiredPower,
|
||||
float PowerBarLevel,
|
||||
bool BuildInProgress,
|
||||
bool RequestInProgress,
|
||||
bool ServerResponsePending,
|
||||
bool RepeatAttackInProgress)
|
||||
{
|
||||
/// <summary>Revision of the last physical AttackDone receipt.</summary>
|
||||
public long CompletionRevision { get; init; }
|
||||
public uint CompletionSequence { get; init; }
|
||||
public uint CompletionWeenieError { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Why an automation combat command did or did not proceed.</summary>
|
||||
public enum PluginCombatCommandStatus
|
||||
{
|
||||
Unavailable = 0,
|
||||
InvalidTarget,
|
||||
WrongMode,
|
||||
Busy,
|
||||
AlreadyReady,
|
||||
ModeChangeSent,
|
||||
Started,
|
||||
Released,
|
||||
Stopped,
|
||||
Refused,
|
||||
}
|
||||
|
||||
public readonly record struct PluginCombatCommandResult(
|
||||
PluginCombatCommandStatus Status,
|
||||
string? Notice = null)
|
||||
{
|
||||
public bool Accepted => Status is
|
||||
PluginCombatCommandStatus.AlreadyReady
|
||||
or PluginCombatCommandStatus.ModeChangeSent
|
||||
or PluginCombatCommandStatus.Started
|
||||
or PluginCombatCommandStatus.Released
|
||||
or PluginCombatCommandStatus.Stopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host combat primitives. The host owns no macro policy: it projects the
|
||||
/// canonical candidates/state and attempts the exact retail input operations
|
||||
/// MossTank asks for.
|
||||
/// </summary>
|
||||
public interface ICombatAutomation
|
||||
{
|
||||
PluginCombatSnapshot Snapshot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Capture currently valid hostile creatures no farther than
|
||||
/// <paramref name="maximumDistance"/> meters from the local player.
|
||||
/// Snapshot semantics: the returned list is never mutated in place.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginCombatTarget> CaptureHostileTargets(float maximumDistance);
|
||||
|
||||
/// <summary>
|
||||
/// Enter the combat mode implied by currently equipped items. If already
|
||||
/// in any combat mode this reports <see cref="PluginCombatCommandStatus.AlreadyReady"/>.
|
||||
/// </summary>
|
||||
PluginCombatCommandResult EnterDefaultMode();
|
||||
|
||||
/// <summary>
|
||||
/// Request one explicit retail combat mode. VTank needs this after
|
||||
/// selecting a caster, melee proc weapon, or grenade; the host still owns
|
||||
/// and sends the canonical mode transition.
|
||||
/// </summary>
|
||||
PluginCombatCommandResult EnterMode(PluginCombatMode mode) =>
|
||||
new(PluginCombatCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Select <paramref name="targetObjectId"/>, set the desired power and
|
||||
/// press the retail attack-height input. Release is a separate command so
|
||||
/// a plugin can wait for the real power bar.
|
||||
/// </summary>
|
||||
PluginCombatCommandResult BeginPhysicalAttack(
|
||||
uint targetObjectId,
|
||||
PluginAttackHeight height,
|
||||
float power);
|
||||
|
||||
PluginCombatCommandResult ReleasePhysicalAttack();
|
||||
PluginCombatCommandResult AbortPhysicalAttack();
|
||||
|
||||
/// <summary>
|
||||
/// Retire a client-side ghost through the host's canonical entity teardown
|
||||
/// path. This never sends a server delete and must reject the local player.
|
||||
/// </summary>
|
||||
PluginCombatCommandResult DismissGhostTarget(uint targetObjectId) =>
|
||||
new(PluginCombatCommandStatus.Unavailable);
|
||||
}
|
||||
35
src/AcDream.Plugin.Abstractions/EnchantmentAutomation.cs
Normal file
35
src/AcDream.Plugin.Abstractions/EnchantmentAutomation.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// One duration spell observed on a world object. This is a timer ledger, not
|
||||
/// an authoritative server enchantment registry: retail VTank built the same
|
||||
/// view from confirmed local casts and casts reported by cooperating plugins.
|
||||
/// </summary>
|
||||
public readonly record struct PluginTrackedEnchantment(
|
||||
uint TargetObjectId,
|
||||
uint SpellId,
|
||||
uint Family,
|
||||
int Quality,
|
||||
bool IsUntargeted,
|
||||
double SecondsRemaining);
|
||||
|
||||
/// <summary>
|
||||
/// Shared per-client duration-spell ledger. The host records successful local
|
||||
/// casts automatically. Plugins that perform casts outside the host's normal
|
||||
/// command surface can report their confirmed result, matching VTank's public
|
||||
/// <c>LogSpellCast(target, spell, duration)</c> capability.
|
||||
/// </summary>
|
||||
public interface IEnchantmentAutomation
|
||||
{
|
||||
IReadOnlyList<PluginTrackedEnchantment> Capture(uint targetObjectId) =>
|
||||
Array.Empty<PluginTrackedEnchantment>();
|
||||
|
||||
/// <summary>
|
||||
/// Report a confirmed duration spell. Dispatch attempts must not be
|
||||
/// reported; <paramref name="durationSeconds"/> is the effective duration.
|
||||
/// </summary>
|
||||
bool ReportCast(
|
||||
uint targetObjectId,
|
||||
uint spellId,
|
||||
double durationSeconds) => false;
|
||||
}
|
||||
64
src/AcDream.Plugin.Abstractions/EquipmentAutomation.cs
Normal file
64
src/AcDream.Plugin.Abstractions/EquipmentAutomation.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>One owned item that can participate in VTank equipment policy.</summary>
|
||||
public readonly record struct PluginEquipmentItem(
|
||||
uint ObjectId,
|
||||
string Name,
|
||||
uint ItemType,
|
||||
uint ValidLocations,
|
||||
uint EquippedLocation,
|
||||
uint ContainerObjectId,
|
||||
uint WielderObjectId,
|
||||
byte CombatUse,
|
||||
int DamageType,
|
||||
int WeaponSkill,
|
||||
int Damage,
|
||||
double DamageVariance)
|
||||
{
|
||||
public bool IsEquipped => EquippedLocation != 0u;
|
||||
/// <summary>Retail AMMO_TYPE bit from PublicWeenieDesc.</summary>
|
||||
public uint AmmoType { get; init; }
|
||||
public int StackSize { get; init; } = 1;
|
||||
public int WeaponType { get; init; }
|
||||
}
|
||||
|
||||
public enum PluginEquipmentCommandStatus
|
||||
{
|
||||
Unavailable = 0,
|
||||
InvalidItem,
|
||||
Busy,
|
||||
AlreadyEquipped,
|
||||
Started,
|
||||
Refused,
|
||||
}
|
||||
|
||||
public readonly record struct PluginEquipmentCommandResult(
|
||||
PluginEquipmentCommandStatus Status,
|
||||
string? Notice = null)
|
||||
{
|
||||
public bool Accepted => Status is
|
||||
PluginEquipmentCommandStatus.AlreadyEquipped
|
||||
or PluginEquipmentCommandStatus.Started;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed inventory equipment view and one request through the client's
|
||||
/// canonical confirmed AutoWield transaction.
|
||||
/// </summary>
|
||||
public interface IEquipmentAutomation
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
bool IsBusy => false;
|
||||
|
||||
IReadOnlyList<PluginEquipmentItem> CaptureOwnedEquipment() =>
|
||||
Array.Empty<PluginEquipmentItem>();
|
||||
|
||||
/// <param name="requestedLocation">
|
||||
/// Zero asks retail AutoWield to choose; otherwise this is the exact
|
||||
/// retail INVENTORY_LOC bit requested by a profile.
|
||||
/// </param>
|
||||
PluginEquipmentCommandResult Equip(
|
||||
uint objectId,
|
||||
uint requestedLocation = 0u) =>
|
||||
new(PluginEquipmentCommandStatus.Unavailable);
|
||||
}
|
||||
72
src/AcDream.Plugin.Abstractions/FellowshipAutomation.cs
Normal file
72
src/AcDream.Plugin.Abstractions/FellowshipAutomation.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>One authoritative fellowship-roster entry plus live range.</summary>
|
||||
public readonly record struct PluginFellowMember(
|
||||
uint ObjectId,
|
||||
string Name,
|
||||
uint CurrentHealth,
|
||||
uint MaxHealth,
|
||||
uint CurrentStamina,
|
||||
uint MaxStamina,
|
||||
uint CurrentMana,
|
||||
uint MaxMana,
|
||||
float Distance)
|
||||
{
|
||||
/// <summary>
|
||||
/// The member's authoritative fellowship Share Loot bit. VTank permits
|
||||
/// immediate corpse access for a fellow only when this bit is set; a
|
||||
/// non-sharing fellow's corpse remains protected for retail's 100-second
|
||||
/// public-loot interval.
|
||||
/// </summary>
|
||||
public bool ShareLoot { get; init; }
|
||||
}
|
||||
|
||||
public enum PluginFellowshipCommandStatus
|
||||
{
|
||||
Unavailable = 0,
|
||||
Accepted,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
public readonly record struct PluginFellowshipCommandResult(
|
||||
PluginFellowshipCommandStatus Status)
|
||||
{
|
||||
public bool Accepted => Status == PluginFellowshipCommandStatus.Accepted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group state and generation-gated retail fellowship commands. Recruitment,
|
||||
/// waiting lists, voting, and social policy remain plugin behavior; the host
|
||||
/// only exposes the canonical wire operations already used by the retail UI.
|
||||
/// </summary>
|
||||
public interface IFellowshipAutomation
|
||||
{
|
||||
bool IsInFellowship => false;
|
||||
string Name => string.Empty;
|
||||
uint LeaderObjectId => 0u;
|
||||
bool IsOpen => false;
|
||||
bool IsLocked => false;
|
||||
int MemberCount => 0;
|
||||
IReadOnlyList<PluginFellowMember> CaptureMembers() =>
|
||||
Array.Empty<PluginFellowMember>();
|
||||
|
||||
/// <summary>
|
||||
/// Complete authoritative roster in server insertion order, including the
|
||||
/// local player. Use <see cref="CaptureMembers"/> for helper/healer policy
|
||||
/// that intentionally excludes self.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginFellowMember> CaptureRoster() => CaptureMembers();
|
||||
|
||||
PluginFellowshipCommandResult Create(string name, bool shareExperience) =>
|
||||
new(PluginFellowshipCommandStatus.Unavailable);
|
||||
PluginFellowshipCommandResult Recruit(uint targetObjectId) =>
|
||||
new(PluginFellowshipCommandStatus.Unavailable);
|
||||
PluginFellowshipCommandResult Dismiss(uint targetObjectId) =>
|
||||
new(PluginFellowshipCommandStatus.Unavailable);
|
||||
PluginFellowshipCommandResult Quit(bool disband) =>
|
||||
new(PluginFellowshipCommandStatus.Unavailable);
|
||||
PluginFellowshipCommandResult AssignLeader(uint targetObjectId) =>
|
||||
new(PluginFellowshipCommandStatus.Unavailable);
|
||||
PluginFellowshipCommandResult SetOpen(bool isOpen) =>
|
||||
new(PluginFellowshipCommandStatus.Unavailable);
|
||||
}
|
||||
|
|
@ -20,6 +20,19 @@ public interface IPluginHost
|
|||
IEvents Events { get; }
|
||||
ISelectionService Selection { get; }
|
||||
IUiRegistry Ui { get; }
|
||||
/// <summary>
|
||||
/// Locally handled slash/at commands. Hosts without command routing expose
|
||||
/// an inert registry so an API-v1 plugin can retain one code path.
|
||||
/// </summary>
|
||||
IPluginCommandRegistry Commands => NoOpPluginCommandRegistry.Instance;
|
||||
/// <summary>
|
||||
/// Durable storage scoped by the host to this plugin's manifest id.
|
||||
/// No-window/test hosts may explicitly expose the inert implementation.
|
||||
/// </summary>
|
||||
IPluginStorage Storage => NoOpPluginStorage.Instance;
|
||||
/// <summary>Unload-safe external VTank-style loot classifiers.</summary>
|
||||
IPluginLootClassifierRegistry LootClassifiers =>
|
||||
NoOpPluginLootClassifierRegistry.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Character reads, spell data and casting. Hosts with no live session
|
||||
|
|
|
|||
22
src/AcDream.Plugin.Abstractions/IPluginStorage.cs
Normal file
22
src/AcDream.Plugin.Abstractions/IPluginStorage.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Per-plugin durable text storage. The host scopes keys to the authenticated
|
||||
/// manifest id, so a plugin cannot collide with another plugin's profile.
|
||||
/// </summary>
|
||||
public interface IPluginStorage
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
string? ReadText(string key) => null;
|
||||
/// <summary>Relative file keys beneath one relative prefix.</summary>
|
||||
IReadOnlyList<string> List(string prefix) => Array.Empty<string>();
|
||||
void WriteText(string key, string content) =>
|
||||
throw new NotSupportedException("Plugin storage is unavailable.");
|
||||
bool Delete(string key) => false;
|
||||
}
|
||||
|
||||
public sealed class NoOpPluginStorage : IPluginStorage
|
||||
{
|
||||
public static NoOpPluginStorage Instance { get; } = new();
|
||||
private NoOpPluginStorage() { }
|
||||
}
|
||||
|
|
@ -1,5 +1,47 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Stable, presentation-neutral description of one top-level plugin window.
|
||||
/// The graphical host uses this metadata for its plugin sidepanel and retained
|
||||
/// window registry; no App/UI type crosses the plugin boundary.
|
||||
/// </summary>
|
||||
/// <param name="WindowId">
|
||||
/// Stable id within the owning plugin. It is part of the persisted window-layout
|
||||
/// key, so it must not be localized or changed between releases.
|
||||
/// </param>
|
||||
/// <param name="Title">User-facing window title.</param>
|
||||
public sealed record PluginPanelDescriptor(string WindowId, string Title)
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional one-to-three-character fallback drawn in the sidepanel button
|
||||
/// when no DAT icon is supplied. The host derives initials from
|
||||
/// <see cref="Title"/> when this is empty.
|
||||
/// </summary>
|
||||
public string? IconText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional installed-client RenderSurface DID. Zero asks the host to draw
|
||||
/// <see cref="IconText"/> instead. Plugins never receive the resulting GPU
|
||||
/// resource and remain BCL-only.
|
||||
/// </summary>
|
||||
public uint IconSurfaceId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial visibility used only when no per-character persisted layout is
|
||||
/// available. Hiding the window never disables the plugin.
|
||||
/// </summary>
|
||||
public bool StartVisible { get; init; } = true;
|
||||
|
||||
/// <summary>Whether this window receives a button in the shared sidepanel.</summary>
|
||||
public bool ShowInSidePanel { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host-authenticated plugin identity attached to registrations by the scoped
|
||||
/// plugin lifetime. Plugins cannot choose or spoof this value.
|
||||
/// </summary>
|
||||
public readonly record struct PluginUiOwner(string Id, string DisplayName);
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-facing UI registration. A plugin ships a markup file (KSML-style) +
|
||||
/// a binding object exposing the data properties the markup binds to, and
|
||||
|
|
@ -13,6 +55,52 @@ public interface IUiRegistry
|
|||
/// <param name="markupPath">Absolute path to the plugin's panel markup file.</param>
|
||||
/// <param name="binding">Object whose properties the markup's {Bindings} resolve against.</param>
|
||||
void AddMarkupPanel(string markupPath, object binding);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a first-class plugin window. The host keeps the plugin lifetime
|
||||
/// independent from the window's visible/minimized state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaulting to the API-v1 method keeps older/custom hosts source-compatible;
|
||||
/// acdream's graphical scoped host overrides this route and preserves all
|
||||
/// descriptor metadata.
|
||||
/// </remarks>
|
||||
void AddPanel(
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupPath,
|
||||
object binding)
|
||||
=> AddMarkupPanel(markupPath, binding);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a window whose lifetime may be ended independently while the
|
||||
/// plugin keeps running. Disposing the token removes the retained window
|
||||
/// and its sidepanel entry.
|
||||
/// </summary>
|
||||
IDisposable RegisterPanel(
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupPath,
|
||||
object binding)
|
||||
{
|
||||
AddPanel(descriptor, markupPath, binding);
|
||||
return NoOpUiRegistration.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an independently removable window from in-memory KSML. This
|
||||
/// is the BCL-only seam used by VTank-compatible Meta Create View actions;
|
||||
/// plugins do not need to create temporary files or import App types.
|
||||
/// </summary>
|
||||
IDisposable RegisterPanelContent(
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupContent,
|
||||
object binding) => NoOpUiRegistration.Instance;
|
||||
|
||||
/// <summary>Queries this plugin's own registered view by title or stable id.</summary>
|
||||
bool ViewExists(string viewName) => false;
|
||||
bool IsViewVisible(string viewName) => false;
|
||||
bool ControlExists(string viewName, string controlName) => false;
|
||||
bool SetControlLabel(string viewName, string controlName, string label) => false;
|
||||
bool SetControlVisible(string viewName, string controlName, bool visible) => false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -25,6 +113,55 @@ public interface IUiRegistry
|
|||
public interface IScopedUiRegistry : IUiRegistry
|
||||
{
|
||||
IDisposable RegisterMarkupPanel(string markupPath, object binding);
|
||||
|
||||
/// <summary>
|
||||
/// Host-only scoped registration carrying the manifest-derived owner.
|
||||
/// Disposal removes both the retained window and its sidepanel entry.
|
||||
/// </summary>
|
||||
IDisposable RegisterPanel(
|
||||
PluginUiOwner owner,
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupPath,
|
||||
object binding)
|
||||
=> RegisterMarkupPanel(markupPath, binding);
|
||||
|
||||
/// <summary>Host-owned registration for in-memory plugin markup.</summary>
|
||||
IDisposable RegisterPanelContent(
|
||||
PluginUiOwner owner,
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupContent,
|
||||
object binding) => NoOpUiRegistration.Instance;
|
||||
|
||||
bool ViewExists(PluginUiOwner owner, string viewName) => false;
|
||||
bool IsViewVisible(PluginUiOwner owner, string viewName) => false;
|
||||
bool ControlExists(
|
||||
PluginUiOwner owner,
|
||||
string viewName,
|
||||
string controlName) => false;
|
||||
bool SetControlLabel(
|
||||
PluginUiOwner owner,
|
||||
string viewName,
|
||||
string controlName,
|
||||
string label) => false;
|
||||
bool SetControlVisible(
|
||||
PluginUiOwner owner,
|
||||
string viewName,
|
||||
string controlName,
|
||||
bool visible) => false;
|
||||
}
|
||||
|
||||
/// <summary>Shared empty registration returned by UI-less/legacy hosts.</summary>
|
||||
public sealed class NoOpUiRegistration : IDisposable
|
||||
{
|
||||
public static NoOpUiRegistration Instance { get; } = new();
|
||||
|
||||
private NoOpUiRegistration()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -44,9 +181,38 @@ public sealed class NoOpUiRegistry : IScopedUiRegistry
|
|||
{
|
||||
}
|
||||
|
||||
public void AddPanel(
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupPath,
|
||||
object binding)
|
||||
{
|
||||
}
|
||||
|
||||
public IDisposable RegisterPanel(
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupPath,
|
||||
object binding) => NoOpUiRegistration.Instance;
|
||||
|
||||
public IDisposable RegisterPanelContent(
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupContent,
|
||||
object binding) => NoOpUiRegistration.Instance;
|
||||
|
||||
public IDisposable RegisterMarkupPanel(string markupPath, object binding) =>
|
||||
NoOpRegistration.Instance;
|
||||
|
||||
public IDisposable RegisterPanel(
|
||||
PluginUiOwner owner,
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupPath,
|
||||
object binding) => NoOpRegistration.Instance;
|
||||
|
||||
public IDisposable RegisterPanelContent(
|
||||
PluginUiOwner owner,
|
||||
PluginPanelDescriptor descriptor,
|
||||
string markupContent,
|
||||
object binding) => NoOpUiRegistration.Instance;
|
||||
|
||||
private sealed class NoOpRegistration : IDisposable
|
||||
{
|
||||
internal static NoOpRegistration Instance { get; } = new();
|
||||
|
|
|
|||
245
src/AcDream.Plugin.Abstractions/ItemAutomation.cs
Normal file
245
src/AcDream.Plugin.Abstractions/ItemAutomation.cs
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// One ordered VTClassic-compatible subpalette sample from an object's model
|
||||
/// description. RGB is sampled at retail/VTank's representative index.
|
||||
/// </summary>
|
||||
public readonly record struct PluginPaletteInfo(
|
||||
uint PaletteId,
|
||||
byte Offset,
|
||||
byte Length,
|
||||
byte Red,
|
||||
byte Green,
|
||||
byte Blue);
|
||||
|
||||
/// <summary>
|
||||
/// One carried item from the character's canonical inventory object table.
|
||||
/// The deliberately raw retail ids let general plugins classify new server
|
||||
/// content without taking a dependency on acdream's Core enums.
|
||||
/// </summary>
|
||||
public readonly record struct PluginInventoryItem(
|
||||
uint ObjectId,
|
||||
uint WeenieClassId,
|
||||
string Name,
|
||||
uint ItemType,
|
||||
uint ContainerObjectId,
|
||||
uint WielderObjectId,
|
||||
uint ValidLocations,
|
||||
uint EquippedLocation,
|
||||
uint Useability,
|
||||
uint TargetType,
|
||||
uint PublicFlags,
|
||||
int StackSize,
|
||||
int Structure,
|
||||
int MaximumStructure,
|
||||
uint SpellId,
|
||||
int PetClass,
|
||||
int SummoningMastery,
|
||||
uint ProcSpellId,
|
||||
bool ProcSpellSelfTargeted,
|
||||
double ProcSpellRate,
|
||||
int WeaponSkill,
|
||||
int DamageType,
|
||||
int Damage,
|
||||
double DamageVariance,
|
||||
int UseRequiresSkill,
|
||||
int UseRequiresSkillLevel,
|
||||
int UseRequiresSkillSpecialized)
|
||||
{
|
||||
public bool IsEquipped => EquippedLocation != 0u;
|
||||
public bool IsPetDevice => PetClass != 0;
|
||||
public bool HasCastOnStrike => ProcSpellId != 0u && ProcSpellRate > 0d;
|
||||
public int CombatUse { get; init; }
|
||||
public int ItemSpellcraft { get; init; }
|
||||
public int WieldRequirements { get; init; }
|
||||
public int WieldSkillType { get; init; }
|
||||
public int WieldDifficulty { get; init; }
|
||||
public int AttackType { get; init; }
|
||||
public int WeaponType { get; init; }
|
||||
/// <summary>
|
||||
/// Retail <c>PropertyInt.BoosterEnum</c>: current Health/Stamina/Mana are
|
||||
/// 2/4/6. VTank uses this to classify both kits and food without relying
|
||||
/// on localized item names.
|
||||
/// </summary>
|
||||
public int BoosterVital { get; init; }
|
||||
public int BoostValue { get; init; }
|
||||
public double HealKitModifier { get; init; }
|
||||
public IReadOnlyList<uint> AppraisedSpellIds { get; init; } =
|
||||
Array.Empty<uint>();
|
||||
public int GearDamage { get; init; }
|
||||
public int GearDamageResistance { get; init; }
|
||||
public int GearCriticalChance { get; init; }
|
||||
public int GearCriticalResistance { get; init; }
|
||||
public int GearCriticalDamage { get; init; }
|
||||
public int GearCriticalDamageResistance { get; init; }
|
||||
/// <summary>Retail PublicWeenieDesc maximum stack size.</summary>
|
||||
public int MaximumStackSize { get; init; } = 1;
|
||||
/// <summary>Current zero-based slot inside <see cref="ContainerObjectId"/>.</summary>
|
||||
public int ContainerSlot { get; init; } = -1;
|
||||
/// <summary>Number of ordinary item slots when this object is a container.</summary>
|
||||
public int ItemsCapacity { get; init; }
|
||||
/// <summary>Number of nested-container slots when this object is a container.</summary>
|
||||
public int ContainersCapacity { get; init; }
|
||||
/// <summary>Current total burden of this object or stack.</summary>
|
||||
public int Burden { get; init; }
|
||||
public int Value { get; init; }
|
||||
public int ItemCurrentMana { get; init; }
|
||||
public int ItemMaximumMana { get; init; }
|
||||
public float Workmanship { get; init; }
|
||||
public uint MaterialType { get; init; }
|
||||
/// <summary>Virindi/Decal's stable object class, not ItemType flags.</summary>
|
||||
public PluginObjectClass ObjectClass { get; init; }
|
||||
public IReadOnlyList<PluginPaletteInfo> Palettes { get; init; } =
|
||||
Array.Empty<PluginPaletteInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On-demand copy of an item's raw retail property tables. Loot and expression
|
||||
/// engines can understand future server content without making every ordinary
|
||||
/// inventory scan clone seven dictionaries per item.
|
||||
/// </summary>
|
||||
public readonly record struct PluginItemProperties(
|
||||
IReadOnlyDictionary<uint, int> Ints,
|
||||
IReadOnlyDictionary<uint, long> Int64s,
|
||||
IReadOnlyDictionary<uint, bool> Bools,
|
||||
IReadOnlyDictionary<uint, double> Floats,
|
||||
IReadOnlyDictionary<uint, string> Strings,
|
||||
IReadOnlyDictionary<uint, uint> DataIds,
|
||||
IReadOnlyDictionary<uint, uint> InstanceIds);
|
||||
|
||||
/// <summary>One server <c>UseDone</c> for a plugin-issued item action.</summary>
|
||||
public readonly record struct PluginItemUseCompletion(
|
||||
long Revision,
|
||||
uint SourceObjectId,
|
||||
uint TargetObjectId,
|
||||
uint WeenieError)
|
||||
{
|
||||
public bool IsSuccess => Revision != 0 && WeenieError == 0u;
|
||||
}
|
||||
|
||||
public enum PluginItemCommandStatus
|
||||
{
|
||||
Unavailable = 0,
|
||||
InvalidItem,
|
||||
InvalidTarget,
|
||||
Busy,
|
||||
Started,
|
||||
Refused,
|
||||
}
|
||||
|
||||
public readonly record struct PluginItemCommandResult(
|
||||
PluginItemCommandStatus Status,
|
||||
string? Notice = null)
|
||||
{
|
||||
public bool Accepted => Status == PluginItemCommandStatus.Started;
|
||||
}
|
||||
|
||||
/// <summary>The retail inventory request that produced a completion receipt.</summary>
|
||||
public enum PluginInventoryCommandKind
|
||||
{
|
||||
Unknown = 0,
|
||||
Pickup,
|
||||
PutInContainer,
|
||||
SplitToContainer,
|
||||
Merge,
|
||||
Move,
|
||||
DropToWorld,
|
||||
SplitToWorld,
|
||||
Wield,
|
||||
Give,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authoritative completion of one plugin or UI inventory transaction. A
|
||||
/// started command is not success until this revision advances for its source.
|
||||
/// </summary>
|
||||
public readonly record struct PluginInventoryCompletion(
|
||||
long Revision,
|
||||
PluginInventoryCommandKind Kind,
|
||||
uint SourceObjectId,
|
||||
uint WeenieError)
|
||||
{
|
||||
public bool IsSuccess => Revision != 0 && WeenieError == 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed inventory view and item actions through the client's one retail
|
||||
/// item-interaction transaction. A successful command means only that the
|
||||
/// request started; <see cref="LastCompletion"/> is the server result.
|
||||
/// </summary>
|
||||
public interface IItemAutomation
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
bool IsBusy => false;
|
||||
int ActiveOwnedPetCount => 0;
|
||||
uint ActiveVendorObjectId => 0u;
|
||||
PluginItemUseCompletion LastCompletion => default;
|
||||
PluginInventoryCompletion LastInventoryCompletion => default;
|
||||
|
||||
IReadOnlyList<PluginInventoryItem> CaptureOwnedItems() =>
|
||||
Array.Empty<PluginInventoryItem>();
|
||||
|
||||
bool TryCaptureProperties(
|
||||
uint objectId,
|
||||
out PluginItemProperties properties)
|
||||
{
|
||||
properties = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginItemCommandResult Use(uint objectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
PluginItemCommandResult Apply(uint objectId, uint targetObjectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Move all or an exact partial quantity into a carried container. An
|
||||
/// amount of zero means the whole current stack.
|
||||
/// </summary>
|
||||
PluginItemCommandResult MoveToContainer(
|
||||
uint objectId,
|
||||
uint containerObjectId,
|
||||
uint amount = 0u,
|
||||
int placement = 0) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Merge up to <paramref name="amount"/> units from source into target.
|
||||
/// Zero means as much as retail permits.
|
||||
/// </summary>
|
||||
PluginItemCommandResult Merge(
|
||||
uint sourceObjectId,
|
||||
uint targetObjectId,
|
||||
uint amount = 0u) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>Drop all or an exact partial stack on the ground.</summary>
|
||||
PluginItemCommandResult Drop(uint objectId, uint amount = 0u) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>Give all or an exact partial stack to a world target.</summary>
|
||||
PluginItemCommandResult Give(
|
||||
uint objectId,
|
||||
uint targetObjectId,
|
||||
uint amount = 0u) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Salvage one or more owned items with an owned tinkering/salvage tool.
|
||||
/// The command is the retail 0x027D operation; source-item removal is the
|
||||
/// authoritative completion signal until a host projects the 0x02B4
|
||||
/// material-result details.
|
||||
/// </summary>
|
||||
PluginItemCommandResult Salvage(
|
||||
uint toolObjectId,
|
||||
IReadOnlyList<uint> itemObjectIds) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Sell an owned item through the currently-open authoritative vendor.
|
||||
/// Zero amount means the complete current stack.
|
||||
/// </summary>
|
||||
PluginItemCommandResult Sell(uint objectId, uint amount = 0u) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
}
|
||||
25
src/AcDream.Plugin.Abstractions/LoginAutomation.cs
Normal file
25
src/AcDream.Plugin.Abstractions/LoginAutomation.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>One character in the account's authoritative login roster.</summary>
|
||||
public readonly record struct PluginLoginCharacter(
|
||||
uint ObjectId,
|
||||
string Name,
|
||||
int ActiveIndex,
|
||||
bool IsPendingDelete);
|
||||
|
||||
/// <summary>
|
||||
/// Account-roster and one-shot next-login control. The host owns the login
|
||||
/// transaction; plugins only select or clear the character to enter when the
|
||||
/// current character returns to character selection.
|
||||
/// </summary>
|
||||
public interface ILoginAutomation
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
uint NextLoginObjectId => 0u;
|
||||
|
||||
IReadOnlyList<PluginLoginCharacter> CaptureRoster() =>
|
||||
Array.Empty<PluginLoginCharacter>();
|
||||
|
||||
bool SetNextLogin(uint characterObjectId) => false;
|
||||
bool ClearNextLogin() => false;
|
||||
}
|
||||
69
src/AcDream.Plugin.Abstractions/LootAutomation.cs
Normal file
69
src/AcDream.Plugin.Abstractions/LootAutomation.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// One live external container that an automation plugin may approach and use.
|
||||
/// The host classifies corpses; plugins decide whether and when to loot them.
|
||||
/// </summary>
|
||||
public readonly record struct PluginLootContainer(
|
||||
uint ObjectId,
|
||||
uint WeenieClassId,
|
||||
string Name,
|
||||
float Distance,
|
||||
bool HasBeenOpened,
|
||||
bool IsRequested,
|
||||
bool IsCurrent)
|
||||
{
|
||||
public string LongDescription { get; init; } = string.Empty;
|
||||
public bool IsGeneratedRare { get; init; }
|
||||
public bool IsIdentified { get; init; }
|
||||
}
|
||||
|
||||
public readonly record struct PluginAppraisalState(
|
||||
long Revision,
|
||||
uint AwaitingObjectId,
|
||||
uint CurrentObjectId);
|
||||
|
||||
/// <summary>
|
||||
/// Read-only corpse/container discovery plus canonical open and pickup commands.
|
||||
/// Successful commands mean the request started; completion is reported through
|
||||
/// <see cref="LastItemUseCompletion"/> or <see cref="LastInventoryCompletion"/>.
|
||||
/// </summary>
|
||||
public interface ILootAutomation
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
bool IsBusy => false;
|
||||
uint RequestedContainerId => 0u;
|
||||
uint CurrentContainerId => 0u;
|
||||
PluginItemUseCompletion LastItemUseCompletion => default;
|
||||
PluginInventoryCompletion LastInventoryCompletion => default;
|
||||
PluginAppraisalState Appraisal => default;
|
||||
|
||||
IReadOnlyList<PluginLootContainer> CaptureCorpses(float maximumDistance) =>
|
||||
Array.Empty<PluginLootContainer>();
|
||||
|
||||
/// <summary>
|
||||
/// Captures the complete currently viewed external-container tree. Entries
|
||||
/// are ordered depth-first in retail container-slot order.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginInventoryItem> CaptureCurrentContents() =>
|
||||
Array.Empty<PluginInventoryItem>();
|
||||
|
||||
bool TryCaptureProperties(
|
||||
uint objectId,
|
||||
out PluginItemProperties properties)
|
||||
{
|
||||
properties = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginItemCommandResult Open(uint containerObjectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
PluginItemCommandResult Identify(uint objectId) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
|
||||
PluginItemCommandResult Pickup(
|
||||
uint objectId,
|
||||
bool mainPack = false) =>
|
||||
new(PluginItemCommandStatus.Unavailable);
|
||||
}
|
||||
93
src/AcDream.Plugin.Abstractions/LootClassifierPlugins.cs
Normal file
93
src/AcDream.Plugin.Abstractions/LootClassifierPlugins.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>VTank's public loot-plugin action vocabulary.</summary>
|
||||
public enum PluginLootAction
|
||||
{
|
||||
NoLoot = 0,
|
||||
Keep = 1,
|
||||
Salvage = 2,
|
||||
Sell = 3,
|
||||
Read = 4,
|
||||
User1 = 5,
|
||||
User2 = 6,
|
||||
User3 = 7,
|
||||
User4 = 8,
|
||||
User5 = 9,
|
||||
KeepUpTo = 10,
|
||||
}
|
||||
|
||||
public readonly record struct PluginLootClassificationContext(
|
||||
PluginInventoryItem Item,
|
||||
PluginItemProperties Properties,
|
||||
IReadOnlyList<PluginInventoryItem> OwnedItems);
|
||||
|
||||
/// <summary>A classifier's detached decision. Matched=false means no rule.</summary>
|
||||
public readonly record struct PluginLootClassification(
|
||||
bool Matched,
|
||||
PluginLootAction Action,
|
||||
string RuleName = "",
|
||||
int Priority = 0,
|
||||
int KeepCount = 0);
|
||||
|
||||
/// <summary>
|
||||
/// A classified item after the server-confirmed move into owned inventory.
|
||||
/// This is VTank's custom-action item ledger boundary.
|
||||
/// </summary>
|
||||
public readonly record struct PluginLootedItem(
|
||||
PluginInventoryItem Item,
|
||||
PluginLootAction Action);
|
||||
|
||||
public interface IPluginLootClassifier
|
||||
{
|
||||
PluginLootClassification Classify(
|
||||
in PluginLootClassificationContext context);
|
||||
|
||||
void OnLooted(in PluginLootedItem item) { }
|
||||
|
||||
void OnItemRemoved(uint objectId) { }
|
||||
}
|
||||
|
||||
public readonly record struct PluginLootClassifierInfo(
|
||||
string Id,
|
||||
string DisplayName);
|
||||
|
||||
/// <summary>
|
||||
/// Machine-local, in-process classifier exchange. Registration lifetime is
|
||||
/// scoped to the owning plugin by the host; callers never retain an unloaded
|
||||
/// plugin's classifier.
|
||||
/// </summary>
|
||||
public interface IPluginLootClassifierRegistry
|
||||
{
|
||||
IReadOnlyList<PluginLootClassifierInfo> Available =>
|
||||
Array.Empty<PluginLootClassifierInfo>();
|
||||
|
||||
IDisposable Register(
|
||||
string classifierId,
|
||||
string displayName,
|
||||
IPluginLootClassifier classifier) =>
|
||||
throw new NotSupportedException("Loot classifiers are unavailable.");
|
||||
|
||||
bool TryClassify(
|
||||
string classifierId,
|
||||
in PluginLootClassificationContext context,
|
||||
out PluginLootClassification classification)
|
||||
{
|
||||
classification = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryNotifyLooted(
|
||||
string classifierId,
|
||||
in PluginLootedItem item) => false;
|
||||
|
||||
bool TryNotifyItemRemoved(
|
||||
string classifierId,
|
||||
uint objectId) => false;
|
||||
}
|
||||
|
||||
public sealed class NoOpPluginLootClassifierRegistry
|
||||
: IPluginLootClassifierRegistry
|
||||
{
|
||||
public static NoOpPluginLootClassifierRegistry Instance { get; } = new();
|
||||
private NoOpPluginLootClassifierRegistry() { }
|
||||
}
|
||||
11
src/AcDream.Plugin.Abstractions/MagicAutomation.cs
Normal file
11
src/AcDream.Plugin.Abstractions/MagicAutomation.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>One authoritative completion of a spell request.</summary>
|
||||
public readonly record struct PluginCastCompletion(
|
||||
long Revision,
|
||||
uint SpellId,
|
||||
uint TargetObjectId,
|
||||
uint WeenieError)
|
||||
{
|
||||
public bool IsSuccess => Revision != 0 && WeenieError == 0u;
|
||||
}
|
||||
115
src/AcDream.Plugin.Abstractions/NavigationAutomation.cs
Normal file
115
src/AcDream.Plugin.Abstractions/NavigationAutomation.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Stable Asheron's Call map coordinate. East/west and north/south use the
|
||||
/// familiar in-game coordinate scale (for example 33.5S, 72.8E); elevation is
|
||||
/// expressed in metres. The cell id is retained because indoor coordinates do
|
||||
/// not have a meaningful outdoor compass label.
|
||||
/// </summary>
|
||||
public readonly record struct PluginNavigationPosition(
|
||||
uint CellId,
|
||||
double EastWest,
|
||||
double NorthSouth,
|
||||
double Elevation,
|
||||
float HeadingDegrees,
|
||||
bool IsOutdoor)
|
||||
{
|
||||
public double HorizontalDistanceMeters(in PluginNavigationPosition other)
|
||||
{
|
||||
double dx = EastWest - other.EastWest;
|
||||
double dy = NorthSouth - other.NorthSouth;
|
||||
return Math.Sqrt(dx * dx + dy * dy) * 240d;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One live object's canonical identity, name, and position.</summary>
|
||||
public readonly record struct PluginNavigationObject(
|
||||
uint ObjectId,
|
||||
string Name,
|
||||
PluginNavigationPosition Position)
|
||||
{
|
||||
public bool IsDoor { get; init; }
|
||||
public bool IsOpen { get; init; }
|
||||
public bool IsLocked { get; init; }
|
||||
public bool HasLockState { get; init; }
|
||||
public int LockDifficulty { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>The local movement state sampled atomically by a plugin tick.</summary>
|
||||
public readonly record struct PluginNavigationSnapshot(
|
||||
bool IsAvailable,
|
||||
bool IsPortalSpace,
|
||||
uint LocalObjectId,
|
||||
PluginNavigationPosition Position,
|
||||
bool IsMoving,
|
||||
bool IsAirborne)
|
||||
{
|
||||
/// <summary>
|
||||
/// Last position accepted from the server for the local player. Ordinary
|
||||
/// point navigation uses the live physics position; VTank checkpoints use
|
||||
/// this acknowledgement so client prediction cannot advance the route.
|
||||
/// </summary>
|
||||
public PluginNavigationPosition ConfirmedPosition { get; init; }
|
||||
public ulong ConfirmedPositionRevision { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Semantic movement levels. They are applied through the same Runtime-owned
|
||||
/// command-interpreter input state as the keyboard; no plugin-only physics or
|
||||
/// movement model exists.
|
||||
/// </summary>
|
||||
public readonly record struct PluginMovementIntent(
|
||||
bool Forward = false,
|
||||
bool Backward = false,
|
||||
bool StrafeLeft = false,
|
||||
bool StrafeRight = false,
|
||||
bool TurnLeft = false,
|
||||
bool TurnRight = false,
|
||||
bool Run = true,
|
||||
bool Jump = false);
|
||||
|
||||
public enum PluginNavigationCommandStatus
|
||||
{
|
||||
Unavailable = 0,
|
||||
Accepted,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host navigation primitives. Route sequencing, path policy, following, and
|
||||
/// waypoint behavior belong to the plugin (as they did in VTank); the host
|
||||
/// exposes only canonical positions and command-interpreter movement.
|
||||
/// </summary>
|
||||
public interface INavigationAutomation
|
||||
{
|
||||
PluginNavigationSnapshot Snapshot { get; }
|
||||
|
||||
bool TryGetObject(uint objectId, out PluginNavigationObject value);
|
||||
|
||||
/// <summary>
|
||||
/// Reacquire a world object whose session-scoped id changed, choosing the
|
||||
/// nearest exact-name match to a saved route position. VTank uses this for
|
||||
/// its Portal2 and UseNPC waypoint records instead of trusting a stale id.
|
||||
/// </summary>
|
||||
bool TryFindObject(
|
||||
string name,
|
||||
in PluginNavigationPosition near,
|
||||
double maximumDistanceMeters,
|
||||
out PluginNavigationObject value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detached live world-object projection used by plugin-owned proximity
|
||||
/// policies such as VTank's door opener. Hosts may return an empty list.
|
||||
/// </summary>
|
||||
IReadOnlyList<PluginNavigationObject> CaptureObjects() =>
|
||||
Array.Empty<PluginNavigationObject>();
|
||||
|
||||
PluginNavigationCommandStatus SetMovementIntent(
|
||||
in PluginMovementIntent intent);
|
||||
|
||||
PluginNavigationCommandStatus ClearMovementIntent();
|
||||
}
|
||||
28
src/AcDream.Plugin.Abstractions/NetworkAutomation.cs
Normal file
28
src/AcDream.Plugin.Abstractions/NetworkAutomation.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// One other live acdream client discovered by the host's local peer service.
|
||||
/// The shape mirrors UtilityBelt's ClientData expression contract.
|
||||
/// </summary>
|
||||
public readonly record struct PluginNetworkClient(
|
||||
uint ClientId,
|
||||
uint PlayerId,
|
||||
string Name,
|
||||
string WorldName,
|
||||
PluginNavigationPosition Position,
|
||||
IReadOnlyList<string> Tags,
|
||||
uint CurrentHealth,
|
||||
uint CurrentMana,
|
||||
uint CurrentStamina,
|
||||
uint MaxHealth,
|
||||
uint MaxMana,
|
||||
uint MaxStamina,
|
||||
float Heading);
|
||||
|
||||
/// <summary>Read-only discovery of other local acdream client processes.</summary>
|
||||
public interface INetworkAutomation
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
IReadOnlyList<PluginNetworkClient> CaptureClients() =>
|
||||
Array.Empty<PluginNetworkClient>();
|
||||
}
|
||||
48
src/AcDream.Plugin.Abstractions/PluginCommands.cs
Normal file
48
src/AcDream.Plugin.Abstractions/PluginCommands.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>One locally handled slash/at command submitted by the player.</summary>
|
||||
public readonly record struct PluginCommand(
|
||||
string Verb,
|
||||
string Arguments,
|
||||
string RawText);
|
||||
|
||||
/// <summary>
|
||||
/// Process-local command registration for gameplay plugins. Registered verbs
|
||||
/// run before an unknown command is sent to the game server, so plugin commands
|
||||
/// work from typed chat, launcher login commands, and other plugins' normal
|
||||
/// chat-submit path.
|
||||
/// </summary>
|
||||
public interface IPluginCommandRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Register one bare verb (for example <c>vt</c>, without a leading slash).
|
||||
/// Matching is case-insensitive and accepts both retail command prefixes.
|
||||
/// The returned lease removes only this exact registration.
|
||||
/// </summary>
|
||||
IDisposable Register(string verb, Action<PluginCommand> handler);
|
||||
}
|
||||
|
||||
/// <summary>Inert command surface for hosts that cannot route local commands.</summary>
|
||||
public sealed class NoOpPluginCommandRegistry : IPluginCommandRegistry
|
||||
{
|
||||
public static NoOpPluginCommandRegistry Instance { get; } = new();
|
||||
|
||||
private NoOpPluginCommandRegistry()
|
||||
{
|
||||
}
|
||||
|
||||
public IDisposable Register(string verb, Action<PluginCommand> handler)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(verb);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
return NoOpLease.Instance;
|
||||
}
|
||||
|
||||
private sealed class NoOpLease : IDisposable
|
||||
{
|
||||
public static NoOpLease Instance { get; } = new();
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/AcDream.Plugin.Abstractions/ProjectileAutomation.cs
Normal file
91
src/AcDream.Plugin.Abstractions/ProjectileAutomation.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>The trajectory family VTank asks the client to validate.</summary>
|
||||
public enum PluginProjectilePathKind
|
||||
{
|
||||
Straight = 0,
|
||||
Arc,
|
||||
Missile,
|
||||
}
|
||||
|
||||
/// <summary>Why a projectile-path query did or did not admit the shot.</summary>
|
||||
public enum PluginProjectilePathStatus
|
||||
{
|
||||
Unavailable = 0,
|
||||
Clear,
|
||||
Blocked,
|
||||
InvalidTarget,
|
||||
BudgetExceeded,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>One VTank collision-debug marker in client world coordinates.</summary>
|
||||
public readonly record struct PluginProjectileDebugSample(
|
||||
Vector3 WorldPosition,
|
||||
bool IsClear,
|
||||
float Radius);
|
||||
|
||||
/// <summary>
|
||||
/// Detached result of one bounded collision probe. The host reports geometry;
|
||||
/// the plugin still decides whether to cast, fire, or choose a fallback.
|
||||
/// </summary>
|
||||
public readonly record struct PluginProjectilePathResult(
|
||||
PluginProjectilePathStatus Status,
|
||||
int CollisionChecks = 0,
|
||||
uint BlockingObjectId = 0u,
|
||||
string? Notice = null)
|
||||
{
|
||||
public bool IsClear => Status == PluginProjectilePathStatus.Clear;
|
||||
public IReadOnlyList<PluginProjectileDebugSample> DebugSamples
|
||||
{ get; init; } = Array.Empty<PluginProjectileDebugSample>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical client-world projectile collision projection. Implementations
|
||||
/// must use the same resident collision world as ordinary client physics and
|
||||
/// must never fabricate a successful path when that world is unavailable.
|
||||
/// </summary>
|
||||
public interface IProjectileAutomation
|
||||
{
|
||||
bool IsAvailable => false;
|
||||
|
||||
PluginProjectilePathResult EvaluatePath(
|
||||
uint targetObjectId,
|
||||
PluginProjectilePathKind kind,
|
||||
PluginAttackHeight targetHeight,
|
||||
float projectileRadius,
|
||||
float stepDistance,
|
||||
int maximumCollisionChecks) =>
|
||||
new(PluginProjectilePathStatus.Unavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Same bounded query with VTank's optional per-quantum debug markers.
|
||||
/// Older hosts safely fall back to the ordinary result.
|
||||
/// </summary>
|
||||
PluginProjectilePathResult EvaluatePathWithDiagnostics(
|
||||
uint targetObjectId,
|
||||
PluginProjectilePathKind kind,
|
||||
PluginAttackHeight targetHeight,
|
||||
float projectileRadius,
|
||||
float stepDistance,
|
||||
int maximumCollisionChecks) =>
|
||||
EvaluatePath(
|
||||
targetObjectId,
|
||||
kind,
|
||||
targetHeight,
|
||||
projectileRadius,
|
||||
stepDistance,
|
||||
maximumCollisionChecks);
|
||||
|
||||
/// <summary>
|
||||
/// Presents a transient copy of diagnostic samples in the game view.
|
||||
/// Graphical hosts draw VTank's green clear/red blocked markers; headless
|
||||
/// and older hosts deliberately ignore the request.
|
||||
/// </summary>
|
||||
void ShowDebugSamples(
|
||||
IReadOnlyList<PluginProjectileDebugSample> samples)
|
||||
{
|
||||
}
|
||||
}
|
||||
21
src/AcDream.Plugin.Abstractions/RecoveryAutomation.cs
Normal file
21
src/AcDream.Plugin.Abstractions/RecoveryAutomation.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>Result of one explicit operator recovery operation.</summary>
|
||||
public readonly record struct PluginRecoveryResult(
|
||||
bool Accepted,
|
||||
int PreviousCount = 0,
|
||||
int CurrentCount = 0,
|
||||
string Message = "");
|
||||
|
||||
/// <summary>
|
||||
/// Narrow debug/recovery access to host-owned action state. Normal plugin
|
||||
/// policy must wait for authoritative receipts; these operations exist for
|
||||
/// VTank-compatible operator commands that deliberately recover a stuck
|
||||
/// client-side reference.
|
||||
/// </summary>
|
||||
public interface IRecoveryAutomation
|
||||
{
|
||||
PluginRecoveryResult ClearOneBusyReference() => new(
|
||||
Accepted: false,
|
||||
Message: "Action recovery is unavailable on this host.");
|
||||
}
|
||||
18
src/AcDream.Plugin.Abstractions/SelectionAutomation.cs
Normal file
18
src/AcDream.Plugin.Abstractions/SelectionAutomation.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Retail target-cycle actions needed by automation which intentionally
|
||||
/// changes selection. These invoke the same selection query/controller as
|
||||
/// keyboard bindings; plugins do not synthesize physical key input.
|
||||
/// </summary>
|
||||
public enum PluginSelectionAction
|
||||
{
|
||||
PreviousSelection = 0,
|
||||
PreviousPlayer,
|
||||
NextPlayer,
|
||||
}
|
||||
|
||||
public interface ISelectionAutomation
|
||||
{
|
||||
bool Execute(PluginSelectionAction action) => false;
|
||||
}
|
||||
117
src/AcDream.Plugin.Abstractions/WorldObjectAutomation.cs
Normal file
117
src/AcDream.Plugin.Abstractions/WorldObjectAutomation.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
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>
|
||||
/// 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);
|
||||
}
|
||||
20
src/AcDream.Plugin.Abstractions/WorldTimeAutomation.cs
Normal file
20
src/AcDream.Plugin.Abstractions/WorldTimeAutomation.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace AcDream.Plugin.Abstractions;
|
||||
|
||||
/// <summary>Authoritative Dereth calendar facts projected from Runtime.</summary>
|
||||
public readonly record struct PluginWorldTimeSnapshot(
|
||||
bool IsAvailable,
|
||||
double GameTicks,
|
||||
int Year,
|
||||
int Month,
|
||||
int Day,
|
||||
int Hour,
|
||||
string MonthName,
|
||||
string HourName,
|
||||
bool IsDay,
|
||||
double MinutesUntilDay,
|
||||
double MinutesUntilNight);
|
||||
|
||||
public interface IWorldTimeAutomation
|
||||
{
|
||||
PluginWorldTimeSnapshot Snapshot => default;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue