refactor(app): compose content effects and audio startup
Move Phase-2 startup behind typed composition/publication boundaries, replace the GameWindow-capturing PhysicsScript gate with a focused deferred source, and own animation-hook registrations reversibly. Make OpenAL construction and teardown transactional so every device, context, source, and buffer prefix is retryable without replay.
This commit is contained in:
parent
1d51e35c14
commit
60a1698ce7
12 changed files with 1901 additions and 152 deletions
105
src/AcDream.App/Composition/AnimationHookRegistrationSet.cs
Normal file
105
src/AcDream.App/Composition/AnimationHookRegistrationSet.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Lifetime owner for animation-hook router edges. Registration order remains
|
||||
/// retail-observable dispatch order; teardown runs in reverse and retains only
|
||||
/// unregister operations that actually failed.
|
||||
/// </summary>
|
||||
internal sealed class AnimationHookRegistrationSet : IDisposable,
|
||||
IRetryableResourceCleanup
|
||||
{
|
||||
private sealed class Entry(IAnimationHookSink sink)
|
||||
{
|
||||
public IAnimationHookSink Sink { get; } = sink;
|
||||
public bool Removed { get; set; }
|
||||
}
|
||||
|
||||
private readonly Action<IAnimationHookSink> _register;
|
||||
private readonly Action<IAnimationHookSink> _unregister;
|
||||
private readonly List<Entry> _entries = [];
|
||||
private bool _disposing;
|
||||
private bool _closed;
|
||||
|
||||
public AnimationHookRegistrationSet(AnimationHookRouter router)
|
||||
: this(
|
||||
(router ?? throw new ArgumentNullException(nameof(router))).Register,
|
||||
router.Unregister)
|
||||
{
|
||||
}
|
||||
|
||||
internal AnimationHookRegistrationSet(
|
||||
Action<IAnimationHookSink> register,
|
||||
Action<IAnimationHookSink> unregister)
|
||||
{
|
||||
_register = register ?? throw new ArgumentNullException(nameof(register));
|
||||
_unregister = unregister ?? throw new ArgumentNullException(nameof(unregister));
|
||||
}
|
||||
|
||||
public bool IsCleanupComplete =>
|
||||
_entries.All(static entry => entry.Removed);
|
||||
|
||||
public int ActiveCount =>
|
||||
_entries.Count(static entry => !entry.Removed);
|
||||
|
||||
public void Register(IAnimationHookSink sink)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sink);
|
||||
ObjectDisposedException.ThrowIf(_closed, this);
|
||||
if (_entries.Any(entry =>
|
||||
!entry.Removed && ReferenceEquals(entry.Sink, sink)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_register(sink);
|
||||
_entries.Add(new Entry(sink));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_closed = true;
|
||||
RetryCleanup();
|
||||
}
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
if (_disposing || IsCleanupComplete)
|
||||
return;
|
||||
|
||||
_closed = true;
|
||||
_disposing = true;
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
for (int i = _entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Entry entry = _entries[i];
|
||||
if (entry.Removed)
|
||||
continue;
|
||||
try
|
||||
{
|
||||
_unregister(entry.Sink);
|
||||
entry.Removed = true;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Animation-hook sink '{entry.Sink.GetType().Name}' could not be unregistered.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposing = false;
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"Animation-hook registration cleanup remains incomplete.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
466
src/AcDream.App/Composition/ContentEffectsAudioComposition.cs
Normal file
466
src/AcDream.App/Composition/ContentEffectsAudioComposition.cs
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
using AcDream.App.Audio;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
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 DatReaderWriter;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
internal sealed record ContentAudioGraph(
|
||||
DatSoundCache SoundCache,
|
||||
OpenAlAudioEngine Engine,
|
||||
DictionaryEntitySoundTable EntitySoundTables,
|
||||
AudioHookSink? HookSink);
|
||||
|
||||
internal sealed record ContentEffectsAudioResult(
|
||||
IDatReaderWriter Dats,
|
||||
MagicCatalog MagicCatalog,
|
||||
IAnimationLoader AnimationLoader,
|
||||
LiveEntityCollisionBuilder CollisionBuilder,
|
||||
EmitterDescRegistry EmitterRegistry,
|
||||
ParticleSystem ParticleSystem,
|
||||
ParticleHookSink ParticleSink,
|
||||
AnimationHookFrameQueue AnimationHookFrames,
|
||||
RetailPhysicsScriptLoader PhysicsScriptLoader,
|
||||
PhysicsScriptRunner ScriptRunner,
|
||||
LightingHookSink LightingSink,
|
||||
TranslucencyHookSink TranslucencySink,
|
||||
AnimationHookRegistrationSet HookRegistrations,
|
||||
ContentAudioGraph? Audio);
|
||||
|
||||
internal sealed record ContentEffectsAudioDependencies(
|
||||
string DatDirectory,
|
||||
PhysicsDataCache PhysicsDataCache,
|
||||
bool DumpMotionEnabled,
|
||||
Spellbook SpellBook,
|
||||
AnimationHookRouter HookRouter,
|
||||
EntityEffectPoseRegistry EffectPoses,
|
||||
DeferredEntityEffectAdvanceSource EntityEffectAdvance,
|
||||
LightManager Lighting,
|
||||
TranslucencyFadeManager TranslucencyFades,
|
||||
bool NoAudio,
|
||||
Action<string> Log,
|
||||
Action<string> Error);
|
||||
|
||||
internal interface IGameWindowContentEffectsAudioPublication
|
||||
{
|
||||
void PublishDatCollection(IDatReaderWriter value);
|
||||
void PublishMagicCatalog(MagicCatalog value);
|
||||
void PublishAnimationLoader(IAnimationLoader value);
|
||||
void PublishLiveEntityCollisionBuilder(LiveEntityCollisionBuilder value);
|
||||
void PublishEmitterRegistry(EmitterDescRegistry value);
|
||||
void PublishParticleSystem(ParticleSystem value);
|
||||
void PublishParticleSink(ParticleHookSink value);
|
||||
void PublishAnimationHookFrames(AnimationHookFrameQueue value);
|
||||
void PublishPhysicsScriptLoader(RetailPhysicsScriptLoader value);
|
||||
void PublishPhysicsScriptRunner(PhysicsScriptRunner value);
|
||||
void PublishLightingSink(LightingHookSink value);
|
||||
void PublishTranslucencySink(TranslucencyHookSink value);
|
||||
void PublishHookRegistrations(AnimationHookRegistrationSet value);
|
||||
void PublishAudio(ContentAudioGraph value);
|
||||
}
|
||||
|
||||
internal interface IContentEffectsAudioCompositionFactory
|
||||
{
|
||||
IDatReaderWriter OpenDatCollection(string datDirectory);
|
||||
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
|
||||
void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog);
|
||||
int GetSpellCount(MagicCatalog catalog);
|
||||
IAnimationLoader CreateAnimationLoader(IDatReaderWriter dats);
|
||||
LiveEntityCollisionBuilder CreateCollisionBuilder(
|
||||
PhysicsDataCache physicsData,
|
||||
IDatReaderWriter dats,
|
||||
IAnimationLoader animationLoader,
|
||||
bool dumpMotionEnabled);
|
||||
EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats);
|
||||
ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters);
|
||||
ParticleHookSink CreateParticleSink(
|
||||
ParticleSystem particles,
|
||||
EntityEffectPoseRegistry poses);
|
||||
AnimationHookFrameQueue CreateAnimationHookFrames(
|
||||
AnimationHookRouter router,
|
||||
EntityEffectPoseRegistry poses);
|
||||
RetailPhysicsScriptLoader CreatePhysicsScriptLoader(IDatReaderWriter dats);
|
||||
PhysicsScriptRunner CreatePhysicsScriptRunner(
|
||||
RetailPhysicsScriptLoader loader,
|
||||
AnimationHookRouter router,
|
||||
IEntityEffectAdvanceSource advanceSource);
|
||||
LightingHookSink CreateLightingSink(
|
||||
LightManager lighting,
|
||||
EntityEffectPoseRegistry poses);
|
||||
TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades);
|
||||
AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router);
|
||||
DatSoundCache CreateSoundCache(IDatReaderWriter dats);
|
||||
OpenAlAudioEngine CreateAudioEngine();
|
||||
DictionaryEntitySoundTable CreateEntitySoundTables();
|
||||
AudioHookSink CreateAudioSink(
|
||||
OpenAlAudioEngine engine,
|
||||
DatSoundCache cache,
|
||||
DictionaryEntitySoundTable entitySoundTables);
|
||||
}
|
||||
|
||||
internal sealed class RetailContentEffectsAudioCompositionFactory
|
||||
: IContentEffectsAudioCompositionFactory
|
||||
{
|
||||
public IDatReaderWriter OpenDatCollection(string datDirectory) =>
|
||||
RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
||||
|
||||
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
|
||||
MagicCatalog.Load(dats);
|
||||
|
||||
public void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog) =>
|
||||
spellBook.InstallMetadata(catalog.SpellTable);
|
||||
|
||||
public int GetSpellCount(MagicCatalog catalog) => catalog.SpellTable.Count;
|
||||
|
||||
public IAnimationLoader CreateAnimationLoader(IDatReaderWriter dats) =>
|
||||
new RetailAnimationLoader(dats);
|
||||
|
||||
public LiveEntityCollisionBuilder CreateCollisionBuilder(
|
||||
PhysicsDataCache physicsData,
|
||||
IDatReaderWriter dats,
|
||||
IAnimationLoader animationLoader,
|
||||
bool dumpMotionEnabled) =>
|
||||
new(
|
||||
physicsData,
|
||||
new LiveEntityDefaultPoseResolver(
|
||||
id => dats.Get<DatReaderWriter.DBObjs.MotionTable>(id),
|
||||
animationLoader,
|
||||
dumpMotionEnabled));
|
||||
|
||||
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) =>
|
||||
new(dats);
|
||||
|
||||
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 RetailPhysicsScriptLoader CreatePhysicsScriptLoader(IDatReaderWriter dats) =>
|
||||
new(dats);
|
||||
|
||||
public PhysicsScriptRunner CreatePhysicsScriptRunner(
|
||||
RetailPhysicsScriptLoader loader,
|
||||
AnimationHookRouter router,
|
||||
IEntityEffectAdvanceSource advanceSource) =>
|
||||
new(
|
||||
loader.LoadPhysicsScript,
|
||||
router,
|
||||
canAdvanceOwner: advanceSource.CanAdvanceOwner);
|
||||
|
||||
public LightingHookSink CreateLightingSink(
|
||||
LightManager lighting,
|
||||
EntityEffectPoseRegistry poses) =>
|
||||
new(lighting, poses);
|
||||
|
||||
public TranslucencyHookSink CreateTranslucencySink(
|
||||
TranslucencyFadeManager fades) =>
|
||||
new(fades);
|
||||
|
||||
public AnimationHookRegistrationSet CreateHookRegistrations(
|
||||
AnimationHookRouter router) =>
|
||||
new(router);
|
||||
|
||||
public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats);
|
||||
|
||||
public OpenAlAudioEngine CreateAudioEngine() => new();
|
||||
|
||||
public DictionaryEntitySoundTable CreateEntitySoundTables() => new();
|
||||
|
||||
public AudioHookSink CreateAudioSink(
|
||||
OpenAlAudioEngine engine,
|
||||
DatSoundCache cache,
|
||||
DictionaryEntitySoundTable entitySoundTables) =>
|
||||
new(engine, cache, entitySoundTables);
|
||||
}
|
||||
|
||||
internal enum ContentEffectsAudioCompositionPoint
|
||||
{
|
||||
DatCollectionPublished,
|
||||
MagicCatalogPublished,
|
||||
SpellMetadataInstalled,
|
||||
AnimationLoaderPublished,
|
||||
CollisionBuilderPublished,
|
||||
EmitterRegistryPublished,
|
||||
ParticleSystemPublished,
|
||||
ParticleSinkPublished,
|
||||
AnimationHookFramesPublished,
|
||||
HookRegistrationsPublished,
|
||||
ParticleHookRegistered,
|
||||
PhysicsScriptLoaderPublished,
|
||||
PhysicsScriptRunnerPublished,
|
||||
LightingSinkPublished,
|
||||
LightingHookRegistered,
|
||||
TranslucencySinkPublished,
|
||||
TranslucencyHookRegistered,
|
||||
SoundCacheCreated,
|
||||
AudioEngineCreated,
|
||||
EntitySoundTablesCreated,
|
||||
AudioSinkCreated,
|
||||
AudioPublished,
|
||||
AudioHookRegistered,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production Phase 2. The sole DAT mapping and every effect/audio owner are
|
||||
/// published through typed lifetime slots. Router edges have one reversible
|
||||
/// owner, and optional audio is swallowed only while its entire unpublished
|
||||
/// native prefix has converged cleanup.
|
||||
/// </summary>
|
||||
internal sealed class ContentEffectsAudioCompositionPhase :
|
||||
IContentEffectsAudioCompositionPhase<
|
||||
GameWindowPlatformResult<GL, IInputContext>,
|
||||
HostInputCameraResult,
|
||||
ContentEffectsAudioResult>
|
||||
{
|
||||
private readonly ContentEffectsAudioDependencies _dependencies;
|
||||
private readonly IGameWindowContentEffectsAudioPublication _publication;
|
||||
private readonly IContentEffectsAudioCompositionFactory _factory;
|
||||
private readonly Action<ContentEffectsAudioCompositionPoint>? _faultInjection;
|
||||
|
||||
public ContentEffectsAudioCompositionPhase(
|
||||
ContentEffectsAudioDependencies dependencies,
|
||||
IGameWindowContentEffectsAudioPublication publication,
|
||||
IContentEffectsAudioCompositionFactory? factory = null,
|
||||
Action<ContentEffectsAudioCompositionPoint>? faultInjection = null)
|
||||
{
|
||||
_dependencies = dependencies
|
||||
?? throw new ArgumentNullException(nameof(dependencies));
|
||||
_publication = publication
|
||||
?? throw new ArgumentNullException(nameof(publication));
|
||||
_factory = factory ?? new RetailContentEffectsAudioCompositionFactory();
|
||||
_faultInjection = faultInjection;
|
||||
}
|
||||
|
||||
public ContentEffectsAudioResult Compose(
|
||||
GameWindowPlatformResult<GL, IInputContext> platform,
|
||||
HostInputCameraResult host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(platform);
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
|
||||
var mainScope = new CompositionAcquisitionScope();
|
||||
try
|
||||
{
|
||||
IDatReaderWriter dats = mainScope.Acquire(
|
||||
"DAT collection",
|
||||
() => _factory.OpenDatCollection(_dependencies.DatDirectory),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishDatCollection);
|
||||
Fault(ContentEffectsAudioCompositionPoint.DatCollectionPublished);
|
||||
|
||||
MagicCatalog magic = _factory.LoadMagicCatalog(dats);
|
||||
_publication.PublishMagicCatalog(magic);
|
||||
Fault(ContentEffectsAudioCompositionPoint.MagicCatalogPublished);
|
||||
_factory.InstallSpellMetadata(_dependencies.SpellBook, magic);
|
||||
_dependencies.Log(
|
||||
$"spells: loaded {_factory.GetSpellCount(magic)} entries from portal.dat");
|
||||
Fault(ContentEffectsAudioCompositionPoint.SpellMetadataInstalled);
|
||||
|
||||
IAnimationLoader animations = _factory.CreateAnimationLoader(dats);
|
||||
_publication.PublishAnimationLoader(animations);
|
||||
Fault(ContentEffectsAudioCompositionPoint.AnimationLoaderPublished);
|
||||
|
||||
LiveEntityCollisionBuilder collision = _factory.CreateCollisionBuilder(
|
||||
_dependencies.PhysicsDataCache,
|
||||
dats,
|
||||
animations,
|
||||
_dependencies.DumpMotionEnabled);
|
||||
_publication.PublishLiveEntityCollisionBuilder(collision);
|
||||
Fault(ContentEffectsAudioCompositionPoint.CollisionBuilderPublished);
|
||||
|
||||
EmitterDescRegistry emitters = _factory.CreateEmitterRegistry(dats);
|
||||
_publication.PublishEmitterRegistry(emitters);
|
||||
Fault(ContentEffectsAudioCompositionPoint.EmitterRegistryPublished);
|
||||
|
||||
ParticleSystem particles = _factory.CreateParticleSystem(emitters);
|
||||
_publication.PublishParticleSystem(particles);
|
||||
Fault(ContentEffectsAudioCompositionPoint.ParticleSystemPublished);
|
||||
|
||||
ParticleHookSink particleSink = _factory.CreateParticleSink(
|
||||
particles,
|
||||
_dependencies.EffectPoses);
|
||||
particleSink.DiagnosticSink = message =>
|
||||
_dependencies.Error($"vfx: {message}");
|
||||
_publication.PublishParticleSink(particleSink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.ParticleSinkPublished);
|
||||
|
||||
AnimationHookFrameQueue hookFrames =
|
||||
_factory.CreateAnimationHookFrames(
|
||||
_dependencies.HookRouter,
|
||||
_dependencies.EffectPoses);
|
||||
_publication.PublishAnimationHookFrames(hookFrames);
|
||||
Fault(ContentEffectsAudioCompositionPoint.AnimationHookFramesPublished);
|
||||
|
||||
AnimationHookRegistrationSet registrations = mainScope.Acquire(
|
||||
"animation hook registrations",
|
||||
() => _factory.CreateHookRegistrations(_dependencies.HookRouter),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishHookRegistrations);
|
||||
Fault(ContentEffectsAudioCompositionPoint.HookRegistrationsPublished);
|
||||
registrations.Register(particleSink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.ParticleHookRegistered);
|
||||
|
||||
RetailPhysicsScriptLoader scriptLoader =
|
||||
_factory.CreatePhysicsScriptLoader(dats);
|
||||
_publication.PublishPhysicsScriptLoader(scriptLoader);
|
||||
Fault(ContentEffectsAudioCompositionPoint.PhysicsScriptLoaderPublished);
|
||||
|
||||
PhysicsScriptRunner scriptRunner = _factory.CreatePhysicsScriptRunner(
|
||||
scriptLoader,
|
||||
_dependencies.HookRouter,
|
||||
_dependencies.EntityEffectAdvance);
|
||||
_publication.PublishPhysicsScriptRunner(scriptRunner);
|
||||
Fault(ContentEffectsAudioCompositionPoint.PhysicsScriptRunnerPublished);
|
||||
|
||||
LightingHookSink lightingSink = _factory.CreateLightingSink(
|
||||
_dependencies.Lighting,
|
||||
_dependencies.EffectPoses);
|
||||
_publication.PublishLightingSink(lightingSink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.LightingSinkPublished);
|
||||
registrations.Register(lightingSink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.LightingHookRegistered);
|
||||
|
||||
TranslucencyHookSink translucencySink =
|
||||
_factory.CreateTranslucencySink(_dependencies.TranslucencyFades);
|
||||
_publication.PublishTranslucencySink(translucencySink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.TranslucencySinkPublished);
|
||||
registrations.Register(translucencySink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.TranslucencyHookRegistered);
|
||||
|
||||
ContentAudioGraph? audio = _dependencies.NoAudio
|
||||
? null
|
||||
: ComposeOptionalAudio(dats, registrations);
|
||||
|
||||
mainScope.Complete();
|
||||
return new ContentEffectsAudioResult(
|
||||
dats,
|
||||
magic,
|
||||
animations,
|
||||
collision,
|
||||
emitters,
|
||||
particles,
|
||||
particleSink,
|
||||
hookFrames,
|
||||
scriptLoader,
|
||||
scriptRunner,
|
||||
lightingSink,
|
||||
translucencySink,
|
||||
registrations,
|
||||
audio);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
mainScope.RollbackAndThrow(failure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
private ContentAudioGraph? ComposeOptionalAudio(
|
||||
IDatReaderWriter dats,
|
||||
AnimationHookRegistrationSet registrations)
|
||||
{
|
||||
var audioScope = new CompositionAcquisitionScope();
|
||||
ContentAudioGraph graph;
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<OpenAlAudioEngine>
|
||||
engineLease;
|
||||
try
|
||||
{
|
||||
DatSoundCache cache = _factory.CreateSoundCache(dats);
|
||||
Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated);
|
||||
engineLease = audioScope.Acquire(
|
||||
"OpenAL audio engine",
|
||||
_factory.CreateAudioEngine,
|
||||
static value => value.Dispose());
|
||||
OpenAlAudioEngine engine = engineLease.Resource;
|
||||
Fault(ContentEffectsAudioCompositionPoint.AudioEngineCreated);
|
||||
DictionaryEntitySoundTable soundTables =
|
||||
_factory.CreateEntitySoundTables();
|
||||
Fault(ContentEffectsAudioCompositionPoint.EntitySoundTablesCreated);
|
||||
AudioHookSink? sink = null;
|
||||
if (engine.IsAvailable)
|
||||
{
|
||||
sink = _factory.CreateAudioSink(engine, cache, soundTables);
|
||||
Fault(ContentEffectsAudioCompositionPoint.AudioSinkCreated);
|
||||
}
|
||||
|
||||
graph = new ContentAudioGraph(cache, engine, soundTables, sink);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
try
|
||||
{
|
||||
audioScope.RollbackAndThrow(failure);
|
||||
}
|
||||
catch (Exception rolledBack) when (!HasIncompleteCleanup(rolledBack))
|
||||
{
|
||||
_dependencies.Log(
|
||||
$"audio: init failed: {rolledBack.Message} — audio disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_publication.PublishAudio(graph);
|
||||
engineLease.Transfer();
|
||||
audioScope.Complete();
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
audioScope.RollbackAndThrow(failure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
|
||||
Fault(ContentEffectsAudioCompositionPoint.AudioPublished);
|
||||
if (graph.HookSink is { } audioSink)
|
||||
{
|
||||
registrations.Register(audioSink);
|
||||
Fault(ContentEffectsAudioCompositionPoint.AudioHookRegistered);
|
||||
_dependencies.Log("audio: OpenAL engine ready (16 voices, 3D positional)");
|
||||
}
|
||||
else
|
||||
{
|
||||
_dependencies.Log(
|
||||
"audio: OpenAL unavailable (driver missing / headless) — audio disabled");
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
private static bool HasIncompleteCleanup(Exception failure)
|
||||
{
|
||||
if (failure is IRetryableResourceCleanup cleanup
|
||||
&& !cleanup.IsCleanupComplete)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (failure is AggregateException aggregate)
|
||||
return aggregate.InnerExceptions.Any(HasIncompleteCleanup);
|
||||
return failure.InnerException is { } inner && HasIncompleteCleanup(inner);
|
||||
}
|
||||
|
||||
private void Fault(ContentEffectsAudioCompositionPoint point) =>
|
||||
_faultInjection?.Invoke(point);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue