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>
1575 lines
71 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|