fix(spells): load complete retail DAT catalog

Replace the incomplete 3,956-row production CSV with one immutable projection of all 6,266 records in portal.dat. Preserve retail formula targeting, shared Spellbook/MagicRuntime metadata ownership, and fail startup when the required SpellTable is absent.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 21:17:13 +02:00
parent 09612f9981
commit 0eab7497c1
19 changed files with 633 additions and 114 deletions

View file

@ -6,10 +6,9 @@ using System.IO;
namespace AcDream.Core.Spells;
/// <summary>
/// Loads + queries <see cref="SpellMetadata"/> from a CSV at startup.
/// Source: <c>docs/research/data/spells.csv</c> (RFC 4180-ish, 35
/// columns, 3,956 rows). Loaded once, used by panels + by
/// <c>EnchantmentMath</c> for buff stacking aggregation.
/// Immutable lookup of retail spell metadata. Production builds create this
/// table from portal.dat through the App-layer <c>MagicCatalog</c>. The CSV
/// parser below remains for historical tooling and small synthetic tests.
///
/// <para>
/// Hand-rolled CSV parser — the only complication is the
@ -35,6 +34,21 @@ public sealed class SpellTable
_byId = byId;
}
/// <summary>Create a canonical immutable lookup from projected metadata.</summary>
public static SpellTable Create(IEnumerable<SpellMetadata> metadata)
{
ArgumentNullException.ThrowIfNull(metadata);
var byId = new Dictionary<uint, SpellMetadata>();
foreach (SpellMetadata spell in metadata)
{
ArgumentNullException.ThrowIfNull(spell);
if (!byId.TryAdd(spell.SpellId, spell))
throw new ArgumentException(
$"Duplicate spell id 0x{spell.SpellId:X8}.", nameof(metadata));
}
return new SpellTable(byId);
}
/// <summary>Number of spells loaded.</summary>
public int Count => _byId.Count;
@ -154,7 +168,10 @@ public sealed class SpellTable
spellId, name, school, family, iconId, words, duration,
mana, isDebuff, isFellow, description, sortKey, difficulty,
flags, generation, fastWindup, offensive, untargeted, speed,
casterEffect, targetEffect, targetMask, spellType);
casterEffect, targetEffect, targetMask, spellType)
{
SchoolId = ParseSchool(school),
};
}
return new SpellTable(byId);
@ -187,6 +204,16 @@ public sealed class SpellTable
return string.Equals(fields[i], "True", StringComparison.OrdinalIgnoreCase);
}
private static MagicSchool ParseSchool(string school) => school switch
{
"War Magic" => MagicSchool.WarMagic,
"Life Magic" => MagicSchool.LifeMagic,
"Item Enchantment" => MagicSchool.ItemEnchantment,
"Creature Enchantment" => MagicSchool.CreatureEnchantment,
"Void Magic" => MagicSchool.VoidMagic,
_ => MagicSchool.None,
};
/// <summary>
/// Hand-rolled RFC 4180-ish CSV row parser. Handles double-quoted
/// fields with embedded commas (the Description column). Embedded