fix(render): integrate reviewed equipped geometry publication and ledger (#480)
This commit is contained in:
parent
f545bc9262
commit
4b9242d00f
13 changed files with 712 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
67
docs/research/2026-09-01-overhaul/476-lead-verification.md
Normal file
67
docs/research/2026-09-01-overhaul/476-lead-verification.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
76
docs/research/2026-09-01-overhaul/480-lead-verification.md
Normal file
76
docs/research/2026-09-01-overhaul/480-lead-verification.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 _ => { }));
|
||||
|
|
|
|||
|
|
@ -1390,7 +1390,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
|
|||
ExactProjectionWithdrawalDisposition.Superseded,
|
||||
null),
|
||||
Shadows,
|
||||
new PhysicsDataCache()),
|
||||
new PhysicsDataCache(),
|
||||
static (_, _) => { }),
|
||||
new ProjectileController(Runtime),
|
||||
animatedEntities,
|
||||
new RemoteMovementObservationTracker(),
|
||||
|
|
|
|||
|
|
@ -655,7 +655,8 @@ public sealed class LiveEntityNetworkRemoteTeleportPresentationTests
|
|||
ExactProjectionWithdrawalDisposition.Superseded,
|
||||
null),
|
||||
Shadows,
|
||||
new PhysicsDataCache()),
|
||||
new PhysicsDataCache(),
|
||||
static (_, _) => { }),
|
||||
new ProjectileController(Runtime),
|
||||
animatedEntities,
|
||||
new RemoteMovementObservationTracker(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue