feat(headless): share immutable gameplay content

This commit is contained in:
Erik 2026-07-27 09:00:48 +02:00
parent 12b500d383
commit 9569dadb57
25 changed files with 1031 additions and 429 deletions

View file

@ -1,163 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AcDream.Core.Items;
using AcDream.Core.Spells;
using AcDream.Runtime.Gameplay;
using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
namespace AcDream.App.Spells;
/// <summary>Immutable presentation metadata for one retail spell component WCID.</summary>
public sealed record SpellComponentDescriptor(
uint WeenieClassId,
string Name,
uint Category,
uint IconId);
/// <summary>One resolved formula cell in retail's spell examination subview.</summary>
public readonly record struct SpellExamineComponent(
uint SpellComponentId,
SpellComponentDescriptor Descriptor,
bool Owned);
/// <summary>
/// App-layer projection of retail's spell, component, and school-focus DAT tables.
/// Keeping this catalog outside <c>GameWindow</c> makes the SCID/WCID boundary and
/// rough spell-level calculation one independently testable ownership seam.
/// </summary>
public sealed class MagicCatalog
{
private const uint SpellTableDid = 0x0E00000Eu;
private const uint SpellComponentTableDid = 0x0E00000Fu;
private const uint ComponentIdMapDid = 0x27000002u;
private readonly IReadOnlyDictionary<uint, SpellFormulaDefinition> _formulaDefinitions;
private readonly IReadOnlyDictionary<uint, uint> _wcidByScid;
private readonly IReadOnlyDictionary<uint, uint> _magicPackWcidBySchool;
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;
_magicPackWcidBySchool = magicPackWcidBySchool;
_spellLevels = spellLevels;
}
public CoreSpellTable SpellTable { get; }
public IReadOnlyDictionary<uint, SpellComponentDescriptor> Components { get; }
public bool TryGetComponentBySpellComponentId(
uint spellComponentId,
out SpellComponentDescriptor descriptor)
{
if (_wcidByScid.TryGetValue(spellComponentId, out uint weenieClassId)
&& Components.TryGetValue(weenieClassId, out SpellComponentDescriptor? found))
{
descriptor = found;
return true;
}
descriptor = null!;
return false;
}
public bool IsComponentPack(uint weenieClassId)
=> Components.ContainsKey(weenieClassId);
public int GetSpellLevel(uint spellId)
=> _spellLevels.TryGetValue(spellId, out int level) ? level : 0;
public SpellComponentRequirementService CreateRequirementService(
ClientObjectTable objects,
Func<uint> playerGuid,
Func<string> accountName)
=> new(
objects,
playerGuid,
accountName,
_formulaDefinitions,
_wcidByScid,
_magicPackWcidBySchool);
public static MagicCatalog Load(IDatReaderWriter dats)
{
ArgumentNullException.ThrowIfNull(dats);
var components = new Dictionary<uint, SpellComponentDescriptor>();
var wcidByScid = new Dictionary<uint, uint>();
SpellComponentTable? componentTable = dats.Get<SpellComponentTable>(SpellComponentTableDid);
DualEnumIDMap? componentIds = dats.Get<DualEnumIDMap>(ComponentIdMapDid);
if (componentTable is not null && componentIds is not null)
{
foreach (var pair in componentTable.Components)
{
if (!componentIds.ClientEnumToID.TryGetValue(pair.Key, out uint wcid))
continue;
wcidByScid[pair.Key] = wcid;
components[wcid] = new SpellComponentDescriptor(
wcid,
pair.Value.Name.Value,
pair.Value.Category,
pair.Value.Icon.DataId);
}
}
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)
?? throw new InvalidOperationException(
$"Required retail SpellTable 0x{SpellTableDid:X8} is missing from portal.dat.");
foreach (var pair in spellTable.Spells)
{
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>();
// Retail IsComponentPack resolves enum key 0x10000001 in category 0x28;
// that key is not itself a portal.dat file id.
uint magicPackMapDid = AcDream.App.UI.RetailDataIdResolver.Resolve(
dats, 0x10000001u, 0x28u);
if (magicPackMapDid != 0u
&& dats.Portal.TryGet<EnumIDMap>(magicPackMapDid, out EnumIDMap? magicPackMap)
&& magicPackMap is not null)
{
foreach (var pair in magicPackMap.ClientEnumToID)
magicPackWcidBySchool[pair.Key] = pair.Value;
}
return new MagicCatalog(
CoreSpellTable.Create(metadata),
components,
formulaDefinitions,
wcidByScid,
magicPackWcidBySchool,
spellLevels);
}
}
/// <summary>
/// App content/policy projection for Runtime's live retail magic request
/// owner. The server owns turning, motion, fizzle, mana/component consumption,