fix(render): integrate reviewed equipped geometry publication and ledger (#480)

This commit is contained in:
Erik 2026-09-05 12:27:48 +02:00
parent f545bc9262
commit 4b9242d00f
13 changed files with 712 additions and 19 deletions

View file

@ -1120,7 +1120,12 @@ and physics cross-cell membership one canonical owner:**
registration/move transaction and retains it both as collision rows
(`GetOwnerCells`) and per-cell `RetailPartEntry` render rows
(`GetRetailPartEntriesInCell`) — one flood drives both, never two
independent ones. `WalkProductionWorldData` no longer reconstructs
independent ones. The equipped-child controller publishes every DAT-resolved
effective Setup part (after `AnimPartChanged`) through the same strict
prepared-collision publisher used by ordinary live hydration before it calls
`AttachChild`; children still inherit the parent's CELLARRAY and acquire only
render part-entry rows, never an independent collision registration or flood.
`WalkProductionWorldData` no longer reconstructs
membership from scene records, prepared visual AABBs, or per-record
aggregate shadow-owner cells: `GetCellStatics`/`GetCellDynamics`/
`GetOutdoorStatics`/`GetOutdoorDynamics` are a borrowed, on-demand view over

View file

@ -726,11 +726,15 @@ this ledger, unchanged five reviewed blobs, campaign73de7403c. Fresh integrated
Release0W0E/314GPU PASS; native r2 exits0/gracefully with zero validation
hazards.477-integrated-validation-r2.md preserves PNG FAIL/limited PASS;
original AMD causation/ordinary soak and G4 remain open.
#476 separately dispatched to James at6347e0f82; #480 returned eight files,
agent92/92 tests, pending lead verification/reviews. No new client run. Lead verified
#480's retail/paired-binary child publication and contracted the separate
shared prepared-cache fix in480-equipped-render-publication-contract.md;
Rawls implements it in fresh s5-480-openai-impl at9556c1748.
#480 lead verification complete: two actual consumer-row mutations FAIL as
intended, exact restoration, Release0W0E/92PASS. Wegener retail/behavior1 and
James production/lifetime2 both PASS, no findings; one prior test-only F1.
Reviewed scratch e58f3cc2c integrates with this ledger; architecture preserves
#477.480-lead-verification.md records proof; connected hand-item PNG owed.
#476 returned twelve files; lead independent two mutations, exact restoration,
Release0W0E/175PASS.476-lead-verification.md records proof; Rawls review1 in
flight, production/lifetime2 follows. Capture fix not integrated yet. No new
client run; both clients absent and ACE9000 PID13340 up at latest preflight.
Rynthid Recall6150→3032001C corroborates owner generation4; historical route
labels are not location evidence (479-rynthid-location-evidence.md). #479
transform cause remains unproven. No change to G4 FAIL or the open obligations.

View file

@ -0,0 +1,67 @@
# #476 — completed-frame capture, lead verification
2026-09-05. Frozen twelve-file scratch s5-476-openai-impl at
6347e0f8269e201340b0c7a6b826ad4cfc21f43e, including new Vulkan validity tests.
Pre-integration proof only. Root read every production/test diff and actual
composition, failure paths, capture controller and Vulkan callers.
Same optional screenshot adapter moves from the still-open private renderer to
the outer orchestrator: drawing, measurement End, exactly one GPU End, capture,
CPU diagnostics, then post diagnostics. ScreenshotCaptured is replaced with the
actual post-close result; other immutable private/portal facts remain those of
the drawn frame. Begin failure/zero area do not close; rendering failure retains
recovery-before-close and aggregate ordering. End failure skips capture; later
capture/diagnostic failure cannot recover or close an already closed frame.
The actual Vulkan owner clears capture validity at Begin and allocation/resize,
records copy presence after CmdCopyImageToBuffer, and advertises it only after
checked QueueSubmit2. Readback rejects missing submitted copy before the existing
DeviceWaitIdle/read. Off remains opt-in with no new waits, copies or allocations.
The unchanged bring-up using(frame) closes before its capture at
VulkanBringUpHost.cs:180-207; the main fix adds no warmup or discarded first PNG.
## Independent lead gates
Both clients absent before every command; no graphical launch. Lead Release
build 0 warnings/0 errors (final3.40s), combined focused/affected lane175/175,
zero skips. Tests include real orchestrator + private renderer + real screenshot
controller, fake GPU pixels published only by End, startup and actual resize
RGBA/orientation, pending requests, zero-area/resume and failure boundaries.
Actual production validity state is exercised with source guards binding its
real Begin/copy/submit/resize/read callers; these tests do NOT execute native GPU
commands. Root also inspected implementer's literal App7096/7096 TRX and hash,
not a root rerun or complete-solution test claim.
Two root actual-production mutations, separately restored with exact raw hashes:
| Mutation | Intended failure | Root TRX under artifacts/476-root |
|---|---|---|
| Move capture before End | first expected RGBA(1,2,3,4), actual(238,238,238,238) | m1/root-476-m1.trx |
| Mark RecordCopy submitted-valid immediately | Assert.False sees True before submission | m2/root-476-m2.trx |
TRX SHA256 respectively:
641B33AE025E448C364C59ADFFD1EB1548C74C8B5D508B688BD996BEE934F735;
17D37A3764A002F10AD7D11662FE3DE8DB6EF2CEA3618D1B2D1ACF31FB9AB607.
Final artifacts/476-root/final/root-476-final175.trx:
664487A18F44115D5FE7D1CD4417FFDD1479B09E6C4ECA5351187DADC3C0711B.
Implementer artifacts/476-round0/app-hermetic-final/476-app-hermetic-final.trx:
4DE8F9A1C77BA0BB93F7C018D358FCD6928A362962AD005D868809C5867C7FE7.
Its filter excludes InstalledDat, Linux, Manual, Timing, Live, PreparedPackage.
Initial fresh-scratch no-restore solution build failed42 NETSDK1004 missing
assets; subsequent restore-enabled full57-project Release build passed0W0E.
Root verified all twelve returned raw hashes and git diff --check. Key hashes:
- Orchestrator:9C15760F12D8732347988017E5708CB4DD2724B6858578F2F5C2A8891912201C
- Device:493E446C42B60ABBE473460F08C97BC3A152F9EA222F2ED1C8071E8ABBA6D159
- Resources:2844C34BB39FE648C9CCE1C421ED3BCEA330FB00C2182AA45899F8BC8927AD55
- Validity tests:FB7E47486BAB58FB3E3DDF5D0B66163057C1E13A0FF468C8CF970D4CC6D1485E
Named retail + paired-executable screenshot-boundary proof remains in
476-capture-lifetime-diagnosis.md. Vulkan retained acquired-image copying is a
modern adaptation; no new AC algorithm or intentional deviation is introduced.
Rawls independent behavior/retail review1 in flight; sequential production/
lifetime review2 follows. No integration or graphical freshness PASS yet.
After review, merge capture-only portions without replacing #477's synchronization
changes, rebuild, then preserve the first PNG and two real distinct-size captures.
All visual results remain provisional until the owner looks; G4 still FAIL.

View file

@ -99,3 +99,38 @@ updates the ledger. Coordinate graphics only after #477 validation is clean:
verify equipped hand objects in a connected client, preserve PNG paths and
label visual PASS provisional until owner inspection. This chunk cannot
close the overall failed owner gate or authorize a main merge.
## Implementation outcome (OpenAI round 0, 2026-09-05)
Implemented in the dedicated uncommitted scratch for independent review.
`EquippedChildRenderController` now requires a GfxObj publication delegate and
invokes it for each non-null effective part while building availability,
before projection materialization or `AttachChild`. `LivePresentationComposition`
binds that dependency to `content.CollisionAssets.CacheGfxObj`, the same
composition-owned strict publisher ordinary live hydration receives.
The focused controller witness drives the real Position-null `OnSpawn` route
from a cold `PhysicsDataCache` through a recording prepared source, a registered
parent CELLARRAY, `ShadowObjectRegistry` child rows, the live render journal,
`ArchRenderScene`, and `WalkProductionWorldData`. It covers a visual-only part,
an effective `AnimPartChanged` replacement, an absent-DAT part, repeat appearance
without duplicate rows or prepared rereads, strict missing-prepared failure
before projection/readiness, and real withdrawal preserving the parent's rows.
An IL ownership test retains the production composition binding. No deviation
row is created and no graphical acceptance is claimed here.
## Fix round 1 outcome (OpenAI, 2026-09-05)
The cold effective-part witness now reaches the actual inherited CELLARRAY
part rows and `WalkProductionWorldData` consumer before checking prepared-read
and cache diagnostics; every original assertion remains. Removing the real
publication call fails the row count with expected 2 / actual 0. Publishing
under original Setup ids instead of effective replacement ids fails the same
consumer with expected 2 / actual 1. Both production mutations were restored
to `EquippedChildRenderController.cs` SHA-256
`5452088FB8980A5571B9007E73BE59A2FA9624659B1C146C9758D90680BCAD5E`.
The final Release solution build completed with zero warnings and errors. The
same six-class affected lane passed 92/92 with no skips. Evidence is under
`artifacts/480-fix1-openai/`; no production source changed in this round and
no graphical result is claimed.

View file

@ -0,0 +1,76 @@
# #480 — equipped-part publication, lead verification
2026-09-05. Eight-file candidate at base9556c17480d042e2353de3dfddc70fb51a88c60e,
reviewed and committed by the lead as e58f3cc2c11cd4b5ee7c1da6725f45d5d1623168.
One test-only F1 corrected the order of consumer versus diagnostic assertions;
it did not change the production repair. No graphical item acceptance yet.
Lead read all eight diffs and the actual controller/composition, strict
publisher, cache, inherited membership, Arch scene/walk and withdrawal paths.
Effective AnimPartChanged IDs are finalized before every DAT-present part
passes through content.CollisionAssets.CacheGfxObj, before materialization and
AttachChild. Missing DAT stays unavailable; missing prepared content fails
before new projection/readiness/membership. Partial immutable cache population
before a later strict failure is not a transactional rollback guarantee.
Children still inherit parent CELLARRAY rows, with no independent collision
registration/flood. Pose, authority, detach and attachment policy are unchanged;
the existing ignored AttachChild Boolean is outside this bounded repair.
The real OnSpawn witness uses a cold strict prepared cache, real publication,
real child rows, live journal/Arch scene and WalkProductionWorldData. It checks
effective replacement identities, visual-only geometry, repeated appearance
without rereads/duplicate rows, strict failure and unparent withdrawal while
preserving parent rows. Fixture no-op delegates are not production fallbacks.
## Independent execution and evidence
Both AcDream.App and acclient absent before every build/test; no graphical
launch. Lead Release build after restoration: 0 warnings/0 errors (3.47s).
Final six-class affected lane: 92 passed, zero failures/skips. Classes:
EquippedChildProjectionWithdrawalTests, LivePresentationCompositionTests,
LiveEntityNetworkOnPositionCollapseMatrixTests,
LiveEntityNetworkRemoteTeleportPresentationTests, LiveCollisionAssetPublisherTests,
WalkProductionWorldDataTests. Command: dotnet test
tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release --no-build
--no-restore --nologo, FullyQualifiedName OR filter for those six classes.
Two independent actual-production mutations, each restored, failed the intended
consumer assertion rather than compilation, setup or prepared-read diagnostics:
| Mutation | Actual inherited child rows | Root TRX under artifacts/480-root |
|---|---|---|
| Remove _publishGfxObj call | expected2, actual0 | m1/root-480-m1.trx |
| Publish original Setup IDs instead of effective IDs | expected2, actual1 | m2/root-480-m2.trx |
TRX SHA256 respectively:
CB885176B17C72B5909BFD15626FC6AE4AB343A5CC57F455A2F9C909E252BB8B;
1AF21E2F1BA8723E14097A9B3BD5AA7AD5B6BA0C6C3F012B0BAADD10E8CDF70F.
Final artifacts/480-root/final/root-480-final92.trx:
C9EA5D0D473A1DFC63860F46E0179263DA4C1373E51338056E96401F1E8D8C06.
Lead read actual XML ErrorInfo/counters. These are managed tests, not GPU/PNG proof.
Restoration was checked by raw SHA256. apply_patch restored the code; its
mutation of one original CRLF call line required a newline-only mechanical
restoration, guarded by the expected full-file hash in memory before writing.
The final rebuild/test above followed that exact restoration. Final hashes:
- Controller:5452088FB8980A5571B9007E73BE59A2FA9624659B1C146C9758D90680BCAD5E
- Composition:F501EEA2928B0503F10178BDBC37E1CD5F8993DE06230DD258913FD382831DF0
- Consumer test:32824B6151B01E4A7FDDF41EC7BD2C7E0A2AFEC34CF1F622C466C1A818B56E4B
## Retail and review boundary
Lead independently verified named CPhysicsObj::add_shadows_to_cells00514AE0
and CPartArray::AddPartsShadow00517E40 against the paired executable SHA256
006FFEADC5D679C871497112A5BD1F87714D0E273E2166BAE5052DDE369297B1.
Range hashes/call bytes are in478-480-readonly-diagnosis.md. Retail recursively
adds every non-null child part to the same CELLARRAY; the shared prepared-cache
publisher is this client's modern implementation, not a claim about retail APIs.
Wegener independent retail/behavior lens1 PASS; James production/lifetime
lens2 PASS. Both read actual production/consumer paths, mutation TRX and hashes;
neither reran tests or claimed graphical acceptance. No findings in either
review, one prior evidence-only F1. Lead integrates the reviewed source/test/
contract blobs; architecture merges only the equipped-child paragraph and keeps
the already-landed #477 GPU paragraph. No new intentional deviation.
Fresh integrated checks and a connected hand-item PNG remain next. G4 FAIL.

View file

@ -7,12 +7,12 @@ timestamp pairing and PerformanceTools summarizer. No instrumentation change.
After pending repairs integrate, use the same green Release, session-v10,
High/near4/far25/MSAA4/aniso16/A2C1/maxCompletions4 and1600x900 profile.
Before launch both clients absent/ACE9000 up; pause agent builds/tests. One
run-selfgate.ps1 invocation with -Label481-entry-profile (with the normal
space after the switch), -Route tools/overhaul-selfgate/route-481-entry-profile.txt,
-Session logs/s5-c5-session-v10.json, -TimeoutSeconds240. No client yet.
run-selfgate.ps1 invocation with -Label 481-entry-profile,
-Route tools/overhaul-selfgate/route-481-entry-profile.txt,
-Session logs/s5-c5-session-v10.json, -TimeoutSeconds 240. No client yet.
Fresh process environment: ACDREAM_DEVTOOLS=0; clear VK_INSTANCE_LAYERS,
VK_LAYER_ENABLES,VK_LAYER_VALIDATE_SYNC,ACDREAM_WB_DIAG; UI_PROBE_DUMP=0;
VK_LAYER_ENABLES,VK_LAYER_VALIDATE_SYNC,ACDREAM_WB_DIAG; ACDREAM_UI_PROBE_DUMP=0;
ACDREAM_FRAME_PROF=1, ACDREAM_FRAME_HISTORY=<unique absolute CSV path>.
Create its artifact directory without overwriting any earlier run. No
validation, screenshots, pack switches, resize or movement during samples.

View file

@ -4745,3 +4745,23 @@ FirstPNGstillFAIL and next retains portal caption unlike its checkpoint;
#477 ordinary soak/AMD attribution and full G4 remain open. #476/480 builds
released after exit. #480 F1 test-only consumer-assert ordering contracted
at17212c810; no production defect found in returned patch.
## 52. Hand-item repair accepted; capture independently verified
#480 F1 closed with real consumer assertions: root removed publication and
restored original IDs separately, observed expected2/actual0 and expected2/
actual1 inherited rows, restored exact hashes and passed Release0W0E/92tests.
Wegener retail/behavior1 PASS and James production/lifetime2 PASS, no findings.
Root committed reviewed scratch e58f3cc2c11cd4b5ee7c1da6725f45d5d1623168 and
integrates with this ledger, preserving the #477 architecture paragraph.
480-lead-verification.md has commands/TRX/hashes and retail boundaries.
No new intentional deviation; connected hands PNG remains owed.
#476 twelve-file return independently read/tested: early-capture mutation
fails exact new-frame pixels; premature-validity mutation fails before submit.
Exact restoration, Release0W0E and175/175 root tests; implementer literal
App7096 TRX inspected, not root rerun.476-lead-verification.md records proof.
Rawls behavior/retail1 in flight; sequential production/lifetime2 follows.
Capture not integrated/graphically accepted. Root will preserve #477's separate
Vulkan changes during integration. Both clients absent; ACE9000 PID13340 up.
No new graphical run. All §49 obligations and G4 FAIL remain current.

View file

@ -624,7 +624,8 @@ internal sealed class LivePresentationCompositionPhase
projectionVersion,
d.PlayerIdentity.ServerGuid),
d.PhysicsEngine.ShadowObjects,
d.PhysicsDataCache),
d.PhysicsDataCache,
content.CollisionAssets.CacheGfxObj),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.CorePresentationCreated);

View file

@ -56,6 +56,16 @@ public sealed class EquippedChildRenderController : IDisposable
/// </summary>
private readonly PhysicsDataCache _physicsData;
/// <summary>
/// #480: the SAME strict prepared-collision publisher ordinary live
/// hydration uses. An attached child resolves its own effective visual
/// part ids (including AnimPartChanged replacements), so those ids must
/// enter the shared prepared cache before the child publishes retail
/// part-entry membership. This is a required composition dependency,
/// never an optional graph-only fallback.
/// </summary>
private readonly Action<uint, GfxObj> _publishGfxObj;
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
/// <summary>Raised after the attached projection is fully registered.</summary>
@ -122,7 +132,8 @@ public sealed class EquippedChildRenderController : IDisposable
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
withdrawProjection,
ShadowObjectRegistry shadows,
PhysicsDataCache physicsData)
PhysicsDataCache physicsData,
Action<uint, GfxObj> publishGfxObj)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
@ -134,6 +145,8 @@ public sealed class EquippedChildRenderController : IDisposable
?? throw new ArgumentNullException(nameof(withdrawProjection));
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
_physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData));
_publishGfxObj = publishGfxObj
?? throw new ArgumentNullException(nameof(publishGfxObj));
_parentOfAttached = static child => child.ParentRecord.ProjectionKey;
_tickAttached = TickChild;
_reconcileAttached = ReconcileChild;
@ -1178,7 +1191,18 @@ public sealed class EquippedChildRenderController : IDisposable
{
var available = new bool[template.Count];
for (int i = 0; i < template.Count; i++)
available[i] = _dats.Get<GfxObj>(template[i].GfxObjId) is not null;
{
uint gfxObjId = template[i].GfxObjId;
GfxObj? gfxObj = _dats.Get<GfxObj>(gfxObjId);
if (gfxObj is null)
continue;
// The effective identity is already final here. Publish its
// strict prepared payload before any attached projection or
// retail part-entry membership can become observable.
_publishGfxObj(gfxObjId, gfxObj);
available[i] = true;
}
return available;
}

View file

@ -154,6 +154,20 @@ public sealed class LivePresentationCompositionTests
field => field.FieldType == typeof(GameWindow));
}
[Fact]
public void EquippedChildConstructionBindsTheCompositionCollisionPublisher()
{
IReadOnlyList<CompiledCall> calls =
CompiledCallGraph.ReadOwned(typeof(LivePresentationCompositionPhase));
Assert.Contains(calls, call =>
call.Target.DeclaringType == typeof(EquippedChildRenderController)
&& call.Target.IsConstructor);
Assert.Contains(calls, call =>
call.Target.DeclaringType == typeof(LiveCollisionAssetPublisher)
&& call.Target.Name == nameof(LiveCollisionAssetPublisher.CacheGfxObj));
}
private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create(
new GpuWorldState(),
new DelegateLiveEntityResourceLifecycle(static _ => { }, static _ => { }));

View file

@ -1390,7 +1390,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
ExactProjectionWithdrawalDisposition.Superseded,
null),
Shadows,
new PhysicsDataCache()),
new PhysicsDataCache(),
static (_, _) => { }),
new ProjectileController(Runtime),
animatedEntities,
new RemoteMovementObservationTracker(),

View file

@ -655,7 +655,8 @@ public sealed class LiveEntityNetworkRemoteTeleportPresentationTests
ExactProjectionWithdrawalDisposition.Superseded,
null),
Shadows,
new PhysicsDataCache()),
new PhysicsDataCache(),
static (_, _) => { }),
new ProjectileController(Runtime),
animatedEntities,
new RemoteMovementObservationTracker(),

View file

@ -1,11 +1,17 @@
using System.Collections;
using System.Numerics;
using System.Reflection;
using System.Collections.Immutable;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Content.Pak;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
@ -1654,6 +1660,320 @@ public sealed class EquippedChildProjectionWithdrawalTests
}
}
[Fact]
public void OnSpawn_ColdEffectivePartsPublishPreparedMembershipIntoWalkAndWithdrawCleanly()
{
const uint basePart = 0x0100E001u;
const uint originalPart = 0x0100E002u;
const uint replacementPart = 0x0100E003u;
const uint absentDatPart = 0x0100E004u;
const uint parentGuid = 0x70000E00u;
const uint childGuid = 0x70000E01u;
Setup setup = AttachedSetup(basePart, originalPart, absentDatPart);
var gfxObjs = new Dictionary<uint, GfxObj>
{
[basePart] = DrawableGfx(0x0800E001u),
[originalPart] = DrawableGfx(0x0800E002u),
[replacementPart] = DrawableGfx(0x0800E003u),
};
var prepared = new RecordingPreparedCollisionSource(
basePart,
originalPart,
replacementPart);
ControllerFixture? fixture = null;
fixture = new ControllerFixture(
(record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new ExactProjectionWithdrawalOutcome(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
},
setup,
gfxObjs,
prepared);
using (fixture)
using (var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1)))
{
Assert.Equal(0, fixture.PhysicsData.FlatGfxObjCount);
LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: 1);
fixture.RegisterParentRetailMembership(parent);
Assert.True(fixture.Shadows.TryGetRetailCellArray(
parent.WorldEntity!.Id,
out IReadOnlyList<uint> parentCells));
Assert.NotEmpty(parentCells);
var journal = new RenderProjectionJournal(
RenderSceneGeneration.FromRaw(1));
var projections = new LiveRenderProjectionJournal(
fixture.Live,
journal,
new GpuWorldRenderTraversalOrderSource(fixture.Spatial));
Assert.True(projections.OnEntityReady(
LiveEntityReadyCandidate.Capture(parent)));
int readyCount = 0;
fixture.Controller.EntityReady += candidate =>
{
readyCount++;
Assert.True(projections.OnEntityReady(candidate));
};
fixture.Controller.ProjectionPoseReady +=
projections.OnProjectionPoseReady;
fixture.Controller.ProjectionRemoved +=
projections.OnProjectionRemoved;
fixture.Poses.Publish(
parent.WorldEntity,
Array.Empty<Matrix4x4>());
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
childGuid,
generation: 1) with
{
Position = null,
ParentGuid = parentGuid,
ParentLocation = 0,
PlacementId = 0,
AnimPartChanges =
[
new CreateObject.AnimPartChange(1, replacementPart),
],
};
childSpawn = childSpawn with
{
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parentGuid, 0u),
AnimationFrame = 0,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
Assert.Equal(1, readyCount);
Assert.True(fixture.Live.TryGetRecord(
childGuid,
out LiveEntityRecord child));
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
Assert.NotNull(child.WorldEntity);
Assert.Equal(
[basePart, replacementPart, absentDatPart],
child.WorldEntity.MeshRefs.Select(static part => part.GfxObjId));
uint childLocalId = child.WorldEntity.Id;
Assert.True(fixture.Shadows.TryGetRetailCellArray(
childLocalId,
out IReadOnlyList<uint> childCells));
Assert.Equal(parentCells, childCells);
Assert.Empty(fixture.Shadows.GetOwnerCells(childLocalId));
foreach (uint cellId in childCells)
{
RetailPartEntry[] childRows = fixture.Shadows
.GetRetailPartEntriesInCell(cellId)
.Where(row => row.EntityId == childLocalId)
.ToArray();
Assert.Equal(2, childRows.Length);
Assert.Equal((0, basePart),
(childRows[0].PartIndex, childRows[0].GfxObjId));
Assert.Equal((1, replacementPart),
(childRows[1].PartIndex, childRows[1].GfxObjId));
}
journal.DrainTo(scene);
var walk = new WalkProductionWorldData(
new WalkBuildingRegistry(),
fixture.Shadows);
walk.BeginFrame(
scene.OpenQuery(),
ControllerFixture.Landblock,
renderCenterLbX: 1,
renderCenterLbY: 1);
foreach (uint cellId in childCells)
{
RenderProjectionRecord projected = Assert.Single(
walk.GetCellDynamics(cellId).Records,
record => record.Source.LocalEntityId == childLocalId);
Assert.Equal(
RenderProjectionClass.EquippedChild,
projected.ProjectionClass);
}
Assert.Equal(0, walk.UnregisteredRenderMembershipCount);
Assert.Equal([basePart, replacementPart], prepared.GfxReads);
Assert.NotNull(fixture.PhysicsData.GetFlatGfxObj(basePart));
Assert.NotNull(fixture.PhysicsData.GetFlatGfxObj(replacementPart));
Assert.Null(fixture.PhysicsData.GetFlatGfxObj(originalPart));
Assert.Null(fixture.PhysicsData.GetFlatGfxObj(absentDatPart));
Assert.True(fixture.Live.TryApplyObjDesc(
new ObjDescEvent.Parsed(
childGuid,
new CreateObject.ModelData(
BasePaletteId: null,
Array.Empty<CreateObject.SubPaletteSwap>(),
Array.Empty<CreateObject.TextureChange>(),
[new CreateObject.AnimPartChange(1, replacementPart)]),
InstanceSequence: 1,
ObjDescSequence: 1),
out _));
Assert.True(fixture.Controller.TryApplyAttachedAppearance(
child,
child.ObjDescAuthorityVersion));
Assert.Equal([basePart, replacementPart], prepared.GfxReads);
foreach (uint cellId in childCells)
{
RetailPartEntry[] childRows = fixture.Shadows
.GetRetailPartEntriesInCell(cellId)
.Where(row => row.EntityId == childLocalId)
.ToArray();
Assert.Equal(2, childRows.Length);
Assert.Equal([basePart, replacementPart],
childRows.Select(static row => row.GfxObjId));
}
Assert.Equal(
ChildUnparentDisposition.Completed,
fixture.Controller.OnChildBecameUnparented(childGuid));
Assert.False(fixture.Shadows.TryGetRetailCellArray(
childLocalId,
out _));
foreach (uint cellId in parentCells)
{
IReadOnlyList<RetailPartEntry> rows =
fixture.Shadows.GetRetailPartEntriesInCell(cellId);
Assert.DoesNotContain(rows, row => row.EntityId == childLocalId);
Assert.Contains(rows, row =>
row.EntityId == parent.WorldEntity.Id);
}
}
}
[Fact]
public void OnSpawn_MissingPreparedEffectivePartFailsBeforeProjectionOrMembership()
{
const uint part = 0x0100E010u;
const uint parentGuid = 0x70000E10u;
const uint childGuid = 0x70000E11u;
Setup setup = AttachedSetup(part);
var gfxObjs = new Dictionary<uint, GfxObj>
{
[part] = DrawableGfx(0x0800E010u),
};
var prepared = new RecordingPreparedCollisionSource(part);
prepared.MissingIds.Add(part);
using var fixture = new ControllerFixture(
static (_, _, _) => new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Superseded,
Failure: null),
setup,
gfxObjs,
prepared);
LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: 1);
fixture.RegisterParentRetailMembership(parent);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
int readyCount = 0;
fixture.Controller.EntityReady += _ => readyCount++;
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
childGuid,
generation: 1) with
{
Position = null,
ParentGuid = parentGuid,
ParentLocation = 0,
PlacementId = 0,
};
childSpawn = childSpawn with
{
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parentGuid, 0u),
AnimationFrame = 0,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
InvalidDataException error = Assert.Throws<InvalidDataException>(
() => fixture.Controller.OnSpawn(childSpawn));
Assert.Contains("Missing", error.Message, StringComparison.Ordinal);
Assert.Equal([part], prepared.GfxReads);
Assert.Equal(0, readyCount);
Assert.False(fixture.Live.TryGetRecord(childGuid, out _));
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.True(fixture.Live.TryGetCanonical(
childGuid,
out RuntimeEntityRecord childCanonical));
Assert.NotNull(childCanonical.LocalEntityId);
Assert.False(fixture.Shadows.TryGetRetailCellArray(
childCanonical.LocalEntityId!.Value,
out _));
Assert.Equal(0, fixture.PhysicsData.GfxObjCount);
Assert.Equal(0, fixture.PhysicsData.FlatGfxObjCount);
}
private static Setup AttachedSetup(params uint[] parts)
{
var setup = new Setup
{
HoldingLocations =
{
[(ParentLocation)0] = new LocationType
{
PartId = -1,
Frame = new Frame { Orientation = Quaternion.Identity },
},
},
};
for (int i = 0; i < parts.Length; i++)
setup.Parts.Add(parts[i]);
return setup;
}
private static GfxObj DrawableGfx(uint surfaceId) => new()
{
Surfaces = { surfaceId },
VertexArray = new VertexArray
{
Vertices =
{
[0] = new SWVertex
{
Origin = Vector3.Zero,
Normal = Vector3.UnitZ,
},
[1] = new SWVertex
{
Origin = Vector3.UnitX,
Normal = Vector3.UnitZ,
},
[2] = new SWVertex
{
Origin = Vector3.UnitY,
Normal = Vector3.UnitZ,
},
},
},
Polygons =
{
[0] = new Polygon
{
PosSurface = 0,
NegSurface = -1,
VertexIds = { 0, 1, 2 },
},
},
};
private static LiveEntityRecord ChildRecord() =>
LiveEntityTestFixture.CreateExactProjectionRecord(
new WorldSession.EntitySpawn(
@ -1672,9 +1992,83 @@ public sealed class EquippedChildProjectionWithdrawalTests
MotionTableId: null,
InstanceSequence: 1));
private sealed class RecordingPreparedCollisionSource(
params uint[] availableIds) : IPreparedCollisionSource
{
private static readonly FlatPhysicsBsp EmptyPhysics = new(
-1,
ImmutableArray<FlatPhysicsBspNode>.Empty,
ImmutableArray<int>.Empty,
FlatPolygonTable.Empty);
private readonly HashSet<uint> _availableIds = [.. availableIds];
internal List<uint> GfxReads { get; } = [];
internal HashSet<uint> MissingIds { get; } = [];
public PreparedAssetPresence ProbeCollision(
PakAssetType type,
uint sourceFileId) =>
type == PakAssetType.GfxObjCollision
&& _availableIds.Contains(sourceFileId)
&& !MissingIds.Contains(sourceFileId)
? PreparedAssetPresence.Available
: PreparedAssetPresence.Missing;
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default)
{
GfxReads.Add(sourceFileId);
if (!_availableIds.Contains(sourceFileId)
|| MissingIds.Contains(sourceFileId))
{
return PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
.Missing;
}
var bounds = new FlatGfxObjVisualBounds(
new Vector3(-0.5f),
new Vector3(0.5f),
Vector3.Zero,
0.8660254f,
new Vector3(0.5f));
return PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
.Loaded(new FlatGfxObjCollisionAsset(
EmptyPhysics,
BoundingSphere: null,
bounds));
}
public PreparedCollisionReadResult<FlatSetupCollision>
ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatSetupCollision>.Missing;
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatCellStructureCollisionAsset>.Missing;
public PreparedCollisionReadResult<FlatEnvCellTopology>
ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatEnvCellTopology>.Missing;
public PreparedCollisionSourceStats CollisionStats => default;
public void Dispose()
{
}
}
private sealed class ControllerFixture : IDisposable
{
private const uint Cell = 0x01010001u;
internal const uint Landblock = 0x0101FFFFu;
private readonly DeferredLiveEntityRuntimeComponentLifecycle _lifecycle = new();
private readonly Setup _setup;
private readonly AcDream.Runtime.Session.RuntimeFirstEntryDriveController _firstEntry;
@ -1724,7 +2118,10 @@ public sealed class EquippedChildProjectionWithdrawalTests
internal ControllerFixture(
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
withdraw)
withdraw,
Setup? setup = null,
IReadOnlyDictionary<uint, GfxObj>? gfxObjs = null,
IPreparedCollisionSource? preparedCollision = null)
{
Spatial.AddLandblock(new LoadedLandblock(
(Cell & 0xFFFF0000u) | 0xFFFFu,
@ -1748,7 +2145,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
_lifecycle,
EntityObjects);
_setup = new Setup
_setup = setup ?? new Setup
{
HoldingLocations =
{
@ -1761,6 +2158,14 @@ public sealed class EquippedChildProjectionWithdrawalTests
};
IDatReaderWriter dat = DispatchProxy.Create<IDatReaderWriter, NullDatProxy>();
((NullDatProxy)(object)dat).Setup = _setup;
((NullDatProxy)(object)dat).GfxObjs = gfxObjs;
Shadows = new ShadowObjectRegistry();
PhysicsData = new PhysicsDataCache();
Action<uint, GfxObj> publishGfxObj = preparedCollision is null
? static (_, _) => { }
: new LiveCollisionAssetPublisher(
PhysicsData,
preparedCollision).CacheGfxObj;
Controller = new EquippedChildRenderController(
dat,
new object(),
@ -1769,8 +2174,9 @@ public sealed class EquippedChildProjectionWithdrawalTests
Poses,
update => Live.TryApplyParent(update, out _),
withdraw,
new ShadowObjectRegistry(),
new PhysicsDataCache());
Shadows,
PhysicsData,
publishGfxObj);
_lifecycle.Bind(new DelegateLiveEntityRuntimeComponentLifecycle(
Controller.OnLogicalTeardown));
}
@ -1795,6 +2201,35 @@ public sealed class EquippedChildProjectionWithdrawalTests
internal EntityEffectPoseRegistry Poses { get; } = new();
internal LiveEntityRuntime Live { get; }
internal EquippedChildRenderController Controller { get; }
internal ShadowObjectRegistry Shadows { get; }
internal PhysicsDataCache PhysicsData { get; }
internal void RegisterParentRetailMembership(LiveEntityRecord parent)
{
var sphere = new FlatCollisionSphere(Vector3.Zero, 1f);
IReadOnlyList<ShadowShape> parts =
[
ShadowShape.Bsp(
0x0100FFFFu,
Vector3.Zero,
Quaternion.Identity,
scale: 1f,
ShadowPartGeometry.Create(sphere, visualBounds: null)),
];
Shadows.RegisterMultiPart(
parent.WorldEntity!.Id,
parent.WorldEntity.Position,
parent.WorldEntity.Rotation,
parts,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: Landblock,
seedCellId: Cell,
isStatic: false,
partArray: parts);
}
internal LiveEntityRecord Spawn(
uint guid,
@ -2012,6 +2447,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
private class NullDatProxy : DispatchProxy
{
internal Setup? Setup { get; set; }
internal IReadOnlyDictionary<uint, GfxObj>? GfxObjs { get; set; }
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
@ -2020,6 +2456,15 @@ public sealed class EquippedChildProjectionWithdrawalTests
{
return Setup;
}
if (targetMethod?.Name == "Get"
&& targetMethod.ReturnType == typeof(GfxObj)
&& args is [uint id])
{
return GfxObjs is not null
&& GfxObjs.TryGetValue(id, out GfxObj? gfxObj)
? gfxObj
: null;
}
if (targetMethod?.ReturnType == typeof(void))
return null;
if (targetMethod?.ReturnType.IsValueType == true)