perf(content): remove exception-driven setup probes
This commit is contained in:
parent
f05afc07c1
commit
230a7df454
11 changed files with 598 additions and 47 deletions
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
**Date:** 2026-07-24
|
||||
|
||||
**Status:** active — C1 and C2 landed; C3 next
|
||||
**Status:** active — C1 and C2 landed; C3 implemented, C4 connected gate next
|
||||
|
||||
**Checkpoint ledger:**
|
||||
|
||||
- C1 typed prepared-source boundary — `c42f93b3`
|
||||
- C2 production renderer injection — implementation complete and gated
|
||||
- C3 activation probes and lookup precedence — next
|
||||
- C2 production renderer injection — `f05afc07`
|
||||
- C3 activation probes and lookup precedence — implemented; automated gate
|
||||
green, connected exception-count gate remains in C4
|
||||
- C4 connected equivalence and performance closeout — pending
|
||||
|
||||
**Parent:** [`2026-07-24-modern-runtime-architecture.md`](2026-07-24-modern-runtime-architecture.md),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
|
|
@ -196,11 +197,35 @@ internal sealed class LivePresentationCompositionPhase
|
|||
var componentLifecycle =
|
||||
new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||
var wbSpawnAdapter = new LandblockSpawnAdapter(foundation.MeshAdapter);
|
||||
Setup? LoadPreparedSetup(uint sourceId)
|
||||
{
|
||||
if (!content.Dats.TryResolvePreferred(
|
||||
sourceId,
|
||||
out IDatDatabase? database,
|
||||
out DatReaderWriter.Enums.DBObjType type)
|
||||
|| type != DatReaderWriter.Enums.DBObjType.Setup)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return database.TryGet<Setup>(
|
||||
sourceId,
|
||||
out Setup? setup)
|
||||
? setup
|
||||
: null;
|
||||
}
|
||||
|
||||
var setupResolver = new PreparedSetupResolver(
|
||||
content.PreparedAssets,
|
||||
LoadPreparedSetup,
|
||||
message => Console.Error.WriteLine(
|
||||
$"setup activation: {message}"));
|
||||
|
||||
AnimationSequencer SequencerFactory(WorldEntity entity)
|
||||
{
|
||||
Setup? setup = content.Dats.Get<Setup>(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
if (setupResolver.TryResolve(
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
out Setup? setup))
|
||||
{
|
||||
uint motionTableId = (uint)setup.DefaultMotionTable;
|
||||
if (motionTableId != 0
|
||||
|
|
@ -247,38 +272,33 @@ internal sealed class LivePresentationCompositionPhase
|
|||
|
||||
ScriptActivationInfo? ResolveActivation(WorldEntity entity)
|
||||
{
|
||||
try
|
||||
{
|
||||
Setup? setup = content.Dats.Get<Setup>(
|
||||
entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null)
|
||||
return null;
|
||||
uint scriptId = setup.DefaultScript.DataId;
|
||||
if (entity.IndexedPartTransforms.Count == 0)
|
||||
{
|
||||
var indexed = IndexedSetupPartPoseBuilder.Build(setup, entity);
|
||||
entity.SetIndexedPartPoses(indexed.Poses, indexed.Available);
|
||||
}
|
||||
|
||||
bool usesStaticAnimationWorkset = entity.ServerGuid == 0
|
||||
|| (liveEntities?.TryGetRecord(
|
||||
entity.ServerGuid,
|
||||
out LiveEntityRecord liveRecord) == true
|
||||
&& (liveRecord.FinalPhysicsState
|
||||
& PhysicsStateFlags.Static) != 0);
|
||||
return new ScriptActivationInfo(
|
||||
scriptId,
|
||||
entity.IndexedPartTransforms,
|
||||
EntityEffectProfile.CreateDatStatic(setup),
|
||||
entity.IndexedPartAvailable,
|
||||
setup,
|
||||
(uint)setup.DefaultAnimation,
|
||||
usesStaticAnimationWorkset);
|
||||
}
|
||||
catch
|
||||
if (!setupResolver.TryResolve(
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
out Setup? setup))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
uint scriptId = setup.DefaultScript.DataId;
|
||||
if (entity.IndexedPartTransforms.Count == 0)
|
||||
{
|
||||
var indexed = IndexedSetupPartPoseBuilder.Build(setup, entity);
|
||||
entity.SetIndexedPartPoses(indexed.Poses, indexed.Available);
|
||||
}
|
||||
|
||||
bool usesStaticAnimationWorkset = entity.ServerGuid == 0
|
||||
|| (liveEntities?.TryGetRecord(
|
||||
entity.ServerGuid,
|
||||
out LiveEntityRecord liveRecord) == true
|
||||
&& (liveRecord.FinalPhysicsState
|
||||
& PhysicsStateFlags.Static) != 0);
|
||||
return new ScriptActivationInfo(
|
||||
scriptId,
|
||||
entity.IndexedPartTransforms,
|
||||
EntityEffectProfile.CreateDatStatic(setup),
|
||||
entity.IndexedPartAvailable,
|
||||
setup,
|
||||
(uint)setup.DefaultAnimation,
|
||||
usesStaticAnimationWorkset);
|
||||
}
|
||||
|
||||
var entityScriptActivator = new EntityScriptActivator(
|
||||
|
|
|
|||
101
src/AcDream.App/Composition/PreparedSetupResolver.cs
Normal file
101
src/AcDream.App/Composition/PreparedSetupResolver.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using AcDream.Content;
|
||||
using AcDream.Content.Pak;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves Setup presentation metadata only when the immutable prepared
|
||||
/// catalog proves that the source DID is a Setup. Ordinary GfxObj IDs never
|
||||
/// reach the DAT deserializer, so absence is ordinary control flow rather
|
||||
/// than an exception.
|
||||
/// </summary>
|
||||
internal sealed class PreparedSetupResolver
|
||||
{
|
||||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly Func<uint, Setup?> _loadSetup;
|
||||
private readonly Action<string> _diagnostic;
|
||||
private readonly ConcurrentDictionary<uint, byte> _diagnosed = new();
|
||||
|
||||
public PreparedSetupResolver(
|
||||
IPreparedAssetSource preparedAssets,
|
||||
Func<uint, Setup?> loadSetup,
|
||||
Action<string> diagnostic)
|
||||
{
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_loadSetup = loadSetup
|
||||
?? throw new ArgumentNullException(nameof(loadSetup));
|
||||
_diagnostic = diagnostic
|
||||
?? throw new ArgumentNullException(nameof(diagnostic));
|
||||
}
|
||||
|
||||
public bool TryResolve(
|
||||
uint sourceId,
|
||||
[NotNullWhen(true)] out Setup? setup)
|
||||
{
|
||||
PreparedAssetPresence presence =
|
||||
_preparedAssets.Probe(PakAssetType.SetupMesh, sourceId);
|
||||
if (presence == PreparedAssetPresence.Missing)
|
||||
{
|
||||
setup = null;
|
||||
return false;
|
||||
}
|
||||
if (presence == PreparedAssetPresence.Corrupt)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Prepared Setup entry 0x{sourceId:X8} is corrupt; " +
|
||||
"activation cannot safely infer presentation metadata.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
setup = _loadSetup(sourceId);
|
||||
if (setup is not null)
|
||||
return true;
|
||||
|
||||
DiagnoseOnce(
|
||||
sourceId,
|
||||
$"Prepared Setup entry 0x{sourceId:X8} exists but its " +
|
||||
"matching DAT record could not be loaded; activation skipped.");
|
||||
return false;
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
return DiagnoseCorruptData(sourceId, ex, out setup);
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
return DiagnoseCorruptData(sourceId, ex, out setup);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
// DatReaderWriter's verified malformed-record failure:
|
||||
// QualifiedDataId<T>.Unpack / Setup.Unpack can surface an
|
||||
// ArgumentOutOfRangeException for invalid record contents.
|
||||
return DiagnoseCorruptData(sourceId, ex, out setup);
|
||||
}
|
||||
}
|
||||
|
||||
private bool DiagnoseCorruptData(
|
||||
uint sourceId,
|
||||
Exception exception,
|
||||
out Setup? setup)
|
||||
{
|
||||
setup = null;
|
||||
DiagnoseOnce(
|
||||
sourceId,
|
||||
$"Setup DAT record 0x{sourceId:X8} is malformed " +
|
||||
$"({exception.GetType().Name}: {exception.Message}); " +
|
||||
"activation skipped.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DiagnoseOnce(uint sourceId, string message)
|
||||
{
|
||||
if (_diagnosed.TryAdd(sourceId, 0))
|
||||
_diagnostic(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -146,6 +146,53 @@ public sealed class DatCollectionAdapter : IDatReaderWriter {
|
|||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct, allocation-free production lookup. Portal precedence is
|
||||
/// load-bearing: the legacy ResolveId ordering is HighRes-first and its
|
||||
/// callers historically re-sorted the materialized results to make Portal
|
||||
/// win. Encode that rule once instead.
|
||||
/// </summary>
|
||||
public bool TryResolvePreferred(
|
||||
uint id,
|
||||
[NotNullWhen(true)] out IDatDatabase? database,
|
||||
out DBObjType type) {
|
||||
if (TryResolve(_portal, id, out type)) {
|
||||
database = _portal;
|
||||
return true;
|
||||
}
|
||||
if (TryResolve(_highRes, id, out type)) {
|
||||
database = _highRes;
|
||||
return true;
|
||||
}
|
||||
if (TryResolve(_language, id, out type)) {
|
||||
database = _language;
|
||||
return true;
|
||||
}
|
||||
if (TryResolve(_cell, id, out type)) {
|
||||
database = _cell;
|
||||
return true;
|
||||
}
|
||||
|
||||
database = null;
|
||||
type = DBObjType.Unknown;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolve(
|
||||
DatDatabaseWrapper database,
|
||||
uint id,
|
||||
out DBObjType type) {
|
||||
DatDatabase raw = database.RawDatabase;
|
||||
if (raw.Tree.TryGetFile(id, out _)) {
|
||||
type = raw.TypeFromId(id);
|
||||
if (type != DBObjType.Unknown)
|
||||
return true;
|
||||
}
|
||||
|
||||
type = DBObjType.Unknown;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException("DatCollectionAdapter is read-only.");
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,57 @@ public interface IDatReaderWriter : IDisposable, IDatObjectSource {
|
|||
/// Resolves a data ID to all possible databases and types.
|
||||
/// </summary>
|
||||
public IEnumerable<IdResolution> ResolveId(uint id);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves one data ID using acdream's canonical precedence without
|
||||
/// materializing a result collection: Portal, HighRes, Language, Cell.
|
||||
/// Production's concrete adapter overrides this with direct B-tree probes.
|
||||
/// </summary>
|
||||
bool TryResolvePreferred(
|
||||
uint id,
|
||||
[NotNullWhen(true)] out IDatDatabase? database,
|
||||
out DBObjType type)
|
||||
{
|
||||
IDatDatabase? highRes = null;
|
||||
DBObjType highResType = DBObjType.Unknown;
|
||||
IDatDatabase? language = null;
|
||||
DBObjType languageType = DBObjType.Unknown;
|
||||
IDatDatabase? cell = null;
|
||||
DBObjType cellType = DBObjType.Unknown;
|
||||
|
||||
foreach (IdResolution resolution in ResolveId(id))
|
||||
{
|
||||
if (ReferenceEquals(resolution.Database, Portal))
|
||||
{
|
||||
database = resolution.Database;
|
||||
type = resolution.Type;
|
||||
return true;
|
||||
}
|
||||
if (ReferenceEquals(resolution.Database, HighRes))
|
||||
{
|
||||
highRes = resolution.Database;
|
||||
highResType = resolution.Type;
|
||||
}
|
||||
else if (ReferenceEquals(resolution.Database, Language))
|
||||
{
|
||||
language = resolution.Database;
|
||||
languageType = resolution.Type;
|
||||
}
|
||||
else if (ReferenceEquals(resolution.Database, Cell))
|
||||
{
|
||||
cell = resolution.Database;
|
||||
cellType = resolution.Type;
|
||||
}
|
||||
}
|
||||
|
||||
database = highRes ?? language ?? cell;
|
||||
type = highRes is not null
|
||||
? highResType
|
||||
: language is not null
|
||||
? languageType
|
||||
: cellType;
|
||||
return database is not null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -356,7 +356,11 @@ public sealed class DatPreparedAssetSource : IPreparedAssetSource
|
|||
};
|
||||
if (expected == DBObjType.Unknown)
|
||||
return PreparedAssetPresence.Missing;
|
||||
return _dats.ResolveId(sourceFileId).Any(r => r.Type == expected)
|
||||
return _dats.TryResolvePreferred(
|
||||
sourceFileId,
|
||||
out _,
|
||||
out DBObjType actual)
|
||||
&& actual == expected
|
||||
? PreparedAssetPresence.Available
|
||||
: PreparedAssetPresence.Missing;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,12 +87,11 @@ public sealed class MeshExtractor {
|
|||
try {
|
||||
// Use the low 32 bits as the DAT file ID
|
||||
var datId = (uint)(id & 0xFFFFFFFFu);
|
||||
var resolutions = _dats.ResolveId(datId);
|
||||
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
|
||||
if (selectedResolution == null) return null;
|
||||
|
||||
var type = selectedResolution.Type;
|
||||
var db = selectedResolution.Database;
|
||||
if (!_dats.TryResolvePreferred(
|
||||
datId,
|
||||
out IDatDatabase? db,
|
||||
out DBObjType type))
|
||||
return null;
|
||||
|
||||
if (type == DBObjType.Setup) {
|
||||
if (!db.TryGet<Setup>(datId, out var setup)) return null;
|
||||
|
|
@ -203,12 +202,11 @@ public sealed class MeshExtractor {
|
|||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var resolutions = _dats.ResolveId(id);
|
||||
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
|
||||
if (selectedResolution == null) return;
|
||||
|
||||
var type = selectedResolution.Type;
|
||||
var db = selectedResolution.Database;
|
||||
if (!_dats.TryResolvePreferred(
|
||||
id,
|
||||
out IDatDatabase? db,
|
||||
out DBObjType type))
|
||||
return;
|
||||
|
||||
if (type == DBObjType.Setup) {
|
||||
if (!db.TryGet<Setup>(id, out var setup)) return;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ public sealed class BoundedTestDatCollection : DatReaderWriter.DatCollection, ID
|
|||
IEnumerable<IDatReaderWriter.IdResolution> IDatReaderWriter.ResolveId(uint id) =>
|
||||
_bounded.ResolveId(id);
|
||||
|
||||
bool IDatReaderWriter.TryResolvePreferred(
|
||||
uint id,
|
||||
[NotNullWhen(true)] out IDatDatabase? database,
|
||||
out DatReaderWriter.Enums.DBObjType type) =>
|
||||
_bounded.TryResolvePreferred(id, out database, out type);
|
||||
|
||||
bool IDatReaderWriter.TrySave<T>(T obj, int iteration) =>
|
||||
_bounded.TrySave(obj, iteration);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
using AcDream.App.Composition;
|
||||
using AcDream.Content;
|
||||
using AcDream.Content.Pak;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Composition;
|
||||
|
||||
public sealed class PreparedSetupResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void MissingPreparedSetupDoesNotTouchDatLoader()
|
||||
{
|
||||
int loads = 0;
|
||||
var resolver = new PreparedSetupResolver(
|
||||
new PresenceSource(PreparedAssetPresence.Missing),
|
||||
_ =>
|
||||
{
|
||||
loads++;
|
||||
return new Setup();
|
||||
},
|
||||
_ => { });
|
||||
|
||||
Assert.False(resolver.TryResolve(0x0100_0001u, out Setup? setup));
|
||||
Assert.Null(setup);
|
||||
Assert.Equal(0, loads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvailablePreparedSetupLoadsMatchingDatRecord()
|
||||
{
|
||||
var expected = new Setup();
|
||||
var resolver = new PreparedSetupResolver(
|
||||
new PresenceSource(PreparedAssetPresence.Available),
|
||||
id =>
|
||||
{
|
||||
Assert.Equal(0x0200_0001u, id);
|
||||
return expected;
|
||||
},
|
||||
_ => { });
|
||||
|
||||
Assert.True(resolver.TryResolve(0x0200_0001u, out Setup? actual));
|
||||
Assert.Same(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorruptPreparedEntryFailsLoudlyWithoutDatGuess()
|
||||
{
|
||||
int loads = 0;
|
||||
var resolver = new PreparedSetupResolver(
|
||||
new PresenceSource(PreparedAssetPresence.Corrupt),
|
||||
_ =>
|
||||
{
|
||||
loads++;
|
||||
return null;
|
||||
},
|
||||
_ => { });
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
resolver.TryResolve(0x0200_0001u, out _));
|
||||
Assert.Equal(0, loads);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownMalformedDatExceptions))]
|
||||
public void KnownMalformedDatRecordIsDiagnosedOnce(
|
||||
Func<Exception> exceptionFactory)
|
||||
{
|
||||
var diagnostics = new List<string>();
|
||||
var resolver = new PreparedSetupResolver(
|
||||
new PresenceSource(PreparedAssetPresence.Available),
|
||||
_ => throw exceptionFactory(),
|
||||
diagnostics.Add);
|
||||
|
||||
Assert.False(resolver.TryResolve(0x0200_0001u, out _));
|
||||
Assert.False(resolver.TryResolve(0x0200_0001u, out _));
|
||||
Assert.Single(diagnostics);
|
||||
Assert.Contains("malformed", diagnostics[0], StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnexpectedDatFailurePropagates()
|
||||
{
|
||||
var expected = new InvalidOperationException("unexpected");
|
||||
var resolver = new PreparedSetupResolver(
|
||||
new PresenceSource(PreparedAssetPresence.Available),
|
||||
_ => throw expected,
|
||||
_ => { });
|
||||
|
||||
InvalidOperationException actual = Assert.Throws<InvalidOperationException>(
|
||||
() => resolver.TryResolve(0x0200_0001u, out _));
|
||||
Assert.Same(expected, actual);
|
||||
}
|
||||
|
||||
public static TheoryData<Func<Exception>> KnownMalformedDatExceptions => new()
|
||||
{
|
||||
() => new InvalidDataException("bad payload"),
|
||||
() => new EndOfStreamException("truncated"),
|
||||
() => new ArgumentOutOfRangeException("index"),
|
||||
};
|
||||
|
||||
private sealed class PresenceSource(PreparedAssetPresence presence)
|
||||
: IPreparedAssetSource
|
||||
{
|
||||
public PreparedAssetSourceStats Stats => default;
|
||||
public CacheStats DecodedTextureCacheStats => default;
|
||||
|
||||
public PreparedAssetPresence Probe(
|
||||
PakAssetType type,
|
||||
uint sourceFileId)
|
||||
{
|
||||
Assert.Equal(PakAssetType.SetupMesh, type);
|
||||
return presence;
|
||||
}
|
||||
|
||||
public PreparedAssetReadResult Read(
|
||||
in PreparedAssetRequest request,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,23 @@ namespace AcDream.App.Tests.Rendering.Wb;
|
|||
|
||||
public sealed class WbTextureResolutionPolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void BakedDefaultPaletteAndRuntimePaletteOverlayKeepDistinctOwnership()
|
||||
{
|
||||
Assert.Equal(
|
||||
WbTextureResolutionKind.SharedAtlas,
|
||||
WbTextureResolutionPolicy.Select(
|
||||
hasOriginalTextureOverride: false,
|
||||
hasPaletteOverride: false,
|
||||
sourceIsPaletteIndexed: true));
|
||||
Assert.Equal(
|
||||
WbTextureResolutionKind.PaletteComposite,
|
||||
WbTextureResolutionPolicy.Select(
|
||||
hasOriginalTextureOverride: false,
|
||||
hasPaletteOverride: true,
|
||||
sourceIsPaletteIndexed: true));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, false, (int)WbTextureResolutionKind.SharedAtlas)]
|
||||
[InlineData(false, true, false, (int)WbTextureResolutionKind.SharedAtlas)]
|
||||
|
|
|
|||
182
tests/AcDream.Content.Tests/DatResolutionPrecedenceTests.cs
Normal file
182
tests/AcDream.Content.Tests/DatResolutionPrecedenceTests.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
|
||||
namespace AcDream.Content.Tests;
|
||||
|
||||
public sealed class DatResolutionPrecedenceTests
|
||||
{
|
||||
[Fact]
|
||||
public void PortalWinsEvenWhenLegacyEnumerationStartsWithHighRes()
|
||||
{
|
||||
var source = new ResolutionSource();
|
||||
source.Resolutions =
|
||||
[
|
||||
new(source.HighRes, DBObjType.GfxObj),
|
||||
new(source.Portal, DBObjType.Setup),
|
||||
new(source.Language, DBObjType.StringTable),
|
||||
new(source.Cell, DBObjType.EnvCell),
|
||||
];
|
||||
|
||||
Assert.True(((IDatReaderWriter)source).TryResolvePreferred(
|
||||
1,
|
||||
out IDatDatabase? database,
|
||||
out DBObjType type));
|
||||
Assert.Same(source.Portal, database);
|
||||
Assert.Equal(DBObjType.Setup, type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemainingPrecedenceIsHighResThenLanguageThenCell()
|
||||
{
|
||||
var source = new ResolutionSource();
|
||||
source.Resolutions =
|
||||
[
|
||||
new(source.Cell, DBObjType.EnvCell),
|
||||
new(source.Language, DBObjType.StringTable),
|
||||
new(source.HighRes, DBObjType.GfxObj),
|
||||
];
|
||||
|
||||
Assert.True(((IDatReaderWriter)source).TryResolvePreferred(
|
||||
1,
|
||||
out IDatDatabase? database,
|
||||
out DBObjType type));
|
||||
Assert.Same(source.HighRes, database);
|
||||
Assert.Equal(DBObjType.GfxObj, type);
|
||||
|
||||
source.Resolutions =
|
||||
[
|
||||
new(source.Cell, DBObjType.EnvCell),
|
||||
new(source.Language, DBObjType.StringTable),
|
||||
];
|
||||
Assert.True(((IDatReaderWriter)source).TryResolvePreferred(
|
||||
1,
|
||||
out database,
|
||||
out type));
|
||||
Assert.Same(source.Language, database);
|
||||
Assert.Equal(DBObjType.StringTable, type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingIdReturnsUnknownWithoutAPlaceholderResolution()
|
||||
{
|
||||
var source = new ResolutionSource();
|
||||
|
||||
Assert.False(((IDatReaderWriter)source).TryResolvePreferred(
|
||||
1,
|
||||
out IDatDatabase? database,
|
||||
out DBObjType type));
|
||||
Assert.Null(database);
|
||||
Assert.Equal(DBObjType.Unknown, type);
|
||||
}
|
||||
|
||||
private sealed class ResolutionSource : IDatReaderWriter
|
||||
{
|
||||
private readonly StubDatabase _portal = new();
|
||||
private readonly StubDatabase _highRes = new();
|
||||
private readonly StubDatabase _language = new();
|
||||
private readonly StubDatabase _cell = new();
|
||||
|
||||
public string SourceDirectory => string.Empty;
|
||||
public IDatDatabase Portal => _portal;
|
||||
public IDatDatabase Cell => _cell;
|
||||
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
|
||||
new(new Dictionary<uint, IDatDatabase>());
|
||||
public IDatDatabase HighRes => _highRes;
|
||||
public IDatDatabase Language => _language;
|
||||
public IDatDatabase Local => _language;
|
||||
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
|
||||
new(new Dictionary<uint, uint>());
|
||||
public int PortalIteration => 0;
|
||||
public int CellIteration => 0;
|
||||
public int HighResIteration => 0;
|
||||
public int LanguageIteration => 0;
|
||||
public IReadOnlyList<IDatReaderWriter.IdResolution> Resolutions { get; set; } =
|
||||
Array.Empty<IDatReaderWriter.IdResolution>();
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint regionId,
|
||||
uint fileId,
|
||||
ref byte[] bytes,
|
||||
out int bytesRead)
|
||||
{
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||
Array.Empty<uint>();
|
||||
|
||||
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
|
||||
Resolutions;
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public bool TrySave<T>(
|
||||
uint regionId,
|
||||
T obj,
|
||||
int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
[return: MaybeNull]
|
||||
public T Get<T>(uint fileId) where T : IDBObj => default;
|
||||
|
||||
public bool TryGet<T>(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubDatabase : IDatDatabase
|
||||
{
|
||||
public DatDatabase Db => throw new NotSupportedException();
|
||||
public int Iteration => 0;
|
||||
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||
Array.Empty<uint>();
|
||||
|
||||
public bool TryGet<T>(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out byte[] value)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint fileId,
|
||||
ref byte[] bytes,
|
||||
out int bytesRead)
|
||||
{
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue