acdream/src/AcDream.App/Composition/LivePresentationComposition.cs
Erik 0c8e1e7df1 fix(chargen): Campaign CC CC5 review fix round — F1-F14
Opus dual-lens review of 34e3a534+a975efd1 returned architectural
PASS-with-items / retail-fidelity FAIL. Every finding fixed:

- F1 (BLOCKER): deleted CharacterCreationSummaryPage's dead
  _suppressNextFieldEvent latch. UiField.SetText never raises
  OnFocusLost/OnSubmit, so the latch never had anything genuine to
  suppress — it stayed armed until the player's own next real commit
  and silently ate their typed name.
- F2: byte-re-derived gmCharGenMainUI::RecvNotice_
  CharGenVerificationResponse @0x004e9030's jump table — Pending is an
  explicit switch case landing on the SAME NameDBDown label as
  Corrupt/DatabaseDown, and Undef/out-of-range falls through the
  function's own unsigned-underflow default arm to that identical
  label. Retail's dispatch has NO silent branch. ApplyCreationResponse
  now produces a real rejection for Pending/Undef instead of a silent
  reset; ReconcileDialogs maps them to NameDBDown. Corrects the wrong
  "retail swallows Pending" claim everywhere it was repeated (plan doc,
  Core.Net doc comment, Runtime doc comments).
- F3: skill rows now use the key/value template with
  CharGenState::GetSkillScore @0x005C4B50 as the value (ported via the
  new RetailSkillFormula.CalculateChargenScore /
  ChargenSkillScoreResolver, wired through a new GetSkillScore
  binding), not template 0/name-only; bucket headers are unconditional.
  Writing this fix's own regression test surfaced a second, more severe
  bug: CharacterCreationSummaryPage never wired _list.TemplateResolver
  at all, so RebuildListbox has been a silent no-op since CC5 shipped —
  fixed by threading templateResolver through the page's constructor,
  matching every sibling UiTemplateListBox owner.
- F4: added the missing _errorMessageDialogContext one-outstanding
  guard to the 0xF643 rejection dialog, matching
  MakeErrorMessageDialog's own guard @0x004e8cc4 and the other four
  sibling dialogs' shape (registered in CloseAllDialogs, suppress-
  callback checked).
- F5: the Summary preview camera now seeds/re-derives retail's
  zoomed-OUT eye (byte-decoded (0,-2.5,0.95) at gmCGSummaryPage::
  InitializePage ~0x0047bd14-0x0047bd44) instead of Appearance's
  zoomed-in default, via a new ChargenPreviewController
  useZoomedOutEye flag.
- F6: retired AP-225 outright — re-derived the ListenToElementMessage
  length gate is NUL-inclusive, so MaxNameLength=32 was always
  byte-correct, not merely internally consistent.
- F7: amended AP-221 to cover the Summary preview's duplicate
  one-shot-composition binding gap (CC5 duplicated the pattern instead
  of closing it).
- F8: byte-decoded GetRandomReal @0x00563940's fmul operand at
  0x007cd650 — an 8-byte double, not a 4-byte float — is EXACTLY
  1.0/32767.0, not 1/32768. Added RollShadeLocked
  (_random.Next(32768) * (1.0/32767.0)) and switched all six shade
  rolls onto it.
- F9: evaluated porting retail's exact empty-name-commit no-op
  (NUL-inclusive length==1 skips SetName entirely) and rejected it —
  it would fight the F1 field-sync model by spontaneously reverting an
  emptied field on the next unrelated revision bump. Kept the clear,
  documented the tradeoff, filed AP-227.
- F11: filed AP-226 documenting retail's static pcProfessions/pcGender/
  pcHeritage/pcTown label tables versus acdream's DAT-sourced labels,
  including the non-human-heritage-renders-bare-"Heritage:" retail
  quirk.
- F12: added exclude-current determinism (count-2 lists), Random-
  clears-name, repeat-identical-rejection-reshows, and RebuildListbox
  content tests (the last one found F3's TemplateResolver bug).
- F13: threaded an optional Random through GameRuntimeDependencies ->
  LiveSessionController -> RuntimeCharacterCreationState, matching the
  existing TimeProvider injection shape, closing the Slice-K
  determinism hazard on a bot-reachable Randomize* command family.
- F14: RandomizeCharacterLocked now assigns _heritageId unconditionally
  before the TryGetHeritage gate, matching retail's SetHeritageGroup
  @0x005C67A0 (mHeritageGroup written before the DAT lookup).

Gates: Runtime 1726/0 (was 1722/0), App 5242/3 skips (was 5240/3),
Headless 166/0, Core.Net 993/994 (the one failure, NakEmissionTests
LossSoak, is a known pre-existing flake — passes standalone), full
solution Release build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:13:52 +02:00

1575 lines
71 KiB
C#

using System.Numerics;
using AcDream.Content;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.Streaming;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.Core.Audio;
using AcDream.Core.Items;
using AcDream.Core.Lighting;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.Selection;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.World;
using AcDream.UI.Abstractions.Panels.SpewBox;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
internal sealed record LivePresentationDependencies(
RuntimeOptions Options,
GameWindowGraphics Graphics,
IWindow Window,
object DatLock,
RuntimeSettingsController Settings,
HostQuiescenceGate HostQuiescence,
PhysicsEngine PhysicsEngine,
PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState,
WorldEvents WorldEvents,
GameRuntime Runtime,
LiveEntityRuntimeSlot RuntimeSlot,
DeferredLiveEntityMotionRuntimeBindings MotionBindings,
DeferredEntityEffectAdvanceSource EffectAdvance,
EntityEffectPoseRegistry EffectPoses,
RemotePhysicsUpdater RemotePhysicsUpdater,
LocalPlayerShadowState LocalPlayerShadow,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
AnimationPresentationDiagnostics AnimationDiagnostics,
EntityClassificationCache ClassificationCache,
TranslucencyFadeManager TranslucencyFades,
RetailAlphaQueue RetailAlphaQueue,
CellVisibility CellVisibility,
LiveWorldOriginState WorldOrigin,
LocalPlayerIdentityState PlayerIdentity,
PointerPositionState PointerPosition,
PlayerApproachCompletionState PlayerApproachCompletions,
GameRenderResourceLifetime RenderResourceLifetime,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
AnimationHookRouter HookRouter,
IRenderFrameDiagnosticLog RenderDiagnosticLog,
WorldTimeService WorldTime,
DeferredCanonicalWorldEntityCountSource? DevWorldEntities,
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
Action<string> Log,
Action<string>? Toast)
{
public SelectionState Selection => Runtime.ActionOwner.Selection;
public RuntimeEntityObjectLifetime EntityObjects =>
Runtime.EntityObjects;
public RuntimeWorldTransitState WorldTransit => Runtime.TransitOwner;
public RuntimeLocalPlayerMovementState PlayerController =>
Runtime.MovementOwner;
public RuntimeCharacterState Character => Runtime.CharacterOwner;
}
internal sealed record LivePresentationResult(
DeferredLiveEntityRuntimeComponentLifecycle ComponentLifecycle,
LiveEntityMotionRuntimeController MotionRuntime,
DeferredLiveEntityParentAcceptance ParentAcceptance,
EntitySpawnAdapter EntitySpawnAdapter,
EntityScriptActivator EntityScriptActivator,
RetailStaticAnimatingObjectScheduler StaticAnimationScheduler,
RuntimeWorldTransitState WorldTransit,
WorldGenerationAvailabilityState WorldAvailability,
GpuWorldState WorldState,
RenderSceneShadowRuntime? RenderSceneShadow,
LiveEntityRuntime LiveEntities,
RuntimePlacementPresentationSink PlacementProjection,
// AP-145 fix (2026-08-05, #318): constructed HERE (before the sink that
// consumes it) rather than later in SessionPlayerComposition, so both
// consumers share the ONE synchronizer instance / ONE
// LocalPlayerShadowState cache. Two instances would themselves
// reintroduce a cache-desync class this fix exists to close.
LocalPlayerShadowSynchronizer LocalPlayerShadowSynchronizer,
ProjectileController ProjectileController,
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
LiveEntityLightController Lights,
LiveEntityAnimationScheduler AnimationScheduler,
LiveEntityAnimationPresenter AnimationPresenter,
EquippedChildRenderController EquippedChildren,
EntityEffectController EntityEffects,
LiveEntityPresentationController Presentation,
WbDrawDispatcher? DrawDispatcher,
RetailSelectionScene SelectionScene,
WorldSelectionQuery SelectionQuery,
SelectionInteractionController SelectionInteractions,
RetainedUiGameplayBinding? RetainedGameplay,
PaperdollViewportRenderer? PaperdollRenderer,
PaperdollFramePresenter? PaperdollPresenter,
CreatureAppraisalViewportRenderer? CreatureAppraisalRenderer,
CreatureAppraisalFramePresenter? CreatureAppraisalPresenter,
// Campaign CC slice CC6b-MOUNT: the chargen Appearance-page preview —
// the renderer (leased/disposed) and the controller (per-frame owner +
// late-bound zoom/rotate control surface) are separate fields because
// ChargenPreviewController does not own the renderer's lifetime (it is
// a leased composition resource, mirroring PaperdollViewportRenderer).
ChargenPreviewRenderer? ChargenPreviewRenderer,
ChargenPreviewController? ChargenPreviewController,
// Campaign CC slice CC5: the Summary page's own gmCG3DView instance —
// a SEPARATE leased renderer/controller pair, same split reasoning as
// the Appearance preview fields immediately above.
ChargenPreviewRenderer? SummaryPreviewRenderer,
ChargenPreviewController? SummaryPreviewController,
WbFrustum EnvCellFrustum,
EnvCellRenderer? EnvCellRenderer,
LandblockPresentationPipeline LandblockPipeline,
ClipFrame ClipFrame,
PortalDepthMaskRenderer? PortalDepthMask,
SkyRenderer? SkyRenderer,
ParticleRenderer? ParticleRenderer,
RenderFrameDiagnosticsController FrameDiagnostics,
LivePresentationRuntimeBindings RuntimeBindings,
DeferredLiveEntityLandblockLoadedSink LandblockLoaded);
internal interface IGameWindowLivePresentationPublication
{
void PublishLivePresentation(LivePresentationResult result);
}
internal enum LivePresentationCompositionPoint
{
CanonicalRuntimeCreated,
CanonicalRuntimeBound,
MotionRuntimeBound,
ProjectionVisibilityBound,
CorePresentationCreated,
EffectRoutingBound,
SelectionAndRadarBound,
RetainedGameplayBound,
PrivateCreatureViewportsCreated,
EnvironmentCellsCreated,
LandblockPipelineCreated,
PortalResourcesCreated,
SkyAndParticlesCreated,
DiagnosticsBound,
ResultPublished,
}
internal sealed class LivePresentationCompositionPhase
: ILivePresentationCompositionPhase<
GameWindowPlatformResult<GameWindowGraphics, Silk.NET.Input.IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult>
{
private readonly LivePresentationDependencies _dependencies;
private readonly IGameWindowLivePresentationPublication _publication;
private readonly Action<LivePresentationCompositionPoint>? _faultInjection;
public LivePresentationCompositionPhase(
LivePresentationDependencies dependencies,
IGameWindowLivePresentationPublication publication,
Action<LivePresentationCompositionPoint>? faultInjection = null)
{
_dependencies = dependencies
?? throw new ArgumentNullException(nameof(dependencies));
_publication = publication
?? throw new ArgumentNullException(nameof(publication));
_faultInjection = faultInjection;
}
public LivePresentationResult Compose(
GameWindowPlatformResult<GameWindowGraphics, Silk.NET.Input.IInputContext> platform,
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world,
InteractionRetainedUiResult interaction)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(interaction);
if (!ReferenceEquals(_dependencies.Graphics, platform.Graphics))
{
throw new InvalidOperationException(
"Live-presentation dependencies do not match the ordered platform result.");
}
return ComposeCore(host, content, world, interaction);
}
private LivePresentationResult ComposeCore(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction)
{
LivePresentationDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
var scope = new CompositionAcquisitionScope();
LivePresentationRuntimeBindings? bindings = null;
bool bindingsOwnedByScope = false;
try
{
CompositionAcquisitionScope.CompositionAcquisitionLease<
RenderSceneShadowRuntime>? renderSceneShadowLease = null;
renderSceneShadowLease = scope.Acquire(
"render scene",
() => new RenderSceneShadowRuntime(
RenderSceneGeneration.FromRaw(1)),
static value => value.Dispose());
RenderSceneShadowRuntime? renderSceneShadow =
renderSceneShadowLease?.Resource;
var componentLifecycle =
new DeferredLiveEntityRuntimeComponentLifecycle();
var wbSpawnAdapter = new LandblockSpawnAdapter(
foundation.MeshAdapter
?? throw new InvalidOperationException(
"The landblock spawn ledger requires the mesh pipeline, which "
+ "Campaign V slice V6i-3 made backend-neutral."));
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<Setup>(
sourceId,
out Setup? setup)
? setup
: null;
}
var setupResolver = new PreparedSetupResolver(
content.PreparedAssets,
LoadPreparedSetup,
message => Console.Error.WriteLine(
$"setup activation: {message}"));
AnimationSequencer SequencerFactory(WorldEntity entity)
{
if (setupResolver.TryResolve(
entity.SourceGfxObjOrSetupId,
out Setup? setup))
{
uint motionTableId = (uint)setup.DefaultMotionTable;
if (motionTableId != 0
&& content.Dats.Get<MotionTable>(motionTableId) is { } motionTable)
{
return new AnimationSequencer(
setup,
motionTable,
content.AnimationLoader);
}
return new AnimationSequencer(
setup,
new MotionTable(),
content.AnimationLoader);
}
return new AnimationSequencer(
new Setup(),
new MotionTable(),
NullAnimationLoader.Instance);
}
var entitySpawnAdapter = new EntitySpawnAdapter(
foundation.TextureCache,
SequencerFactory,
foundation.MeshAdapter);
EntityEffectController? entityEffects = null;
LiveEntityRuntime? liveEntities = null;
var staticRootCommitter = new StaticLiveRootCommitter(
d.RuntimeSlot,
d.PhysicsEngine.ShadowObjects,
d.WorldOrigin,
d.EffectPoses);
var staticResidency = new LiveStaticAnimationResidency(d.RuntimeSlot);
(LiveEntityAnimationState Animation, PhysicsBody Body)?
ResolveLiveStaticOwner(WorldEntity entity)
{
if (entity.ServerGuid == 0
|| liveEntities?.TryGetRecord(
entity.ServerGuid,
out LiveEntityRecord record) != true
|| !ReferenceEquals(record.WorldEntity, entity)
|| !record.IsSpatiallyProjected
|| !record.IsSpatiallyVisible
|| record.AnimationRuntime
is not LiveEntityAnimationState animation
|| record.PhysicsBody is not { } body)
{
return null;
}
return (animation, body);
}
var staticAnimationScheduler =
new RetailStaticAnimatingObjectScheduler(
content.AnimationLoader,
content.AnimationHookFrames.Capture,
d.EffectPoses.Publish,
staticResidency.IsResident,
(entity, body) => _ = staticRootCommitter.Commit(entity, body),
staticResidency.ProjectionVersion,
ResolveLiveStaticOwner);
ScriptActivationInfo? ResolveActivation(WorldEntity entity)
{
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(
content.ScriptRunner,
content.ParticleSink,
d.EffectPoses,
ResolveActivation,
(ownerId, entity, profile) =>
entityEffects?.OnDatStaticEntityReady(ownerId, entity, profile),
ownerId =>
{
entityEffects?.OnDatStaticEntityRemoved(ownerId);
content.LightingSink.UnregisterOwner(ownerId);
d.TranslucencyFades.ClearEntity(ownerId);
},
(entity, info) => staticAnimationScheduler.Register(entity, info),
staticAnimationScheduler.Unregister,
(entity, info) => staticAnimationScheduler.Rebind(entity, info));
RuntimeWorldTransitState worldTransit = d.WorldTransit;
var worldAvailability =
new WorldGenerationAvailabilityState(worldTransit);
var worldState = new GpuWorldState(
wbSpawnAdapter,
d.ClassificationCache.InvalidateLandblock,
entityScriptActivator,
worldAvailability);
bindings = new LivePresentationRuntimeBindings();
if (d.DevWorldEntities is { } devWorldEntities)
{
bindings.Adopt(
"developer world-entity source",
devWorldEntities.BindOwned(worldState));
}
var resourceOwners =
new List<CompositeLiveEntityResourceLifecycle.Owner>
{
new(
entity =>
{
if (liveEntities is null
|| !liveEntities.TryGetRecordByLocalEntityId(
entity.Id,
out LiveEntityRecord record)
|| record.ProjectionKey is not { } key)
{
throw new InvalidOperationException(
"Live presentation registration requires an exact Runtime projection key.");
}
_ = entitySpawnAdapter.OnCreate(key, entity);
},
entity => _ = entitySpawnAdapter.OnRemove(entity)),
new(
entityScriptActivator.OnCreate,
entityScriptActivator.OnRemove),
};
if (renderSceneShadow is not null)
{
resourceOwners.Add(new(
renderSceneShadow.OnLiveResourceRegistered,
renderSceneShadow.OnLiveResourceUnregistered));
}
liveEntities = new LiveEntityRuntime(
worldState,
new CompositeLiveEntityResourceLifecycle(
[.. resourceOwners]),
componentLifecycle,
d.EntityObjects);
var liveRuntimeLease = scope.Own(
"canonical live-entity runtime",
liveEntities,
static runtime => runtime.Clear());
LiveRenderProjectionJournal? liveRenderProjections =
renderSceneShadow?.BindLiveRuntime(
liveEntities,
new GpuWorldRenderTraversalOrderSource(worldState));
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
bindings.Adopt(
"canonical live-runtime slot",
d.RuntimeSlot.BindOwned(liveEntities));
Fault(LivePresentationCompositionPoint.CanonicalRuntimeBound);
var selectionInteractionSource =
new DeferredSelectionInteractionSource();
var motionRuntime = new LiveEntityMotionRuntimeController(
liveEntities,
d.PhysicsDataCache,
() => selectionInteractionSource.Current,
d.Selection,
d.WorldOrigin);
bindings.Adopt(
"live motion runtime",
d.MotionBindings.BindOwned(motionRuntime));
Fault(LivePresentationCompositionPoint.MotionRuntimeBound);
Action<LiveEntityRecord, bool> wbVisibility = (record, visible) =>
{
if (record.WorldEntity is { } entity)
entitySpawnAdapter.SetPresentationResident(entity, visible);
};
bindings.BindProjectionVisibility(
liveEntities,
wbVisibility,
"WB projection visibility");
if (liveRenderProjections is not null)
{
bindings.BindProjectionVisibility(
liveEntities,
liveRenderProjections.OnProjectionVisibilityChanged,
"shadow render projection visibility");
}
Action<LiveEntityRecord, bool> particleVisibility = (record, visible) =>
{
if (record.WorldEntity is { } entity)
content.ParticleSink.SetEntityPresentationVisible(entity.Id, visible);
};
bindings.BindProjectionVisibility(
liveEntities,
particleVisibility,
"particle projection visibility");
var placementVisibilitySinks = new List<
Action<LiveEntityRecord, bool>>(4)
{
wbVisibility,
};
if (liveRenderProjections is not null)
{
placementVisibilitySinks.Add(
liveRenderProjections.OnProjectionVisibilityChanged);
}
placementVisibilitySinks.Add(particleVisibility);
// Retail enters the CPhysicsObj before ProcessObjectNetBlobs.
// C3c's initial Runtime placement is the equivalent world-entry
// edge, so open/replay the one-shot F754/F755 barrier only after
// mesh poses and particle presentation have both been published.
placementVisibilitySinks.Add((record, visible) =>
{
if (visible)
entityEffects?.OnPresentationBound(record);
});
// AP-145 fix (2026-08-05, #318): constructed here, BEFORE the
// sink, so the sink can publish the local player's Place
// through the same seam ordinary per-tick movement uses rather
// than writing LocalPlayerShadowState directly. Threaded through
// to SessionPlayerComposition via LivePresentationResult so
// there remains exactly one synchronizer / one cache for the
// whole session — SessionPlayerComposition no longer constructs
// its own.
var localPlayerShadowSynchronizer = new LocalPlayerShadowSynchronizer(
d.PhysicsEngine,
liveEntities,
d.PlayerIdentity,
d.WorldOrigin,
d.LocalPlayerShadow);
var placementProjection = new RuntimePlacementPresentationSink(
liveEntities,
worldTransit,
d.WorldGameState,
d.WorldEvents,
d.EffectPoses,
localPlayerShadowSynchronizer,
() => d.PlayerIdentity.ServerGuid,
guid =>
{
if (d.Selection.SelectedObjectId == guid)
{
d.Selection.Clear(
SelectionChangeSource.System,
SelectionChangeReason.SelectedObjectRemoved);
}
},
placementVisibilitySinks);
Fault(LivePresentationCompositionPoint.ProjectionVisibilityBound);
var projectileController = new ProjectileController(
liveEntities,
new DatProjectileSetupResolver(content.Dats, d.DatLock),
new EntityRootPosePublisher(d.EffectPoses),
d.WorldOrigin)
{
DiagnosticSink = message =>
Console.Error.WriteLine($"projectile: {message}"),
};
var projectionWithdrawal =
new LiveEntityProjectionWithdrawalController(
liveEntities,
projectileController,
d.WorldGameState,
d.WorldEvents,
d.PhysicsEngine.ShadowObjects,
d.EffectPoses,
d.LocalPlayerShadow);
var lightsLease = scope.Acquire(
"live-entity lights",
() => new LiveEntityLightController(
liveEntities,
d.EffectPoses,
content.LightingSink,
setupId => content.Dats.Get<Setup>(setupId)),
static value => value.Dispose());
var ordinaryPhysicsUpdater = new LiveEntityOrdinaryPhysicsUpdater(
d.EntityObjects.Physics,
d.MotionBindings.GetSetupCylinder,
d.MotionBindings.GetSetupMoverShape,
// TS-23 (Campaign P Slice P3, 2026-07-30): the ordinary
// mover's own PK/PKLite/Impenetrable bits — same
// ClientObjectTable-backed lookup GameWindow's remote
// updater uses.
guid => AcDream.Core.Physics.EntityCollisionFlagsExt.ResolveMoverPvpState(
d.EntityObjects.Objects,
guid));
var animationScheduler = new LiveEntityAnimationScheduler(
liveEntities,
d.PlayerIdentity,
d.RemotePhysicsUpdater,
ordinaryPhysicsUpdater,
projectileController,
new EntityRootPosePublisher(d.EffectPoses),
new AnimationHookCaptureSink(content.AnimationHookFrames));
var animationPresenter = new LiveEntityAnimationPresenter(
liveEntities,
staticAnimationScheduler,
d.EffectPoses,
new LiveAnimationPresentationContext(
liveEntities,
d.PlayerIdentity,
d.PlayerController),
d.AnimationDiagnostics,
d.Options.HidePartIndex);
var parentAcceptance = new DeferredLiveEntityParentAcceptance();
var equippedLease = scope.Acquire(
"equipped-child renderer",
() => new EquippedChildRenderController(
content.Dats,
d.DatLock,
d.EntityObjects.Objects,
liveEntities,
d.EffectPoses,
parentAcceptance.TryAccept,
(childRecord, positionVersion, projectionVersion) =>
projectionWithdrawal.WithdrawExact(
childRecord,
positionVersion,
projectionVersion,
d.PlayerIdentity.ServerGuid)),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.CorePresentationCreated);
var tableResolver = new PhysicsScriptTableResolver(
id => content.Dats.Get<PhysicsScriptTable>(id));
entityEffects = new EntityEffectController(
liveEntities,
content.ScriptRunner,
tableResolver,
d.EffectPoses,
(parentLocalId, partIndex) =>
equippedLease.Resource.FindChildLocalIdAtPart(
parentLocalId,
partIndex),
equippedLease.Resource.FindParentLocalId,
ownerId => content.Audio?.EntitySoundTables.Remove(ownerId),
(ownerId, soundTableDid) =>
{
content.Audio?.EntitySoundTables.Remove(ownerId);
if (soundTableDid is { } did)
content.Audio?.EntitySoundTables.Set(ownerId, did);
},
(ownerId, worldPosition, soundType, wireVolume) =>
content.Audio?.HookSink?.PlayServerSound(
ownerId,
worldPosition,
soundType,
wireVolume));
bindings.Adopt(
"entity-effect advance",
d.EffectAdvance.BindOwned(entityEffects));
entityEffects.DiagnosticSink = message =>
Console.Error.WriteLine($"vfx: {message}");
var partArrayLifecycle = new LiveEntityPartArrayLifecycle(
d.AnimatedEntities);
var presentationLease = scope.Acquire(
"live-entity presentation",
() => new LiveEntityPresentationController(
liveEntities,
d.PhysicsEngine.ShadowObjects,
entityEffects.PlayTypedFromHiddenTransition,
new LiveEntityPartArrayEnterWorldPort(
partArrayLifecycle.HandleEnterWorld),
equippedLease.Resource.SetDirectChildrenNoDraw,
d.MotionBindings.ClearTargetForHiddenEntity,
d.WorldOrigin.GetCenter),
static value => value.Dispose());
// #297 second edge: keep every live entity's shadow-registry
// PK/PKLite/Impenetrable flags in sync with its live PWD
// bitfield, not just the mover-side read the table already
// resolves fresh on every call.
bindings.Adopt(
"live-entity pvp bitfield sync",
new LiveEntityPvpBitfieldSync(
d.EntityObjects.Objects,
liveEntities,
d.PhysicsEngine.ShadowObjects));
bindings.BindProjectionPoseReady(
equippedLease.Resource,
lightsLease.Resource.OnAttachedPoseReady);
if (liveRenderProjections is not null)
{
bindings.BindProjectionPoseReady(
equippedLease.Resource,
liveRenderProjections.OnProjectionPoseReady);
bindings.BindProjectionRemoved(
equippedLease.Resource,
liveRenderProjections.OnProjectionRemoved);
}
bindings.Adopt(
"entity-effect animation hooks",
content.HookRegistrations.RegisterOwned(entityEffects));
Fault(LivePresentationCompositionPoint.EffectRoutingBound);
return CompletePresentation(
host,
content,
world,
interaction,
componentLifecycle,
motionRuntime,
parentAcceptance,
entitySpawnAdapter,
entityScriptActivator,
staticAnimationScheduler,
worldTransit,
worldAvailability,
worldState,
renderSceneShadow,
renderSceneShadowLease,
liveEntities,
placementProjection,
localPlayerShadowSynchronizer,
projectileController,
projectionWithdrawal,
lightsLease,
animationScheduler,
animationPresenter,
equippedLease,
entityEffects,
presentationLease,
selectionInteractionSource,
bindings,
scope,
ref bindingsOwnedByScope,
liveRuntimeLease);
}
catch (Exception failure)
{
if (bindings is not null && !bindingsOwnedByScope)
{
scope.Own(
"live-presentation runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
}
scope.RollbackAndThrow(failure);
throw new System.Diagnostics.UnreachableException();
}
}
private LivePresentationResult CompletePresentation(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
DeferredLiveEntityRuntimeComponentLifecycle componentLifecycle,
LiveEntityMotionRuntimeController motionRuntime,
DeferredLiveEntityParentAcceptance parentAcceptance,
EntitySpawnAdapter entitySpawnAdapter,
EntityScriptActivator entityScriptActivator,
RetailStaticAnimatingObjectScheduler staticAnimationScheduler,
RuntimeWorldTransitState worldTransit,
WorldGenerationAvailabilityState worldAvailability,
GpuWorldState worldState,
RenderSceneShadowRuntime? renderSceneShadow,
CompositionAcquisitionScope.CompositionAcquisitionLease<
RenderSceneShadowRuntime>? renderSceneShadowLease,
LiveEntityRuntime liveEntities,
RuntimePlacementPresentationSink placementProjection,
LocalPlayerShadowSynchronizer localPlayerShadowSynchronizer,
ProjectileController projectileController,
LiveEntityProjectionWithdrawalController projectionWithdrawal,
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityLightController> lightsLease,
LiveEntityAnimationScheduler animationScheduler,
LiveEntityAnimationPresenter animationPresenter,
CompositionAcquisitionScope.CompositionAcquisitionLease<EquippedChildRenderController> equippedLease,
EntityEffectController entityEffects,
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityPresentationController> presentationLease,
DeferredSelectionInteractionSource selectionInteractionSource,
LivePresentationRuntimeBindings bindings,
CompositionAcquisitionScope scope,
ref bool bindingsOwnedByScope,
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityRuntime> liveRuntimeLease)
{
LivePresentationDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
AlphaScratchBudgetProfile alphaScratchBudgets =
AlphaScratchBudgetProfile.Create(
d.Options.ResidencyBudgets.AlphaScratchBytes);
var selectionScene = new RetailSelectionScene(
new RetailSelectionGeometryCache(content.Dats, d.DatLock));
// Campaign V slice V6j: the world dispatcher records into the pass the
// world scene phase publishes on this scope. The raw-GL arm was
// deleted at slice V11.
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
var dispatcherLease = scope.Acquire(
"WB draw dispatcher",
() => new WbDrawDispatcher(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
foundation.TextureCache,
foundation.MeshAdapter!,
entitySpawnAdapter,
d.ClassificationCache,
d.TranslucencyFades,
selectionScene,
d.RetailAlphaQueue,
alphaScratchBudgets.DispatcherBytes),
static value => value.Dispose());
var selectionQuery = new WorldSelectionQuery(
liveEntities,
d.EntityObjects.Objects,
selectionScene,
() => d.PlayerIdentity.ServerGuid,
interaction.LateBindings.SelectionCamera.Snapshot,
() => new Vector2(
d.PointerPosition.X,
d.PointerPosition.Y),
() => d.PlayerController.Controller is { } player
? new PlayerInteractionPose(player.CellId, player.Position)
: null,
d.MotionBindings.GetSetupCylinder,
setupId =>
{
lock (d.DatLock)
{
if (!content.Dats.TryGet<Setup>(setupId, out Setup? setup)
|| setup.SelectionSphere is not { } sphere)
{
return null;
}
return (sphere.Origin, sphere.Radius);
}
},
// CPhysicsObj::UpdateChild @ 0x00512D50 recomposes an equipped
// child's own m_position from Frame::combine(parent part frame,
// holding frame) every tick; SmartBox::GetObjectBoundingBox @
// 0x00452E20 anchors the selection sphere on that own frame.
// EquippedChildRenderController publishes exactly that composed
// root here each frame, so selection borrows it rather than the
// parent-derived bookkeeping pose.
localEntityId =>
d.EffectPoses.TryGetRootPose(localEntityId, out Matrix4x4 childRoot)
? childRoot
: null);
var radarSnapshotProvider = new RadarSnapshotProvider(
d.EntityObjects.Objects,
liveEntities,
() => liveEntities.Snapshots,
playerGuid: () => d.PlayerIdentity.ServerGuid,
playerYawRadians: () => d.PlayerController.Controller?.Yaw ?? 0f,
playerCellId: () => d.PlayerController.Controller?.CellId ?? 0u,
selectedGuid: () => d.Selection.SelectedObjectId,
// D7 Group-C re-point (Campaign OP OP4, 2026-08-11): server
// bit, not the client-local GameplaySettings record — see
// CharacterOptionCombatSettingsSource's doc comment
// (AcDream.App.Combat).
coordinatesOnRadar: () => d.Character.Options.GetOptionBit(
CharacterOptionId.CoordinatesOnRadar),
uiLocked: () => d.Character.Options.GetOptionBit(
CharacterOptionId.LockUI),
spatialQuery: () => worldState);
bindings.Adopt(
"radar snapshot",
interaction.LateBindings.Radar.Bind(radarSnapshotProvider));
var selectionInteractions = new SelectionInteractionController(
d.Selection,
selectionQuery,
interaction.ItemInteraction,
new WorldSessionSelectionInteractionTransport(
() => interaction.LateBindings.Session.CurrentSession),
new PlayerInteractionMovementSink(
() => d.PlayerController.Controller,
d.PlayerApproachCompletions),
d.Toast,
d.PlayerApproachCompletions);
selectionInteractionSource.Bind(selectionInteractions);
bindings.Adopt(
"world selection",
interaction.LateBindings.Selection.Bind(
selectionQuery,
selectionInteractions));
Fault(LivePresentationCompositionPoint.SelectionAndRadarBound);
CompositionAcquisitionScope.CompositionAcquisitionLease<
RetainedUiGameplayBinding>? retainedGameplayLease = null;
if (interaction.RetainedUi is { } retainedUi)
{
retainedGameplayLease = scope.Acquire(
"retained gameplay binding",
() => RetainedUiGameplayBinding.Create(
retainedUi.Host.Root,
(item, x, y) =>
selectionInteractions.PlaceDraggedItem(item, x, y),
d.HostQuiescence),
static value => value.Dispose());
retainedGameplayLease.Resource.Attach();
}
Fault(LivePresentationCompositionPoint.RetainedGameplayBound);
if (dispatcherLease.Resource is { } alphaDispatcher)
{
alphaDispatcher.AlphaToCoverage =
d.Settings.ResolvedQuality.AlphaToCoverage;
}
CompositionAcquisitionScope.CompositionAcquisitionLease<
PaperdollViewportRenderer>? paperdollLease = null;
PaperdollFramePresenter? paperdollPresenter = null;
// Campaign V slice V6l: both retained-UI viewports exist on BOTH arms.
// The RHI arm needed two backend fixes first — a layered sampled view per
// render target, so a colour attachment can legally enter the
// sampler2DArray table, and sample-count pipeline variants in
// WbDrawDispatcher, because a world pipeline is built at the backbuffer's
// count and an offscreen target is single-sampled by contract (plan
// §5.5.16 defect 3).
if (dispatcherLease.Resource is { } paperdollDispatcher
&& interaction.RetainedUi?.Runtime.PaperdollViewportWidget is { } viewport
&& interaction.RetainedUi.Runtime.InventoryFrame is { } inventoryFrame)
{
paperdollLease = scope.Acquire(
"paperdoll viewport",
() => new PaperdollViewportRenderer(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
paperdollDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!),
static value => value.Dispose());
IUiViewportRenderer? previousRenderer = viewport.Renderer;
viewport.Renderer = paperdollLease.Resource;
bindings.AdoptRelease(
"paperdoll viewport target",
() =>
{
if (ReferenceEquals(viewport.Renderer, paperdollLease.Resource))
viewport.Renderer = previousRenderer;
});
paperdollPresenter = new PaperdollFramePresenter(
paperdollLease.Resource,
new RetailPaperdollFrameView(
viewport,
new PaperdollInventoryVisibility(inventoryFrame)),
new RetailPaperdollDollFactory(
new LivePaperdollEntityLookup(liveEntities),
d.PlayerIdentity,
new RetailPaperdollPoseApplicator(
content.Dats,
content.AnimationLoader,
d.DatLock)));
}
CompositionAcquisitionScope.CompositionAcquisitionLease<
CreatureAppraisalViewportRenderer>? creatureAppraisalLease = null;
CreatureAppraisalFramePresenter? creatureAppraisalPresenter = null;
if (dispatcherLease.Resource is { } appraisalDispatcher
&& interaction.RetainedUi?.Runtime.CreatureAppraisalViewportWidget
is { } creatureViewport
&& interaction.RetainedUi.Runtime.ExaminationFrame
is { } examinationFrame
&& interaction.RetainedUi.Runtime.AppraisalController
is { } appraisalController)
{
creatureAppraisalLease = scope.Acquire(
"creature appraisal viewport",
() => new CreatureAppraisalViewportRenderer(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
appraisalDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!),
static value => value.Dispose());
IUiViewportRenderer? previousRenderer = creatureViewport.Renderer;
creatureViewport.Renderer = creatureAppraisalLease.Resource;
bindings.AdoptRelease(
"creature appraisal viewport target",
() =>
{
if (ReferenceEquals(
creatureViewport.Renderer,
creatureAppraisalLease.Resource))
{
creatureViewport.Renderer = previousRenderer;
}
});
creatureAppraisalPresenter = new CreatureAppraisalFramePresenter(
creatureAppraisalLease.Resource,
new RetailCreatureAppraisalFrameView(
creatureViewport,
examinationFrame,
appraisalController),
new RetailCreatureAppraisalCloneFactory(
new LiveCreatureAppraisalEntityLookup(liveEntities)));
}
// Campaign CC slice CC6b-MOUNT: the chargen Appearance-page preview.
// Same "both arms exist, needs a dispatcher + the retained-UI
// viewport widget" shape as paperdoll/creature-appraisal above —
// this is the THIRD private creature viewport, not a new pattern.
//
// Fix round F8 disposition: unlike paperdoll's PaperdollViewportWidget
// (an eager, non-retryable auto-property — see that property's own
// corrected doc comment), ChargenPreviewViewportWidget is
// computed-through a coordinator (CharacterCreationUiMountCoordinator)
// that IS explicitly retryable/idempotent across frames. This
// composition pass itself runs EXACTLY ONCE, synchronously, inside
// GameWindow.OnLoad — if the coordinator's mount hasn't succeeded
// yet at this exact instant, this block is skipped and NEVER
// retried; the coordinator's own later per-frame retries (driven
// from RetailUiRuntime.Tick) can still complete the CONTROLLER mount
// afterward, but this GPU-side renderer/viewport binding will not
// pick that up. DECIDED at the review: this composition pass is a
// one-shot GPU-resource wiring step (matching paperdoll's and
// creature-appraisal's own one-shot binding in this exact method,
// and PublishLivePresentation's own "set exactly once" invariant a
// few hundred lines below) — retrofitting cross-frame retry here
// would mean restructuring this whole composition's one-shot
// contract (and the fixed PrivateEntityViewportFrameGroup array
// FrameRootComposition builds from its result) for every private
// viewport, not just this one; that is out of this fix round's
// blast radius. What changes here instead: a loud diagnostic
// instead of a silent skip, so an operator can SEE the preview
// failed to bind this session rather than the symptom (dead
// zoom/rotate buttons) reading as unexplained.
CompositionAcquisitionScope.CompositionAcquisitionLease<
ChargenPreviewRenderer>? chargenPreviewLease = null;
ChargenPreviewController? chargenPreviewController = null;
if (dispatcherLease.Resource is { } chargenDispatcher
&& interaction.RetainedUi?.Runtime.ChargenPreviewViewportWidget is { } chargenViewport)
{
var chargenCamera = new ChargenPreviewCamera();
chargenPreviewLease = scope.Acquire(
"chargen preview viewport",
() => new ChargenPreviewRenderer(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
chargenDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!,
camera: chargenCamera),
static value => value.Dispose());
IUiViewportRenderer? previousChargenRenderer = chargenViewport.Renderer;
chargenViewport.Renderer = chargenPreviewLease.Resource;
bindings.AdoptRelease(
"chargen preview viewport target",
() =>
{
if (ReferenceEquals(chargenViewport.Renderer, chargenPreviewLease.Resource))
chargenViewport.Renderer = previousChargenRenderer;
});
var chargenCatalog = new AcDream.Content.CharGen.ChargenAppearanceCatalog(content.Dats);
chargenPreviewController = new ChargenPreviewController(
chargenPreviewLease.Resource,
chargenCamera,
new RetailChargenPreviewFrameView(
chargenViewport,
new RetailChargenPreviewPageVisibility(interaction.RetainedUi.Runtime)),
content.Dats,
content.AnimationLoader,
chargenCatalog,
chargenCatalog,
d.DatLock);
interaction.RetainedUi.Runtime.ChargenPreviewControl = chargenPreviewController;
bindings.AdoptRelease(
"chargen preview control",
() =>
{
if (ReferenceEquals(
interaction.RetainedUi.Runtime.ChargenPreviewControl,
chargenPreviewController))
{
interaction.RetainedUi.Runtime.ChargenPreviewControl = null;
}
});
}
else if (dispatcherLease.Resource is not null && interaction.RetainedUi is not null)
{
// Fix round F8: dispatcher is available but the mount coordinator
// hadn't resolved ChargenPreviewViewportWidget by this one-shot
// pass — loud instead of silent, since the coordinator's own
// later per-frame retries cannot recover this GPU-side binding
// (see this block's own disposition comment above).
//
// Re-review R1: the retained UI arm (`interaction.RetainedUi`)
// is null in the default configuration (ACDREAM_RETAIL_UI
// unset — see InteractionRetainedUiComposition.cs's own gate on
// RuntimeOptions.RetailUi), and in that configuration there is
// no Appearance page at all. The dispatcher lease is
// acquired unconditionally regardless of retained-UI presence,
// so without this second guard every ordinary launch printed
// this diagnostic even though nothing was actually broken.
// Narrowed to fire only in the one configuration it is meant to
// diagnose: retained UI mounted, dispatcher ready, but the
// coordinator's widget resolution missed this one-shot pass.
Console.WriteLine(
"[UI] chargen preview viewport unavailable at composition "
+ "time — the Appearance page's zoom/rotate controls and "
+ "3D preview will not function this session.");
}
// Campaign CC slice CC5: the Summary page's OWN gmCG3DView instance
// (gmCGSummaryPage::InitializePage @0x0047bbf0, confirmed a SEPARATE
// instance from the Appearance page's own during the CC6b-MOUNT
// review) — same one-shot binding shape as the Appearance preview
// immediately above. Review fix round F7 (2026-08-16): AP-221 is now
// AMENDED to cover this second binding explicitly (it originally
// named CC5 as the slice that should CLOSE the gap; CC5 duplicated
// the pattern here instead) — a DAT/resource read not ready on this
// exact composition frame means the Summary preview stays
// permanently unbound for the session, same tracked follow-up as
// the Appearance preview, now under the same amended row. No
// zoom/rotate control surface is wired — retail's Summary page has
// no such buttons (only <c>StartAnimation</c>'s idle loop and a
// fixed 180° heading), so this controller's ZoomIn/RotateClockwise
// etc. simply never get called.
CompositionAcquisitionScope.CompositionAcquisitionLease<
ChargenPreviewRenderer>? summaryPreviewLease = null;
ChargenPreviewController? summaryPreviewController = null;
if (dispatcherLease.Resource is { } summaryDispatcher
&& interaction.RetainedUi?.Runtime.SummaryPreviewViewportWidget is { } summaryViewport)
{
var summaryCamera = new ChargenPreviewCamera();
summaryPreviewLease = scope.Acquire(
"summary preview viewport",
() => new ChargenPreviewRenderer(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
summaryDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!,
camera: summaryCamera),
static value => value.Dispose());
IUiViewportRenderer? previousSummaryRenderer = summaryViewport.Renderer;
summaryViewport.Renderer = summaryPreviewLease.Resource;
bindings.AdoptRelease(
"summary preview viewport target",
() =>
{
if (ReferenceEquals(summaryViewport.Renderer, summaryPreviewLease.Resource))
summaryViewport.Renderer = previousSummaryRenderer;
});
var summaryCatalog = new AcDream.Content.CharGen.ChargenAppearanceCatalog(content.Dats);
summaryPreviewController = new ChargenPreviewController(
summaryPreviewLease.Resource,
summaryCamera,
new RetailChargenPreviewFrameView(
summaryViewport,
new RetailSummaryPreviewPageVisibility(interaction.RetainedUi.Runtime)),
content.Dats,
content.AnimationLoader,
summaryCatalog,
summaryCatalog,
d.DatLock,
// F5 (2026-08-16): the Summary preview is retail's zoomed-
// OUT full-body framing (gmCGSummaryPage::InitializePage @
// 0x0047bbf0), not the Appearance page's zoomed-in default —
// see ChargenPreviewController's own ctor doc comment.
useZoomedOutEye: true);
interaction.RetainedUi.Runtime.SummaryPreviewControl = summaryPreviewController;
bindings.AdoptRelease(
"summary preview control",
() =>
{
if (ReferenceEquals(
interaction.RetainedUi.Runtime.SummaryPreviewControl,
summaryPreviewController))
{
interaction.RetainedUi.Runtime.SummaryPreviewControl = null;
}
});
}
else if (dispatcherLease.Resource is not null && interaction.RetainedUi is not null)
{
Console.WriteLine(
"[UI] summary preview viewport unavailable at composition "
+ "time — the Summary page's 3D preview will not function "
+ "this session.");
}
Fault(LivePresentationCompositionPoint.PrivateCreatureViewportsCreated);
var envCellFrustum = new WbFrustum();
var envCellLease = scope.Acquire(
"environment-cell renderer",
() => new EnvCellRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
foundation.MeshAdapter!.MeshManager!,
envCellFrustum),
static value => value.Dispose());
// The three pipelines ARE its program, built at construction — the
// raw-GL arm's separate Initialize(Shader) step was deleted at V11.
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
// The streaming pipeline itself is backend-neutral and runs on both
// arms: landblocks load, heightfields and collision build, and the
// spatial index fills. Only publication into GPU state is renderer-
// owned, so the Vulkan arm publishes into nothing until the world arm
// lands.
TerrainModernRenderer? terrainRenderer = foundation.Terrain;
EnvCellRenderer? envCells = envCellLease.Resource;
var landblockRenderPublisher = new LandblockRenderPublisher(
(landblockId, meshData, origin) =>
terrainRenderer?.AddLandblockWithMesh(
landblockId,
meshData,
origin),
landblockId => terrainRenderer?.RemoveLandblock(landblockId),
d.CellVisibility,
worldState,
prepareEnvCells: build =>
{
if (foundation.MeshAdapter?.MeshManager is { } envCellMeshes)
EnvCellMeshPreparationScheduler.Schedule(build, envCellMeshes);
},
removeEnvCells: landblockId => envCells?.RemoveLandblock(landblockId),
envCellPublisher: envCells);
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
d.EntityObjects.Physics,
world.TerrainBuild.HeightTable);
var landblockStaticPublisher =
new LandblockStaticPresentationPublisher(
content.LightingSink,
d.TranslucencyFades,
d.WorldGameState,
d.WorldEvents);
var landblockRetirementOwner =
new LandblockPresentationRetirementOwner(
landblockRenderPublisher,
landblockPhysicsPublisher,
landblockStaticPublisher,
content.LightingSink,
d.TranslucencyFades);
var landblockLoaded = new DeferredLiveEntityLandblockLoadedSink();
var landblockPipeline = new LandblockPresentationPipeline(
landblockRenderPublisher,
landblockPhysicsPublisher,
landblockStaticPublisher,
worldState,
landblockRetirementOwner,
landblockLoaded.OnLandblockLoaded,
landblockRenderPublisher.PrepareAfterRenderPins,
renderSceneShadow?.StaticProjections);
Fault(LivePresentationCompositionPoint.LandblockPipelineCreated);
var clipFrameLease = scope.Acquire(
"portal clip frame",
ClipFrame.NoClip,
static value => value.Dispose());
// Campaign V slice V6l: the portal depth mask compiles portal_depth
// from SPIR-V into three pipelines and records into the pass the
// world scene phase publishes on this scope. The raw-GL inline
// program and raw draws were deleted at slice V11.
var portalDepthLease = scope.Acquire(
"portal depth mask",
() => new PortalDepthMaskRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope.")),
static value => value.Dispose());
// Campaign CH user-gate round 2, item 2: the user reported retail's
// portal-space notice at the TOP of the screen in SMALL tell-yellow
// text — the SpewBox — while the former PortalWaitNoticeController
// rendered a big centered white/yellow overlay of its own. The
// decomp confirms the user's report: gmSmartBoxUI::UseTime
// @0x004D6E30 emits via ECM_UI::SendNotice_DisplayStringInfo(0x1A,
// ...), which forwards to AddTextToScroll(str, 0x1A, 1, 0) — type
// 0x1A is HARDCODED to the SpewBox
// (docs/research/2026-08-09-chat-retail-interface-text.md
// §1.1/§4.2), the SAME surface every other ClientLocal refusal uses
// (PlayerMovementController.ReportJumpRefusal, etc.), not a
// dedicated overlay. PortalWaitNoticeController is deleted along
// with its lease; this delegate now writes straight into the
// canonical AddText router every other on-screen interface-text
// site already uses. Unlike the deleted controller, this has no
// retained-UI dependency (Runtime state, not a UI element), so it
// is always wired, not gated on interaction.RetainedUi.
Action<string> displayPortalWaitNotice = text =>
d.Runtime.CommunicationOwner.AddText(
text,
AcDream.Core.Chat.RetailLogTextType.ClientLocal);
// Campaign CH slice CH2: the SpewBox is retail's OTHER on-screen
// interface-text surface (research doc §1.1/§7.3/§7.4), wired into
// the retained-UI host. The transferred controller is reclaimed by
// the retained-UI root's own teardown (its Dispose only detaches
// children from that root).
CompositionAcquisitionScope.CompositionAcquisitionLease<
SpewBoxController>? spewBoxLease = null;
if (interaction.RetainedUi is { } spewBoxRetainedUi)
{
// Campaign CH user-gate round 3: resolve the SpewBox's own
// retail dat font through the SAME memoized resolver the rest
// of the retained UI uses (RetailUiRuntime.Assets), rather than
// silently falling back to the debug bitmap font every prior
// round shipped with. See SpewBoxController's class remarks
// and register row AP-178.
RetailUiAssets spewBoxAssets = spewBoxRetainedUi.Runtime.Assets;
UiDatFont? spewBoxFont =
spewBoxAssets.ResolveFont(SpewBoxController.RetailFontId);
spewBoxLease = scope.Acquire(
"spew box",
() => new SpewBoxController(
spewBoxRetainedUi.Host.Root,
new SpewBoxVM(d.Runtime.CommunicationOwner.SpewBox),
spewBoxFont,
spewBoxAssets.DebugFont),
static value => value.Dispose());
}
CompositionAcquisitionScope.CompositionAcquisitionLease<
PortalTunnelPresentation>? portalTunnelLease = null;
// Campaign V slice V6m: portal space opens a backbuffer pass of its
// own and publishes it on the scope for the span of the draw, the way
// the two offscreen viewports do. The raw-GL arm was deleted at V11.
if (dispatcherLease.Resource is { } portalDispatcher)
{
PortalTunnelPresentation portalTunnel;
try
{
portalTunnel = d.PortalTunnelFallback.AcquirePrepared(
() => PortalTunnelPresentation.CreateRequired(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuFrameLifetime,
content.Dats,
content.AnimationLoader,
// The tunnel is UI-owned (retail gmSmartBoxUI): its
// sound hooks route to the interface bus; every other
// hook still reaches the shared router.
new AcDream.App.Audio.UiPresentationHookSink(
d.HookRouter,
content.Audio?.HookSink),
portalDispatcher,
foundation.SceneLighting!,
foundation.MeshAdapter!,
displayPortalWaitNotice),
static tunnel => tunnel.PrepareResources());
}
catch (Exception acquisitionFailure)
{
try
{
d.PortalTunnelFallback.ReleaseFallback();
}
catch (Exception cleanupFailure)
{
throw new AggregateException(
"Portal-tunnel construction and fallback rollback both failed.",
acquisitionFailure,
cleanupFailure);
}
throw;
}
portalTunnelLease = scope.Own(
"portal tunnel fallback",
portalTunnel,
_ => d.PortalTunnelFallback.ReleaseFallback());
}
Fault(LivePresentationCompositionPoint.PortalResourcesCreated);
// Campaign V slice V6k: the sky compiles its pair from SPIR-V and
// records into the pass the world scene phase publishes on this
// scope. The raw-GL arm — its own shader pair (acquired through
// GameRenderResourceLifetime.AcquireSkyShader, deleted with it) and
// its own bindless/device-table wiring — was deleted at slice V11.
var skyLease = scope.Acquire(
"sky renderer",
() => new SkyRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
content.Dats,
foundation.TextureCache)
{
// Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS
// is set, which is every run but a differential gate's.
AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
},
static value => value.Dispose());
// Campaign V slice V6l: the RHI arm compiles the two particle pairs
// from SPIR-V, draws instances through the vertex binding the V6l
// contract amendment added, and records into the pass the world scene
// phase publishes on this scope. The raw-GL arm was deleted at V11.
var particleLease = scope.AcquireOptional(
"particle renderer",
() => new ParticleRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
content.ParticleSystem,
foundation.TextureCache,
content.Dats,
foundation.MeshAdapter!,
d.RetailAlphaQueue,
alphaScratchBudgets.ParticleBytes),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.SkyAndParticlesCreated);
IRenderFrameResourceDiagnosticsSource? resourceDiagnostics =
d.Options.UiProbeDump
&& dispatcherLease.Resource is { } diagnosticDispatcher
&& envCellLease.Resource is { } diagnosticEnvCells
&& particleLease.Resource is { } diagnosticParticles
&& portalDepthLease.Resource is { } diagnosticPortalDepth
? new RuntimeRenderFrameResourceDiagnosticsSource(
content.ParticleSystem,
content.ParticleSink,
diagnosticDispatcher,
diagnosticEnvCells,
diagnosticParticles,
interaction.RetainedUi?.Host.TextRenderer,
diagnosticPortalDepth,
clipFrameLease.Resource,
foundation.Terrain!,
foundation.SceneLighting!,
foundation.MeshAdapter!,
foundation.TextureCache,
content.PreparedAssets)
: null;
var frameDiagnostics = new RenderFrameDiagnosticsController(
new RuntimeRenderFrameTitleFactsSource(
worldState,
d.AnimatedEntities,
d.WorldTime),
new SilkRenderFrameTitleSink(d.Window),
d.RenderDiagnosticLog,
d.Options.UiProbeDump,
resourceDiagnostics);
if (d.DevFrameDiagnostics is { } devFrameDiagnostics)
{
bindings.Adopt(
"developer frame diagnostics",
devFrameDiagnostics.BindOwned(frameDiagnostics));
}
bindings.Adopt(
"retained-UI frame diagnostics",
d.UiFrameDiagnostics.BindOwned(frameDiagnostics));
Fault(LivePresentationCompositionPoint.DiagnosticsBound);
var bindingsLease = scope.Own(
"live-presentation runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
var result = new LivePresentationResult(
componentLifecycle,
motionRuntime,
parentAcceptance,
entitySpawnAdapter,
entityScriptActivator,
staticAnimationScheduler,
worldTransit,
worldAvailability,
worldState,
renderSceneShadow,
liveEntities,
placementProjection,
localPlayerShadowSynchronizer,
projectileController,
projectionWithdrawal,
lightsLease.Resource,
animationScheduler,
animationPresenter,
equippedLease.Resource,
entityEffects,
presentationLease.Resource,
dispatcherLease.Resource,
selectionScene,
selectionQuery,
selectionInteractions,
retainedGameplayLease?.Resource,
paperdollLease?.Resource,
paperdollPresenter,
creatureAppraisalLease?.Resource,
creatureAppraisalPresenter,
chargenPreviewLease?.Resource,
chargenPreviewController,
summaryPreviewLease?.Resource,
summaryPreviewController,
envCellFrustum,
envCellLease.Resource,
landblockPipeline,
clipFrameLease.Resource,
portalDepthLease.Resource,
skyLease.Resource,
particleLease.Resource,
frameDiagnostics,
bindings,
landblockLoaded);
foundation.Residency.RegisterDomainSource(
new DelegateResidencyDomainSource(
ResidencyDomain.AlphaScratch,
() => new ResidencyDomainSnapshot(
ResidencyDomain.AlphaScratch,
EntryCount: 3,
OwnerCount: 3,
Charges: new ResidencyCharges(
ScratchBytes: checked(
d.RetailAlphaQueue.RetainedScratchBytes
+ (dispatcherLease.Resource?.RetainedAlphaScratchBytes ?? 0)
+ (particleLease.Resource?.RetainedAlphaScratchBytes ?? 0))),
BudgetBytes: alphaScratchBudgets.TotalBytes)));
_publication.PublishLivePresentation(result);
liveRuntimeLease.Transfer();
renderSceneShadowLease?.Transfer();
lightsLease.Transfer();
equippedLease.Transfer();
presentationLease.Transfer();
dispatcherLease.Transfer();
retainedGameplayLease?.Transfer();
paperdollLease?.Transfer();
creatureAppraisalLease?.Transfer();
envCellLease.Transfer();
clipFrameLease.Transfer();
portalDepthLease.Transfer();
portalTunnelLease?.Transfer();
spewBoxLease?.Transfer();
skyLease.Transfer();
particleLease.Transfer();
bindingsLease.Transfer();
Fault(LivePresentationCompositionPoint.ResultPublished);
scope.Complete();
return result;
}
private void Fault(LivePresentationCompositionPoint point) =>
_faultInjection?.Invoke(point);
private sealed class NullAnimationLoader : IAnimationLoader
{
public static NullAnimationLoader Instance { get; } = new();
public Animation? LoadAnimation(uint id) => null;
}
private sealed class DeferredSelectionInteractionSource
{
public SelectionInteractionController? Current { get; private set; }
public void Bind(SelectionInteractionController value)
{
ArgumentNullException.ThrowIfNull(value);
if (Current is not null)
{
throw new InvalidOperationException(
"Live motion selection interactions are already bound.");
}
Current = value;
}
}
}