Inventory: the backend's item record and the framework-key rebuild
The backend reads items in the shape the original plugin serialized them: its item record's fields and every public getter, with property tables keyed the way the original's client framework keyed them (raw property ids mixed with framework pseudo keys). The record is carried over member for member, with the getters' lookup tables verbatim, so Newtonsoft writes the same members in the same order with the same JSON types. The builder rebuilds the framework keys from what the plugin contract exposes. Which keys exist, and when, follows the original's own item dumps (3058 items): a header field's key is present exactly when the server sent the field, the value key always (-1 when unsent), the underlay key with the second header word, spell counts with a non-empty appraised spell book, weapon and armor keys with their profiles; the slot key never appears. Header words the contract does not expose yet (header flag words, physics words, hook type, parent) sit behind one seam that reports them absent, and while the header flags are absent a field's presence falls back to its value being non-zero. Tables are written in ascending key order: the original's order was its framework's hash-table order and cannot be reproduced; the backend reads by key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5dbb294a96
commit
f4a44bd9fc
5 changed files with 1202 additions and 0 deletions
262
src/OpenAC.MosswartMassacre/Inventory/DecalItemBuilder.cs
Normal file
262
src/OpenAC.MosswartMassacre/Inventory/DecalItemBuilder.cs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace OpenAC.MosswartMassacre.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Everything the builder reads for one item, gathered on the tick thread.
|
||||
/// </summary>
|
||||
/// <param name="Item">The item's header fields as the host reports them.</param>
|
||||
/// <param name="Properties">The item's raw property tables and appraisal profiles, when the host has them.</param>
|
||||
/// <param name="HasIdData">True once the item has been appraised this session.</param>
|
||||
/// <param name="LastIdTime">When it was appraised, in Unix seconds; zero when it has not been.</param>
|
||||
/// <param name="Words">The header words the contract does not expose yet.</param>
|
||||
internal readonly record struct ItemSource(
|
||||
PluginInventoryItem Item,
|
||||
PluginItemProperties? Properties,
|
||||
bool HasIdData,
|
||||
int LastIdTime,
|
||||
ItemHeaderWords Words);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the item record the backend expects from what the host reports:
|
||||
/// the raw property tables as they are, plus the framework keys the original
|
||||
/// filled from the object's header and appraisal profiles.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Which keys appear, and when, follows the original's own item dumps: a
|
||||
/// header field's key is present exactly when the server sent that field,
|
||||
/// the value key is always present (-1 when the server sent none), the
|
||||
/// underlay key accompanies the second header word, the spell counts come
|
||||
/// with a non-empty appraised spell book, and the weapon and armor keys with
|
||||
/// their appraisal profiles. The slot key was never in the tables and is not
|
||||
/// written.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The tables are written in ascending key order. The original wrote them in
|
||||
/// its framework's hash-table order, which depends on the insertion history
|
||||
/// and cannot be reproduced; the backend reads them by key.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class DecalItemBuilder
|
||||
{
|
||||
/// <summary>The item's own active enchantments carry this bit in the appraised spell book.</summary>
|
||||
public const uint ActiveSpellBit = 0x80000000u;
|
||||
|
||||
private const uint IconBase = 0x06000000u;
|
||||
|
||||
/// <summary>The original's <c>MyWorldObjectCreator.Create</c>.</summary>
|
||||
public static MyWorldObject Create(in ItemSource source)
|
||||
{
|
||||
PluginInventoryItem item = source.Item;
|
||||
ItemHeaderWords words = source.Words;
|
||||
var ints = new Dictionary<int, int>();
|
||||
var doubles = new Dictionary<int, double>();
|
||||
var bools = new Dictionary<int, bool>();
|
||||
var strings = new Dictionary<int, string>();
|
||||
|
||||
if (source.Properties is { } properties)
|
||||
{
|
||||
foreach (KeyValuePair<uint, int> kvp in properties.Ints ?? EmptyInts)
|
||||
ints[unchecked((int)kvp.Key)] = kvp.Value;
|
||||
foreach (KeyValuePair<uint, double> kvp in properties.Floats ?? EmptyDoubles)
|
||||
doubles[unchecked((int)kvp.Key)] = kvp.Value;
|
||||
foreach (KeyValuePair<uint, bool> kvp in properties.Bools ?? EmptyBools)
|
||||
bools[unchecked((int)kvp.Key)] = kvp.Value;
|
||||
foreach (KeyValuePair<uint, string> kvp in properties.Strings ?? EmptyStrings)
|
||||
strings[unchecked((int)kvp.Key)] = kvp.Value;
|
||||
}
|
||||
|
||||
bool Sent(uint flag, bool whenUnknown) =>
|
||||
words.WeenieHeaderFlags is uint flags ? (flags & flag) != 0u : whenUnknown;
|
||||
|
||||
// Always in the header.
|
||||
strings[DecalKeys.RawName] = item.Name;
|
||||
ints[DecalKeys.WeenieClassId] = unchecked((int)item.WeenieClassId);
|
||||
ints[DecalKeys.Icon] = ShortIcon(item.IconId);
|
||||
ints[DecalKeys.Category] = unchecked((int)item.ItemType);
|
||||
ints[DecalKeys.Behavior] = unchecked((int)item.PublicFlags);
|
||||
int container = ContainerOf(item);
|
||||
ints[DecalKeys.Container] = container;
|
||||
|
||||
if (Sent(HeaderFlags.PluralName, !string.IsNullOrEmpty(item.PluralName)))
|
||||
strings[DecalKeys.SecondaryName] = item.PluralName;
|
||||
if (Sent(HeaderFlags.ItemsCapacity | HeaderFlags.ContainersCapacity, item.ItemsCapacity != 0 || item.ContainersCapacity != 0))
|
||||
{
|
||||
ints[DecalKeys.ItemSlots] = item.ItemsCapacity;
|
||||
ints[DecalKeys.PackSlots] = item.ContainersCapacity;
|
||||
}
|
||||
if (Sent(HeaderFlags.Value, item.Value != 0))
|
||||
ints[DecalKeys.RawValue] = item.Value;
|
||||
else if (!ints.ContainsKey(DecalKeys.RawValue))
|
||||
ints[DecalKeys.RawValue] = -1;
|
||||
if (Sent(HeaderFlags.Usable, item.Useability != 0u))
|
||||
ints[DecalKeys.Usable] = unchecked((int)item.Useability);
|
||||
if (Sent(HeaderFlags.UseRadius, item.UseRadius != 0f))
|
||||
doubles[DecalKeys.ApproachDistance] = item.UseRadius;
|
||||
if (Sent(HeaderFlags.UiEffects, item.Effects != 0u))
|
||||
ints[DecalKeys.IconOutline] = unchecked((int)item.Effects);
|
||||
if (Sent(HeaderFlags.AmmoType, item.AmmoType != 0u))
|
||||
ints[DecalKeys.MissileType] = unchecked((int)item.AmmoType);
|
||||
if (Sent(HeaderFlags.CombatUse, item.CombatUse != 0))
|
||||
ints[DecalKeys.EquipType] = item.CombatUse;
|
||||
if (Sent(HeaderFlags.Structure | HeaderFlags.MaxStructure, item.MaximumStructure != 0 || item.Structure != 0))
|
||||
{
|
||||
ints[DecalKeys.RawMaxStructure] = item.MaximumStructure;
|
||||
ints[DecalKeys.RawStructure] = item.Structure;
|
||||
}
|
||||
if (Sent(HeaderFlags.StackSize | HeaderFlags.MaxStackSize, item.MaximumStackSize > 1))
|
||||
{
|
||||
ints[DecalKeys.StackCount] = item.StackSize;
|
||||
ints[DecalKeys.StackMax] = item.MaximumStackSize;
|
||||
}
|
||||
if (Sent(HeaderFlags.ValidLocations, item.ValidLocations != 0u))
|
||||
ints[DecalKeys.EquipableSlots] = unchecked((int)item.ValidLocations);
|
||||
if (Sent(HeaderFlags.CurrentlyWieldedLocation, item.EquippedLocation != 0u))
|
||||
ints[DecalKeys.RawEquippedSlots] = unchecked((int)item.EquippedLocation);
|
||||
if (Sent(HeaderFlags.Priority, item.CoverageMask != 0u))
|
||||
ints[DecalKeys.Coverage] = unchecked((int)item.CoverageMask);
|
||||
if (Sent(HeaderFlags.TargetType, item.TargetType != 0u))
|
||||
ints[DecalKeys.UsageMask] = unchecked((int)item.TargetType);
|
||||
if (Sent(HeaderFlags.Burden, item.Burden != 0))
|
||||
ints[DecalKeys.RawBurden] = item.Burden;
|
||||
if (Sent(HeaderFlags.Spell, item.SpellId != 0u))
|
||||
ints[DecalKeys.SpellDid] = unchecked((int)item.SpellId);
|
||||
if (Sent(HeaderFlags.Workmanship, item.Workmanship != 0f))
|
||||
doubles[DecalKeys.SalvageWorkmanship] = item.Workmanship;
|
||||
if (words.HookType is int hookType && Sent(HeaderFlags.HookType, true))
|
||||
ints[DecalKeys.HookType] = hookType;
|
||||
if (Sent(HeaderFlags.IconOverlay, item.IconOverlayId != 0u))
|
||||
ints[DecalKeys.IconOverlay] = ShortIcon(item.IconOverlayId);
|
||||
if (Sent(HeaderFlags.MaterialType, item.MaterialType != 0u))
|
||||
ints[DecalKeys.RawMaterial] = unchecked((int)item.MaterialType);
|
||||
|
||||
if ((item.PublicFlags & HeaderFlags.IncludesSecondHeader) != 0u)
|
||||
{
|
||||
ints[DecalKeys.IconUnderlay] = ShortIcon(item.IconUnderlayId);
|
||||
if (words.WeenieHeaderFlags2 is uint flags2)
|
||||
ints[DecalKeys.CreateFlags2] = unchecked((int)flags2);
|
||||
}
|
||||
|
||||
if (words.WeenieHeaderFlags is uint headerFlags)
|
||||
ints[DecalKeys.CreateFlags1] = unchecked((int)headerFlags);
|
||||
if (words.PhysicsDescriptionFlags is uint physicsFlags)
|
||||
ints[DecalKeys.PhysicsDataFlags] = unchecked((int)physicsFlags);
|
||||
if (words.PhysicsWord is uint physicsWord)
|
||||
ints[DecalKeys.PhysicsWord] = unchecked((int)physicsWord);
|
||||
if (words.SetupId is uint setup)
|
||||
ints[DecalKeys.Setup] = unchecked((int)setup);
|
||||
if (words.Scale is double scale)
|
||||
doubles[DecalKeys.Scale] = scale;
|
||||
if (words.ParentObjectId is uint parent)
|
||||
ints[DecalKeys.Wielder] = unchecked((int)parent);
|
||||
if (words.ParentLocation is int parentLocation)
|
||||
ints[DecalKeys.WieldingSlot] = parentLocation;
|
||||
|
||||
// From the appraisal.
|
||||
var spells = new List<int>();
|
||||
var activeSpells = new List<int>();
|
||||
IReadOnlyList<uint> spellBook = item.AppraisedSpellIds ?? Array.Empty<uint>();
|
||||
foreach (uint entry in spellBook)
|
||||
{
|
||||
if ((entry & ActiveSpellBit) != 0u)
|
||||
activeSpells.Add(unchecked((int)(entry & ~ActiveSpellBit)));
|
||||
else
|
||||
spells.Add(unchecked((int)entry));
|
||||
}
|
||||
if (spellBook.Count > 0)
|
||||
{
|
||||
ints[DecalKeys.SpellCount] = spells.Count;
|
||||
ints[DecalKeys.ActiveSpellCount] = activeSpells.Count;
|
||||
}
|
||||
|
||||
if (source.Properties?.WeaponProfile is { } weapon)
|
||||
{
|
||||
ints[DecalKeys.WeapSpeed] = weapon.WeaponTime;
|
||||
ints[DecalKeys.EquipSkill] = unchecked((int)weapon.WeaponSkill);
|
||||
ints[DecalKeys.DamageType] = weapon.DamageType;
|
||||
ints[DecalKeys.MaxDamage] = weapon.Damage;
|
||||
doubles[DecalKeys.Variance] = weapon.DamageVariance;
|
||||
doubles[DecalKeys.AttackBonus] = weapon.WeaponOffense;
|
||||
doubles[DecalKeys.Range] = weapon.MaxVelocity;
|
||||
doubles[DecalKeys.DamageBonus] = weapon.DamageMod;
|
||||
}
|
||||
|
||||
if (source.Properties?.ArmorProfile is { } armor)
|
||||
{
|
||||
doubles[DecalKeys.SlashProt] = armor.SlashMod;
|
||||
doubles[DecalKeys.PierceProt] = armor.PierceMod;
|
||||
doubles[DecalKeys.BludgeonProt] = armor.BludgeonMod;
|
||||
doubles[DecalKeys.AcidProt] = armor.AcidMod;
|
||||
doubles[DecalKeys.LightningProt] = armor.ElectricMod;
|
||||
doubles[DecalKeys.FireProt] = armor.FireMod;
|
||||
doubles[DecalKeys.ColdProt] = armor.ColdMod;
|
||||
}
|
||||
|
||||
return new MyWorldObject
|
||||
{
|
||||
HasIdData = source.HasIdData,
|
||||
Id = unchecked((int)item.ObjectId),
|
||||
LastIdTime = source.LastIdTime,
|
||||
ObjectClass = (int)item.ObjectClass,
|
||||
Icon = ShortIcon(item.IconId),
|
||||
ContainerId = container,
|
||||
BoolValues = Sorted(bools),
|
||||
DoubleValues = Sorted(doubles),
|
||||
IntValues = Sorted(ints),
|
||||
StringValues = Sorted(strings),
|
||||
ActiveSpells = activeSpells,
|
||||
Spells = spells,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The original's <c>MyWorldObjectCreator.Combine</c>: an item remembered
|
||||
/// with appraisal data from an earlier session keeps that data while the
|
||||
/// live item has none, refreshed by whatever the live item does carry.
|
||||
/// </summary>
|
||||
public static MyWorldObject Combine(MyWorldObject older, MyWorldObject newer)
|
||||
{
|
||||
// Repairs an icon missing from an older saved record.
|
||||
if (older.Icon == 0 && newer.Icon != 0)
|
||||
older.Icon = newer.Icon;
|
||||
|
||||
if (!older.HasIdData || newer.HasIdData)
|
||||
return newer;
|
||||
|
||||
older.AddTo(newer.BoolValues, newer.DoubleValues, newer.IntValues, newer.StringValues);
|
||||
older.ContainerId = newer.ContainerId;
|
||||
older.BoolValues = Sorted(older.BoolValues);
|
||||
older.DoubleValues = Sorted(older.DoubleValues);
|
||||
older.IntValues = Sorted(older.IntValues);
|
||||
older.StringValues = Sorted(older.StringValues);
|
||||
return older;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An icon id as the framework reported it: without the icon table's
|
||||
/// 0x06000000 prefix.
|
||||
/// </summary>
|
||||
public static int ShortIcon(uint iconId) =>
|
||||
iconId >= IconBase ? unchecked((int)(iconId - IconBase)) : unchecked((int)iconId);
|
||||
|
||||
/// <summary>
|
||||
/// The framework's container of an item: the pack holding it, or the
|
||||
/// creature wielding it when it is wielded.
|
||||
/// </summary>
|
||||
public static int ContainerOf(in PluginInventoryItem item) =>
|
||||
unchecked((int)(item.ContainerObjectId != 0u ? item.ContainerObjectId : item.WielderObjectId));
|
||||
|
||||
private static Dictionary<int, T> Sorted<T>(Dictionary<int, T> table)
|
||||
{
|
||||
var sorted = new Dictionary<int, T>(table.Count);
|
||||
foreach (int key in table.Keys.Order())
|
||||
sorted[key] = table[key];
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<uint, int> EmptyInts = new Dictionary<uint, int>();
|
||||
private static readonly IReadOnlyDictionary<uint, double> EmptyDoubles = new Dictionary<uint, double>();
|
||||
private static readonly IReadOnlyDictionary<uint, bool> EmptyBools = new Dictionary<uint, bool>();
|
||||
private static readonly IReadOnlyDictionary<uint, string> EmptyStrings = new Dictionary<uint, string>();
|
||||
}
|
||||
110
src/OpenAC.MosswartMassacre/Inventory/DecalKeys.cs
Normal file
110
src/OpenAC.MosswartMassacre/Inventory/DecalKeys.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
namespace OpenAC.MosswartMassacre.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// The pseudo keys the original's client framework mixed into an item's
|
||||
/// property tables next to the raw game property ids. The backend reads
|
||||
/// them by these numbers, so they are part of the wire.
|
||||
/// </summary>
|
||||
internal static class DecalKeys
|
||||
{
|
||||
// Integer table.
|
||||
public const int WeenieClassId = 218103808;
|
||||
public const int Icon = 218103809;
|
||||
public const int Container = 218103810;
|
||||
public const int ItemSlots = 218103812;
|
||||
public const int PackSlots = 218103813;
|
||||
public const int StackCount = 218103814;
|
||||
public const int StackMax = 218103815;
|
||||
public const int SpellDid = 218103816;
|
||||
public const int Wielder = 218103818;
|
||||
public const int WieldingSlot = 218103819;
|
||||
public const int Coverage = 218103821;
|
||||
public const int EquipableSlots = 218103822;
|
||||
public const int EquipType = 218103823;
|
||||
public const int IconOutline = 218103824;
|
||||
public const int MissileType = 218103825;
|
||||
public const int UsageMask = 218103826;
|
||||
public const int HookType = 218103828;
|
||||
public const int Setup = 218103830;
|
||||
public const int PhysicsWord = 218103831;
|
||||
public const int CreateFlags1 = 218103832;
|
||||
public const int CreateFlags2 = 218103833;
|
||||
public const int Category = 218103834;
|
||||
public const int Behavior = 218103835;
|
||||
public const int SpellCount = 218103838;
|
||||
public const int WeapSpeed = 218103839;
|
||||
public const int EquipSkill = 218103840;
|
||||
public const int DamageType = 218103841;
|
||||
public const int MaxDamage = 218103842;
|
||||
public const int Usable = 218103843;
|
||||
public const int PhysicsDataFlags = 218103847;
|
||||
public const int ActiveSpellCount = 218103848;
|
||||
public const int IconOverlay = 218103849;
|
||||
public const int IconUnderlay = 218103850;
|
||||
|
||||
// Floating-point table.
|
||||
public const int SlashProt = 167772160;
|
||||
public const int PierceProt = 167772161;
|
||||
public const int BludgeonProt = 167772162;
|
||||
public const int AcidProt = 167772163;
|
||||
public const int LightningProt = 167772164;
|
||||
public const int FireProt = 167772165;
|
||||
public const int ColdProt = 167772166;
|
||||
public const int ApproachDistance = 167772168;
|
||||
public const int SalvageWorkmanship = 167772169;
|
||||
public const int Scale = 167772170;
|
||||
public const int Variance = 167772171;
|
||||
public const int AttackBonus = 167772172;
|
||||
public const int Range = 167772173;
|
||||
public const int DamageBonus = 167772174;
|
||||
|
||||
// Text table.
|
||||
public const int SecondaryName = 184549376;
|
||||
|
||||
// Raw game property ids the framework filled from the object's header.
|
||||
public const int RawBurden = 5;
|
||||
public const int RawEquippedSlots = 10;
|
||||
public const int RawValue = 19;
|
||||
public const int RawMaxStructure = 91;
|
||||
public const int RawStructure = 92;
|
||||
public const int RawMaterial = 131;
|
||||
public const int RawName = 1;
|
||||
public const int RawManaRate = 5;
|
||||
public const int RawCurrentMana = 107;
|
||||
public const int RawMaximumMana = 108;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The bits of the object header's first flag word, which say which optional
|
||||
/// header fields the server sent. The framework's key tables held a field
|
||||
/// exactly when its bit was set.
|
||||
/// </summary>
|
||||
internal static class HeaderFlags
|
||||
{
|
||||
public const uint PluralName = 0x00000001;
|
||||
public const uint ItemsCapacity = 0x00000002;
|
||||
public const uint ContainersCapacity = 0x00000004;
|
||||
public const uint Value = 0x00000008;
|
||||
public const uint Usable = 0x00000010;
|
||||
public const uint UseRadius = 0x00000020;
|
||||
public const uint UiEffects = 0x00000080;
|
||||
public const uint AmmoType = 0x00000100;
|
||||
public const uint CombatUse = 0x00000200;
|
||||
public const uint Structure = 0x00000400;
|
||||
public const uint MaxStructure = 0x00000800;
|
||||
public const uint StackSize = 0x00001000;
|
||||
public const uint MaxStackSize = 0x00002000;
|
||||
public const uint ValidLocations = 0x00010000;
|
||||
public const uint CurrentlyWieldedLocation = 0x00020000;
|
||||
public const uint Priority = 0x00040000;
|
||||
public const uint TargetType = 0x00080000;
|
||||
public const uint Burden = 0x00200000;
|
||||
public const uint Spell = 0x00400000;
|
||||
public const uint Workmanship = 0x01000000;
|
||||
public const uint HookType = 0x10000000;
|
||||
public const uint IconOverlay = 0x40000000;
|
||||
public const uint MaterialType = 0x80000000;
|
||||
|
||||
/// <summary>The public-flags bit that says a second header word follows.</summary>
|
||||
public const uint IncludesSecondHeader = 0x04000000;
|
||||
}
|
||||
441
src/OpenAC.MosswartMassacre/Inventory/ItemDictionaries.cs
Normal file
441
src/OpenAC.MosswartMassacre/Inventory/ItemDictionaries.cs
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
namespace OpenAC.MosswartMassacre.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// The lookup tables the item record's getters read (material, skill, mastery
|
||||
/// and item-set names; the item-spell effects behind the "buffed" and
|
||||
/// "calced" numbers). Copied verbatim from the original plugin's shared code:
|
||||
/// the getters' output is part of the wire, so a changed name or number here
|
||||
/// changes a frame.
|
||||
/// </summary>
|
||||
internal static class ItemDictionaries
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a dictionary of skill ids vs names
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static readonly Dictionary<int, string> SkillInfo = new Dictionary<int, string>
|
||||
{
|
||||
// This list was taken from the Alinco source
|
||||
{ 0x1, "Axe" },
|
||||
{ 0x2, "Bow" },
|
||||
{ 0x3, "Crossbow" },
|
||||
{ 0x4, "Dagger" },
|
||||
{ 0x5, "Mace" },
|
||||
{ 0x6, "Melee Defense" },
|
||||
{ 0x7, "Missile Defense" },
|
||||
{ 0x8, "Sling" },
|
||||
{ 0x9, "Spear" },
|
||||
{ 0xA, "Staff" },
|
||||
{ 0xB, "Sword" },
|
||||
{ 0xC, "Thrown Weapons" },
|
||||
{ 0xD, "Unarmed Combat" },
|
||||
{ 0xE, "Arcane Lore" },
|
||||
{ 0xF, "Magic Defense" },
|
||||
{ 0x10, "Mana Conversion" },
|
||||
{ 0x12, "Item Tinkering" },
|
||||
{ 0x13, "Assess Person" },
|
||||
{ 0x14, "Deception" },
|
||||
{ 0x15, "Healing" },
|
||||
{ 0x16, "Jump" },
|
||||
{ 0x17, "Lockpick" },
|
||||
{ 0x18, "Run" },
|
||||
{ 0x1B, "Assess Creature" },
|
||||
{ 0x1C, "Weapon Tinkering" },
|
||||
{ 0x1D, "Armor Tinkering" },
|
||||
{ 0x1E, "Magic Item Tinkering" },
|
||||
{ 0x1F, "Creature Enchantment" },
|
||||
{ 0x20, "Item Enchantment" },
|
||||
{ 0x21, "Life Magic" },
|
||||
{ 0x22, "War Magic" },
|
||||
{ 0x23, "Leadership" },
|
||||
{ 0x24, "Loyalty" },
|
||||
{ 0x25, "Fletching" },
|
||||
{ 0x26, "Alchemy" },
|
||||
{ 0x27, "Cooking" },
|
||||
{ 0x28, "Salvaging" },
|
||||
{ 0x29, "Two Handed Combat" },
|
||||
{ 0x2A, "Gearcraft"},
|
||||
{ 0x2B, "Void" },
|
||||
{ 0x2C, "Heavy Weapons" },
|
||||
{ 0x2D, "Light Weapons" },
|
||||
{ 0x2E, "Finesse Weapons" },
|
||||
{ 0x2F, "Missile Weapons" },
|
||||
{ 0x30, "Shield" },
|
||||
{ 0x31, "Dual Wield" },
|
||||
{ 0x32, "Recklessness" },
|
||||
{ 0x33, "Sneak Attack" },
|
||||
{ 0x34, "Dirty Fighting" },
|
||||
{ 0x35, "Challenge" },
|
||||
{ 0x36, "Summoning" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of mastery ids vs names
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static readonly Dictionary<int, string> MasteryInfo = new Dictionary<int, string>
|
||||
{
|
||||
{ 1, "Unarmed Weapon" },
|
||||
{ 2, "Sword" },
|
||||
{ 3, "Axe" },
|
||||
{ 4, "Mace" },
|
||||
{ 5, "Spear" },
|
||||
{ 6, "Dagger" },
|
||||
{ 7, "Staff" },
|
||||
{ 8, "Bow" },
|
||||
{ 9, "Crossbow" },
|
||||
{ 10, "Thrown" },
|
||||
{ 11, "Two Handed Combat" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of attribute set ids vs names
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static readonly Dictionary<int, string> AttributeSetInfo = new Dictionary<int, string>
|
||||
{
|
||||
// This list was taken from Virindi Tank Loot Editor
|
||||
// 01
|
||||
{ 02, "Test"},
|
||||
// 03
|
||||
{ 04, "Carraida's Benediction"},
|
||||
{ 05, "Noble Relic Set" },
|
||||
{ 06, "Ancient Relic Set" },
|
||||
{ 07, "Relic Alduressa Set" },
|
||||
{ 08, "Shou-jen Set" },
|
||||
{ 09, "Empyrean Rings Set" },
|
||||
{ 10, "Arm, Mind, Heart Set" },
|
||||
{ 11, "Coat of the Perfect Light Set" },
|
||||
{ 12, "Leggings of Perfect Light Set" },
|
||||
{ 13, "Soldier's Set" },
|
||||
{ 14, "Adept's Set" },
|
||||
{ 15, "Archer's Set" },
|
||||
{ 16, "Defender's Set" },
|
||||
{ 17, "Tinker's Set" },
|
||||
{ 18, "Crafter's Set" },
|
||||
{ 19, "Hearty Set" },
|
||||
{ 20, "Dexterous Set" },
|
||||
{ 21, "Wise Set" },
|
||||
{ 22, "Swift Set" },
|
||||
{ 23, "Hardenend Set" },
|
||||
{ 24, "Reinforced Set" },
|
||||
{ 25, "Interlocking Set" },
|
||||
{ 26, "Flame Proof Set" },
|
||||
{ 27, "Acid Proof Set" },
|
||||
{ 28, "Cold Proof Set" },
|
||||
{ 29, "Lightning Proof Set" },
|
||||
{ 30, "Dedication Set" },
|
||||
{ 31, "Gladiatorial Clothing Set" },
|
||||
{ 32, "Ceremonial Clothing" },
|
||||
{ 33, "Protective Clothing" },
|
||||
{ 34, "Noobie Armor" },
|
||||
{ 35, "Sigil of Defense" },
|
||||
{ 36, "Sigil of Destruction" },
|
||||
{ 37, "Sigil of Fury" },
|
||||
{ 38, "Sigil of Growth" },
|
||||
{ 39, "Sigil of Vigor" },
|
||||
{ 40, "Heroic Protector Set" },
|
||||
{ 41, "Heroic Destroyer Set" },
|
||||
{ 42, "Olthoi Armor D Red" },
|
||||
{ 43, "Olthoi Armor C Rat" },
|
||||
{ 44, "Olthoi Armor C Red" },
|
||||
{ 45, "Olthoi Armor D Rat" },
|
||||
{ 46, "Upgraded Relic Alduressa Set" },
|
||||
{ 47, "Upgraded Ancient Relic Set" },
|
||||
{ 48, "Upgraded Noble Relic Set" },
|
||||
{ 49, "Weave of Alchemy" },
|
||||
{ 50, "Weave of Arcane Lore" },
|
||||
{ 51, "Weave of Armor Tinkering" },
|
||||
{ 52, "Weave of Assess Person" },
|
||||
{ 53, "Weave of Light Weapons" },
|
||||
{ 54, "Weave of Missile Weapons" },
|
||||
{ 55, "Weave of Cooking" },
|
||||
{ 56, "Weave of Creature Enchantment" },
|
||||
{ 57, "Weave of Missile Weapons" },
|
||||
{ 58, "Weave of Finesse" },
|
||||
{ 59, "Weave of Deception" },
|
||||
{ 60, "Weave of Fletching" },
|
||||
{ 61, "Weave of Healing" },
|
||||
{ 62, "Weave of Item Enchantment" },
|
||||
{ 63, "Weave of Item Tinkering" },
|
||||
{ 64, "Weave of Leadership" },
|
||||
{ 65, "Weave of Life Magic" },
|
||||
{ 66, "Weave of Loyalty" },
|
||||
{ 67, "Weave of Light Weapons" },
|
||||
{ 68, "Weave of Magic Defense" },
|
||||
{ 69, "Weave of Magic Item Tinkering" },
|
||||
{ 70, "Weave of Mana Conversion" },
|
||||
{ 71, "Weave of Melee Defense" },
|
||||
{ 72, "Weave of Missile Defense" },
|
||||
{ 73, "Weave of Salvaging" },
|
||||
{ 74, "Weave of Light Weapons" },
|
||||
{ 75, "Weave of Light Weapons" },
|
||||
{ 76, "Weave of Heavy Weapons" },
|
||||
{ 77, "Weave of Missile Weapons" },
|
||||
{ 78, "Weave of Two Handed Combat" },
|
||||
{ 79, "Weave of Light Weapons" },
|
||||
{ 80, "Weave of Void Magic" },
|
||||
{ 81, "Weave of War Magic" },
|
||||
{ 82, "Weave of Weapon Tinkering" },
|
||||
{ 83, "Weave of Assess Creature " },
|
||||
{ 84, "Weave of Dirty Fighting" },
|
||||
{ 85, "Weave of Dual Wield" },
|
||||
{ 86, "Weave of Recklessness" },
|
||||
{ 87, "Weave of Shield" },
|
||||
{ 88, "Weave of Sneak Attack" },
|
||||
{ 89, "Ninja_New" },
|
||||
{ 90, "Weave of Summoning" },
|
||||
|
||||
{ 91, "Shrouded Soul" },
|
||||
{ 92, "Darkened Mind" },
|
||||
{ 93, "Clouded Spirit" },
|
||||
{ 94, "Minor Stinging Shrouded Soul" },
|
||||
{ 95, "Minor Sparking Shrouded Soul" },
|
||||
{ 96, "Minor Smoldering Shrouded Soul" },
|
||||
{ 97, "Minor Shivering Shrouded Soul" },
|
||||
{ 98, "Minor Stinging Darkened Mind" },
|
||||
{ 99, "Minor Sparking Darkened Mind" },
|
||||
|
||||
{ 100, "Minor Smoldering Darkened Mind" },
|
||||
{ 101, "Minor Shivering Darkened Mind" },
|
||||
{ 102, "Minor Stinging Clouded Spirit" },
|
||||
{ 103, "Minor Sparking Clouded Spirit" },
|
||||
{ 104, "Minor Smoldering Clouded Spirit" },
|
||||
{ 105, "Minor Shivering Clouded Spirit" },
|
||||
{ 106, "Major Stinging Shrouded Soul" },
|
||||
{ 107, "Major Sparking Shrouded Soul" },
|
||||
{ 108, "Major Smoldering Shrouded Soul" },
|
||||
{ 109, "Major Shivering Shrouded Soul" },
|
||||
|
||||
{ 110, "Major Stinging Darkened Mind" },
|
||||
{ 111, "Major Sparking Darkened Mind" },
|
||||
{ 112, "Major Smoldering Darkened Mind" },
|
||||
{ 113, "Major Shivering Darkened Mind" },
|
||||
{ 114, "Major Stinging Clouded Spirit" },
|
||||
{ 115, "Major Sparking Clouded Spirit" },
|
||||
{ 116, "Major Smoldering Clouded Spirit" },
|
||||
{ 117, "Major Shivering Clouded Spirit" },
|
||||
{ 118, "Blackfire Stinging Shrouded Soul" },
|
||||
{ 119, "Blackfire Sparking Shrouded Soul" },
|
||||
|
||||
{ 120, "Blackfire Smoldering Shrouded Soul" },
|
||||
{ 121, "Blackfire Shivering Shrouded Soul" },
|
||||
{ 122, "Blackfire Stinging Darkened Mind" },
|
||||
{ 123, "Blackfire Sparking Darkened Mind" },
|
||||
{ 124, "Blackfire Smoldering Darkened Mind" },
|
||||
{ 125, "Blackfire Shivering Darkened Mind" },
|
||||
{ 126, "Blackfire Stinging Clouded Spirit" },
|
||||
{ 127, "Blackfire Sparking Clouded Spirit" },
|
||||
{ 128, "Blackfire Smoldering Clouded Spirit" },
|
||||
{ 129, "Blackfire Shivering Clouded Spirit" },
|
||||
|
||||
{ 130, "Shimmering Shadows" },
|
||||
|
||||
{ 131, "Brown Society Locket" },
|
||||
{ 132, "Yellow Society Locket" },
|
||||
{ 133, "Red Society Band" },
|
||||
{ 134, "Green Society Band" },
|
||||
{ 135, "Purple Society Band" },
|
||||
{ 136, "Blue Society Band" },
|
||||
|
||||
{ 137, "Gauntlet Garb" },
|
||||
|
||||
{ 138, "UNKNOWN_138" }, // Possibly Paragon Missile Weapons
|
||||
{ 139, "UNKNOWN_139" }, // Possibly Paragon Casters
|
||||
{ 140, "UNKNOWN_140" }, // Possibly Paragon Melee Weapons
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of material ids vs names
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static readonly Dictionary<int, string> MaterialInfo = new Dictionary<int, string>
|
||||
{
|
||||
{ 1, "Ceramic" },
|
||||
{ 2, "Porcelain" },
|
||||
// 3
|
||||
{ 4, "Linen" },
|
||||
{ 5, "Satin" },
|
||||
{ 6, "Silk" },
|
||||
{ 7, "Velvet" },
|
||||
{ 8, "Wool" },
|
||||
// 9
|
||||
{ 10, "Agate" },
|
||||
{ 11, "Amber" },
|
||||
{ 12, "Amethyst" },
|
||||
{ 13, "Aquamarine" },
|
||||
{ 14, "Azurite" },
|
||||
{ 15, "Black Garnet" },
|
||||
{ 16, "Black Opal" },
|
||||
{ 17, "Bloodstone" },
|
||||
{ 18, "Carnelian" },
|
||||
{ 19, "Citrine" },
|
||||
{ 20, "Diamond" },
|
||||
{ 21, "Emerald" },
|
||||
{ 22, "Fire Opal" },
|
||||
{ 23, "Green Garnet" },
|
||||
{ 24, "Green Jade" },
|
||||
{ 25, "Hematite" },
|
||||
{ 26, "Imperial Topaz" },
|
||||
{ 27, "Jet" },
|
||||
{ 28, "Lapis Lazuli" },
|
||||
{ 29, "Lavender Jade" },
|
||||
{ 30, "Malachite" },
|
||||
{ 31, "Moonstone" },
|
||||
{ 32, "Onyx" },
|
||||
{ 33, "Opal" },
|
||||
{ 34, "Peridot" },
|
||||
{ 35, "Red Garnet" },
|
||||
{ 36, "Red Jade" },
|
||||
{ 37, "Rose Quartz" },
|
||||
{ 38, "Ruby" },
|
||||
{ 39, "Sapphire" },
|
||||
{ 40, "Smokey Quartz" },
|
||||
{ 41, "Sunstone" },
|
||||
{ 42, "Tiger Eye" },
|
||||
{ 43, "Tourmaline" },
|
||||
{ 44, "Turquoise" },
|
||||
{ 45, "White Jade" },
|
||||
{ 46, "White Quartz" },
|
||||
{ 47, "White Sapphire" },
|
||||
{ 48, "Yellow Garnet" },
|
||||
{ 49, "Yellow Topaz" },
|
||||
{ 50, "Zircon" },
|
||||
{ 51, "Ivory" },
|
||||
{ 52, "Leather" },
|
||||
{ 53, "Armoredillo Hide" },
|
||||
{ 54, "Gromnie Hide" },
|
||||
{ 55, "Reed Shark Hide" },
|
||||
// 56
|
||||
{ 57, "Brass" },
|
||||
{ 58, "Bronze" },
|
||||
{ 59, "Copper" },
|
||||
{ 60, "Gold" },
|
||||
{ 61, "Iron" },
|
||||
{ 62, "Pyreal" },
|
||||
{ 63, "Silver" },
|
||||
{ 64, "Steel" },
|
||||
// 65
|
||||
{ 66, "Alabaster" },
|
||||
{ 67, "Granite" },
|
||||
{ 68, "Marble" },
|
||||
{ 69, "Obsidian" },
|
||||
{ 70, "Sandstone" },
|
||||
{ 71, "Serpentine" },
|
||||
{ 73, "Ebony" },
|
||||
{ 74, "Mahogany" },
|
||||
{ 75, "Oak" },
|
||||
{ 76, "Pine" },
|
||||
{ 77, "Teak" },
|
||||
};
|
||||
|
||||
public struct SpellInfo<T>
|
||||
where T : struct
|
||||
{
|
||||
public readonly int Key;
|
||||
public readonly T Change;
|
||||
public readonly T Bonus;
|
||||
|
||||
public SpellInfo(int key, T change, T bonus = default)
|
||||
{
|
||||
Key = key;
|
||||
Change = change;
|
||||
Bonus = bonus;
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from Decal.Adapter.Wrappers.LongValueKey
|
||||
const int LongValueKey_MaxDamage = 218103842;
|
||||
const int LongValueKey_ArmorLevel = 28;
|
||||
|
||||
public static readonly Dictionary<int, SpellInfo<int>> LongValueKeySpellEffects = new Dictionary<int, SpellInfo<int>>()
|
||||
{
|
||||
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
|
||||
{ 1616, new SpellInfo<int>(LongValueKey_MaxDamage, 20)}, // Blood Drinker VI
|
||||
{ 2096, new SpellInfo<int>(LongValueKey_MaxDamage, 22)}, // Infected Caress
|
||||
//{ 5183, new SpellInfo<LongValueKey>(LongValueKey_MaxDamage, 22)}, // Incantation of Blood Drinker Pre Feb-2013
|
||||
//{ 4395, new SpellInfo<LongValueKey>(LongValueKey_MaxDamage, 24, 2)}, // Incantation of Blood Drinker, this spell on the item adds 2 more points of damage over a user casted 8 Pre Feb-2013
|
||||
{ 5183, new SpellInfo<int>(LongValueKey_MaxDamage, 24)}, // Incantation of Blood Drinker Post Feb-2013
|
||||
{ 4395, new SpellInfo<int>(LongValueKey_MaxDamage, 24)}, // Incantation of Blood Drinker Post Feb-2013
|
||||
|
||||
{ 2598, new SpellInfo<int>(LongValueKey_MaxDamage, 2, 2)}, // Minor Blood Thirst
|
||||
{ 2586, new SpellInfo<int>(LongValueKey_MaxDamage, 4, 4)}, // Major Blood Thirst
|
||||
{ 4661, new SpellInfo<int>(LongValueKey_MaxDamage, 7, 7)}, // Epic Blood Thirst
|
||||
{ 6089, new SpellInfo<int>(LongValueKey_MaxDamage, 10, 10)}, // Legendary Blood Thirst
|
||||
|
||||
{ 3688, new SpellInfo<int>(LongValueKey_MaxDamage, 300)}, // Prodigal Blood Drinker
|
||||
|
||||
|
||||
{ 1486, new SpellInfo<int>(LongValueKey_ArmorLevel, 200)}, // Impenetrability VI
|
||||
{ 2108, new SpellInfo<int>(LongValueKey_ArmorLevel, 220)}, // Brogard's Defiance
|
||||
{ 4407, new SpellInfo<int>(LongValueKey_ArmorLevel, 240)}, // Incantation of Impenetrability
|
||||
|
||||
{ 2604, new SpellInfo<int>(LongValueKey_ArmorLevel, 20, 20)}, // Minor Impenetrability
|
||||
{ 2592, new SpellInfo<int>(LongValueKey_ArmorLevel, 40, 40)}, // Major Impenetrability
|
||||
{ 4667, new SpellInfo<int>(LongValueKey_ArmorLevel, 60, 60)}, // Epic Impenetrability
|
||||
{ 6095, new SpellInfo<int>(LongValueKey_ArmorLevel, 80, 80)}, // Legendary Impenetrability
|
||||
};
|
||||
|
||||
// Taken from Decal.Adapter.Wrappers.DoubleValueKey
|
||||
const int DoubleValueKey_ElementalDamageVersusMonsters = 152;
|
||||
const int DoubleValueKey_AttackBonus = 167772172;
|
||||
const int DoubleValueKey_MeleeDefenseBonus = 29;
|
||||
const int DoubleValueKey_ManaCBonus = 144;
|
||||
|
||||
public static readonly Dictionary<int, SpellInfo<double>> DoubleValueKeySpellEffects = new Dictionary<int, SpellInfo<double>>()
|
||||
{
|
||||
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
|
||||
{ 3258, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .06)}, // Spirit Drinker VI
|
||||
{ 3259, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .07)}, // Infected Spirit Caress
|
||||
//{ 5182, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .07)}, // Incantation of Spirit Drinker Pre Feb-2013
|
||||
//{ 4414, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .08, .01)}, // Incantation of Spirit Drinker, this spell on the item adds 1 more % of damage over a user casted 8 Pre Feb-2013
|
||||
{ 5182, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .08)}, // Incantation of Spirit Drinker Post Feb-2013
|
||||
{ 4414, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .08)}, // Incantation of Spirit Drinker, this spell on the item adds 1 more % of damage over a user casted 8 Post Feb-2013
|
||||
|
||||
{ 3251, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .01, .01)}, // Minor Spirit Thirst
|
||||
{ 3250, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .03, .03)}, // Major Spirit Thirst
|
||||
{ 4670, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .05, .05)}, // Epic Spirit Thirst
|
||||
{ 6098, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .07, .07)}, // Legendary Spirit Thirst
|
||||
|
||||
{ 3735, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .15)}, // Prodigal Spirit Drinker
|
||||
|
||||
|
||||
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
|
||||
{ 1592, new SpellInfo<double>(DoubleValueKey_AttackBonus, .15)}, // Heart Seeker VI
|
||||
{ 2106, new SpellInfo<double>(DoubleValueKey_AttackBonus, .17)}, // Elysa's Sight
|
||||
{ 4405, new SpellInfo<double>(DoubleValueKey_AttackBonus, .20)}, // Incantation of Heart Seeker
|
||||
|
||||
{ 2603, new SpellInfo<double>(DoubleValueKey_AttackBonus, .03, .03)}, // Minor Heart Thirst
|
||||
{ 2591, new SpellInfo<double>(DoubleValueKey_AttackBonus, .05, .05)}, // Major Heart Thirst
|
||||
{ 4666, new SpellInfo<double>(DoubleValueKey_AttackBonus, .07, .07)}, // Epic Heart Thirst
|
||||
{ 6094, new SpellInfo<double>(DoubleValueKey_AttackBonus, .09, .09)}, // Legendary Heart Thirst
|
||||
|
||||
|
||||
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
|
||||
{ 1605, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .15)}, // Defender VI
|
||||
{ 2101, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .17)}, // Cragstone's Will
|
||||
//{ 4400, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .17)}, // Incantation of Defender Pre Feb-2013
|
||||
{ 4400, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .20)}, // Incantation of Defender Post Feb-2013
|
||||
|
||||
{ 2600, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .03, .03)}, // Minor Defender
|
||||
{ 3985, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .04, .04)}, // Mukkir Sense
|
||||
{ 2588, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .05, .05)}, // Major Defender
|
||||
{ 4663, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .07, .07)}, // Epic Defender
|
||||
{ 6091, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .09, .09)}, // Legendary Defender
|
||||
|
||||
{ 3699, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .25)}, // Prodigal Defender
|
||||
|
||||
|
||||
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
|
||||
{ 1480, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.60)}, // Hermetic Link VI
|
||||
{ 2117, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.70)}, // Mystic's Blessing
|
||||
{ 4418, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.80)}, // Incantation of Hermetic Link
|
||||
|
||||
{ 3201, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.05, 1.05)}, // Feeble Hermetic Link
|
||||
{ 3199, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.10, 1.10)}, // Minor Hermetic Link
|
||||
{ 3202, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.15, 1.15)}, // Moderate Hermetic Link
|
||||
{ 3200, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.20, 1.20)}, // Major Hermetic Link
|
||||
{ 6086, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.25, 1.25)}, // Epic Hermetic Link
|
||||
{ 6087, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.30, 1.30)}, // Legendary Hermetic Link
|
||||
};
|
||||
}
|
||||
76
src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs
Normal file
76
src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace OpenAC.MosswartMassacre.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// The header words the original's key tables carried for every item that
|
||||
/// the plugin contract does not hand a plugin yet. Each one that is null is
|
||||
/// left out of the item record, which is what the original sent for an item
|
||||
/// whose framework lacked the value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="WeenieHeaderFlags"/> matters beyond its own key: it says which
|
||||
/// optional header fields the server sent. While it is null the builder
|
||||
/// decides presence from the field's value instead (present when non-zero),
|
||||
/// which is wrong for a field the server sent as zero, most often the
|
||||
/// equipable-slots word.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The physics words come from the object's physics description, which
|
||||
/// the contract does not expose at all.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal readonly record struct ItemHeaderWords
|
||||
{
|
||||
/// <summary>The object header's first flag word (key 218103832).</summary>
|
||||
public uint? WeenieHeaderFlags { get; init; }
|
||||
|
||||
/// <summary>The object header's second flag word (key 218103833), when the header has one.</summary>
|
||||
public uint? WeenieHeaderFlags2 { get; init; }
|
||||
|
||||
/// <summary>The physics description's flag word (key 218103847).</summary>
|
||||
public uint? PhysicsDescriptionFlags { get; init; }
|
||||
|
||||
/// <summary>The physics word the framework filed under key 218103831.</summary>
|
||||
public uint? PhysicsWord { get; init; }
|
||||
|
||||
/// <summary>The model setup id (key 218103830).</summary>
|
||||
public uint? SetupId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value the framework filed under its scale key (167772170). It was
|
||||
/// 0.0 in every recorded item whose physics description carried a scale.
|
||||
/// </summary>
|
||||
public double? Scale { get; init; }
|
||||
|
||||
/// <summary>The object a wielded item hangs from (key 218103818).</summary>
|
||||
public uint? ParentObjectId { get; init; }
|
||||
|
||||
/// <summary>Where on its parent a wielded item hangs (key 218103819).</summary>
|
||||
public int? ParentLocation { get; init; }
|
||||
|
||||
/// <summary>The header's hook type (key 218103828).</summary>
|
||||
public int? HookType { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Reads the header words for one owned item.</summary>
|
||||
internal interface IItemHeaderSource
|
||||
{
|
||||
ItemHeaderWords Read(in PluginInventoryItem item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The contract this plugin builds against exposes none of these words, so
|
||||
/// every one is absent. Replace this when the contract grows them.
|
||||
/// </summary>
|
||||
internal sealed class HostItemHeaderSource : IItemHeaderSource
|
||||
{
|
||||
public static HostItemHeaderSource Instance { get; } = new();
|
||||
|
||||
private HostItemHeaderSource()
|
||||
{
|
||||
}
|
||||
|
||||
public ItemHeaderWords Read(in PluginInventoryItem item) => default;
|
||||
}
|
||||
313
src/OpenAC.MosswartMassacre/Inventory/MyWorldObject.cs
Normal file
313
src/OpenAC.MosswartMassacre/Inventory/MyWorldObject.cs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace OpenAC.MosswartMassacre.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// One item as the backend receives it: the original plugin's item record,
|
||||
/// carried over member for member because it is serialized as-is into
|
||||
/// <c>full_inventory</c> and <c>inventory_delta</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Newtonsoft writes the public fields first, in declaration order, then every
|
||||
/// public getter, in declaration order. The getters are part of the wire: the
|
||||
/// backend reads several of them (Material, EquipSkill, MaxDamage, WieldLevel,
|
||||
/// ...). Keep every member, its type, its name and its position exactly as
|
||||
/// they are; a double getter must stay a double so <c>-1</c> is written
|
||||
/// <c>-1.0</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The four tables are keyed the way the original's client framework keyed
|
||||
/// them: raw game property ids mixed with that framework's own pseudo keys
|
||||
/// (see <see cref="DecalKeys"/>). <see cref="DecalItemBuilder"/> fills them.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class MyWorldObject
|
||||
{
|
||||
public bool HasIdData;
|
||||
public int Id;
|
||||
public int LastIdTime;
|
||||
public int ObjectClass;
|
||||
public int Icon;
|
||||
public int ContainerId;
|
||||
|
||||
public Dictionary<int, bool> BoolValues = [];
|
||||
public Dictionary<int, double> DoubleValues = [];
|
||||
public Dictionary<int, int> IntValues = [];
|
||||
public Dictionary<int, string> StringValues = [];
|
||||
|
||||
public List<int> ActiveSpells = [];
|
||||
public List<int> Spells = [];
|
||||
|
||||
/// <summary>
|
||||
/// Copies every entry of the given tables over this record's, keeping any
|
||||
/// key only this record has. The original guarded each copy with a check
|
||||
/// against the table it was iterating, which is always true, so every
|
||||
/// incoming entry simply overwrites.
|
||||
/// </summary>
|
||||
public void AddTo(
|
||||
IDictionary<int, bool> boolValues,
|
||||
IDictionary<int, double> doubleValues,
|
||||
IDictionary<int, int> intValues,
|
||||
IDictionary<int, string> stringValues)
|
||||
{
|
||||
foreach (KeyValuePair<int, bool> kvp in boolValues)
|
||||
BoolValues[kvp.Key] = kvp.Value;
|
||||
foreach (KeyValuePair<int, double> kvp in doubleValues)
|
||||
DoubleValues[kvp.Key] = kvp.Value;
|
||||
foreach (KeyValuePair<int, int> kvp in intValues)
|
||||
IntValues[kvp.Key] = kvp.Value;
|
||||
foreach (KeyValuePair<int, string> kvp in stringValues)
|
||||
StringValues[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
public string? Material { get { if (IntValues.ContainsKey(131)) return ItemDictionaries.MaterialInfo.ContainsKey(IntValues[131]) ? ItemDictionaries.MaterialInfo[IntValues[131]] : IntValues[131].ToString(CultureInfo.InvariantCulture); return null; } }
|
||||
|
||||
public string? Name { get { return StringValues.ContainsKey(1) ? StringValues[1] : null; } }
|
||||
|
||||
public string? EquipSkill { get { if (IntValues.ContainsKey(218103840)) return ItemDictionaries.SkillInfo.ContainsKey(IntValues[218103840]) ? ItemDictionaries.SkillInfo[IntValues[218103840]] : IntValues[218103840].ToString(CultureInfo.InvariantCulture); return null; } }
|
||||
|
||||
public string? Mastery { get { if (IntValues.ContainsKey(353)) return ItemDictionaries.MasteryInfo.ContainsKey(IntValues[353]) ? ItemDictionaries.MasteryInfo[IntValues[353]] : IntValues[353].ToString(CultureInfo.InvariantCulture); return null; } }
|
||||
|
||||
public string? ItemSet { get { if (IntValues.ContainsKey(265)) return ItemDictionaries.AttributeSetInfo.ContainsKey(IntValues[265]) ? ItemDictionaries.AttributeSetInfo[IntValues[265]] : IntValues[265].ToString(CultureInfo.InvariantCulture); return null; } }
|
||||
|
||||
public int ArmorLevel { get { return IntValues.ContainsKey(28) ? IntValues[28] : -1; } }
|
||||
|
||||
public string? Imbue
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IntValues.ContainsKey(179) || IntValues[179] == 0) return null;
|
||||
|
||||
string retVal = string.Empty;
|
||||
if ((IntValues[179] & 1) == 1) retVal += " CS";
|
||||
if ((IntValues[179] & 2) == 2) retVal += " CB";
|
||||
if ((IntValues[179] & 4) == 4) retVal += " AR";
|
||||
if ((IntValues[179] & 8) == 8) retVal += " SlashRend";
|
||||
if ((IntValues[179] & 16) == 16) retVal += " PierceRend";
|
||||
if ((IntValues[179] & 32) == 32) retVal += " BludgeRend";
|
||||
if ((IntValues[179] & 64) == 64) retVal += " AcidRend";
|
||||
if ((IntValues[179] & 128) == 128) retVal += " FrostRend";
|
||||
if ((IntValues[179] & 256) == 256) retVal += " LightRend";
|
||||
if ((IntValues[179] & 512) == 512) retVal += " FireRend";
|
||||
if ((IntValues[179] & 1024) == 1024) retVal += " MeleeImbue";
|
||||
if ((IntValues[179] & 4096) == 4096) retVal += " MagicImbue";
|
||||
if ((IntValues[179] & 8192) == 8192) retVal += " Hematited";
|
||||
if ((IntValues[179] & 536870912) == 536870912) retVal += " MagicAbsorb";
|
||||
retVal = retVal.Trim();
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
|
||||
public int Tinks { get { return IntValues.ContainsKey(171) ? IntValues[171] : -1; } }
|
||||
|
||||
public int MaxDamage { get { return IntValues.ContainsKey(218103842) ? IntValues[218103842] : -1; } }
|
||||
|
||||
public int ElementalDmgBonus { get { return IntValues.ContainsKey(204) ? IntValues[204] : -1; } }
|
||||
|
||||
public double Variance { get { return DoubleValues.ContainsKey(167772171) ? DoubleValues[167772171] : -1; } }
|
||||
|
||||
public double DamageBonus { get { return DoubleValues.ContainsKey(167772174) ? DoubleValues[167772174] : -1; } }
|
||||
|
||||
public double ElementalDamageVersusMonsters { get { return DoubleValues.ContainsKey(152) ? DoubleValues[152] : -1; } }
|
||||
|
||||
public double AttackBonus { get { return DoubleValues.ContainsKey(167772172) ? DoubleValues[167772172] : -1; } }
|
||||
|
||||
public double MeleeDefenseBonus { get { return DoubleValues.ContainsKey(29) ? DoubleValues[29] : -1; } }
|
||||
|
||||
public double MagicDBonus { get { return DoubleValues.ContainsKey(150) ? DoubleValues[150] : -1; } }
|
||||
|
||||
public double MissileDBonus { get { return DoubleValues.ContainsKey(149) ? DoubleValues[149] : -1; } }
|
||||
|
||||
public double ManaCBonus { get { return DoubleValues.ContainsKey(144) ? DoubleValues[144] : -1; } }
|
||||
|
||||
public int WieldLevel { get { if (IntValues.ContainsKey(160) && IntValues[160] > 0 && IntValues.ContainsKey(158) && IntValues[158] == 7 && IntValues.ContainsKey(159) && IntValues[159] == 1) return IntValues[160]; return -1; } }
|
||||
|
||||
public int SkillLevel { get { if (IntValues.ContainsKey(160) && IntValues[160] > 0 && (!IntValues.ContainsKey(158) || IntValues[158] != 7) && IntValues.ContainsKey(159)) return IntValues[160]; return -1; } }
|
||||
|
||||
public int LoreRequirement { get { return IntValues.ContainsKey(109) ? IntValues[109] : -1; } }
|
||||
|
||||
public double SalvageWorkmanship { get { return DoubleValues.ContainsKey(167772169) ? DoubleValues[167772169] : -1; } }
|
||||
|
||||
public int Workmanship { get { return IntValues.ContainsKey(105) ? IntValues[105] : -1; } }
|
||||
|
||||
public int Value { get { return IntValues.ContainsKey(19) ? IntValues[19] : -1; } }
|
||||
|
||||
public int Burden { get { return IntValues.ContainsKey(5) ? IntValues[5] : -1; } }
|
||||
|
||||
public int DamRating { get { return IntValues.ContainsKey(370) ? IntValues[370] : -1; } }
|
||||
|
||||
public int DamResistRating { get { return IntValues.ContainsKey(371) ? IntValues[371] : -1; } }
|
||||
|
||||
public int CritRating { get { return IntValues.ContainsKey(372) ? IntValues[372] : -1; } }
|
||||
|
||||
public int CritResistRating { get { return IntValues.ContainsKey(373) ? IntValues[373] : -1; } }
|
||||
|
||||
public int CritDamRating { get { return IntValues.ContainsKey(374) ? IntValues[374] : -1; } }
|
||||
|
||||
public int CritDamResistRating { get { return IntValues.ContainsKey(375) ? IntValues[375] : -1; } }
|
||||
|
||||
public int HealBoostRating { get { return IntValues.ContainsKey(376) ? IntValues[376] : -1; } }
|
||||
|
||||
public int VitalityRating { get { return IntValues.ContainsKey(379) ? IntValues[379] : -1; } }
|
||||
|
||||
/// <summary>The sum of every rating on the item, or -1 when it carries none.</summary>
|
||||
public int TotalRating
|
||||
{
|
||||
get
|
||||
{
|
||||
if (DamRating == -1 && DamResistRating == -1 && CritRating == -1 && CritResistRating == -1 && CritDamRating == -1 && CritDamResistRating == -1 && HealBoostRating == -1 && VitalityRating == -1)
|
||||
return -1;
|
||||
|
||||
return Math.Max(DamRating, 0) + Math.Max(DamResistRating, 0) + Math.Max(CritRating, 0) + Math.Max(CritResistRating, 0) + Math.Max(CritDamRating, 0) + Math.Max(CritDamResistRating, 0) + Math.Max(HealBoostRating, 0) + Math.Max(VitalityRating, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The armor level less buffs and tinks (20 each, the imbue excluded), plus
|
||||
/// any impenetrability cantrip; a non-loot item (no material) loses 200.
|
||||
/// </summary>
|
||||
public int CalcedStartingArmorLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
int armorFromTinks = 0;
|
||||
int armorFromBuffs = 0;
|
||||
|
||||
if (Tinks > 0 && ArmorLevel > 0)
|
||||
armorFromTinks = (Imbue != null) ? (Tinks - 1) * 20 : Tinks * 20;
|
||||
|
||||
if ((!IntValues.ContainsKey(131) || IntValues[131] == 0) && ArmorLevel > 0)
|
||||
armorFromTinks = 200;
|
||||
|
||||
foreach (int spell in ActiveSpells)
|
||||
{
|
||||
foreach (KeyValuePair<int, ItemDictionaries.SpellInfo<int>> effect in ItemDictionaries.LongValueKeySpellEffects)
|
||||
{
|
||||
if (spell == effect.Key && effect.Value.Key == 28)
|
||||
armorFromBuffs += effect.Value.Change;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (int spell in Spells)
|
||||
{
|
||||
foreach (KeyValuePair<int, ItemDictionaries.SpellInfo<int>> effect in ItemDictionaries.LongValueKeySpellEffects)
|
||||
{
|
||||
if (spell == effect.Key && effect.Value.Key == 28)
|
||||
armorFromBuffs -= effect.Value.Bonus;
|
||||
}
|
||||
}
|
||||
|
||||
return ArmorLevel - armorFromTinks - armorFromBuffs;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A melee weapon's best damage over time after ten tinks, from its variance, max damage and tinks.</summary>
|
||||
public double CalcedBuffedTinkedDoT
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!DoubleValues.ContainsKey(167772171) || !IntValues.ContainsKey(218103842))
|
||||
return -1;
|
||||
|
||||
double variance = DoubleValues.ContainsKey(167772171) ? DoubleValues[167772171] : 0;
|
||||
int maxDamage = GetBuffedIntValueKey(218103842);
|
||||
|
||||
int numberOfTinksLeft = Math.Max(10 - Math.Max(Tinks, 0), 0);
|
||||
|
||||
if (!IntValues.ContainsKey(179) || IntValues[179] == 0)
|
||||
numberOfTinksLeft--;
|
||||
|
||||
if (!IntValues.ContainsKey(131) || IntValues[131] == 0)
|
||||
numberOfTinksLeft = 0;
|
||||
|
||||
for (int i = 1; i <= numberOfTinksLeft; i++)
|
||||
{
|
||||
double ironTinkDoT = CalculateDamageOverTime(maxDamage + 24 + 1, variance);
|
||||
double graniteTinkDoT = CalculateDamageOverTime(maxDamage + 24, variance * .8);
|
||||
|
||||
if (ironTinkDoT >= graniteTinkDoT)
|
||||
maxDamage++;
|
||||
else
|
||||
variance *= .8;
|
||||
}
|
||||
|
||||
return CalculateDamageOverTime(maxDamage + 24, variance);
|
||||
}
|
||||
}
|
||||
|
||||
public double CalcedBuffedMissileDamage { get { if (!IntValues.ContainsKey(218103842) || !DoubleValues.ContainsKey(167772174) || !IntValues.ContainsKey(204)) return -1; return GetBuffedIntValueKey(218103842) + (((GetBuffedDoubleValueKey(167772174) - 1) * 100) / 3) + GetBuffedIntValueKey(204); } }
|
||||
|
||||
public double BuffedElementalDamageVersusMonsters { get { return GetBuffedDoubleValueKey(152, -1); } }
|
||||
|
||||
public double BuffedAttackBonus { get { return GetBuffedDoubleValueKey(167772172, -1); } }
|
||||
|
||||
public double BuffedMeleeDefenseBonus { get { return GetBuffedDoubleValueKey(29, -1); } }
|
||||
|
||||
public double BuffedManaCBonus { get { return GetBuffedDoubleValueKey(144, -1); } }
|
||||
|
||||
public int GetBuffedIntValueKey(int key, int defaultValue = 0)
|
||||
{
|
||||
if (!IntValues.ContainsKey(key))
|
||||
return defaultValue;
|
||||
|
||||
int value = IntValues[key];
|
||||
|
||||
foreach (int spell in ActiveSpells)
|
||||
{
|
||||
if (ItemDictionaries.LongValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.LongValueKeySpellEffects[spell].Key == key)
|
||||
value -= ItemDictionaries.LongValueKeySpellEffects[spell].Change;
|
||||
}
|
||||
|
||||
foreach (int spell in Spells)
|
||||
{
|
||||
if (ItemDictionaries.LongValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.LongValueKeySpellEffects[spell].Key == key)
|
||||
value += ItemDictionaries.LongValueKeySpellEffects[spell].Bonus;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public double GetBuffedDoubleValueKey(int key, double defaultValue = 0)
|
||||
{
|
||||
if (!DoubleValues.ContainsKey(key))
|
||||
return defaultValue;
|
||||
|
||||
double value = DoubleValues[key];
|
||||
|
||||
foreach (int spell in ActiveSpells)
|
||||
{
|
||||
if (ItemDictionaries.DoubleValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.DoubleValueKeySpellEffects[spell].Key == key)
|
||||
{
|
||||
if (Math.Abs(ItemDictionaries.DoubleValueKeySpellEffects[spell].Change - 1) < double.Epsilon)
|
||||
value /= ItemDictionaries.DoubleValueKeySpellEffects[spell].Change;
|
||||
else
|
||||
value -= ItemDictionaries.DoubleValueKeySpellEffects[spell].Change;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (int spell in Spells)
|
||||
{
|
||||
if (ItemDictionaries.DoubleValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.DoubleValueKeySpellEffects[spell].Key == key && Math.Abs(ItemDictionaries.DoubleValueKeySpellEffects[spell].Bonus - 0) > double.Epsilon)
|
||||
{
|
||||
if (Math.Abs(ItemDictionaries.DoubleValueKeySpellEffects[spell].Change - 1) < double.Epsilon)
|
||||
value *= ItemDictionaries.DoubleValueKeySpellEffects[spell].Bonus;
|
||||
else
|
||||
value += ItemDictionaries.DoubleValueKeySpellEffects[spell].Bonus;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>maxDamage * ((1 - critChance) * (2 - variance) / 2 + (critChance * critMultiplier)).</summary>
|
||||
public static double CalculateDamageOverTime(int maxDamage, double variance, double critChance = .1, double critMultiplier = 2)
|
||||
{
|
||||
return maxDamage * ((1 - critChance) * (2 - variance) / 2 + (critChance * critMultiplier));
|
||||
}
|
||||
|
||||
public override string? ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue