Retail's UI sound bank was absent, so three families of cue were silent: the portal enter/exit stingers, the AdminEnvirons dungeon atmosphere (chanting, drums, whispers, thunder — what players remember as dungeon 'music'), and every other interface slot. The bank's DID is not a literal anywhere in retail: GetUISoundTable @0x00563FB0 asks GetByEnum for enum slot 7, and DBCache::GetDIDFromEnum @0x00413940 resolves it through two EnumIDMap hops off the portal dat header's master map. UiSoundTableResolver walks that chain the way RetailCursorResolver already walks it for cursors. Against the shipped dats it resolves to 0x2000004B, and that table holds exactly the 32 UI_* slots (UI_EnterPortal 0x6A .. UI_Thunder6 0x8A) — content that confirms the walk independently of the decode. UiSoundTableResolutionTests pins the walk, the DID, and the content, and skips when dats are absent. Two corrections to the research along the way. The lane-5 note recorded GetByEnum's arguments transposed: the 0x22 it called a fileType is the CACHE type (CLOCache(cache, CSoundTable::Allocator, 0x22)) and the real second-hop key is 0x10000003; walking it the other way finds nothing. And its claim that the interface volume pref applies is wrong — GetAttenuation with ambient=0 multiplies by the EFFECT knob, so retail's interface_sound_volume stays the dead knob lane 1 byte-decoded it to be. EnvironSoundCueMap is an explicit 21-case table read straight out of Handle_Admin__Environs @0x0055DE20, not arithmetic: codes 0x65..0x72 sit 0x11 below their SoundType, but 0x73/0x74 have no case, so 0x75 lands on UI_Squeal (0x84) where an offset gives 0x86, and the switch ends at 0x7B with no 0x7C case. Verified case-by-case against the decomp rather than from the lane note, whose tail table was ambiguous. Cues are attached where retail plays them: the teleport-animation boundary for the portal pair, and the AdminEnvirons handler for the stingers. PlaySoundFromCenter's pan-0 / distance-0 shape is what PlayUiWave already implements after A2. Retires TS-54. Narrows AP-115 to its notice-presentation residual. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
566 lines
22 KiB
C#
566 lines
22 KiB
C#
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using AcDream.App.Audio;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Physics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Spells;
|
|
using AcDream.Content;
|
|
using AcDream.Content.Vfx;
|
|
using AcDream.Core.Audio;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.Spells;
|
|
using AcDream.Core.Vfx;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime.Physics;
|
|
using DatReaderWriter.DBObjs;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.OpenAL;
|
|
|
|
namespace AcDream.App.Tests.Composition;
|
|
|
|
public sealed class ContentEffectsAudioCompositionTests
|
|
{
|
|
[Fact]
|
|
public void ProductionPhasePublishesExactOwnersAndRegistersHooksInRetailOrder()
|
|
{
|
|
using var fixture = new Fixture();
|
|
|
|
ContentEffectsAudioResult result = fixture.Phase().Compose(
|
|
fixture.Platform,
|
|
fixture.Host);
|
|
|
|
Assert.Equal(
|
|
Enum.GetValues<ContentEffectsAudioCompositionPoint>(),
|
|
fixture.Points);
|
|
Assert.Same(fixture.Publication.Dats, result.Dats);
|
|
Assert.Same(fixture.Publication.PreparedAssets, result.PreparedAssets);
|
|
Assert.Equal("test.pak", fixture.Factory.PreparedAssetPath);
|
|
Assert.Same(result.Dats, fixture.Factory.PreparedAssetDats);
|
|
Assert.Same(fixture.Publication.Magic, result.MagicCatalog);
|
|
Assert.Same(fixture.Publication.Animations, result.AnimationLoader);
|
|
Assert.Same(fixture.Publication.Particles, result.ParticleSystem);
|
|
Assert.Same(fixture.Publication.ScriptRunner, result.ScriptRunner);
|
|
Assert.Same(fixture.Publication.Audio, result.Audio);
|
|
Assert.Equal(
|
|
[
|
|
result.ParticleSink,
|
|
result.LightingSink,
|
|
result.TranslucencySink,
|
|
result.Audio!.HookSink!,
|
|
],
|
|
fixture.Router.Sinks);
|
|
Assert.Equal(4, result.HookRegistrations.ActiveCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoAudioAcquiresNothingFromTheOptionalFactoryPrefix()
|
|
{
|
|
using var fixture = new Fixture(noAudio: true);
|
|
|
|
ContentEffectsAudioResult result = fixture.Phase().Compose(
|
|
fixture.Platform,
|
|
fixture.Host);
|
|
|
|
Assert.Null(result.Audio);
|
|
Assert.Null(fixture.Publication.Audio);
|
|
Assert.Equal(0, fixture.Factory.AudioFactoryCalls);
|
|
Assert.DoesNotContain(
|
|
ContentEffectsAudioCompositionPoint.SoundCacheCreated,
|
|
fixture.Points);
|
|
Assert.Equal(3, fixture.Router.Sinks.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData((int)ContentEffectsAudioCompositionPoint.SoundCacheCreated)]
|
|
[InlineData((int)ContentEffectsAudioCompositionPoint.AudioEngineCreated)]
|
|
[InlineData((int)ContentEffectsAudioCompositionPoint.EntitySoundTablesCreated)]
|
|
[InlineData((int)ContentEffectsAudioCompositionPoint.AudioSinkCreated)]
|
|
public void OptionalAudioPrefixFailureDisablesAudioOnlyAfterRollbackConverges(
|
|
int failurePointValue)
|
|
{
|
|
var failurePoint =
|
|
(ContentEffectsAudioCompositionPoint)failurePointValue;
|
|
using var fixture = new Fixture(failurePoint: failurePoint);
|
|
|
|
ContentEffectsAudioResult result = fixture.Phase().Compose(
|
|
fixture.Platform,
|
|
fixture.Host);
|
|
|
|
Assert.Null(result.Audio);
|
|
Assert.Null(fixture.Publication.Audio);
|
|
Assert.Equal(3, fixture.Router.Sinks.Count);
|
|
if (fixture.Factory.LastAudioEngine is { } engine)
|
|
Assert.True(engine.IsDisposalComplete);
|
|
Assert.Contains(fixture.Logs, message =>
|
|
message.Contains("audio: init failed", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void FailureAfterAtomicAudioPublicationPropagatesToLifetimeOwner()
|
|
{
|
|
using var fixture = new Fixture(
|
|
failurePoint: ContentEffectsAudioCompositionPoint.AudioPublished);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose(
|
|
fixture.Platform,
|
|
fixture.Host));
|
|
Assert.False(
|
|
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
|
|
|
|
Assert.NotNull(fixture.Publication.Audio);
|
|
Assert.False(fixture.Publication.Audio!.Engine.IsDisposalComplete);
|
|
Assert.Equal(3, fixture.Router.Sinks.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(RequiredFailurePoints))]
|
|
public void RequiredBoundaryFailureStopsAtTheExactPointAndNeverRunsASuffix(
|
|
int failurePointValue)
|
|
{
|
|
var failurePoint =
|
|
(ContentEffectsAudioCompositionPoint)failurePointValue;
|
|
using var fixture = new Fixture(failurePoint: failurePoint);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose(
|
|
fixture.Platform,
|
|
fixture.Host));
|
|
Assert.False(
|
|
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
|
|
|
|
Assert.Equal(
|
|
Enum.GetValues<ContentEffectsAudioCompositionPoint>()
|
|
.TakeWhile(point => point <= failurePoint),
|
|
fixture.Points);
|
|
}
|
|
|
|
public static TheoryData<int> RequiredFailurePoints()
|
|
{
|
|
var data = new TheoryData<int>();
|
|
foreach (ContentEffectsAudioCompositionPoint point in
|
|
Enum.GetValues<ContentEffectsAudioCompositionPoint>())
|
|
{
|
|
// Everything from the sound cache through the UI sound bank is the
|
|
// OPTIONAL audio prefix: a fault there is swallowed and audio is
|
|
// disabled, so those points are not required boundaries. A4 added
|
|
// UiSoundsCreated to that prefix.
|
|
if (point is >= ContentEffectsAudioCompositionPoint.SoundCacheCreated
|
|
and <= ContentEffectsAudioCompositionPoint.UiSoundsCreated)
|
|
{
|
|
continue;
|
|
}
|
|
data.Add((int)point);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowUsesTheProductionPhaseAndNoLongerConstructsItsBodyInline()
|
|
{
|
|
string source = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
|
|
Assert.Contains("new ContentEffectsAudioCompositionPhase(", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);",
|
|
source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_hookRouter.Register(_particleSink)", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("new AcDream.App.Audio.OpenAlAudioEngine()", source,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionRendererConsumesOnlyThePublishedPreparedAssetSource()
|
|
{
|
|
string root = FindRepoRoot();
|
|
string contentPhase = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Composition",
|
|
"ContentEffectsAudioComposition.cs"));
|
|
string worldPhase = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Composition",
|
|
"WorldRenderComposition.cs"));
|
|
string manager = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"Wb",
|
|
"ObjectMeshManager.cs"));
|
|
string adapter = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"Wb",
|
|
"WbMeshAdapter.cs"));
|
|
string lifetime = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindowLifetime.cs"));
|
|
|
|
Assert.Contains("new PakPreparedAssetSource(path, dats, diagnostic)",
|
|
contentPhase, StringComparison.Ordinal);
|
|
Assert.Contains("content.PreparedAssets", worldPhase,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("_preparedAssets.Read(request.Asset, ct)", manager,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("MeshExtractor", manager,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("IDatReaderWriter", manager,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("GfxObjMesh.Build", adapter,
|
|
StringComparison.Ordinal);
|
|
|
|
int meshStage = lifetime.IndexOf(
|
|
"new ResourceShutdownStage(\"mesh adapter\"",
|
|
StringComparison.Ordinal);
|
|
int preparedRelease = lifetime.IndexOf(
|
|
"Hard(\"prepared asset source\"",
|
|
StringComparison.Ordinal);
|
|
int datRelease = lifetime.IndexOf(
|
|
"Hard(\"DAT collection\"",
|
|
StringComparison.Ordinal);
|
|
Assert.True(meshStage >= 0);
|
|
Assert.True(preparedRelease > meshStage);
|
|
Assert.True(datRelease > preparedRelease);
|
|
}
|
|
|
|
private sealed class Fixture : IDisposable
|
|
{
|
|
private readonly ContentEffectsAudioCompositionPoint? _failurePoint;
|
|
|
|
public Fixture(
|
|
bool noAudio = false,
|
|
ContentEffectsAudioCompositionPoint? failurePoint = null)
|
|
{
|
|
_failurePoint = failurePoint;
|
|
Router = new AnimationHookRouter();
|
|
Poses = new EntityEffectPoseRegistry();
|
|
Factory = new Factory();
|
|
Publication = new Publication();
|
|
Platform = new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.Instance, null!);
|
|
Host = (HostInputCameraResult)RuntimeHelpers.GetUninitializedObject(
|
|
typeof(HostInputCameraResult));
|
|
Dependencies = new ContentEffectsAudioDependencies(
|
|
"test-dat",
|
|
"test.pak",
|
|
ResidencyBudgetOptions.Default,
|
|
new PhysicsDataCache(),
|
|
false,
|
|
GameRuntimeTestFactory.Create(),
|
|
Router,
|
|
Poses,
|
|
new DeferredEntityEffectAdvanceSource(),
|
|
new LightManager(),
|
|
new TranslucencyFadeManager(),
|
|
noAudio,
|
|
Logs.Add,
|
|
Logs.Add);
|
|
}
|
|
|
|
public List<ContentEffectsAudioCompositionPoint> Points { get; } = [];
|
|
public List<string> Logs { get; } = [];
|
|
public AnimationHookRouter Router { get; }
|
|
public EntityEffectPoseRegistry Poses { get; }
|
|
public Factory Factory { get; }
|
|
public Publication Publication { get; }
|
|
public ContentEffectsAudioDependencies Dependencies { get; }
|
|
public GameWindowPlatformResult<GameWindowGraphics, IInputContext> Platform { get; }
|
|
public HostInputCameraResult Host { get; }
|
|
|
|
public ContentEffectsAudioCompositionPhase Phase() => new(
|
|
Dependencies,
|
|
Publication,
|
|
Factory,
|
|
point =>
|
|
{
|
|
Points.Add(point);
|
|
if (_failurePoint == point)
|
|
throw new InvalidOperationException($"fault at {point}");
|
|
});
|
|
|
|
public void Dispose()
|
|
{
|
|
Publication.Dispose();
|
|
Dependencies.Runtime.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class Publication :
|
|
IGameWindowContentEffectsAudioPublication,
|
|
IDisposable
|
|
{
|
|
public IDatReaderWriter? Dats { get; private set; }
|
|
public IPreparedAssetSource? PreparedAssets { get; private set; }
|
|
public MagicCatalog? Magic { get; private set; }
|
|
public IAnimationLoader? Animations { get; private set; }
|
|
public LiveEntityCollisionBuilder? Collision { get; private set; }
|
|
public EmitterDescRegistry? Emitters { get; private set; }
|
|
public ParticleSystem? Particles { get; private set; }
|
|
public ParticleHookSink? ParticleSink { get; private set; }
|
|
public AnimationHookFrameQueue? HookFrames { get; private set; }
|
|
public RetailPhysicsScriptLoader? ScriptLoader { get; private set; }
|
|
public PhysicsScriptRunner? ScriptRunner { get; private set; }
|
|
public LightingHookSink? LightingSink { get; private set; }
|
|
public TranslucencyHookSink? TranslucencySink { get; private set; }
|
|
public AnimationHookRegistrationSet? Registrations { get; private set; }
|
|
public ContentAudioGraph? Audio { get; private set; }
|
|
|
|
public void PublishDatCollection(IDatReaderWriter value) => Dats = Once(Dats, value);
|
|
public void PublishPreparedAssetSource(IPreparedAssetSource value) =>
|
|
PreparedAssets = Once(PreparedAssets, value);
|
|
public void PublishMagicCatalog(MagicCatalog value) => Magic = Once(Magic, value);
|
|
public void PublishAnimationLoader(IAnimationLoader value) =>
|
|
Animations = Once(Animations, value);
|
|
public void PublishLiveEntityCollisionBuilder(LiveEntityCollisionBuilder value) =>
|
|
Collision = Once(Collision, value);
|
|
public void PublishEmitterRegistry(EmitterDescRegistry value) =>
|
|
Emitters = Once(Emitters, value);
|
|
public void PublishParticleSystem(ParticleSystem value) =>
|
|
Particles = Once(Particles, value);
|
|
public void PublishParticleSink(ParticleHookSink value) =>
|
|
ParticleSink = Once(ParticleSink, value);
|
|
public void PublishAnimationHookFrames(AnimationHookFrameQueue value) =>
|
|
HookFrames = Once(HookFrames, value);
|
|
public void PublishPhysicsScriptLoader(RetailPhysicsScriptLoader value) =>
|
|
ScriptLoader = Once(ScriptLoader, value);
|
|
public void PublishPhysicsScriptRunner(PhysicsScriptRunner value) =>
|
|
ScriptRunner = Once(ScriptRunner, value);
|
|
public void PublishLightingSink(LightingHookSink value) =>
|
|
LightingSink = Once(LightingSink, value);
|
|
public void PublishTranslucencySink(TranslucencyHookSink value) =>
|
|
TranslucencySink = Once(TranslucencySink, value);
|
|
public void PublishHookRegistrations(AnimationHookRegistrationSet value) =>
|
|
Registrations = Once(Registrations, value);
|
|
public void PublishAudio(ContentAudioGraph value) => Audio = Once(Audio, value);
|
|
|
|
public void Dispose()
|
|
{
|
|
Registrations?.Dispose();
|
|
Registrations = null;
|
|
Audio?.Engine.Dispose();
|
|
Audio = null;
|
|
PreparedAssets?.Dispose();
|
|
PreparedAssets = null;
|
|
Dats?.Dispose();
|
|
Dats = null;
|
|
}
|
|
|
|
private static T Once<T>(T? current, T value) where T : class
|
|
{
|
|
if (current is not null)
|
|
throw new InvalidOperationException("duplicate publication");
|
|
return value;
|
|
}
|
|
}
|
|
|
|
private sealed class Factory : IContentEffectsAudioCompositionFactory
|
|
{
|
|
private readonly IDatReaderWriter _dats =
|
|
DispatchProxy.Create<IDatReaderWriter, NullProxy>();
|
|
private readonly MagicCatalog _magic =
|
|
(MagicCatalog)RuntimeHelpers.GetUninitializedObject(typeof(MagicCatalog));
|
|
private readonly IAnimationLoader _animations = new NullAnimationLoader();
|
|
private readonly IPreparedAssetSource _preparedAssets =
|
|
new NullPreparedAssetSource();
|
|
|
|
public int AudioFactoryCalls { get; private set; }
|
|
public OpenAlAudioEngine? LastAudioEngine { get; private set; }
|
|
public string? PreparedAssetPath { get; private set; }
|
|
public IDatReaderWriter? PreparedAssetDats { get; private set; }
|
|
|
|
public IDatReaderWriter OpenDatCollection(string datDirectory) => _dats;
|
|
public IPreparedAssetSource OpenPreparedAssetSource(
|
|
string path,
|
|
IDatReaderWriter dats,
|
|
Action<string> diagnostic)
|
|
{
|
|
PreparedAssetPath = path;
|
|
PreparedAssetDats = dats;
|
|
return _preparedAssets;
|
|
}
|
|
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) => _magic;
|
|
public void InstallSpellMetadata(
|
|
RuntimeCharacterState character,
|
|
MagicCatalog catalog) { }
|
|
public int GetSpellCount(MagicCatalog catalog) => 0;
|
|
public IAnimationLoader CreateAnimationLoader(
|
|
IDatReaderWriter dats,
|
|
long maximumEstimatedBytes,
|
|
int maximumEntries)
|
|
{
|
|
Assert.Equal(
|
|
ResidencyBudgetOptions.Default.AnimationBytes,
|
|
maximumEstimatedBytes);
|
|
Assert.Equal(
|
|
ResidencyBudgetOptions.Default.AnimationEntries,
|
|
maximumEntries);
|
|
return _animations;
|
|
}
|
|
|
|
public LiveEntityCollisionBuilder CreateCollisionBuilder(
|
|
PhysicsDataCache physicsData,
|
|
IDatReaderWriter dats,
|
|
IAnimationLoader animationLoader,
|
|
bool dumpMotionEnabled) =>
|
|
(LiveEntityCollisionBuilder)RuntimeHelpers.GetUninitializedObject(
|
|
typeof(LiveEntityCollisionBuilder));
|
|
|
|
public LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
|
PhysicsDataCache physicsData,
|
|
IPreparedAssetSource preparedAssets) =>
|
|
(LiveCollisionAssetPublisher)RuntimeHelpers.GetUninitializedObject(
|
|
typeof(LiveCollisionAssetPublisher));
|
|
|
|
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) => new();
|
|
public ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters) => new(emitters);
|
|
public ParticleHookSink CreateParticleSink(
|
|
ParticleSystem particles,
|
|
EntityEffectPoseRegistry poses) => new(particles, poses);
|
|
public AnimationHookFrameQueue CreateAnimationHookFrames(
|
|
AnimationHookRouter router,
|
|
EntityEffectPoseRegistry poses) => new(router, poses);
|
|
public AnimationHookRegistrationSet CreateHookRegistrations(
|
|
AnimationHookRouter router) => new(router);
|
|
public RetailPhysicsScriptLoader CreatePhysicsScriptLoader(IDatReaderWriter dats) =>
|
|
(RetailPhysicsScriptLoader)RuntimeHelpers.GetUninitializedObject(
|
|
typeof(RetailPhysicsScriptLoader));
|
|
public PhysicsScriptRunner CreatePhysicsScriptRunner(
|
|
RetailPhysicsScriptLoader loader,
|
|
AnimationHookRouter router,
|
|
IEntityEffectAdvanceSource advanceSource) =>
|
|
new(_ => null, router, canAdvanceOwner: advanceSource.CanAdvanceOwner);
|
|
public LightingHookSink CreateLightingSink(
|
|
LightManager lighting,
|
|
EntityEffectPoseRegistry poses) => new(lighting, poses);
|
|
public TranslucencyHookSink CreateTranslucencySink(
|
|
TranslucencyFadeManager fades) => new(fades);
|
|
public DatSoundCache CreateSoundCache(
|
|
IDatReaderWriter dats,
|
|
long maximumDecodedBytes)
|
|
{
|
|
Assert.Equal(
|
|
ResidencyBudgetOptions.Default.AudioBytes,
|
|
maximumDecodedBytes);
|
|
return new DatSoundCache(dats, maximumDecodedBytes);
|
|
}
|
|
|
|
public OpenAlAudioEngine CreateAudioEngine()
|
|
{
|
|
AudioFactoryCalls++;
|
|
LastAudioEngine = new OpenAlAudioEngine(
|
|
new AudioApiFactory(new FakeAudioApi()));
|
|
return LastAudioEngine;
|
|
}
|
|
|
|
public DictionaryEntitySoundTable CreateEntitySoundTables() => new();
|
|
public AudioHookSink CreateAudioSink(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache,
|
|
DictionaryEntitySoundTable entitySoundTables) =>
|
|
new(engine, cache, entitySoundTables);
|
|
|
|
// The stub dats carry no EnumIDMap chain, so a real resolve would return
|
|
// 0 anyway; pass 0 explicitly so the composition test exercises the
|
|
// "bank unresolved" branch deterministically.
|
|
public UiSoundController CreateUiSounds(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache,
|
|
IDatReaderWriter dats) =>
|
|
new(engine, cache, tableDid: 0u);
|
|
}
|
|
|
|
public class NullProxy : DispatchProxy
|
|
{
|
|
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
|
{
|
|
if (targetMethod?.ReturnType == typeof(void))
|
|
return null;
|
|
Type returnType = targetMethod?.ReturnType ?? typeof(void);
|
|
return returnType.IsValueType
|
|
? Activator.CreateInstance(returnType)
|
|
: null;
|
|
}
|
|
}
|
|
|
|
private sealed class NullAnimationLoader : IAnimationLoader
|
|
{
|
|
public Animation? LoadAnimation(uint id) => null;
|
|
}
|
|
|
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
|
{
|
|
public PreparedAssetSourceStats Stats => default;
|
|
public CacheStats DecodedTextureCacheStats => default;
|
|
|
|
public PreparedAssetPresence Probe(
|
|
AcDream.Content.Pak.PakAssetType type,
|
|
uint sourceFileId) =>
|
|
PreparedAssetPresence.Missing;
|
|
|
|
public PreparedAssetReadResult Read(
|
|
in PreparedAssetRequest request,
|
|
CancellationToken cancellationToken = default) =>
|
|
PreparedAssetReadResult.Missing;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class AudioApiFactory(IOpenAlResourceApi api)
|
|
: IOpenAlResourceApiFactory
|
|
{
|
|
public IOpenAlResourceApi Create() => api;
|
|
}
|
|
|
|
private sealed class FakeAudioApi : IOpenAlResourceApi
|
|
{
|
|
private uint _nextSource = 1;
|
|
public AL? AudioApi => null;
|
|
public ALContext? ContextApi => null;
|
|
public nint OpenDevice() => 1;
|
|
public nint CreateContext(nint device) => 2;
|
|
public bool MakeContextCurrent(nint context) => true;
|
|
public uint GenerateSource() => _nextSource++;
|
|
public void Configure3DSource(uint source) { }
|
|
public void ConfigureUiSource(uint source) { }
|
|
public void DisableAlDistanceAttenuation() { }
|
|
public void StopSource(uint source) { }
|
|
public void DeleteSource(uint source) { }
|
|
public void DeleteBuffer(uint buffer) { }
|
|
public void DestroyContext(nint context) { }
|
|
public void CloseDevice(nint device) { }
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
while (directory is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
return directory.FullName;
|
|
directory = directory.Parent;
|
|
}
|
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
}
|
|
}
|