Mounts gmCharGenMainUI (enum 0x10000039, root 0x100003CC) via CharacterCreationUiController/CharacterCreationUiMountCoordinator, cloning CharacterManagementUiController's recipe. Master shell ports SetProgressState @0x004e7a10 (Olthoi tab-hide + redirect) and ListenToElementMessage @0x004e9450 (Back/Next/Finish/Help/Exit/Random nav) verbatim, with free tab navigation over all six pages. Heritage, Profession, Skills, and Town pages bind to CC3's RuntimeCharacterCreationState commands; Appearance and Summary mount as content-inert placeholders for CC6b/CC5. Live-DAT probing (CharacterCreationLiveDatTests) found two widget- mapping surprises the decomp's DynamicCast hints don't predict: the Profession slider's value field imports as an editable UiField (wired for direct numeric entry), and the avail/health/stamina/mana/credits displays author as UIElement_Button hosts whose Type-12 value child is swallowed by UiButton.ConsumesDatChildren — substituted with the button's own Label. No new DatWidgetFactory widget types were needed. Threads the installed DAT's real ChargenOptions into Runtime via the new RuntimeCharacterCreationState.InstallOptions, called from ContentEffectsAudioCompositionPhase.Compose (mirrors InstallSpellMetadata's pattern); headless keeps ChargenOptions.Empty unchanged. Wires CC3's F14 status-hook gap (ApplyCharacterCreated/ ApplyCreationFailed) to SessionStatusWriter for both graphical and headless hosts, and adds the CharacterCreation view/command seam through CurrentGameRuntimeAdapter and DeferredGameRuntimeStateCommands alongside CharacterSelection's existing shape. Register: AD-101/102/103, AP-212/213, TS-82 filed for the auto-gender- select interim default, the omitted ToD-account gate, the button-Label widget substitution, the Random-button approximation, the flat-listbox Skills simplification, and the Appearance/Summary placeholders. Runtime 1713/0 (was 1707), App 5117/13 skips (was 5101/6), Headless 165/0 unaffected, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
578 lines
23 KiB
C#
578 lines
23 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.CharGen;
|
|
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 AcDream.Runtime.Session;
|
|
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.AmbientCreated)
|
|
{
|
|
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 ChargenOptions LoadChargenOptions(IDatReaderWriter dats) =>
|
|
ChargenOptions.Empty;
|
|
public void InstallChargenOptions(
|
|
LiveSessionController session,
|
|
ChargenOptions options) { }
|
|
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 AmbientSoundController CreateAmbient(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache) =>
|
|
new(engine, cache);
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|