diff --git a/docs/plans/2026-07-24-modern-runtime-slice-c-prepared-assets.md b/docs/plans/2026-07-24-modern-runtime-slice-c-prepared-assets.md index 1a038aa2..84e52097 100644 --- a/docs/plans/2026-07-24-modern-runtime-slice-c-prepared-assets.md +++ b/docs/plans/2026-07-24-modern-runtime-slice-c-prepared-assets.md @@ -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), diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index e52bc858..45146137 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -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( + 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(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( - 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( diff --git a/src/AcDream.App/Composition/PreparedSetupResolver.cs b/src/AcDream.App/Composition/PreparedSetupResolver.cs new file mode 100644 index 00000000..741b9deb --- /dev/null +++ b/src/AcDream.App/Composition/PreparedSetupResolver.cs @@ -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; + +/// +/// 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. +/// +internal sealed class PreparedSetupResolver +{ + private readonly IPreparedAssetSource _preparedAssets; + private readonly Func _loadSetup; + private readonly Action _diagnostic; + private readonly ConcurrentDictionary _diagnosed = new(); + + public PreparedSetupResolver( + IPreparedAssetSource preparedAssets, + Func loadSetup, + Action 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.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); + } +} diff --git a/src/AcDream.Content/DatCollectionAdapter.cs b/src/AcDream.Content/DatCollectionAdapter.cs index 5ae3d399..3b058955 100644 --- a/src/AcDream.Content/DatCollectionAdapter.cs +++ b/src/AcDream.Content/DatCollectionAdapter.cs @@ -146,6 +146,53 @@ public sealed class DatCollectionAdapter : IDatReaderWriter { return results; } + /// + /// 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. + /// + 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 obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException("DatCollectionAdapter is read-only."); diff --git a/src/AcDream.Content/IDatReaderWriter.cs b/src/AcDream.Content/IDatReaderWriter.cs index a18efe0f..20848500 100644 --- a/src/AcDream.Content/IDatReaderWriter.cs +++ b/src/AcDream.Content/IDatReaderWriter.cs @@ -103,6 +103,57 @@ public interface IDatReaderWriter : IDisposable, IDatObjectSource { /// Resolves a data ID to all possible databases and types. /// public IEnumerable ResolveId(uint id); + + /// + /// 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. + /// + 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; + } } /// diff --git a/src/AcDream.Content/IPreparedAssetSource.cs b/src/AcDream.Content/IPreparedAssetSource.cs index cdc603b4..0b6c599b 100644 --- a/src/AcDream.Content/IPreparedAssetSource.cs +++ b/src/AcDream.Content/IPreparedAssetSource.cs @@ -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; } diff --git a/src/AcDream.Content/MeshExtractor.cs b/src/AcDream.Content/MeshExtractor.cs index e6b84501..0213d139 100644 --- a/src/AcDream.Content/MeshExtractor.cs +++ b/src/AcDream.Content/MeshExtractor.cs @@ -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(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(id, out var setup)) return; diff --git a/tests/AcDream.App.Tests/BoundedTestDatCollection.cs b/tests/AcDream.App.Tests/BoundedTestDatCollection.cs index 931faad3..bb5d2d1f 100644 --- a/tests/AcDream.App.Tests/BoundedTestDatCollection.cs +++ b/tests/AcDream.App.Tests/BoundedTestDatCollection.cs @@ -68,6 +68,12 @@ public sealed class BoundedTestDatCollection : DatReaderWriter.DatCollection, ID IEnumerable 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 obj, int iteration) => _bounded.TrySave(obj, iteration); diff --git a/tests/AcDream.App.Tests/Composition/PreparedSetupResolverTests.cs b/tests/AcDream.App.Tests/Composition/PreparedSetupResolverTests.cs new file mode 100644 index 00000000..62c4f9ca --- /dev/null +++ b/tests/AcDream.App.Tests/Composition/PreparedSetupResolverTests.cs @@ -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(() => + resolver.TryResolve(0x0200_0001u, out _)); + Assert.Equal(0, loads); + } + + [Theory] + [MemberData(nameof(KnownMalformedDatExceptions))] + public void KnownMalformedDatRecordIsDiagnosedOnce( + Func exceptionFactory) + { + var diagnostics = new List(); + 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( + () => resolver.TryResolve(0x0200_0001u, out _)); + Assert.Same(expected, actual); + } + + public static TheoryData> 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() + { + } + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Wb/WbTextureResolutionPolicyTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/WbTextureResolutionPolicyTests.cs index 7bb6261d..9b6e5f7f 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/WbTextureResolutionPolicyTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/WbTextureResolutionPolicyTests.cs @@ -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)] diff --git a/tests/AcDream.Content.Tests/DatResolutionPrecedenceTests.cs b/tests/AcDream.Content.Tests/DatResolutionPrecedenceTests.cs new file mode 100644 index 00000000..9b980be4 --- /dev/null +++ b/tests/AcDream.Content.Tests/DatResolutionPrecedenceTests.cs @@ -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 CellRegions { get; } = + new(new Dictionary()); + public IDatDatabase HighRes => _highRes; + public IDatDatabase Language => _language; + public IDatDatabase Local => _language; + public ReadOnlyDictionary RegionFileMap { get; } = + new(new Dictionary()); + public int PortalIteration => 0; + public int CellIteration => 0; + public int HighResIteration => 0; + public int LanguageIteration => 0; + public IReadOnlyList Resolutions { get; set; } = + Array.Empty(); + + public bool TryGetFileBytes( + uint regionId, + uint fileId, + ref byte[] bytes, + out int bytesRead) + { + bytesRead = 0; + return false; + } + + public IEnumerable GetAllIdsOfType() where T : IDBObj => + Array.Empty(); + + public IEnumerable ResolveId(uint id) => + Resolutions; + + public bool TrySave(T obj, int iteration = 0) where T : IDBObj => + throw new NotSupportedException(); + + public bool TrySave( + uint regionId, + T obj, + int iteration = 0) where T : IDBObj => + throw new NotSupportedException(); + + [return: MaybeNull] + public T Get(uint fileId) where T : IDBObj => default; + + public bool TryGet( + 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 GetAllIdsOfType() where T : IDBObj => + Array.Empty(); + + public bool TryGet( + 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 obj, int iteration = 0) where T : IDBObj => + throw new NotSupportedException(); + + public void Dispose() + { + } + } +}