feat(headless): share immutable gameplay content
This commit is contained in:
parent
12b500d383
commit
9569dadb57
25 changed files with 1031 additions and 429 deletions
|
|
@ -392,7 +392,7 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
|
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
|
||||||
private AcDream.App.World.ExternalContainerLifecycleController? _externalContainerLifecycle;
|
private AcDream.App.World.ExternalContainerLifecycleController? _externalContainerLifecycle;
|
||||||
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
|
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
|
||||||
private AcDream.App.Spells.MagicCatalog? _magicCatalog;
|
private MagicCatalog? _magicCatalog;
|
||||||
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
|
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
|
||||||
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
||||||
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
||||||
|
|
@ -750,7 +750,7 @@ public sealed class GameWindow :
|
||||||
"prepared asset source");
|
"prepared asset source");
|
||||||
|
|
||||||
void IGameWindowContentEffectsAudioPublication.PublishMagicCatalog(
|
void IGameWindowContentEffectsAudioPublication.PublishMagicCatalog(
|
||||||
AcDream.App.Spells.MagicCatalog value) =>
|
MagicCatalog value) =>
|
||||||
PublishCompositionOwner(ref _magicCatalog, value, "magic catalog");
|
PublishCompositionOwner(ref _magicCatalog, value, "magic catalog");
|
||||||
|
|
||||||
void IGameWindowContentEffectsAudioPublication.PublishAnimationLoader(
|
void IGameWindowContentEffectsAudioPublication.PublishAnimationLoader(
|
||||||
|
|
|
||||||
|
|
@ -1,163 +1,18 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using DatReaderWriter;
|
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using DatReaderWriter.DBObjs;
|
|
||||||
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
|
|
||||||
|
|
||||||
namespace AcDream.App.Spells;
|
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>
|
/// <summary>One resolved formula cell in retail's spell examination subview.</summary>
|
||||||
public readonly record struct SpellExamineComponent(
|
public readonly record struct SpellExamineComponent(
|
||||||
uint SpellComponentId,
|
uint SpellComponentId,
|
||||||
SpellComponentDescriptor Descriptor,
|
SpellComponentDescriptor Descriptor,
|
||||||
bool Owned);
|
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>
|
/// <summary>
|
||||||
/// App content/policy projection for Runtime's live retail magic request
|
/// App content/policy projection for Runtime's live retail magic request
|
||||||
/// owner. The server owns turning, motion, fizzle, mana/component consumption,
|
/// owner. The server owns turning, motion, fizzle, mana/component consumption,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Collections.Immutable;
|
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
||||||
|
|
@ -102,7 +101,9 @@ public sealed class LandblockBuildFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
AcDream.Core.World.LandblockCollisionBuild collisions =
|
AcDream.Core.World.LandblockCollisionBuild collisions =
|
||||||
BuildPreparedCollisionClosure(build.Landblock);
|
LandblockPhysicsContentBuilder.BuildPreparedCollisionClosure(
|
||||||
|
_preparedCollisions,
|
||||||
|
build.Landblock);
|
||||||
AcDream.Core.World.PhysicsDatBundle publicationDats =
|
AcDream.Core.World.PhysicsDatBundle publicationDats =
|
||||||
(build.Landblock.PhysicsDats
|
(build.Landblock.PhysicsDats
|
||||||
?? AcDream.Core.World.PhysicsDatBundle.Empty)
|
?? AcDream.Core.World.PhysicsDatBundle.Empty)
|
||||||
|
|
@ -155,59 +156,11 @@ public sealed class LandblockBuildFactory
|
||||||
// expand into per-part MeshRefs via SetupMesh.Flatten.
|
// expand into per-part MeshRefs via SetupMesh.Flatten.
|
||||||
// GPU upload happens later through the render-thread presentation
|
// GPU upload happens later through the render-thread presentation
|
||||||
// pipeline, never in this worker-side build.
|
// pipeline, never in this worker-side build.
|
||||||
var hydrated = new List<AcDream.Core.World.WorldEntity>(baseLoaded.Entities.Count);
|
IReadOnlyList<AcDream.Core.World.WorldEntity> hydrated =
|
||||||
foreach (var e in baseLoaded.Entities)
|
LandblockPhysicsContentBuilder.HydrateStaticEntities(
|
||||||
{
|
_dats,
|
||||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
baseLoaded,
|
||||||
var stabBounds = new AcDream.Core.Meshing.LocalBoundsAccumulator();
|
worldOffset);
|
||||||
|
|
||||||
if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x01000000u)
|
|
||||||
{
|
|
||||||
// Single GfxObj stab — identity part transform.
|
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
|
||||||
if (gfx is not null)
|
|
||||||
{
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
|
||||||
if (pb is not null) stabBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
|
||||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
|
||||||
e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
|
||||||
{
|
|
||||||
// Multi-part Setup — flatten to per-part GfxObj refs.
|
|
||||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
|
||||||
if (setup is not null)
|
|
||||||
{
|
|
||||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
|
||||||
foreach (var mr in flat)
|
|
||||||
{
|
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
|
||||||
if (gfx is null) continue;
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
|
||||||
if (pb is not null) stabBounds.Add(mr.PartTransform, pb.Value);
|
|
||||||
meshRefs.Add(mr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meshRefs.Count == 0) continue;
|
|
||||||
|
|
||||||
var entity = new AcDream.Core.World.WorldEntity
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId,
|
|
||||||
Position = e.Position + worldOffset,
|
|
||||||
Rotation = e.Rotation,
|
|
||||||
MeshRefs = meshRefs,
|
|
||||||
EffectCellId = e.EffectCellId,
|
|
||||||
IsBuildingShell = e.IsBuildingShell, // Phase A8: preserve dat-level tag
|
|
||||||
BuildingShellAnchorCellId = e.BuildingShellAnchorCellId,
|
|
||||||
};
|
|
||||||
if (stabBounds.TryGet(out var sbMin, out var sbMax))
|
|
||||||
entity.SetLocalBounds(sbMin, sbMax);
|
|
||||||
hydrated.Add(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task 8: merge stabs + scenery + interior into one entity list.
|
// Task 8: merge stabs + scenery + interior into one entity list.
|
||||||
var merged = new List<AcDream.Core.World.WorldEntity>(hydrated);
|
var merged = new List<AcDream.Core.World.WorldEntity>(hydrated);
|
||||||
|
|
@ -226,7 +179,10 @@ public sealed class LandblockBuildFactory
|
||||||
|
|
||||||
// Pre-read every DAT object publication consumes. Build this after
|
// Pre-read every DAT object publication consumes. Build this after
|
||||||
// `merged` so all entity GfxObj/Setup ids are known.
|
// `merged` so all entity GfxObj/Setup ids are known.
|
||||||
var physicsDats = BuildPhysicsDatBundle(landblockId, merged);
|
var physicsDats = LandblockPhysicsContentBuilder.BuildDatBundle(
|
||||||
|
_dats,
|
||||||
|
landblockId,
|
||||||
|
merged);
|
||||||
var completedEnvCells = envCellBuild.Build();
|
var completedEnvCells = envCellBuild.Build();
|
||||||
return new AcDream.App.Streaming.LandblockBuild(
|
return new AcDream.App.Streaming.LandblockBuild(
|
||||||
new AcDream.Core.World.LoadedLandblock(
|
new AcDream.Core.World.LoadedLandblock(
|
||||||
|
|
@ -237,180 +193,6 @@ public sealed class LandblockBuildFactory
|
||||||
completedEnvCells,
|
completedEnvCells,
|
||||||
request.Origin);
|
request.Origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pre-reads every DAT object that landblock publication consumes, so the
|
|
||||||
/// update thread performs no DAT reads. Gather and publication must
|
|
||||||
/// enumerate the same identifiers. Reads only; no runtime registration.
|
|
||||||
/// </summary>
|
|
||||||
private AcDream.Core.World.PhysicsDatBundle BuildPhysicsDatBundle(
|
|
||||||
uint landblockId,
|
|
||||||
System.Collections.Generic.IReadOnlyList<AcDream.Core.World.WorldEntity> entities)
|
|
||||||
{
|
|
||||||
var envCells = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.EnvCell>();
|
|
||||||
var environments = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.Environment>();
|
|
||||||
var setups = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.Setup>();
|
|
||||||
var gfxObjs = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.GfxObj>();
|
|
||||||
|
|
||||||
// (1) LandBlockInfo
|
|
||||||
var lbInfo = _dats!.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
|
|
||||||
(landblockId & 0xFFFF0000u) | 0xFFFEu);
|
|
||||||
|
|
||||||
if (lbInfo is not null)
|
|
||||||
{
|
|
||||||
// (2)+(3) EnvCell + Environment, per cell
|
|
||||||
if (lbInfo.NumCells > 0)
|
|
||||||
{
|
|
||||||
uint firstCellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
|
||||||
for (uint offset = 0; offset < lbInfo.NumCells; offset++)
|
|
||||||
{
|
|
||||||
uint envCellId = firstCellId + offset;
|
|
||||||
var envCell = _dats.Get<DatReaderWriter.DBObjs.EnvCell>(envCellId);
|
|
||||||
if (envCell is null) continue;
|
|
||||||
envCells[envCellId] = envCell;
|
|
||||||
if (envCell.EnvironmentId == 0) continue;
|
|
||||||
uint envId = 0x0D000000u | envCell.EnvironmentId;
|
|
||||||
if (!environments.ContainsKey(envId))
|
|
||||||
{
|
|
||||||
var environment = _dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
|
|
||||||
if (environment is not null) environments[envId] = environment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (4) building shell Setup, per building
|
|
||||||
foreach (var building in lbInfo.Buildings)
|
|
||||||
{
|
|
||||||
uint modelId = building.ModelId;
|
|
||||||
if ((modelId & 0xFF000000u) == 0x02000000u && !setups.ContainsKey(modelId))
|
|
||||||
{
|
|
||||||
var bldSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(modelId);
|
|
||||||
if (bldSetup is not null) setups[modelId] = bldSetup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (5)+(6) entity GfxObj (BSP cache) + entity Setup (ShadowObjects parts/lights)
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
uint src = entity.SourceGfxObjOrSetupId;
|
|
||||||
if ((src & 0xFF000000u) == 0x02000000u && !setups.ContainsKey(src))
|
|
||||||
{
|
|
||||||
var s = _dats.Get<DatReaderWriter.DBObjs.Setup>(src);
|
|
||||||
if (s is not null) setups[src] = s;
|
|
||||||
}
|
|
||||||
foreach (var meshRef in entity.MeshRefs)
|
|
||||||
{
|
|
||||||
uint gid = meshRef.GfxObjId;
|
|
||||||
if ((gid & 0xFF000000u) != 0x01000000u) continue;
|
|
||||||
if (gfxObjs.ContainsKey(gid)) continue;
|
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(gid);
|
|
||||||
if (gfx is not null) gfxObjs[gid] = gfx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads the exact prepared collision closure after the serialized DAT
|
|
||||||
/// transaction completes. The immutable package is independently
|
|
||||||
/// thread-safe, so these reads never extend the shared DAT-reader lock.
|
|
||||||
/// Missing or corrupt prepared data fails the complete near-tier build;
|
|
||||||
/// production must not publish a graph-only partial generation.
|
|
||||||
/// </summary>
|
|
||||||
private AcDream.Core.World.LandblockCollisionBuild
|
|
||||||
BuildPreparedCollisionClosure(
|
|
||||||
AcDream.Core.World.LoadedLandblock landblock)
|
|
||||||
{
|
|
||||||
AcDream.Core.World.PhysicsDatBundle dats =
|
|
||||||
landblock.PhysicsDats ??
|
|
||||||
AcDream.Core.World.PhysicsDatBundle.Empty;
|
|
||||||
ImmutableArray<uint> gfxObjIds = [.. dats.GfxObjs.Keys.Order()];
|
|
||||||
ImmutableArray<uint> setupIds = [.. dats.Setups.Keys.Order()];
|
|
||||||
ImmutableArray<uint> envCellIds = [.. dats.EnvCells.Keys.Order()];
|
|
||||||
var gfxObjs =
|
|
||||||
ImmutableDictionary.CreateBuilder<
|
|
||||||
uint,
|
|
||||||
AcDream.Core.Physics.FlatGfxObjCollisionAsset>();
|
|
||||||
var setups =
|
|
||||||
ImmutableDictionary.CreateBuilder<
|
|
||||||
uint,
|
|
||||||
AcDream.Core.Physics.FlatSetupCollision>();
|
|
||||||
var cellStructures =
|
|
||||||
ImmutableDictionary.CreateBuilder<
|
|
||||||
uint,
|
|
||||||
AcDream.Core.Physics.FlatCellStructureCollisionAsset>();
|
|
||||||
var envCells =
|
|
||||||
ImmutableDictionary.CreateBuilder<
|
|
||||||
uint,
|
|
||||||
AcDream.Core.Physics.FlatEnvCellTopology>();
|
|
||||||
|
|
||||||
foreach (uint id in gfxObjIds)
|
|
||||||
{
|
|
||||||
gfxObjs.Add(
|
|
||||||
id,
|
|
||||||
RequirePrepared(
|
|
||||||
_preparedCollisions.ReadGfxObjCollision(id),
|
|
||||||
"GfxObj collision",
|
|
||||||
id));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (uint id in setupIds)
|
|
||||||
{
|
|
||||||
setups.Add(
|
|
||||||
id,
|
|
||||||
RequirePrepared(
|
|
||||||
_preparedCollisions.ReadSetupCollision(id),
|
|
||||||
"Setup collision",
|
|
||||||
id));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (uint id in envCellIds)
|
|
||||||
{
|
|
||||||
cellStructures.Add(
|
|
||||||
id,
|
|
||||||
RequirePrepared(
|
|
||||||
_preparedCollisions.ReadCellStructureCollision(id),
|
|
||||||
"CellStruct collision",
|
|
||||||
id));
|
|
||||||
envCells.Add(
|
|
||||||
id,
|
|
||||||
RequirePrepared(
|
|
||||||
_preparedCollisions.ReadEnvCellTopology(id),
|
|
||||||
"EnvCell topology",
|
|
||||||
id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AcDream.Core.World.LandblockCollisionBuild(
|
|
||||||
gfxObjs.ToImmutable(),
|
|
||||||
setups.ToImmutable(),
|
|
||||||
cellStructures.ToImmutable(),
|
|
||||||
envCells.ToImmutable(),
|
|
||||||
gfxObjIds,
|
|
||||||
setupIds,
|
|
||||||
envCellIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static T RequirePrepared<T>(
|
|
||||||
PreparedCollisionReadResult<T> result,
|
|
||||||
string kind,
|
|
||||||
uint id)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
if (result.Status == PreparedAssetReadStatus.Loaded &&
|
|
||||||
result.Data is not null)
|
|
||||||
{
|
|
||||||
return result.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidDataException(
|
|
||||||
$"{kind} 0x{id:X8} is {result.Status}. " +
|
|
||||||
"The complete near-tier generation cannot be published.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
|
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
|
||||||
/// landblock on the worker thread. Pure CPU — no GL calls.
|
/// landblock on the worker thread. Pure CPU — no GL calls.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Spells;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Content;
|
||||||
|
|
||||||
namespace AcDream.App.UI.Layout;
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Ui;
|
using AcDream.Core.Ui;
|
||||||
|
using AcDream.Content;
|
||||||
|
|
||||||
namespace AcDream.App.UI.Layout;
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
|
|
||||||
266
src/AcDream.Content/LandblockPhysicsContentBuilder.cs
Normal file
266
src/AcDream.Content/LandblockPhysicsContentBuilder.cs
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
|
||||||
|
|
||||||
|
namespace AcDream.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Presentation-independent content half of one near-landblock collision
|
||||||
|
/// build. Graphical and no-window hosts consume the same parsed DAT closure,
|
||||||
|
/// immutable package collision records, and static Setup/GfxObj identities.
|
||||||
|
/// No Runtime world or backend resource is mutated here.
|
||||||
|
/// </summary>
|
||||||
|
public static class LandblockPhysicsContentBuilder
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<WorldEntity> HydrateStaticEntities(
|
||||||
|
IDatReaderWriter dats,
|
||||||
|
LoadedLandblock source,
|
||||||
|
Vector3 worldOffset)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(dats);
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
var hydrated = new List<WorldEntity>(source.Entities.Count);
|
||||||
|
foreach (WorldEntity candidate in source.Entities)
|
||||||
|
{
|
||||||
|
var meshRefs = new List<MeshRef>();
|
||||||
|
var bounds = new LocalBoundsAccumulator();
|
||||||
|
uint sourceId = candidate.SourceGfxObjOrSetupId;
|
||||||
|
if ((sourceId & 0xFF000000u) == 0x01000000u)
|
||||||
|
{
|
||||||
|
GfxObj? gfx = dats.Get<GfxObj>(sourceId);
|
||||||
|
if (gfx is not null)
|
||||||
|
{
|
||||||
|
(Vector3 Min, Vector3 Max)? partBounds =
|
||||||
|
GfxObjBounds.Get(gfx);
|
||||||
|
if (partBounds is not null)
|
||||||
|
{
|
||||||
|
bounds.Add(
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
partBounds.Value);
|
||||||
|
}
|
||||||
|
meshRefs.Add(new MeshRef(
|
||||||
|
sourceId,
|
||||||
|
Matrix4x4.Identity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((sourceId & 0xFF000000u) == 0x02000000u)
|
||||||
|
{
|
||||||
|
Setup? setup = dats.Get<Setup>(sourceId);
|
||||||
|
if (setup is not null)
|
||||||
|
{
|
||||||
|
foreach (MeshRef meshRef in SetupMesh.Flatten(setup))
|
||||||
|
{
|
||||||
|
GfxObj? gfx =
|
||||||
|
dats.Get<GfxObj>(meshRef.GfxObjId);
|
||||||
|
if (gfx is null)
|
||||||
|
continue;
|
||||||
|
(Vector3 Min, Vector3 Max)? partBounds =
|
||||||
|
GfxObjBounds.Get(gfx);
|
||||||
|
if (partBounds is not null)
|
||||||
|
{
|
||||||
|
bounds.Add(
|
||||||
|
meshRef.PartTransform,
|
||||||
|
partBounds.Value);
|
||||||
|
}
|
||||||
|
meshRefs.Add(meshRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meshRefs.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var entity = new WorldEntity
|
||||||
|
{
|
||||||
|
Id = candidate.Id,
|
||||||
|
SourceGfxObjOrSetupId = sourceId,
|
||||||
|
Position = candidate.Position + worldOffset,
|
||||||
|
Rotation = candidate.Rotation,
|
||||||
|
MeshRefs = meshRefs,
|
||||||
|
EffectCellId = candidate.EffectCellId,
|
||||||
|
IsBuildingShell = candidate.IsBuildingShell,
|
||||||
|
BuildingShellAnchorCellId =
|
||||||
|
candidate.BuildingShellAnchorCellId,
|
||||||
|
};
|
||||||
|
if (bounds.TryGet(out Vector3 min, out Vector3 max))
|
||||||
|
entity.SetLocalBounds(min, max);
|
||||||
|
hydrated.Add(entity);
|
||||||
|
}
|
||||||
|
return hydrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PhysicsDatBundle BuildDatBundle(
|
||||||
|
IDatReaderWriter dats,
|
||||||
|
uint landblockId,
|
||||||
|
IReadOnlyList<WorldEntity> entities)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(dats);
|
||||||
|
ArgumentNullException.ThrowIfNull(entities);
|
||||||
|
|
||||||
|
var envCells = new Dictionary<uint, EnvCell>();
|
||||||
|
var environments = new Dictionary<uint, DatEnvironment>();
|
||||||
|
var setups = new Dictionary<uint, Setup>();
|
||||||
|
var gfxObjs = new Dictionary<uint, GfxObj>();
|
||||||
|
LandBlockInfo? info = dats.Get<LandBlockInfo>(
|
||||||
|
(landblockId & 0xFFFF0000u) | 0xFFFEu);
|
||||||
|
|
||||||
|
if (info is not null)
|
||||||
|
{
|
||||||
|
uint firstCellId =
|
||||||
|
(landblockId & 0xFFFF0000u) | 0x0100u;
|
||||||
|
for (uint offset = 0; offset < info.NumCells; offset++)
|
||||||
|
{
|
||||||
|
uint envCellId = firstCellId + offset;
|
||||||
|
EnvCell? envCell = dats.Get<EnvCell>(envCellId);
|
||||||
|
if (envCell is null)
|
||||||
|
continue;
|
||||||
|
envCells[envCellId] = envCell;
|
||||||
|
if (envCell.EnvironmentId == 0)
|
||||||
|
continue;
|
||||||
|
uint environmentId =
|
||||||
|
0x0D000000u | envCell.EnvironmentId;
|
||||||
|
if (!environments.ContainsKey(environmentId)
|
||||||
|
&& dats.Get<DatEnvironment>(
|
||||||
|
environmentId) is { } environment)
|
||||||
|
{
|
||||||
|
environments[environmentId] = environment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var building in info.Buildings)
|
||||||
|
{
|
||||||
|
uint setupId = building.ModelId;
|
||||||
|
if ((setupId & 0xFF000000u) == 0x02000000u
|
||||||
|
&& !setups.ContainsKey(setupId)
|
||||||
|
&& dats.Get<Setup>(setupId) is { } setup)
|
||||||
|
{
|
||||||
|
setups[setupId] = setup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (WorldEntity entity in entities)
|
||||||
|
{
|
||||||
|
uint sourceId = entity.SourceGfxObjOrSetupId;
|
||||||
|
if ((sourceId & 0xFF000000u) == 0x02000000u
|
||||||
|
&& !setups.ContainsKey(sourceId)
|
||||||
|
&& dats.Get<Setup>(sourceId) is { } setup)
|
||||||
|
{
|
||||||
|
setups[sourceId] = setup;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MeshRef meshRef in entity.MeshRefs)
|
||||||
|
{
|
||||||
|
uint gfxObjId = meshRef.GfxObjId;
|
||||||
|
if ((gfxObjId & 0xFF000000u) != 0x01000000u
|
||||||
|
|| gfxObjs.ContainsKey(gfxObjId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dats.Get<GfxObj>(gfxObjId) is { } gfxObj)
|
||||||
|
gfxObjs[gfxObjId] = gfxObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PhysicsDatBundle(
|
||||||
|
info,
|
||||||
|
envCells,
|
||||||
|
environments,
|
||||||
|
setups,
|
||||||
|
gfxObjs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LandblockCollisionBuild BuildPreparedCollisionClosure(
|
||||||
|
IPreparedCollisionSource source,
|
||||||
|
LoadedLandblock landblock)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(landblock);
|
||||||
|
|
||||||
|
PhysicsDatBundle dats =
|
||||||
|
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||||
|
ImmutableArray<uint> gfxObjIds =
|
||||||
|
[.. dats.GfxObjs.Keys.Order()];
|
||||||
|
ImmutableArray<uint> setupIds =
|
||||||
|
[.. dats.Setups.Keys.Order()];
|
||||||
|
ImmutableArray<uint> envCellIds =
|
||||||
|
[.. dats.EnvCells.Keys.Order()];
|
||||||
|
var gfxObjs = ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
FlatGfxObjCollisionAsset>();
|
||||||
|
var setups = ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
FlatSetupCollision>();
|
||||||
|
var cellStructures = ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
FlatCellStructureCollisionAsset>();
|
||||||
|
var envCells = ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
FlatEnvCellTopology>();
|
||||||
|
|
||||||
|
foreach (uint id in gfxObjIds)
|
||||||
|
{
|
||||||
|
gfxObjs.Add(
|
||||||
|
id,
|
||||||
|
Require(
|
||||||
|
source.ReadGfxObjCollision(id),
|
||||||
|
"GfxObj collision",
|
||||||
|
id));
|
||||||
|
}
|
||||||
|
foreach (uint id in setupIds)
|
||||||
|
{
|
||||||
|
setups.Add(
|
||||||
|
id,
|
||||||
|
Require(
|
||||||
|
source.ReadSetupCollision(id),
|
||||||
|
"Setup collision",
|
||||||
|
id));
|
||||||
|
}
|
||||||
|
foreach (uint id in envCellIds)
|
||||||
|
{
|
||||||
|
cellStructures.Add(
|
||||||
|
id,
|
||||||
|
Require(
|
||||||
|
source.ReadCellStructureCollision(id),
|
||||||
|
"CellStruct collision",
|
||||||
|
id));
|
||||||
|
envCells.Add(
|
||||||
|
id,
|
||||||
|
Require(
|
||||||
|
source.ReadEnvCellTopology(id),
|
||||||
|
"EnvCell topology",
|
||||||
|
id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LandblockCollisionBuild(
|
||||||
|
gfxObjs.ToImmutable(),
|
||||||
|
setups.ToImmutable(),
|
||||||
|
cellStructures.ToImmutable(),
|
||||||
|
envCells.ToImmutable(),
|
||||||
|
gfxObjIds,
|
||||||
|
setupIds,
|
||||||
|
envCellIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Require<T>(
|
||||||
|
PreparedCollisionReadResult<T> result,
|
||||||
|
string kind,
|
||||||
|
uint id)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (result.Status == PreparedAssetReadStatus.Loaded
|
||||||
|
&& result.Data is not null)
|
||||||
|
{
|
||||||
|
return result.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{kind} 0x{id:X8} is {result.Status}. "
|
||||||
|
+ "The complete near-tier generation cannot be published.");
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/AcDream.Content/MagicCatalog.cs
Normal file
192
src/AcDream.Content/MagicCatalog.cs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
|
||||||
|
|
||||||
|
namespace AcDream.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable presentation and gameplay metadata for one retail spell
|
||||||
|
/// component WCID.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SpellComponentDescriptor(
|
||||||
|
uint WeenieClassId,
|
||||||
|
string Name,
|
||||||
|
uint Category,
|
||||||
|
uint IconId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-shareable projection of retail's spell, component, and
|
||||||
|
/// school-focus DAT tables. The catalog contains no player, inventory,
|
||||||
|
/// session, or presentation state.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MagicCatalog
|
||||||
|
{
|
||||||
|
private const uint SpellTableDid = 0x0E00000Eu;
|
||||||
|
private const uint SpellComponentTableDid = 0x0E00000Fu;
|
||||||
|
private const uint ComponentIdMapDid = 0x27000002u;
|
||||||
|
|
||||||
|
private readonly FrozenDictionary<uint, SpellFormulaDefinition>
|
||||||
|
_formulaDefinitions;
|
||||||
|
private readonly FrozenDictionary<uint, uint> _wcidByScid;
|
||||||
|
private readonly FrozenDictionary<uint, uint> _magicPackWcidBySchool;
|
||||||
|
private readonly FrozenDictionary<uint, int> _spellLevels;
|
||||||
|
|
||||||
|
private MagicCatalog(
|
||||||
|
CoreSpellTable spellTable,
|
||||||
|
FrozenDictionary<uint, SpellComponentDescriptor> components,
|
||||||
|
FrozenDictionary<uint, SpellFormulaDefinition> formulaDefinitions,
|
||||||
|
FrozenDictionary<uint, uint> wcidByScid,
|
||||||
|
FrozenDictionary<uint, uint> magicPackWcidBySchool,
|
||||||
|
FrozenDictionary<uint, int> spellLevels)
|
||||||
|
{
|
||||||
|
SpellTable = spellTable;
|
||||||
|
Components = components;
|
||||||
|
_formulaDefinitions = formulaDefinitions;
|
||||||
|
_wcidByScid = wcidByScid;
|
||||||
|
_magicPackWcidBySchool = magicPackWcidBySchool;
|
||||||
|
_spellLevels = spellLevels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MagicCatalog Empty { get; } = new(
|
||||||
|
CoreSpellTable.Empty,
|
||||||
|
EmptyFrozen<SpellComponentDescriptor>(),
|
||||||
|
EmptyFrozen<SpellFormulaDefinition>(),
|
||||||
|
EmptyFrozen<uint>(),
|
||||||
|
EmptyFrozen<uint>(),
|
||||||
|
EmptyFrozen<int>());
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the per-session policy view over this immutable catalog. The
|
||||||
|
/// returned service borrows only the supplied session's object table and
|
||||||
|
/// identity/account delegates.
|
||||||
|
/// </summary>
|
||||||
|
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 = 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.ToFrozenDictionary(),
|
||||||
|
formulaDefinitions.ToFrozenDictionary(),
|
||||||
|
wcidByScid.ToFrozenDictionary(),
|
||||||
|
magicPackWcidBySchool.ToFrozenDictionary(),
|
||||||
|
spellLevels.ToFrozenDictionary());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FrozenDictionary<uint, TValue> EmptyFrozen<TValue>() =>
|
||||||
|
new Dictionary<uint, TValue>().ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using AcDream.Content;
|
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
namespace AcDream.App.UI;
|
namespace AcDream.Content;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ports <c>DBCache::GetDIDFromEnumStatic @ 0x00413940</c>: the portal master map
|
/// Ports <c>DBCache::GetDIDFromEnumStatic @ 0x00413940</c>: the portal master map
|
||||||
|
|
@ -45,7 +45,7 @@ public sealed class RuntimeDatCollection : IDatReaderWriter
|
||||||
{
|
{
|
||||||
private readonly DatCollection _raw;
|
private readonly DatCollection _raw;
|
||||||
private readonly DatCollectionAdapter _bounded;
|
private readonly DatCollectionAdapter _bounded;
|
||||||
private bool _disposed;
|
private int _disposeStage;
|
||||||
|
|
||||||
internal RuntimeDatCollection(DatCollection raw)
|
internal RuntimeDatCollection(DatCollection raw)
|
||||||
{
|
{
|
||||||
|
|
@ -95,11 +95,22 @@ public sealed class RuntimeDatCollection : IDatReaderWriter
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
while (_disposeStage < 2)
|
||||||
return;
|
{
|
||||||
|
switch (_disposeStage)
|
||||||
_disposed = true;
|
{
|
||||||
_bounded.Dispose();
|
case 0:
|
||||||
_raw.Dispose();
|
_bounded.Dispose();
|
||||||
|
_disposeStage++;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
_raw.Dispose();
|
||||||
|
_disposeStage++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Unknown DAT owner teardown stage.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
245
src/AcDream.Content/SharedPreparedCollisionCache.cs
Normal file
245
src/AcDream.Content/SharedPreparedCollisionCache.cs
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.Content;
|
||||||
|
|
||||||
|
public readonly record struct SharedPreparedCollisionCacheSnapshot(
|
||||||
|
int Count,
|
||||||
|
int Capacity,
|
||||||
|
long Hits,
|
||||||
|
long Misses,
|
||||||
|
bool IsDisposed)
|
||||||
|
{
|
||||||
|
public bool IsBounded => Count >= 0 && Count <= Capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-scoped bounded cache over immutable prepared collision payloads.
|
||||||
|
/// The wrapped package remains owned by the caller; disposing this cache only
|
||||||
|
/// retires its decoded references. Sessions retain separate
|
||||||
|
/// <see cref="PhysicsDataCache"/> instances and share only these immutable
|
||||||
|
/// values.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SharedPreparedCollisionCache
|
||||||
|
: IPreparedCollisionSource
|
||||||
|
{
|
||||||
|
public const int DefaultCapacity = 4096;
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly IPreparedCollisionSource _source;
|
||||||
|
private readonly int _capacity;
|
||||||
|
private readonly Dictionary<CacheKey, LinkedListNode<CacheEntry>>
|
||||||
|
_entries = new();
|
||||||
|
private readonly LinkedList<CacheEntry> _lru = new();
|
||||||
|
private long _probes;
|
||||||
|
private long _reads;
|
||||||
|
private long _loaded;
|
||||||
|
private long _missing;
|
||||||
|
private long _corrupt;
|
||||||
|
private long _hits;
|
||||||
|
private long _misses;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public SharedPreparedCollisionCache(
|
||||||
|
IPreparedCollisionSource source,
|
||||||
|
int capacity = DefaultCapacity)
|
||||||
|
{
|
||||||
|
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||||
|
if (capacity <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||||
|
_capacity = capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionSourceStats CollisionStats => new(
|
||||||
|
Interlocked.Read(ref _probes),
|
||||||
|
Interlocked.Read(ref _reads),
|
||||||
|
Interlocked.Read(ref _loaded),
|
||||||
|
Interlocked.Read(ref _missing),
|
||||||
|
Interlocked.Read(ref _corrupt));
|
||||||
|
|
||||||
|
public SharedPreparedCollisionCacheSnapshot CaptureSnapshot()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return new SharedPreparedCollisionCacheSnapshot(
|
||||||
|
_entries.Count,
|
||||||
|
_capacity,
|
||||||
|
Interlocked.Read(ref _hits),
|
||||||
|
Interlocked.Read(ref _misses),
|
||||||
|
_disposed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedAssetPresence ProbeCollision(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId)
|
||||||
|
{
|
||||||
|
PreparedCollisionTypeContract.Validate(type);
|
||||||
|
Interlocked.Increment(ref _probes);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
if (_entries.TryGetValue(
|
||||||
|
new CacheKey(type, sourceFileId),
|
||||||
|
out LinkedListNode<CacheEntry>? node))
|
||||||
|
{
|
||||||
|
Touch(node);
|
||||||
|
return node.Value.Status switch
|
||||||
|
{
|
||||||
|
PreparedAssetReadStatus.Loaded =>
|
||||||
|
PreparedAssetPresence.Available,
|
||||||
|
PreparedAssetReadStatus.Corrupt =>
|
||||||
|
PreparedAssetPresence.Corrupt,
|
||||||
|
_ => PreparedAssetPresence.Missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _source.ProbeCollision(type, sourceFileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Read(
|
||||||
|
PakAssetType.GfxObjCollision,
|
||||||
|
sourceFileId,
|
||||||
|
token => _source.ReadGfxObjCollision(sourceFileId, token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Read(
|
||||||
|
PakAssetType.SetupCollision,
|
||||||
|
sourceFileId,
|
||||||
|
token => _source.ReadSetupCollision(sourceFileId, token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Read(
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
sourceFileId,
|
||||||
|
token => _source.ReadCellStructureCollision(
|
||||||
|
sourceFileId,
|
||||||
|
token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Read(
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
sourceFileId,
|
||||||
|
token => _source.ReadEnvCellTopology(sourceFileId, token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_entries.Clear();
|
||||||
|
_lru.Clear();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedCollisionReadResult<T> Read<T>(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId,
|
||||||
|
Func<CancellationToken, PreparedCollisionReadResult<T>> loader,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
Interlocked.Increment(ref _reads);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
var key = new CacheKey(type, sourceFileId);
|
||||||
|
if (_entries.TryGetValue(
|
||||||
|
key,
|
||||||
|
out LinkedListNode<CacheEntry>? node))
|
||||||
|
{
|
||||||
|
Touch(node);
|
||||||
|
Interlocked.Increment(ref _hits);
|
||||||
|
return CountAndConvert<T>(node.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _misses);
|
||||||
|
PreparedCollisionReadResult<T> loaded =
|
||||||
|
loader(cancellationToken);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var entry = new CacheEntry(
|
||||||
|
key,
|
||||||
|
loaded.Status,
|
||||||
|
loaded.Data);
|
||||||
|
LinkedListNode<CacheEntry> added = _lru.AddFirst(entry);
|
||||||
|
_entries.Add(key, added);
|
||||||
|
Trim();
|
||||||
|
return CountAndConvert<T>(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedCollisionReadResult<T> CountAndConvert<T>(
|
||||||
|
CacheEntry entry)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
switch (entry.Status)
|
||||||
|
{
|
||||||
|
case PreparedAssetReadStatus.Loaded
|
||||||
|
when entry.Data is T data:
|
||||||
|
Interlocked.Increment(ref _loaded);
|
||||||
|
return PreparedCollisionReadResult<T>.Loaded(data);
|
||||||
|
case PreparedAssetReadStatus.Missing:
|
||||||
|
Interlocked.Increment(ref _missing);
|
||||||
|
return PreparedCollisionReadResult<T>.Missing;
|
||||||
|
case PreparedAssetReadStatus.Corrupt:
|
||||||
|
Interlocked.Increment(ref _corrupt);
|
||||||
|
return PreparedCollisionReadResult<T>.Corrupt;
|
||||||
|
default:
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Prepared collision cache entry {entry.Key} has an invalid payload.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Touch(LinkedListNode<CacheEntry> node)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_lru.First, node))
|
||||||
|
return;
|
||||||
|
_lru.Remove(node);
|
||||||
|
_lru.AddFirst(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Trim()
|
||||||
|
{
|
||||||
|
while (_entries.Count > _capacity)
|
||||||
|
{
|
||||||
|
LinkedListNode<CacheEntry> tail =
|
||||||
|
_lru.Last
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Prepared collision LRU lost its terminal entry.");
|
||||||
|
_entries.Remove(tail.Value.Key);
|
||||||
|
_lru.RemoveLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureNotDisposed() =>
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
private readonly record struct CacheKey(
|
||||||
|
PakAssetType Type,
|
||||||
|
uint SourceFileId);
|
||||||
|
|
||||||
|
private sealed record CacheEntry(
|
||||||
|
CacheKey Key,
|
||||||
|
PreparedAssetReadStatus Status,
|
||||||
|
object? Data);
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
using CoreMagicSchool = AcDream.Core.Spells.MagicSchool;
|
using CoreMagicSchool = AcDream.Core.Spells.MagicSchool;
|
||||||
|
|
||||||
namespace AcDream.App.Spells;
|
namespace AcDream.Content;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Projects portal.dat's complete retail <see cref="SpellBase"/> record into
|
/// Projects portal.dat's complete retail <see cref="SpellBase"/> record into
|
||||||
|
|
@ -19,7 +19,7 @@ namespace AcDream.App.Spells;
|
||||||
/// School names: <c>CSpellBase::SchoolEnumToName @ 0x00597400</c>.
|
/// School names: <c>CSpellBase::SchoolEnumToName @ 0x00597400</c>.
|
||||||
/// Target type: <c>SpellFormula::GetTargetingType @ 0x005BC910</c>.
|
/// Target type: <c>SpellFormula::GetTargetingType @ 0x005BC910</c>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static class RetailSpellMetadataProjector
|
public static class RetailSpellMetadataProjector
|
||||||
{
|
{
|
||||||
public static SpellMetadata Project(
|
public static SpellMetadata Project(
|
||||||
uint spellId,
|
uint spellId,
|
||||||
|
|
@ -2,9 +2,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Spells;
|
|
||||||
|
|
||||||
namespace AcDream.App.Spells;
|
namespace AcDream.Core.Spells;
|
||||||
|
|
||||||
public sealed record SpellFormulaDefinition(
|
public sealed record SpellFormulaDefinition(
|
||||||
uint FormulaVersion,
|
uint FormulaVersion,
|
||||||
|
|
@ -7,7 +7,8 @@ namespace AcDream.Core.Spells;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable lookup of retail spell metadata. Production builds create this
|
/// Immutable lookup of retail spell metadata. Production builds create this
|
||||||
/// table from portal.dat through the App-layer <c>MagicCatalog</c>. The CSV
|
/// table from portal.dat through Content's process-shareable
|
||||||
|
/// <c>MagicCatalog</c>. The CSV
|
||||||
/// parser below remains for historical tooling and small synthetic tests.
|
/// parser below remains for historical tooling and small synthetic tests.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
|
@ -43,12 +44,29 @@ internal sealed class HeadlessGameplayOperations
|
||||||
|
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private GameRuntime? _runtime;
|
private GameRuntime? _runtime;
|
||||||
|
private SpellComponentRequirementService? _componentRequirements;
|
||||||
private WorldSession? _session;
|
private WorldSession? _session;
|
||||||
private SessionRoute? _route;
|
private SessionRoute? _route;
|
||||||
|
|
||||||
internal void Bind(GameRuntime runtime) =>
|
internal void Bind(
|
||||||
_runtime = runtime
|
GameRuntime runtime,
|
||||||
?? throw new ArgumentNullException(nameof(runtime));
|
MagicCatalog? catalog,
|
||||||
|
Func<string> accountName)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
ArgumentNullException.ThrowIfNull(accountName);
|
||||||
|
if (_runtime is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Headless gameplay operations are already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_runtime = runtime;
|
||||||
|
_componentRequirements = catalog?.CreateRequirementService(
|
||||||
|
runtime.InventoryOwner.Objects,
|
||||||
|
() => runtime.PlayerIdentity.ServerGuid,
|
||||||
|
accountName);
|
||||||
|
}
|
||||||
|
|
||||||
internal ILiveSessionCommandRouting CreateRoute(
|
internal ILiveSessionCommandRouting CreateRoute(
|
||||||
WorldSession session)
|
WorldSession session)
|
||||||
|
|
@ -151,12 +169,16 @@ internal sealed class HeadlessGameplayOperations
|
||||||
|
|
||||||
public bool HasRequiredComponents(uint spellId)
|
public bool HasRequiredComponents(uint spellId)
|
||||||
{
|
{
|
||||||
|
if (_componentRequirements is { } requirements)
|
||||||
|
return requirements.HasRequiredComponents(spellId);
|
||||||
|
|
||||||
GameRuntime runtime = RequireRuntime();
|
GameRuntime runtime = RequireRuntime();
|
||||||
ClientObject? player = runtime.InventoryOwner.Objects.Get(
|
ClientObject? player = runtime.InventoryOwner.Objects.Get(
|
||||||
runtime.PlayerIdentity.ServerGuid);
|
runtime.PlayerIdentity.ServerGuid);
|
||||||
return player is not null
|
return player is not null
|
||||||
&& !player.Properties.GetBool(
|
&& !player.Properties.GetBool(
|
||||||
SpellComponentsRequiredProperty,
|
SpellComponentRequirementService
|
||||||
|
.SpellComponentsRequiredProperty,
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,6 +286,4 @@ internal sealed class HeadlessGameplayOperations
|
||||||
_route = null;
|
_route = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const uint SpellComponentsRequiredProperty = 68u;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,20 @@ internal readonly record struct HeadlessProcessContentSnapshot(
|
||||||
|
|
||||||
internal interface IHeadlessProcessContentFactory
|
internal interface IHeadlessProcessContentFactory
|
||||||
{
|
{
|
||||||
(IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
HeadlessOpenedProcessContent Open(
|
||||||
HeadlessContentDescriptor descriptor,
|
HeadlessContentDescriptor descriptor,
|
||||||
Action<string> diagnostic);
|
Action<string> diagnostic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed record HeadlessOpenedProcessContent(
|
||||||
|
IDatReaderWriter Dats,
|
||||||
|
IPreparedAssetSource Prepared,
|
||||||
|
MagicCatalog Magic);
|
||||||
|
|
||||||
internal sealed class ProductionHeadlessProcessContentFactory
|
internal sealed class ProductionHeadlessProcessContentFactory
|
||||||
: IHeadlessProcessContentFactory
|
: IHeadlessProcessContentFactory
|
||||||
{
|
{
|
||||||
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
public HeadlessOpenedProcessContent Open(
|
||||||
HeadlessContentDescriptor descriptor,
|
HeadlessContentDescriptor descriptor,
|
||||||
Action<string> diagnostic)
|
Action<string> diagnostic)
|
||||||
{
|
{
|
||||||
|
|
@ -36,18 +41,30 @@ internal sealed class ProductionHeadlessProcessContentFactory
|
||||||
string preparedAssetPath =
|
string preparedAssetPath =
|
||||||
Path.GetFullPath(descriptor.PreparedAssetPath);
|
Path.GetFullPath(descriptor.PreparedAssetPath);
|
||||||
IDatReaderWriter? dats = null;
|
IDatReaderWriter? dats = null;
|
||||||
|
IPreparedAssetSource? prepared = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
dats = RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
dats = RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
||||||
var prepared = new PakPreparedAssetSource(
|
prepared = new PakPreparedAssetSource(
|
||||||
preparedAssetPath,
|
preparedAssetPath,
|
||||||
dats,
|
dats,
|
||||||
diagnostic);
|
diagnostic);
|
||||||
return (dats, prepared);
|
MagicCatalog magic = MagicCatalog.Load(dats);
|
||||||
|
return new HeadlessOpenedProcessContent(
|
||||||
|
dats,
|
||||||
|
prepared,
|
||||||
|
magic);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
dats?.Dispose();
|
try
|
||||||
|
{
|
||||||
|
prepared?.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
dats?.Dispose();
|
||||||
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,8 +79,10 @@ internal sealed class ProductionHeadlessProcessContentFactory
|
||||||
internal sealed class HeadlessProcessContentOwner : IDisposable
|
internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
|
private readonly MagicCatalog _magic;
|
||||||
private IDatReaderWriter? _dats;
|
private IDatReaderWriter? _dats;
|
||||||
private IPreparedAssetSource? _prepared;
|
private IPreparedAssetSource? _prepared;
|
||||||
|
private SharedPreparedCollisionCache? _collision;
|
||||||
private int _leaseCount;
|
private int _leaseCount;
|
||||||
private bool _disposeRequested;
|
private bool _disposeRequested;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
@ -75,30 +94,41 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(descriptor);
|
ArgumentNullException.ThrowIfNull(descriptor);
|
||||||
ArgumentNullException.ThrowIfNull(diagnostic);
|
ArgumentNullException.ThrowIfNull(diagnostic);
|
||||||
(IDatReaderWriter dats, IPreparedAssetSource prepared) =
|
HeadlessOpenedProcessContent? opened =
|
||||||
(factory ?? new ProductionHeadlessProcessContentFactory())
|
(factory ?? new ProductionHeadlessProcessContentFactory())
|
||||||
.Open(descriptor, diagnostic);
|
.Open(descriptor, diagnostic);
|
||||||
_dats = dats
|
if (opened is null)
|
||||||
?? throw new InvalidOperationException(
|
{
|
||||||
"The headless content factory returned no DAT owner.");
|
throw new InvalidOperationException(
|
||||||
_prepared = prepared
|
"The headless content factory returned no content owner.");
|
||||||
?? throw new InvalidOperationException(
|
}
|
||||||
"The headless content factory returned no prepared source.");
|
IDatReaderWriter? dats = opened.Dats;
|
||||||
if (prepared is not IPreparedCollisionSource)
|
IPreparedAssetSource? prepared = opened.Prepared;
|
||||||
|
MagicCatalog? magic = opened.Magic;
|
||||||
|
bool incomplete =
|
||||||
|
dats is null || prepared is null || magic is null;
|
||||||
|
if (incomplete || prepared is not IPreparedCollisionSource)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
prepared.Dispose();
|
prepared?.Dispose();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
dats.Dispose();
|
dats?.Dispose();
|
||||||
}
|
}
|
||||||
_prepared = null;
|
throw !incomplete
|
||||||
_dats = null;
|
&& prepared is not IPreparedCollisionSource
|
||||||
throw new NotSupportedException(
|
? new NotSupportedException(
|
||||||
"Headless production content must expose prepared collision.");
|
"Headless production content must expose prepared collision.")
|
||||||
|
: new InvalidOperationException(
|
||||||
|
"The headless content factory returned an incomplete owner.");
|
||||||
}
|
}
|
||||||
|
_dats = dats!;
|
||||||
|
_prepared = prepared!;
|
||||||
|
_magic = magic!;
|
||||||
|
_collision = new SharedPreparedCollisionCache(
|
||||||
|
(IPreparedCollisionSource)prepared!);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal HeadlessProcessContentLease AcquireLease(string sessionId)
|
internal HeadlessProcessContentLease AcquireLease(string sessionId)
|
||||||
|
|
@ -117,7 +147,9 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||||
this,
|
this,
|
||||||
sessionId,
|
sessionId,
|
||||||
_dats!,
|
_dats!,
|
||||||
_prepared!);
|
_prepared!,
|
||||||
|
_collision!,
|
||||||
|
_magic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +195,11 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||||
|
|
||||||
private void DrainResources()
|
private void DrainResources()
|
||||||
{
|
{
|
||||||
|
if (_collision is not null)
|
||||||
|
{
|
||||||
|
_collision.Dispose();
|
||||||
|
_collision = null;
|
||||||
|
}
|
||||||
if (_prepared is not null)
|
if (_prepared is not null)
|
||||||
{
|
{
|
||||||
_prepared.Dispose();
|
_prepared.Dispose();
|
||||||
|
|
@ -181,17 +218,23 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||||
private HeadlessProcessContentOwner? _owner;
|
private HeadlessProcessContentOwner? _owner;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly IPreparedAssetSource _prepared;
|
private readonly IPreparedAssetSource _prepared;
|
||||||
|
private readonly IPreparedCollisionSource _collision;
|
||||||
|
private readonly MagicCatalog _magic;
|
||||||
|
|
||||||
internal HeadlessProcessContentLease(
|
internal HeadlessProcessContentLease(
|
||||||
HeadlessProcessContentOwner owner,
|
HeadlessProcessContentOwner owner,
|
||||||
string sessionId,
|
string sessionId,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
IPreparedAssetSource prepared)
|
IPreparedAssetSource prepared,
|
||||||
|
IPreparedCollisionSource collision,
|
||||||
|
MagicCatalog magic)
|
||||||
{
|
{
|
||||||
_owner = owner;
|
_owner = owner;
|
||||||
SessionId = sessionId;
|
SessionId = sessionId;
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_prepared = prepared;
|
_prepared = prepared;
|
||||||
|
_collision = collision;
|
||||||
|
_magic = magic;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal string SessionId { get; }
|
internal string SessionId { get; }
|
||||||
|
|
@ -214,8 +257,23 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IPreparedCollisionSource PreparedCollision =>
|
internal IPreparedCollisionSource PreparedCollision
|
||||||
(IPreparedCollisionSource)PreparedAssets;
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
||||||
|
return _collision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal MagicCatalog MagicCatalog
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
||||||
|
return _magic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
private long _reconnectDeadline;
|
private long _reconnectDeadline;
|
||||||
private bool _reconnectPending;
|
private bool _reconnectPending;
|
||||||
private ulong _stoppedGeneration;
|
private ulong _stoppedGeneration;
|
||||||
|
private string _accountName = string.Empty;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
internal HeadlessSessionHost(
|
internal HeadlessSessionHost(
|
||||||
|
|
@ -172,7 +173,15 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
CombatTime: () =>
|
CombatTime: () =>
|
||||||
runtimeRef?.Clock.SimulationTimeSeconds ?? 0d));
|
runtimeRef?.Clock.SimulationTimeSeconds ?? 0d));
|
||||||
runtimeRef = runtime;
|
runtimeRef = runtime;
|
||||||
gameplay.Bind(runtime);
|
if (contentLease is { } content)
|
||||||
|
{
|
||||||
|
runtime.CharacterOwner.InstallSpellMetadata(
|
||||||
|
content.MagicCatalog.SpellTable);
|
||||||
|
}
|
||||||
|
gameplay.Bind(
|
||||||
|
runtime,
|
||||||
|
contentLease?.MagicCatalog,
|
||||||
|
() => _accountName);
|
||||||
|
|
||||||
var bridge = new SessionCommandBridge();
|
var bridge = new SessionCommandBridge();
|
||||||
var commands = new DirectGameRuntimeCommandAdapter(
|
var commands = new DirectGameRuntimeCommandAdapter(
|
||||||
|
|
@ -448,6 +457,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
password,
|
password,
|
||||||
MapCharacterSelector(_descriptor.Character));
|
MapCharacterSelector(_descriptor.Character));
|
||||||
LiveSessionStartResult result = _liveSession.Start(options);
|
LiveSessionStartResult result = _liveSession.Start(options);
|
||||||
|
if (result.Selection is { } selection)
|
||||||
|
_accountName = selection.AccountName;
|
||||||
RuntimeSessionStartResult converted = Convert(result);
|
RuntimeSessionStartResult converted = Convert(result);
|
||||||
if (converted.Error is { } error)
|
if (converted.Error is { } error)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Composition;
|
||||||
using AcDream.App.Diagnostics;
|
using AcDream.App.Diagnostics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Spells;
|
using AcDream.App.Spells;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.Content.Vfx;
|
using AcDream.Content.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Selection;
|
using AcDream.App.Rendering.Selection;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Spells;
|
using AcDream.App.Spells;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.Spells;
|
using AcDream.App.Spells;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
|
|
||||||
143
tests/AcDream.Content.Tests/SharedPreparedCollisionCacheTests.cs
Normal file
143
tests/AcDream.Content.Tests/SharedPreparedCollisionCacheTests.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.Content.Tests;
|
||||||
|
|
||||||
|
public sealed class SharedPreparedCollisionCacheTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void LoadedPayloadIsSharedByReferenceAndLeastRecentlyUsedIsBounded()
|
||||||
|
{
|
||||||
|
var source = new FixtureSource();
|
||||||
|
using var cache = new SharedPreparedCollisionCache(
|
||||||
|
source,
|
||||||
|
capacity: 2);
|
||||||
|
|
||||||
|
FlatSetupCollision first =
|
||||||
|
Assert.IsType<FlatSetupCollision>(
|
||||||
|
cache.ReadSetupCollision(1u).Data);
|
||||||
|
Assert.Same(
|
||||||
|
first,
|
||||||
|
cache.ReadSetupCollision(1u).Data);
|
||||||
|
_ = cache.ReadSetupCollision(2u);
|
||||||
|
Assert.Same(
|
||||||
|
first,
|
||||||
|
cache.ReadSetupCollision(1u).Data);
|
||||||
|
_ = cache.ReadSetupCollision(3u);
|
||||||
|
|
||||||
|
SharedPreparedCollisionCacheSnapshot snapshot =
|
||||||
|
cache.CaptureSnapshot();
|
||||||
|
Assert.Equal(2, snapshot.Count);
|
||||||
|
Assert.True(snapshot.IsBounded);
|
||||||
|
Assert.Equal(2, snapshot.Hits);
|
||||||
|
Assert.Equal(3, snapshot.Misses);
|
||||||
|
Assert.Equal(1, source.SetupReads[1u]);
|
||||||
|
Assert.Equal(1, source.SetupReads[2u]);
|
||||||
|
|
||||||
|
_ = cache.ReadSetupCollision(2u);
|
||||||
|
|
||||||
|
Assert.Equal(2, source.SetupReads[2u]);
|
||||||
|
Assert.True(cache.CaptureSnapshot().IsBounded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TypeParticipatesInIdentityAndDisposeDoesNotOwnPackage()
|
||||||
|
{
|
||||||
|
var source = new FixtureSource();
|
||||||
|
var cache = new SharedPreparedCollisionCache(source, capacity: 4);
|
||||||
|
|
||||||
|
FlatSetupCollision setup =
|
||||||
|
Assert.IsType<FlatSetupCollision>(
|
||||||
|
cache.ReadSetupCollision(7u).Data);
|
||||||
|
FlatGfxObjCollisionAsset gfx =
|
||||||
|
Assert.IsType<FlatGfxObjCollisionAsset>(
|
||||||
|
cache.ReadGfxObjCollision(7u).Data);
|
||||||
|
|
||||||
|
Assert.NotSame(setup, gfx);
|
||||||
|
Assert.Equal(1, source.SetupReads[7u]);
|
||||||
|
Assert.Equal(1, source.GfxReads[7u]);
|
||||||
|
|
||||||
|
cache.Dispose();
|
||||||
|
|
||||||
|
Assert.False(source.IsDisposed);
|
||||||
|
Assert.True(cache.CaptureSnapshot().IsDisposed);
|
||||||
|
Assert.Throws<ObjectDisposedException>(
|
||||||
|
() => cache.ReadSetupCollision(7u));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FixtureSource : IPreparedCollisionSource
|
||||||
|
{
|
||||||
|
internal Dictionary<uint, int> SetupReads { get; } = new();
|
||||||
|
internal Dictionary<uint, int> GfxReads { get; } = new();
|
||||||
|
internal bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
|
public PreparedCollisionSourceStats CollisionStats => default;
|
||||||
|
|
||||||
|
public PreparedAssetPresence ProbeCollision(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId) =>
|
||||||
|
PreparedAssetPresence.Available;
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
Increment(GfxReads, sourceFileId);
|
||||||
|
return PreparedCollisionReadResult<
|
||||||
|
FlatGfxObjCollisionAsset>.Loaded(
|
||||||
|
new FlatGfxObjCollisionAsset(
|
||||||
|
EmptyPhysicsBsp(),
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
Increment(SetupReads, sourceFileId);
|
||||||
|
return PreparedCollisionReadResult<FlatSetupCollision>.Loaded(
|
||||||
|
new FlatSetupCollision(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<
|
||||||
|
FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedCollisionReadResult<
|
||||||
|
FlatCellStructureCollisionAsset>.Missing;
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedCollisionReadResult<FlatEnvCellTopology>.Missing;
|
||||||
|
|
||||||
|
public void Dispose() => IsDisposed = true;
|
||||||
|
|
||||||
|
private static FlatPhysicsBsp EmptyPhysicsBsp() => new(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||||
|
ImmutableArray<int>.Empty,
|
||||||
|
FlatPolygonTable.Empty);
|
||||||
|
|
||||||
|
private static void Increment(
|
||||||
|
Dictionary<uint, int> counts,
|
||||||
|
uint id) =>
|
||||||
|
counts[id] = counts.TryGetValue(id, out int count)
|
||||||
|
? count + 1
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.Spells;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -9,7 +9,7 @@ using DatMagicSchool = DatReaderWriter.Enums.MagicSchool;
|
||||||
using DatSpellCategory = DatReaderWriter.Enums.SpellCategory;
|
using DatSpellCategory = DatReaderWriter.Enums.SpellCategory;
|
||||||
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
|
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Spells;
|
namespace AcDream.Content.Tests.Spells;
|
||||||
|
|
||||||
public sealed class RetailSpellMetadataProjectorTests
|
public sealed class RetailSpellMetadataProjectorTests
|
||||||
{
|
{
|
||||||
|
|
@ -79,7 +79,8 @@ public sealed class RetailSpellMetadataProjectorTests
|
||||||
string? csvPath = ResolveRepoFile("docs", "research", "data", "spells.csv");
|
string? csvPath = ResolveRepoFile("docs", "research", "data", "spells.csv");
|
||||||
if (datDir is null || csvPath is null) return;
|
if (datDir is null || csvPath is null) return;
|
||||||
|
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
using var rawDats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
using var dats = new DatCollectionAdapter(rawDats);
|
||||||
MagicCatalog catalog = MagicCatalog.Load(dats);
|
MagicCatalog catalog = MagicCatalog.Load(dats);
|
||||||
CoreSpellTable legacy = CoreSpellTable.LoadFromCsv(csvPath);
|
CoreSpellTable legacy = CoreSpellTable.LoadFromCsv(csvPath);
|
||||||
|
|
||||||
|
|
@ -106,7 +107,8 @@ public sealed class RetailSpellMetadataProjectorTests
|
||||||
string? csvPath = ResolveRepoFile("docs", "research", "data", "spells.csv");
|
string? csvPath = ResolveRepoFile("docs", "research", "data", "spells.csv");
|
||||||
if (datDir is null || csvPath is null) return;
|
if (datDir is null || csvPath is null) return;
|
||||||
|
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
using var rawDats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
using var dats = new DatCollectionAdapter(rawDats);
|
||||||
MagicCatalog catalog = MagicCatalog.Load(dats);
|
MagicCatalog catalog = MagicCatalog.Load(dats);
|
||||||
CoreSpellTable legacy = CoreSpellTable.LoadFromCsv(csvPath);
|
CoreSpellTable legacy = CoreSpellTable.LoadFromCsv(csvPath);
|
||||||
var mismatches = new List<string>();
|
var mismatches = new List<string>();
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
using AcDream.App.Spells;
|
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Spells;
|
namespace AcDream.Core.Tests.Spells;
|
||||||
|
|
||||||
public sealed class SpellComponentRequirementServiceTests
|
public sealed class SpellComponentRequirementServiceTests
|
||||||
{
|
{
|
||||||
|
|
@ -111,6 +111,9 @@ public sealed class HeadlessProcessContentOwnerTests
|
||||||
Assert.Equal(1, factory.OpenCount);
|
Assert.Equal(1, factory.OpenCount);
|
||||||
Assert.Equal(sessionCount, host.Sessions.Count);
|
Assert.Equal(sessionCount, host.Sessions.Count);
|
||||||
Assert.Equal(sessionCount, host.Content?.LeaseCount);
|
Assert.Equal(sessionCount, host.Content?.LeaseCount);
|
||||||
|
IPreparedCollisionSource sharedCollision =
|
||||||
|
Assert.IsAssignableFrom<IPreparedCollisionSource>(
|
||||||
|
host.Sessions[0].Content?.PreparedCollision);
|
||||||
Assert.All(
|
Assert.All(
|
||||||
host.Sessions,
|
host.Sessions,
|
||||||
session =>
|
session =>
|
||||||
|
|
@ -121,6 +124,12 @@ public sealed class HeadlessProcessContentOwnerTests
|
||||||
Assert.Same(
|
Assert.Same(
|
||||||
factory.PreparedResource,
|
factory.PreparedResource,
|
||||||
session.Content?.PreparedAssets);
|
session.Content?.PreparedAssets);
|
||||||
|
Assert.Same(
|
||||||
|
MagicCatalog.Empty,
|
||||||
|
session.Content?.MagicCatalog);
|
||||||
|
Assert.Same(
|
||||||
|
sharedCollision,
|
||||||
|
session.Content?.PreparedCollision);
|
||||||
});
|
});
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
sessionCount,
|
sessionCount,
|
||||||
|
|
@ -206,12 +215,15 @@ public sealed class HeadlessProcessContentOwnerTests
|
||||||
internal TestResourceProxy Dats { get; }
|
internal TestResourceProxy Dats { get; }
|
||||||
internal TestResourceProxy Prepared { get; }
|
internal TestResourceProxy Prepared { get; }
|
||||||
|
|
||||||
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
public HeadlessOpenedProcessContent Open(
|
||||||
HeadlessContentDescriptor descriptor,
|
HeadlessContentDescriptor descriptor,
|
||||||
Action<string> diagnostic)
|
Action<string> diagnostic)
|
||||||
{
|
{
|
||||||
OpenCount++;
|
OpenCount++;
|
||||||
return (DatsResource, PreparedResource);
|
return new HeadlessOpenedProcessContent(
|
||||||
|
DatsResource,
|
||||||
|
PreparedResource,
|
||||||
|
MagicCatalog.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue