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>
605 lines
24 KiB
C#
605 lines
24 KiB
C#
using AcDream.App.Audio;
|
|
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.Core.CharGen;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Runtime.Session;
|
|
using DatReaderWriter;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record ContentAudioGraph(
|
|
DatSoundCache SoundCache,
|
|
OpenAlAudioEngine Engine,
|
|
DictionaryEntitySoundTable EntitySoundTables,
|
|
AudioHookSink? HookSink,
|
|
UiSoundController? UiSounds,
|
|
AmbientSoundController? Ambient);
|
|
|
|
internal sealed record ContentEffectsAudioResult(
|
|
IDatReaderWriter Dats,
|
|
IPreparedAssetSource PreparedAssets,
|
|
MagicCatalog MagicCatalog,
|
|
IAnimationLoader AnimationLoader,
|
|
LiveEntityCollisionBuilder CollisionBuilder,
|
|
LiveCollisionAssetPublisher CollisionAssets,
|
|
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,
|
|
string PreparedAssetPath,
|
|
ResidencyBudgetOptions ResidencyBudgets,
|
|
PhysicsDataCache PhysicsDataCache,
|
|
bool DumpMotionEnabled,
|
|
GameRuntime Runtime,
|
|
AnimationHookRouter HookRouter,
|
|
EntityEffectPoseRegistry EffectPoses,
|
|
DeferredEntityEffectAdvanceSource EntityEffectAdvance,
|
|
LightManager Lighting,
|
|
TranslucencyFadeManager TranslucencyFades,
|
|
bool NoAudio,
|
|
Action<string> Log,
|
|
Action<string> Error)
|
|
{
|
|
public RuntimeCharacterState Character => Runtime.CharacterOwner;
|
|
|
|
/// <summary>Campaign CC slice CC4: the character-creation options
|
|
/// install target — see <c>ContentEffectsAudioCompositionPhase.Compose</c>'s
|
|
/// <c>ChargenOptionsInstalled</c> step and
|
|
/// <see cref="RuntimeCharacterCreationState.InstallOptions"/>'s own doc
|
|
/// for why this is safe at composition time (strictly before any
|
|
/// session's <c>Begin</c>).</summary>
|
|
public LiveSessionController Session => Runtime.Session;
|
|
}
|
|
|
|
internal interface IGameWindowContentEffectsAudioPublication
|
|
{
|
|
void PublishDatCollection(IDatReaderWriter value);
|
|
void PublishPreparedAssetSource(IPreparedAssetSource 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);
|
|
IPreparedAssetSource OpenPreparedAssetSource(
|
|
string path,
|
|
IDatReaderWriter dats,
|
|
Action<string> diagnostic);
|
|
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
|
|
void InstallSpellMetadata(
|
|
RuntimeCharacterState character,
|
|
MagicCatalog catalog);
|
|
int GetSpellCount(MagicCatalog catalog);
|
|
/// <summary>Campaign CC slice CC4: mirrors the
|
|
/// <see cref="LoadMagicCatalog"/>/<see cref="InstallSpellMetadata"/>
|
|
/// pair's "load off dats, install once onto the owning Runtime state"
|
|
/// shape for the chargen options
|
|
/// (<c>AcDream.Content.CharGen.ChargenTableReader.Load</c>).</summary>
|
|
ChargenOptions LoadChargenOptions(IDatReaderWriter dats);
|
|
void InstallChargenOptions(LiveSessionController session, ChargenOptions options);
|
|
IAnimationLoader CreateAnimationLoader(
|
|
IDatReaderWriter dats,
|
|
long maximumEstimatedBytes,
|
|
int maximumEntries);
|
|
LiveEntityCollisionBuilder CreateCollisionBuilder(
|
|
PhysicsDataCache physicsData,
|
|
IDatReaderWriter dats,
|
|
IAnimationLoader animationLoader,
|
|
bool dumpMotionEnabled);
|
|
LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
|
PhysicsDataCache physicsData,
|
|
IPreparedAssetSource preparedAssets);
|
|
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,
|
|
long maximumDecodedBytes);
|
|
OpenAlAudioEngine CreateAudioEngine();
|
|
DictionaryEntitySoundTable CreateEntitySoundTables();
|
|
AudioHookSink CreateAudioSink(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache,
|
|
DictionaryEntitySoundTable entitySoundTables);
|
|
UiSoundController CreateUiSounds(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache,
|
|
IDatReaderWriter dats);
|
|
AmbientSoundController CreateAmbient(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache);
|
|
}
|
|
|
|
internal sealed class RetailContentEffectsAudioCompositionFactory
|
|
: IContentEffectsAudioCompositionFactory
|
|
{
|
|
public IDatReaderWriter OpenDatCollection(string datDirectory) =>
|
|
RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
|
|
|
public IPreparedAssetSource OpenPreparedAssetSource(
|
|
string path,
|
|
IDatReaderWriter dats,
|
|
Action<string> diagnostic) =>
|
|
new PakPreparedAssetSource(path, dats, diagnostic);
|
|
|
|
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
|
|
MagicCatalog.Load(dats);
|
|
|
|
public void InstallSpellMetadata(
|
|
RuntimeCharacterState character,
|
|
MagicCatalog catalog) =>
|
|
character.InstallSpellMetadata(catalog.SpellTable);
|
|
|
|
public int GetSpellCount(MagicCatalog catalog) => catalog.SpellTable.Count;
|
|
|
|
public ChargenOptions LoadChargenOptions(IDatReaderWriter dats) =>
|
|
AcDream.Content.CharGen.ChargenTableReader.Load(dats);
|
|
|
|
public void InstallChargenOptions(LiveSessionController session, ChargenOptions options) =>
|
|
session.CharacterCreationState.InstallOptions(options);
|
|
|
|
public IAnimationLoader CreateAnimationLoader(
|
|
IDatReaderWriter dats,
|
|
long maximumEstimatedBytes,
|
|
int maximumEntries) =>
|
|
new RetailAnimationLoader(
|
|
dats,
|
|
maximumEstimatedBytes,
|
|
maximumEntries);
|
|
|
|
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 LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
|
PhysicsDataCache physicsData,
|
|
IPreparedAssetSource preparedAssets) =>
|
|
new(
|
|
physicsData,
|
|
preparedAssets as IPreparedCollisionSource
|
|
?? throw new NotSupportedException(
|
|
"Production prepared assets must expose collision data."));
|
|
|
|
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,
|
|
long maximumDecodedBytes) =>
|
|
new(dats, maximumDecodedBytes);
|
|
|
|
public OpenAlAudioEngine CreateAudioEngine() => new();
|
|
|
|
public DictionaryEntitySoundTable CreateEntitySoundTables() => new();
|
|
|
|
public AudioHookSink CreateAudioSink(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache,
|
|
DictionaryEntitySoundTable entitySoundTables) =>
|
|
new(engine, cache, entitySoundTables);
|
|
|
|
public UiSoundController CreateUiSounds(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache,
|
|
IDatReaderWriter dats) =>
|
|
new(engine, cache, UiSoundTableResolver.Resolve(dats));
|
|
|
|
public AmbientSoundController CreateAmbient(
|
|
OpenAlAudioEngine engine,
|
|
DatSoundCache cache) =>
|
|
new(engine, cache);
|
|
}
|
|
|
|
internal enum ContentEffectsAudioCompositionPoint
|
|
{
|
|
DatCollectionPublished,
|
|
PreparedAssetSourcePublished,
|
|
MagicCatalogPublished,
|
|
SpellMetadataInstalled,
|
|
ChargenOptionsInstalled,
|
|
AnimationLoaderPublished,
|
|
CollisionBuilderPublished,
|
|
EmitterRegistryPublished,
|
|
ParticleSystemPublished,
|
|
ParticleSinkPublished,
|
|
AnimationHookFramesPublished,
|
|
HookRegistrationsPublished,
|
|
ParticleHookRegistered,
|
|
PhysicsScriptLoaderPublished,
|
|
PhysicsScriptRunnerPublished,
|
|
LightingSinkPublished,
|
|
LightingHookRegistered,
|
|
TranslucencySinkPublished,
|
|
TranslucencyHookRegistered,
|
|
SoundCacheCreated,
|
|
AudioEngineCreated,
|
|
EntitySoundTablesCreated,
|
|
AudioSinkCreated,
|
|
UiSoundsCreated,
|
|
AmbientCreated,
|
|
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<GameWindowGraphics, 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<GameWindowGraphics, 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);
|
|
|
|
IPreparedAssetSource preparedAssets = mainScope.Acquire(
|
|
"prepared asset source",
|
|
() => _factory.OpenPreparedAssetSource(
|
|
_dependencies.PreparedAssetPath,
|
|
dats,
|
|
_dependencies.Error),
|
|
static value => value.Dispose()).Publish(
|
|
_publication.PublishPreparedAssetSource);
|
|
Fault(ContentEffectsAudioCompositionPoint.PreparedAssetSourcePublished);
|
|
_dependencies.Log(
|
|
$"prepared assets: opened '{_dependencies.PreparedAssetPath}'");
|
|
|
|
MagicCatalog magic = _factory.LoadMagicCatalog(dats);
|
|
_publication.PublishMagicCatalog(magic);
|
|
Fault(ContentEffectsAudioCompositionPoint.MagicCatalogPublished);
|
|
_factory.InstallSpellMetadata(_dependencies.Character, magic);
|
|
_dependencies.Log(
|
|
$"spells: loaded {_factory.GetSpellCount(magic)} entries from portal.dat");
|
|
Fault(ContentEffectsAudioCompositionPoint.SpellMetadataInstalled);
|
|
|
|
ChargenOptions chargen = _factory.LoadChargenOptions(dats);
|
|
_factory.InstallChargenOptions(_dependencies.Session, chargen);
|
|
_dependencies.Log(
|
|
$"chargen: loaded {chargen.HeritagesById.Count} heritage(s) from portal.dat");
|
|
Fault(ContentEffectsAudioCompositionPoint.ChargenOptionsInstalled);
|
|
|
|
IAnimationLoader animations = _factory.CreateAnimationLoader(
|
|
dats,
|
|
_dependencies.ResidencyBudgets.AnimationBytes,
|
|
_dependencies.ResidencyBudgets.AnimationEntries);
|
|
_publication.PublishAnimationLoader(animations);
|
|
Fault(ContentEffectsAudioCompositionPoint.AnimationLoaderPublished);
|
|
|
|
LiveEntityCollisionBuilder collision = _factory.CreateCollisionBuilder(
|
|
_dependencies.PhysicsDataCache,
|
|
dats,
|
|
animations,
|
|
_dependencies.DumpMotionEnabled);
|
|
_publication.PublishLiveEntityCollisionBuilder(collision);
|
|
Fault(ContentEffectsAudioCompositionPoint.CollisionBuilderPublished);
|
|
LiveCollisionAssetPublisher collisionAssets =
|
|
_factory.CreateLiveCollisionAssetPublisher(
|
|
_dependencies.PhysicsDataCache,
|
|
preparedAssets);
|
|
|
|
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,
|
|
preparedAssets,
|
|
magic,
|
|
animations,
|
|
collision,
|
|
collisionAssets,
|
|
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,
|
|
_dependencies.ResidencyBudgets.AudioBytes);
|
|
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;
|
|
UiSoundController? uiSounds = null;
|
|
AmbientSoundController? ambient = null;
|
|
if (engine.IsAvailable)
|
|
{
|
|
sink = _factory.CreateAudioSink(engine, cache, soundTables);
|
|
Fault(ContentEffectsAudioCompositionPoint.AudioSinkCreated);
|
|
uiSounds = _factory.CreateUiSounds(engine, cache, dats);
|
|
_dependencies.Log(
|
|
uiSounds.TableDid == 0
|
|
? "audio: UI sound bank unresolved (no enum chain in dats) "
|
|
+ "- interface cues silent"
|
|
: $"audio: UI sound bank = 0x{uiSounds.TableDid:X8}");
|
|
Fault(ContentEffectsAudioCompositionPoint.UiSoundsCreated);
|
|
ambient = _factory.CreateAmbient(engine, cache);
|
|
Fault(ContentEffectsAudioCompositionPoint.AmbientCreated);
|
|
}
|
|
|
|
graph = new ContentAudioGraph(
|
|
cache, engine, soundTables, sink, uiSounds, ambient);
|
|
}
|
|
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);
|
|
}
|