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:
parent
09612f9981
commit
0eab7497c1
19 changed files with 633 additions and 114 deletions
|
|
@ -5,6 +5,7 @@ using AcDream.Core.Items;
|
|||
using AcDream.Core.Spells;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
|
|
@ -32,12 +33,14 @@ public sealed class MagicCatalog
|
|||
private readonly IReadOnlyDictionary<uint, int> _spellLevels;
|
||||
|
||||
private MagicCatalog(
|
||||
CoreSpellTable spellTable,
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
||||
IReadOnlyDictionary<uint, SpellFormulaDefinition> formulaDefinitions,
|
||||
IReadOnlyDictionary<uint, uint> wcidByScid,
|
||||
IReadOnlyDictionary<uint, uint> magicPackWcidBySchool,
|
||||
IReadOnlyDictionary<uint, int> spellLevels)
|
||||
{
|
||||
SpellTable = spellTable;
|
||||
Components = components;
|
||||
_formulaDefinitions = formulaDefinitions;
|
||||
_wcidByScid = wcidByScid;
|
||||
|
|
@ -45,6 +48,7 @@ public sealed class MagicCatalog
|
|||
_spellLevels = spellLevels;
|
||||
}
|
||||
|
||||
public CoreSpellTable SpellTable { get; }
|
||||
public IReadOnlyDictionary<uint, SpellComponentDescriptor> Components { get; }
|
||||
|
||||
public bool IsComponentPack(uint weenieClassId)
|
||||
|
|
@ -89,22 +93,24 @@ public sealed class MagicCatalog
|
|||
}
|
||||
}
|
||||
|
||||
var metadata = new List<SpellMetadata>();
|
||||
var formulaDefinitions = new Dictionary<uint, SpellFormulaDefinition>();
|
||||
var spellLevels = new Dictionary<uint, int>();
|
||||
DatReaderWriter.DBObjs.SpellTable? spellTable =
|
||||
dats.Get<DatReaderWriter.DBObjs.SpellTable>(SpellTableDid);
|
||||
if (spellTable is not null)
|
||||
DatReaderWriter.DBObjs.SpellTable spellTable =
|
||||
dats.Get<DatReaderWriter.DBObjs.SpellTable>(SpellTableDid)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Required retail SpellTable 0x{SpellTableDid:X8} is missing from portal.dat.");
|
||||
foreach (var pair in spellTable.Spells)
|
||||
{
|
||||
foreach (var pair in spellTable.Spells)
|
||||
{
|
||||
uint[] formula = pair.Value.Components.Take(8).ToArray();
|
||||
formulaDefinitions[pair.Key] = new SpellFormulaDefinition(
|
||||
pair.Value.FormulaVersion,
|
||||
formula,
|
||||
(uint)pair.Value.School);
|
||||
spellLevels[pair.Key] =
|
||||
RetailSpellFormula.InqSpellLevelByRoughHeuristic(formula);
|
||||
}
|
||||
SpellMetadata spell = RetailSpellMetadataProjector.Project(
|
||||
pair.Key, pair.Value, componentTable);
|
||||
metadata.Add(spell);
|
||||
uint[] formula = spell.FormulaComponents.ToArray();
|
||||
formulaDefinitions[pair.Key] = new SpellFormulaDefinition(
|
||||
spell.FormulaVersion,
|
||||
formula,
|
||||
(uint)spell.SchoolId);
|
||||
spellLevels[pair.Key] = spell.Generation;
|
||||
}
|
||||
|
||||
var magicPackWcidBySchool = new Dictionary<uint, uint>();
|
||||
|
|
@ -121,6 +127,7 @@ public sealed class MagicCatalog
|
|||
}
|
||||
|
||||
return new MagicCatalog(
|
||||
CoreSpellTable.Create(metadata),
|
||||
components,
|
||||
formulaDefinitions,
|
||||
wcidByScid,
|
||||
|
|
|
|||
140
src/AcDream.App/Spells/RetailSpellMetadataProjector.cs
Normal file
140
src/AcDream.App/Spells/RetailSpellMetadataProjector.cs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using AcDream.Core.Spells;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
using CoreMagicSchool = AcDream.Core.Spells.MagicSchool;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
/// <summary>
|
||||
/// Projects portal.dat's complete retail <see cref="SpellBase"/> record into
|
||||
/// the backend-neutral Core metadata consumed by spellbook and casting state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Schema/order: <c>CSpellBase::UnPack @ 0x00597290</c>.
|
||||
/// School names: <c>CSpellBase::SchoolEnumToName @ 0x00597400</c>.
|
||||
/// Target type: <c>SpellFormula::GetTargetingType @ 0x005BC910</c>.
|
||||
/// </remarks>
|
||||
internal static class RetailSpellMetadataProjector
|
||||
{
|
||||
public static SpellMetadata Project(
|
||||
uint spellId,
|
||||
SpellBase spell,
|
||||
SpellComponentTable? componentTable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(spell);
|
||||
|
||||
uint[] formula = spell.Components.Take(8).ToArray();
|
||||
uint flags = (uint)spell.Bitfield;
|
||||
uint formulaTargetType = RetailSpellFormula.GetTargetingType(formula);
|
||||
uint targetMask = formulaTargetType;
|
||||
int level = RetailSpellFormula.InqSpellLevelByRoughHeuristic(formula);
|
||||
bool selfTargeted = (flags & (uint)SpellFlags.SelfTargeted) != 0u;
|
||||
bool beneficial = (flags & (uint)SpellFlags.Beneficial) != 0u;
|
||||
|
||||
return new SpellMetadata(
|
||||
spellId,
|
||||
spell.Name.Value,
|
||||
SchoolName(spell.School),
|
||||
(uint)spell.Category,
|
||||
spell.Icon,
|
||||
BuildSpellWords(formula, componentTable),
|
||||
checked((float)spell.Duration),
|
||||
checked((int)spell.BaseMana),
|
||||
(flags & (uint)SpellFlags.Reversed) != 0u,
|
||||
(flags & (uint)SpellFlags.FellowshipSpell) != 0u,
|
||||
spell.Description.Value,
|
||||
unchecked((int)spell.DisplayOrder),
|
||||
checked((int)spell.Power),
|
||||
flags,
|
||||
level,
|
||||
(flags & (uint)SpellFlags.FastCast) != 0u,
|
||||
!beneficial && !selfTargeted,
|
||||
formulaTargetType == 0u,
|
||||
Speed: 0f,
|
||||
(uint)spell.CasterEffect,
|
||||
(uint)spell.TargetEffect,
|
||||
targetMask,
|
||||
checked((int)spell.MetaSpellType))
|
||||
{
|
||||
SchoolId = ToCoreSchool(spell.School),
|
||||
FormulaComponents = formula,
|
||||
FormulaVersion = spell.FormulaVersion,
|
||||
ComponentLoss = spell.ComponentLoss,
|
||||
BaseRangeConstant = spell.BaseRangeConstant,
|
||||
BaseRangeModifier = spell.BaseRangeMod,
|
||||
SpellEconomyModifier = spell.SpellEconomyMod,
|
||||
FizzleEffect = (uint)spell.FizzleEffect,
|
||||
RecoveryInterval = spell.RecoveryInterval,
|
||||
RecoveryAmount = spell.RecoveryAmount,
|
||||
NonComponentTargetType = (uint)spell.NonComponentTargetType,
|
||||
FormulaTargetType = formulaTargetType,
|
||||
ManaModifier = spell.ManaMod,
|
||||
DegradeModifier = spell.DegradeModifier,
|
||||
DegradeLimit = spell.DegradeLimit,
|
||||
PortalLifetime = spell.PortalLifetime,
|
||||
};
|
||||
}
|
||||
|
||||
private static string SchoolName(DatReaderWriter.Enums.MagicSchool school) => school switch
|
||||
{
|
||||
DatReaderWriter.Enums.MagicSchool.WarMagic => "War Magic",
|
||||
DatReaderWriter.Enums.MagicSchool.LifeMagic => "Life Magic",
|
||||
DatReaderWriter.Enums.MagicSchool.ItemEnchantment => "Item Enchantment",
|
||||
DatReaderWriter.Enums.MagicSchool.CreatureEnchantment => "Creature Enchantment",
|
||||
DatReaderWriter.Enums.MagicSchool.VoidMagic => "Void Magic",
|
||||
_ => "None",
|
||||
};
|
||||
|
||||
private static CoreMagicSchool ToCoreSchool(DatReaderWriter.Enums.MagicSchool school) => school switch
|
||||
{
|
||||
DatReaderWriter.Enums.MagicSchool.WarMagic => CoreMagicSchool.WarMagic,
|
||||
DatReaderWriter.Enums.MagicSchool.LifeMagic => CoreMagicSchool.LifeMagic,
|
||||
DatReaderWriter.Enums.MagicSchool.ItemEnchantment => CoreMagicSchool.ItemEnchantment,
|
||||
DatReaderWriter.Enums.MagicSchool.CreatureEnchantment => CoreMagicSchool.CreatureEnchantment,
|
||||
DatReaderWriter.Enums.MagicSchool.VoidMagic => CoreMagicSchool.VoidMagic,
|
||||
_ => CoreMagicSchool.None,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ACE's independently ported <c>SpellComponentsTable.GetSpellWords</c>:
|
||||
/// Herb supplies word one; Powder + lower-cased Potion form word two.
|
||||
/// </summary>
|
||||
private static string BuildSpellWords(
|
||||
IReadOnlyList<uint> formula,
|
||||
SpellComponentTable? componentTable)
|
||||
{
|
||||
if (componentTable is null) return string.Empty;
|
||||
|
||||
string first = string.Empty;
|
||||
string second = string.Empty;
|
||||
string third = string.Empty;
|
||||
foreach (uint componentId in formula)
|
||||
{
|
||||
if (!componentTable.Components.TryGetValue(componentId, out SpellComponentBase? component))
|
||||
continue;
|
||||
|
||||
switch (component.Type)
|
||||
{
|
||||
case ComponentType.Herb:
|
||||
first = component.Text.Value;
|
||||
break;
|
||||
case ComponentType.Powder:
|
||||
second = component.Text.Value;
|
||||
break;
|
||||
case ComponentType.Potion:
|
||||
third = component.Text.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string tail = second + third.ToLower(CultureInfo.InvariantCulture);
|
||||
if (tail.Length != 0)
|
||||
tail = char.ToUpperInvariant(tail[0]) + tail[1..];
|
||||
return $"{first} {tail}".Trim();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue