namespace AcDream.Plugin.Abstractions;
/// How far a character has taken a skill.
public enum PluginSkillTraining
{
Unknown = 0,
Untrained,
Trained,
Specialized,
}
///
/// One spell, as much of it as a plugin needs to make its own decisions.
///
///
/// Deliberately data, not policy. The host publishes what the spell
/// table says; the plugin decides what to cast and when. That line is the whole
/// architectural point of this surface — a Virindi-Tank-class engine belongs in
/// plugin-land, built on host primitives, exactly as VTank itself was built on
/// Decal's.
///
///
/// Retail's stacking bucket, and the correct identity for a duration
/// buff line. It is NOT a safe identity in general: the instantaneous vital
/// transfers share a family per source vital, so family 89 holds both
/// "Stamina to Health" and "Stamina to Mana". Group by family only after
/// filtering to duration buffs.
///
///
/// Retail's spell Generation — the roman-numeral level. Higher is
/// stronger within a family.
///
///
/// Skill id of the magic school that casts this spell, so a plugin can weigh
/// the character's skill in that school against .
///
///
/// Retail's own spell description. Load-bearing rather than cosmetic: the
/// client's spell table carries no link between a spell and the stat it
/// raises — that arrives from the server with the enchantment — but the
/// description states it in words ("Increases the caster's Life Magic skill by
/// 10 points"), so a plugin can derive the mapping from shipped data instead of
/// hard-coding one.
///
public readonly record struct PluginSpellInfo(
uint SpellId,
string Name,
uint Family,
int Tier,
int Difficulty,
int ManaCost,
float DurationSeconds,
uint School,
string Description,
bool IsSelfTargeted,
bool IsBeneficial);
/// One enchantment currently in force on the local player.
public readonly record struct PluginActiveEnchantment(
uint SpellId,
uint Family,
int Tier,
double SecondsRemaining);
/// One of the character's skills, named from the retail skill table.
public readonly record struct PluginSkillInfo(
uint SkillId,
string Name,
PluginSkillTraining Training,
uint Current);
/// One primary attribute. is 0..5.
public readonly record struct PluginAttributeInfo(
int Kind,
string Name,
uint Current);
/// Why a cast would or would not be accepted right now.
public enum PluginCastGate
{
/// No live session, or the surface is not bound yet.
Unavailable = 0,
Ready,
NotKnown,
/// A cast is already in flight.
Busy,
/// The host rejected it for a reason not modelled here.
Refused,
}
/// Local-player reads a plugin needs to decide what to cast.
public interface ICharacterInfo
{
bool IsInWorld { get; }
uint CurrentHealth { get; }
uint MaxHealth { get; }
uint CurrentStamina { get; }
uint MaxStamina { get; }
uint CurrentMana { get; }
uint MaxMana { get; }
/// Skills the character has, with training state and current level.
IReadOnlyList Skills { get; }
/// The six primary attributes.
IReadOnlyList Attributes { get; }
///
/// Enchantments in force on the local player. Snapshot semantics: the list
/// is rebuilt by the host, never mutated in place under a reader.
///
IReadOnlyList ActiveEnchantments { get; }
bool TryGetSkill(uint skillId, out PluginSkillInfo skill);
}
/// Spell-table data, filtered to what the local character knows.
public interface ISpellCatalog
{
///
/// Every spell in the character's spellbook that targets self and is
/// beneficial.
///
IReadOnlyList KnownSelfBuffs { get; }
bool TryGet(uint spellId, out PluginSpellInfo info);
}
/// Casting, with a preflight so a plugin need not guess.
public interface IMagicCommands
{
bool IsCasting { get; }
PluginCastGate EvaluateGate(uint spellId);
///
/// Request a cast. Returns whether the request was accepted for dispatch —
/// not whether the spell ultimately lands, which the server decides.
///
bool Cast(uint spellId);
}
///
/// The automation surface: reads, spell data, and commands, grouped so
/// grows by one member rather than several.
///
public interface IAutomationSurface
{
///
/// on hosts that never bind a live session, and
/// while a graphical host is between sessions — including at character
/// select, which is what lets a plugin panel stay hidden until there is a
/// character to act on.
///
bool IsAvailable { get; }
ICharacterInfo Character { get; }
ISpellCatalog Spells { get; }
IMagicCommands Magic { get; }
}
///
/// BCL-only inert surface for hosts with no live session. Every read is empty
/// and every command refuses, so a plugin can keep one code path.
///
public sealed class NoOpAutomationSurface
: IAutomationSurface, ICharacterInfo, ISpellCatalog, IMagicCommands
{
public static NoOpAutomationSurface Instance { get; } = new();
private NoOpAutomationSurface()
{
}
public bool IsAvailable => false;
public ICharacterInfo Character => this;
public ISpellCatalog Spells => this;
public IMagicCommands Magic => this;
public bool IsInWorld => false;
public uint CurrentHealth => 0;
public uint MaxHealth => 0;
public uint CurrentStamina => 0;
public uint MaxStamina => 0;
public uint CurrentMana => 0;
public uint MaxMana => 0;
public IReadOnlyList Skills { get; } = Array.Empty();
public IReadOnlyList Attributes { get; } =
Array.Empty();
public IReadOnlyList ActiveEnchantments { get; } =
Array.Empty();
public IReadOnlyList KnownSelfBuffs { get; } =
Array.Empty();
public bool TryGetSkill(uint skillId, out PluginSkillInfo skill)
{
skill = default;
return false;
}
public bool TryGet(uint spellId, out PluginSpellInfo info)
{
info = default;
return false;
}
public bool IsCasting => false;
public PluginCastGate EvaluateGate(uint spellId) => PluginCastGate.Unavailable;
public bool Cast(uint spellId) => false;
}