feat(physics): port canonical retail set-position core
This commit is contained in:
parent
6b28ff999c
commit
e84a388e6f
6 changed files with 1952 additions and 21 deletions
|
|
@ -74,6 +74,12 @@ by the DAT-authored portal-space viewport. Recent additions and splits:
|
||||||
AD-47/AD-48 (Vulkan sample/present behavior), AD-50..AD-52 (Campaign N), and
|
AD-47/AD-48 (Vulkan sample/present behavior), AD-50..AD-52 (Campaign N), and
|
||||||
AD-53..AD-55 (Campaign P response-layer findings).
|
AD-53..AD-55 (Campaign P response-layer findings).
|
||||||
|
|
||||||
|
AD-2 clarification (placement Slice 4A, 2026-07-31): Core never creates cells
|
||||||
|
during placement, so retail `DoNotCreateCells` has no differential synchronous
|
||||||
|
loader branch there. Slice 4B must preserve the flag while mapping successful
|
||||||
|
deferred placement to exact-cell, generation-scoped asynchronous admission;
|
||||||
|
the presence of the flag in the immutable request is not claimed as exactness.
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ~~AD-53~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `Transition.CliffSlide` now consumes only `collision_info.last_known_contact_plane.N`, exactly as retail does. The invented `LastWalkablePlane -> LastKnownContactPlane -> UnitZ` fallback chain is gone; invalid/default or parallel data takes retail's degenerate `OK_TS` return. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07) |
|
| ~~AD-53~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `Transition.CliffSlide` now consumes only `collision_info.last_known_contact_plane.N`, exactly as retail does. The invented `LastWalkablePlane -> LastKnownContactPlane -> UnitZ` fallback chain is gone; invalid/default or parallel data takes retail's degenerate `OK_TS` return. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07) |
|
||||||
|
|
@ -126,7 +132,7 @@ AD-53..AD-55 (Campaign P response-layer findings).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Documented approximation (AP) — 87 active rows (AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
|
## 3. Documented approximation (AP) — 87 active rows (AP-1 narrowed 2026-07-31 by placement/streaming Slice 4A — the pure canonical retail `SetPosition` transaction exists, but production routes and lost-cell lifetime remain on the legacy resolver until Slice 4B; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
|
||||||
|
|
||||||
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
||||||
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
||||||
|
|
@ -134,7 +140,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| AP-1 | Snap-path Z settle: validated claims ground on their own walkable polys, but floor-less claims (thresholds, stair lips) fall through to a legacy nearest-in-Z scan over every CellSurface in the landblock; retail settles via `CheckPositionInternal` → `find_valid_position` | `src/AcDream.Core/Physics/PhysicsEngine.cs:614` | `find_valid_position` unported; the **#111** fix narrowed the legacy pick's blast radius (validated claims bypass it) rather than replacing it | A threshold/stair-lip snap can still pick a neighbouring cell's same-height floor by iteration order — wrong cell or Z at login/teleport arrival (the #111 clobber class) | `SetPositionInternal` :283426 → find_valid_position |
|
| AP-1 | **NARROWED 2026-07-31 (placement/streaming Slice 4A).** Core now exposes the pure retail `SetPosition` → `AdjustPosition` → `CheckPositionInternal` → `find_valid_position` transaction, including exact sphere initialization, two distinct placement validators, step-down schedule, force classes, flags/scatter, signed no-slide predicate, error values, and an explicit deferred-residence result. Production zero-delta routes deliberately still use the legacy nearest-in-Z/terrain-lift resolver because its `ResolveResult` cannot represent a successful lost-cell transition. Slice 4B must cut every authoritative route to the canonical packet and install the Runtime lost-cell owner before this row retires. | `src/AcDream.Core/Physics/PhysicsSetPosition.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs`; `src/AcDream.Core/Physics/TransitionTypes.cs`; `tests/AcDream.Core.Tests/Physics/PhysicsSetPositionTests.cs`; `docs/research/2026-07-31-canonical-set-position.md` | Split preserves production behavior while the immutable placement mechanism and its oracle gates land independently | Until 4B, fresh spawn, same-generation refresh, authoritative Position, portal arrival, external teleport, parent detach, pickup release, and world-drop hydration can still run the old approximation and cannot park/wake an exact lost-cell frame | `CPhysicsObj::SetPosition` 0x005160C0; `SetPositionInternal` 0x00515BD0; `AdjustPosition` 0x00511D80; `CheckPositionInternal` 0x00511E90; `CTransition::find_valid_position` 0x0050C310; `find_placement_position` 0x0050C170; `validate_placement_transition` 0x0050ADC0; `validate_placement` 0x0050B210 |
|
||||||
|
|
||||||
| ~~AP-3~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `TransitionalInsert` now returns `OK_TS` immediately for every valid contact plane. Its ordinary StepDown tail is reachable only from invalid contact and retains the retail Contact / `!sphere_path.step_down` / check-cell / ObjectInfo.StepDown gates plus the exact one-versus-two-sphere probe split. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`TransitionalInsert`, `GetStepDownProbePlan`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::transitional_insert` 0x0050B6F0, named-retail pseudo-C pc:273191–273307 |
|
| ~~AP-3~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `TransitionalInsert` now returns `OK_TS` immediately for every valid contact plane. Its ordinary StepDown tail is reachable only from invalid contact and retains the retail Contact / `!sphere_path.step_down` / check-cell / ObjectInfo.StepDown gates plus the exact one-versus-two-sphere probe split. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`TransitionalInsert`, `GetStepDownProbePlan`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::transitional_insert` 0x0050B6F0, named-retail pseudo-C pc:273191–273307 |
|
||||||
| ~~AP-4~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `EdgeSlideAfterStepDownFailed` now evaluates retail Branch 1 (`!OnWalkable || !EdgeSlide` → restore + `OK_TS`) before the steep-contact `CliffSlide` branch. The former compensation is removed. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::edge_slide` 0x0050B3D0, named-retail pseudo-C pc:273001–273090 |
|
| ~~AP-4~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `EdgeSlideAfterStepDownFailed` now evaluates retail Branch 1 (`!OnWalkable || !EdgeSlide` → restore + `OK_TS`) before the steep-contact `CliffSlide` branch. The former compensation is removed. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::edge_slide` 0x0050B3D0, named-retail pseudo-C pc:273001–273090 |
|
||||||
|
|
|
||||||
142
docs/research/2026-07-31-canonical-set-position.md
Normal file
142
docs/research/2026-07-31-canonical-set-position.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# Canonical retail `SetPosition` — placement/streaming Slice 4A
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This note pins the pure physics half of the placement/streaming closeout.
|
||||||
|
Slice 4A lands the retail placement transaction as a separately testable Core
|
||||||
|
mechanism. It deliberately does **not** replace the production snap-only
|
||||||
|
resolver yet: Runtime lost-cell ownership and the complete inbound-route
|
||||||
|
cutover remain Slice 4B. AP-1 and AD-1 therefore remain active until that
|
||||||
|
cutover is complete.
|
||||||
|
|
||||||
|
Named-retail oracle, Sept 2013 EoR:
|
||||||
|
|
||||||
|
- `CPhysicsObj::SetPosition` `0x005160C0`
|
||||||
|
- `CPhysicsObj::SetPositionInternal` `0x00515BD0`
|
||||||
|
- `CPhysicsObj::AdjustPosition` `0x00511D80`
|
||||||
|
- `CPhysicsObj::CheckPositionInternal` `0x00511E90`
|
||||||
|
- `CTransition::find_valid_position` `0x0050C310`
|
||||||
|
- `CTransition::find_placement_position` `0x0050C170`
|
||||||
|
- `CTransition::find_placement_pos` `0x0050BA50`
|
||||||
|
- `CTransition::validate_placement_transition` `0x0050ADC0`
|
||||||
|
- `CTransition::validate_placement` `0x0050B210`
|
||||||
|
- `CPhysicsObj::ForceIntoCell` `0x00515660`
|
||||||
|
- `CPhysicsObj::handle_all_collisions` `0x00514780`
|
||||||
|
|
||||||
|
## Retail transaction
|
||||||
|
|
||||||
|
```text
|
||||||
|
SetPosition(request):
|
||||||
|
transition = makeTransition() // GENERAL_FAILURE if none
|
||||||
|
init_object(transition, object)
|
||||||
|
|
||||||
|
if the PartArray has no spheres:
|
||||||
|
init_sphere(1, dummy center=(0,0,0.1), radius=0.1, scale=1)
|
||||||
|
else:
|
||||||
|
init_sphere(first min(count,2) authored spheres, exact object scale)
|
||||||
|
|
||||||
|
if flags & RANDOM_SCATTER (0x200):
|
||||||
|
return scatter only
|
||||||
|
|
||||||
|
result = SetPositionInternal(request)
|
||||||
|
if result != OK and flags & SCATTER (0x100):
|
||||||
|
return scatter
|
||||||
|
return result
|
||||||
|
|
||||||
|
SetPositionInternal(request):
|
||||||
|
AdjustPosition(request frame, first sphere, noCreate=(flags & 0x20))
|
||||||
|
if no resident cell:
|
||||||
|
store the adjusted authoritative frame and enter lost-cell lifetime
|
||||||
|
return OK
|
||||||
|
|
||||||
|
if the live weenie is Hook, Storage, or Corpse:
|
||||||
|
return ForceIntoCell(resident cell, frame)
|
||||||
|
|
||||||
|
set do_not_load_cells from flag 0x20
|
||||||
|
if !CheckPositionInternal(...):
|
||||||
|
handled = handle_all_collisions(...)
|
||||||
|
return handled ? COLLIDED : NO_VALID_POSITION
|
||||||
|
if transition.curr_cell == null:
|
||||||
|
return NO_CELL
|
||||||
|
commit the complete transition
|
||||||
|
return OK
|
||||||
|
```
|
||||||
|
|
||||||
|
`AdjustPosition` branches on the claimed cell shape. A direct outdoor claim
|
||||||
|
runs pure `LandDefs::adjust_to_outside` normalization before visible-cell
|
||||||
|
lookup. An indoor claim first resolves the visible cell and child; only a
|
||||||
|
resident indoor cell marked `seen_outside` falls back to outdoor
|
||||||
|
normalization. An absent indoor cell (including the `0xFFFF` sentinel) remains
|
||||||
|
the exact claimed cell/frame. A map-edge outdoor normalization failure stores
|
||||||
|
cell zero with the otherwise unchanged frame. The old `max(terrainZ, z)` lift
|
||||||
|
and nearest-in-Z scan do not occur in this canonical mechanism.
|
||||||
|
|
||||||
|
`CheckPositionInternal` calls the complete placement transition. Without the
|
||||||
|
slide flag, retail accepts the result only when signed
|
||||||
|
`resolvedX-requestedX <= 0.0500000007`, the same signed Y condition holds, and
|
||||||
|
the cell is unchanged. Z is not part of that predicate. The resolved origin is
|
||||||
|
accepted while the requested orientation remains intact.
|
||||||
|
|
||||||
|
## Two validators, not one
|
||||||
|
|
||||||
|
The similarly named retail helpers have different contracts and stay separate
|
||||||
|
in the port:
|
||||||
|
|
||||||
|
- `validate_placement_transition` is the inner `find_placement_pos` validator.
|
||||||
|
Any non-OK state from `COLLIDED` through `SLID`, when sliding is permitted,
|
||||||
|
resets `COLLISIONINFO`; it never retries placement.
|
||||||
|
- `validate_placement` is the outer initial/final validator. Only `ADJUSTED`
|
||||||
|
or `SLID`, and only while its retry argument is true, performs one
|
||||||
|
`placement_insert`; `COLLIDED` neither resets nor retries.
|
||||||
|
|
||||||
|
Step-down is disabled only for missiles. For fewer than two spheres retail
|
||||||
|
first clamps the requested height to half the radius when the sphere diameter
|
||||||
|
is less than or equal to that height. It then performs one full probe when the
|
||||||
|
diameter is greater than the resulting height, otherwise two half probes.
|
||||||
|
Equality belongs to the two-half-probe branch.
|
||||||
|
|
||||||
|
## Modern seam
|
||||||
|
|
||||||
|
`PhysicsEngine.SetPosition` returns one immutable
|
||||||
|
`PhysicsSetPositionResult`. `SetPositionError` retains the header values
|
||||||
|
(`OK=0`, `GENERAL=1`, `NO_VALID=2`, `NO_CELL=3`, `COLLIDED=4`,
|
||||||
|
`INVALID_ARGS=0x100`) while `PhysicsResidenceDisposition` separately reports
|
||||||
|
`Committed`, `DeferredCell`, or `Unchanged`. Missing content is therefore
|
||||||
|
successful-but-deferred, never misreported as a placement failure.
|
||||||
|
|
||||||
|
The result carries the complete commit packet: root/cell-local frame,
|
||||||
|
contact/walkable/water state, sliding and collision normals, stationary-fall
|
||||||
|
counter, a complete immutable `COLLISIONINFO` snapshot for the real
|
||||||
|
`handle_all_collisions` callback (including contact/last-contact, sliding,
|
||||||
|
collision normal, stationary-fall, environment, adjustment, and object
|
||||||
|
fields), the callback result, and an explicit shadow action. A PhysicsBSP or changed-cell force commit
|
||||||
|
requests canonical shadow recalculation; a non-BSP transition replaces its
|
||||||
|
shadows only when the transition produced a nonempty cell array, otherwise it
|
||||||
|
preserves the prior list. Unchanged force placement changes only the frame.
|
||||||
|
|
||||||
|
Core never creates cells synchronously, so retail flag `0x20`
|
||||||
|
(`DoNotCreateCells`) has no differential loader branch inside this pure
|
||||||
|
mechanism. Both flag states can only observe already-published immutable cell
|
||||||
|
content and otherwise return `DeferredCell`. Carrying the flag into exact-cell,
|
||||||
|
generation-scoped async admission is part of the still-open Slice 4B
|
||||||
|
adaptation tracked with AD-2; this slice does not claim a dead SpherePath field
|
||||||
|
as exact behavior.
|
||||||
|
|
||||||
|
The existing public `Resolve` compatibility entry remains entirely unchanged
|
||||||
|
for production movement and zero-delta callers. Its result cannot represent a
|
||||||
|
successful-but-deferred residence, so hiding `DeferredCell` inside
|
||||||
|
`ResolveResult.Ok` would corrupt the contract. Slice 4B will route every
|
||||||
|
authoritative placement family through `SetPosition`, atomically install its
|
||||||
|
packet, and own exact lost-cell wakeup/commit.
|
||||||
|
|
||||||
|
## Automated oracle
|
||||||
|
|
||||||
|
`PhysicsSetPositionTests` pins error values, absent/invalid outdoor and indoor
|
||||||
|
claims, cross-landblock frame normalization, map-edge failure and the `0xFFFF`
|
||||||
|
sentinel, dummy/authored sphere setup, nonpositive scale, Ethereal seeding,
|
||||||
|
explicit-only PathClipped, missile step-down, exact equality schedules, both validators,
|
||||||
|
the late compass sample's float bits, signed no-slide behavior, actual
|
||||||
|
collision-handler mapping, force-class policy, explicit shadow actions, null
|
||||||
|
current-cell wakeup, ten-record scratch exhaustion, exact scatter ordering,
|
||||||
|
failed-probe scratch lifetime, and deferred-scatter stop. The legacy public
|
||||||
|
Resolve fixture remains unchanged until Slice 4B.
|
||||||
|
|
@ -89,6 +89,14 @@ public sealed class PhysicsEngine
|
||||||
TransitionState,
|
TransitionState,
|
||||||
TransitionState>? TransitionCellCollisionTestHook { get; set; }
|
TransitionState>? TransitionCellCollisionTestHook { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deterministic seam for retail <c>Random::RollDice(-1, 1)</c> used by
|
||||||
|
/// SetPosition scatter. Values are expected in [0,1); production uses the
|
||||||
|
/// process RNG and tests inject a fixed sequence.
|
||||||
|
/// </summary>
|
||||||
|
internal Func<double> SetPositionRandomUnit { get; set; } =
|
||||||
|
Random.Shared.NextDouble;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True once the landblock covering <paramref name="cellOrLandblockId"/> has had its
|
/// True once the landblock covering <paramref name="cellOrLandblockId"/> has had its
|
||||||
/// terrain + cells registered via <see cref="AddLandblock"/>. Accepts a canonical
|
/// terrain + cells registered via <see cref="AddLandblock"/>. Accepts a canonical
|
||||||
|
|
@ -1530,6 +1538,391 @@ public sealed class PhysicsEngine
|
||||||
/// green).
|
/// green).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
private readonly record struct AdjustedSetPosition(
|
||||||
|
uint CellId,
|
||||||
|
Vector3 CellLocalPosition,
|
||||||
|
bool Resident);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SetPosition's exact AdjustPosition input/output shape. Unlike the
|
||||||
|
/// camera helper above, this carries retail's block-local frame and can
|
||||||
|
/// therefore run outdoor <c>LandDefs::adjust_to_outside</c> without
|
||||||
|
/// consulting the resident-landblock registry. A valid adjusted id with
|
||||||
|
/// no visible cell is retained for the lost-cell path.
|
||||||
|
/// </summary>
|
||||||
|
private AdjustedSetPosition AdjustSetPosition(
|
||||||
|
uint seedCellId,
|
||||||
|
Vector3 cellLocalPosition,
|
||||||
|
Vector3 firstWorldSphereCenter)
|
||||||
|
{
|
||||||
|
uint low = seedCellId & 0xFFFFu;
|
||||||
|
bool lowInRange = low is (>= 1u and <= 0x40u)
|
||||||
|
or (>= 0x0100u and <= 0xFFFDu)
|
||||||
|
or 0xFFFFu;
|
||||||
|
if (!lowInRange)
|
||||||
|
return new AdjustedSetPosition(
|
||||||
|
seedCellId,
|
||||||
|
cellLocalPosition,
|
||||||
|
Resident: false);
|
||||||
|
|
||||||
|
uint adjustedCell = seedCellId;
|
||||||
|
Vector3 adjustedLocal = cellLocalPosition;
|
||||||
|
if (low >= 0x0100u)
|
||||||
|
{
|
||||||
|
PhysicsDataCache? cache = DataCache;
|
||||||
|
if (cache is null || cache.GetCellStruct(seedCellId) is null)
|
||||||
|
{
|
||||||
|
return new AdjustedSetPosition(
|
||||||
|
seedCellId,
|
||||||
|
cellLocalPosition,
|
||||||
|
Resident: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint child = CellTransit.FindVisibleChildCell(
|
||||||
|
cache,
|
||||||
|
seedCellId,
|
||||||
|
firstWorldSphereCenter,
|
||||||
|
useStabList: true);
|
||||||
|
if (child != 0u)
|
||||||
|
{
|
||||||
|
return new AdjustedSetPosition(
|
||||||
|
child,
|
||||||
|
adjustedLocal,
|
||||||
|
Resident: cache.GetCellStruct(child) is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
CellPhysics? claimed = cache.GetCellStruct(seedCellId);
|
||||||
|
if (claimed is null || !claimed.SeenOutside)
|
||||||
|
{
|
||||||
|
return new AdjustedSetPosition(
|
||||||
|
seedCellId,
|
||||||
|
adjustedLocal,
|
||||||
|
Resident: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outdoor adjustment is pure cell-relative LandDefs math. Residency
|
||||||
|
// is observed only after the id/frame have been mutated.
|
||||||
|
bool adjusted = LandDefs.AdjustToOutside(
|
||||||
|
ref adjustedCell,
|
||||||
|
ref adjustedLocal);
|
||||||
|
bool resident = adjusted
|
||||||
|
&& IsLandblockTerrainResident(adjustedCell);
|
||||||
|
return new AdjustedSetPosition(
|
||||||
|
adjustedCell,
|
||||||
|
adjustedLocal,
|
||||||
|
resident);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical retail placement transaction:
|
||||||
|
/// <c>CPhysicsObj::SetPosition</c> (0x005160C0) ->
|
||||||
|
/// <c>SetPositionInternal</c> (0x00515BD0) ->
|
||||||
|
/// <c>AdjustPosition</c> (0x00511D80) ->
|
||||||
|
/// <c>CheckPositionInternal</c> (0x00511E90) ->
|
||||||
|
/// <c>CTransition::find_valid_position</c> (0x0050C310).
|
||||||
|
/// A destination whose cell is not resident returns DeferredCell; callers
|
||||||
|
/// must retain the authoritative frame rather than demoting it outdoors.
|
||||||
|
/// </summary>
|
||||||
|
internal PhysicsSetPositionResult SetPosition(
|
||||||
|
in PhysicsSetPositionRequest request,
|
||||||
|
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions = null)
|
||||||
|
{
|
||||||
|
if (_transitionScratch?.ActiveDepth >= TransitionScratchArena.Capacity)
|
||||||
|
{
|
||||||
|
return ErrorResult(
|
||||||
|
request,
|
||||||
|
PhysicsSetPositionError.GeneralFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transition transition = RentTransition();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InitializeSetPositionTransition(transition, request);
|
||||||
|
bool randomOnly = request.Flags.HasFlag(
|
||||||
|
PhysicsSetPositionFlags.RandomScatter);
|
||||||
|
if (randomOnly)
|
||||||
|
{
|
||||||
|
return SetScatterPositionInternal(
|
||||||
|
transition,
|
||||||
|
request,
|
||||||
|
handleCollisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsSetPositionResult result =
|
||||||
|
SetPositionInternal(transition, request, handleCollisions);
|
||||||
|
if (result.Error != PhysicsSetPositionError.Ok
|
||||||
|
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
|
||||||
|
{
|
||||||
|
return SetScatterPositionInternal(
|
||||||
|
transition,
|
||||||
|
request,
|
||||||
|
handleCollisions);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReturnTransition(transition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeSetPositionTransition(
|
||||||
|
Transition transition,
|
||||||
|
in PhysicsSetPositionRequest request)
|
||||||
|
{
|
||||||
|
transition.ObjectInfo.StepUpHeight = request.StepUpHeight;
|
||||||
|
transition.ObjectInfo.StepDownHeight = request.StepDownHeight;
|
||||||
|
transition.ObjectInfo.StepDown =
|
||||||
|
!request.MoverPhysicsState.HasFlag(PhysicsStateFlags.Missile);
|
||||||
|
transition.ObjectInfo.MoverPhysicsState = request.MoverPhysicsState;
|
||||||
|
transition.ObjectInfo.SelfEntityId = request.MovingEntityId;
|
||||||
|
transition.ObjectInfo.State = request.MoverFlags;
|
||||||
|
transition.ObjectInfo.Ethereal = request.MoverPhysicsState.HasFlag(
|
||||||
|
PhysicsStateFlags.Ethereal);
|
||||||
|
transition.SpherePath.PlacementAllowsSliding =
|
||||||
|
request.Flags.HasFlag(PhysicsSetPositionFlags.Slide);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PhysicsSetPositionResult SetScatterPositionInternal(
|
||||||
|
Transition transition,
|
||||||
|
in PhysicsSetPositionRequest request,
|
||||||
|
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
|
||||||
|
{
|
||||||
|
PhysicsSetPositionResult result = ErrorResult(
|
||||||
|
request,
|
||||||
|
PhysicsSetPositionError.GeneralFailure);
|
||||||
|
for (uint attempt = 0u; attempt < request.ScatterAttempts; attempt++)
|
||||||
|
{
|
||||||
|
float dx = ((float)((SetPositionRandomUnit() * 2d) - 1d))
|
||||||
|
* request.ScatterRadiusX;
|
||||||
|
float dy = ((float)((SetPositionRandomUnit() * 2d) - 1d))
|
||||||
|
* request.ScatterRadiusY;
|
||||||
|
var scattered = request with
|
||||||
|
{
|
||||||
|
Position = request.Position + new Vector3(dx, dy, 0f),
|
||||||
|
CellLocalPosition = request.CellLocalPosition
|
||||||
|
+ new Vector3(dx, dy, 0f),
|
||||||
|
};
|
||||||
|
result = SetPositionInternal(
|
||||||
|
transition,
|
||||||
|
scattered,
|
||||||
|
handleCollisions);
|
||||||
|
if (result.Error == PhysicsSetPositionError.Ok)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PhysicsSetPositionResult SetPositionInternal(
|
||||||
|
Transition transition,
|
||||||
|
in PhysicsSetPositionRequest request,
|
||||||
|
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
|
||||||
|
{
|
||||||
|
transition.SpherePath.CellCandidates.Clear();
|
||||||
|
transition.SpherePath.ClearWalkable();
|
||||||
|
ImmutableArray<FlatCollisionSphere> spheres = request.Spheres;
|
||||||
|
float sphereScale = spheres.IsDefaultOrEmpty ? 1f : request.Scale;
|
||||||
|
Vector3 firstLocalCenter = spheres.IsDefaultOrEmpty
|
||||||
|
? new Vector3(0f, 0f, PhysicsGlobals.DummySphereRadius)
|
||||||
|
: spheres[0].Origin * sphereScale;
|
||||||
|
Vector3 firstWorldCenter =
|
||||||
|
Vector3.Transform(firstLocalCenter, request.Orientation)
|
||||||
|
+ request.Position;
|
||||||
|
AdjustedSetPosition adjusted = AdjustSetPosition(
|
||||||
|
request.CellId,
|
||||||
|
request.CellLocalPosition,
|
||||||
|
firstWorldCenter);
|
||||||
|
if (!adjusted.Resident)
|
||||||
|
{
|
||||||
|
return new PhysicsSetPositionResult(
|
||||||
|
PhysicsSetPositionError.Ok,
|
||||||
|
PhysicsResidenceDisposition.DeferredCell,
|
||||||
|
request.Position,
|
||||||
|
request.Orientation,
|
||||||
|
adjusted.CellId,
|
||||||
|
adjusted.CellLocalPosition,
|
||||||
|
CrossCellIds: ImmutableArray<uint>.Empty,
|
||||||
|
CollidedObjectIds: ImmutableArray<uint>.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool forceIntoCell = request.PlacementClass is
|
||||||
|
PhysicsPlacementClass.Hook
|
||||||
|
or PhysicsPlacementClass.Storage
|
||||||
|
or PhysicsPlacementClass.Corpse;
|
||||||
|
if (forceIntoCell)
|
||||||
|
{
|
||||||
|
if (adjusted.CellId == 0u)
|
||||||
|
{
|
||||||
|
return ErrorResult(
|
||||||
|
request,
|
||||||
|
PhysicsSetPositionError.NoCell);
|
||||||
|
}
|
||||||
|
bool changedCell = request.CurrentCellId is null
|
||||||
|
|| request.CurrentCellId.Value != adjusted.CellId;
|
||||||
|
return new PhysicsSetPositionResult(
|
||||||
|
PhysicsSetPositionError.Ok,
|
||||||
|
PhysicsResidenceDisposition.Committed,
|
||||||
|
request.Position,
|
||||||
|
request.Orientation,
|
||||||
|
adjusted.CellId,
|
||||||
|
adjusted.CellLocalPosition,
|
||||||
|
CellChanged: changedCell,
|
||||||
|
ShadowAction: changedCell
|
||||||
|
? PhysicsShadowCommitAction.Recalculate
|
||||||
|
: PhysicsShadowCommitAction.None,
|
||||||
|
CrossCellIds: ImmutableArray<uint>.Empty,
|
||||||
|
CollidedObjectIds: ImmutableArray<uint>.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.SpherePath.InitPath(
|
||||||
|
request.Position,
|
||||||
|
request.Position,
|
||||||
|
adjusted.CellId,
|
||||||
|
spheres,
|
||||||
|
sphereScale,
|
||||||
|
request.Orientation,
|
||||||
|
request.Orientation);
|
||||||
|
transition.SpherePath.InsertType = InsertType.Placement;
|
||||||
|
transition.SpherePath.PlacementAllowsSliding =
|
||||||
|
request.Flags.HasFlag(PhysicsSetPositionFlags.Slide);
|
||||||
|
|
||||||
|
bool valid = transition.FindValidPosition(this);
|
||||||
|
SpherePath spherePath = transition.SpherePath;
|
||||||
|
if (valid
|
||||||
|
&& !request.Flags.HasFlag(PhysicsSetPositionFlags.Slide))
|
||||||
|
{
|
||||||
|
valid = AcceptNoSlidePlacement(
|
||||||
|
spherePath.CurPos,
|
||||||
|
request.Position,
|
||||||
|
spherePath.CurCellId,
|
||||||
|
adjusted.CellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollisionInfo collision = transition.CollisionInfo;
|
||||||
|
var collisionReport = new PhysicsSetPositionCollisionReport(
|
||||||
|
collision.ContactPlaneValid,
|
||||||
|
collision.ContactPlane,
|
||||||
|
collision.ContactPlaneCellId,
|
||||||
|
collision.ContactPlaneIsWater,
|
||||||
|
collision.LastKnownContactPlaneValid,
|
||||||
|
collision.LastKnownContactPlane,
|
||||||
|
collision.LastKnownContactPlaneCellId,
|
||||||
|
collision.LastKnownContactPlaneIsWater,
|
||||||
|
collision.SlidingNormalValid,
|
||||||
|
collision.SlidingNormal,
|
||||||
|
collision.CollisionNormalValid,
|
||||||
|
collision.CollisionNormal,
|
||||||
|
collision.CollidedWithEnvironment,
|
||||||
|
collision.FramesStationaryFall,
|
||||||
|
collision.AdjustOffset,
|
||||||
|
collision.LastCollidedObjectGuid,
|
||||||
|
collision.CollideObjectGuids.ToImmutableArray());
|
||||||
|
bool collisionHandlerResult = !valid
|
||||||
|
&& handleCollisions?.Invoke(collisionReport) == true;
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
return new PhysicsSetPositionResult(
|
||||||
|
collisionHandlerResult
|
||||||
|
? PhysicsSetPositionError.Collided
|
||||||
|
: PhysicsSetPositionError.NoValidPosition,
|
||||||
|
PhysicsResidenceDisposition.Unchanged,
|
||||||
|
request.Position,
|
||||||
|
request.Orientation,
|
||||||
|
request.CellId,
|
||||||
|
request.CellLocalPosition,
|
||||||
|
InContact: collision.ContactPlaneValid,
|
||||||
|
OnWalkable: PhysicsObjUpdate.IsWalkableContact(
|
||||||
|
collision.ContactPlaneValid,
|
||||||
|
collision.ContactPlane.Normal),
|
||||||
|
ContactPlane: collision.ContactPlane,
|
||||||
|
ContactPlaneCellId: collision.ContactPlaneCellId,
|
||||||
|
ContactPlaneIsWater: collision.ContactPlaneIsWater,
|
||||||
|
SlidingNormalValid: collision.SlidingNormalValid,
|
||||||
|
SlidingNormal: collision.SlidingNormal,
|
||||||
|
CollisionNormalValid: collision.CollisionNormalValid,
|
||||||
|
CollisionNormal: collision.CollisionNormal,
|
||||||
|
FramesStationaryFall: collision.FramesStationaryFall,
|
||||||
|
CollisionHandlerResult: collisionHandlerResult,
|
||||||
|
CollidedWithEnvironment: collision.CollidedWithEnvironment,
|
||||||
|
CrossCellIds: ImmutableArray<uint>.Empty,
|
||||||
|
CollidedObjectIds: collisionReport.CollidedObjectIds);
|
||||||
|
}
|
||||||
|
if (spherePath.CurCellId == 0u)
|
||||||
|
{
|
||||||
|
return ErrorResult(request, PhysicsSetPositionError.NoCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inContact = collision.ContactPlaneValid;
|
||||||
|
bool onWalkable = PhysicsObjUpdate.IsWalkableContact(
|
||||||
|
inContact,
|
||||||
|
collision.ContactPlane.Normal);
|
||||||
|
Vector3 resultLocal = adjusted.CellLocalPosition
|
||||||
|
+ (spherePath.CurPos - request.Position)
|
||||||
|
- LandDefs.GetBlockOffset(adjusted.CellId, spherePath.CurCellId);
|
||||||
|
bool hasPhysicsBsp = request.MoverPhysicsState.HasFlag(
|
||||||
|
PhysicsStateFlags.HasPhysicsBsp);
|
||||||
|
ImmutableArray<uint> transitionCells =
|
||||||
|
spherePath.CellCandidates.OrderedIds.ToImmutableArray();
|
||||||
|
PhysicsShadowCommitAction shadowAction = hasPhysicsBsp
|
||||||
|
? PhysicsShadowCommitAction.Recalculate
|
||||||
|
: transitionCells.Length != 0
|
||||||
|
? PhysicsShadowCommitAction.Replace
|
||||||
|
: PhysicsShadowCommitAction.Preserve;
|
||||||
|
return new PhysicsSetPositionResult(
|
||||||
|
PhysicsSetPositionError.Ok,
|
||||||
|
PhysicsResidenceDisposition.Committed,
|
||||||
|
spherePath.CurPos,
|
||||||
|
// CheckPositionInternal mutates only origin in no-slide mode;
|
||||||
|
// SetPosition retains the requested frame orientation.
|
||||||
|
request.Orientation,
|
||||||
|
spherePath.CurCellId,
|
||||||
|
resultLocal,
|
||||||
|
inContact,
|
||||||
|
onWalkable,
|
||||||
|
collision.ContactPlane,
|
||||||
|
collision.ContactPlaneCellId,
|
||||||
|
collision.ContactPlaneIsWater,
|
||||||
|
collision.SlidingNormalValid,
|
||||||
|
collision.SlidingNormal,
|
||||||
|
collision.CollisionNormalValid,
|
||||||
|
collision.CollisionNormal,
|
||||||
|
collision.FramesStationaryFall,
|
||||||
|
collision.CollidedWithEnvironment,
|
||||||
|
collisionHandlerResult,
|
||||||
|
CellChanged: request.CurrentCellId is null
|
||||||
|
|| request.CurrentCellId.Value != spherePath.CurCellId,
|
||||||
|
ShadowAction: shadowAction,
|
||||||
|
CrossCellIds: shadowAction == PhysicsShadowCommitAction.Replace
|
||||||
|
? transitionCells
|
||||||
|
: ImmutableArray<uint>.Empty,
|
||||||
|
CollidedObjectIds:
|
||||||
|
collision.CollideObjectGuids.ToImmutableArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PhysicsSetPositionResult ErrorResult(
|
||||||
|
in PhysicsSetPositionRequest request,
|
||||||
|
PhysicsSetPositionError error) => new(
|
||||||
|
error,
|
||||||
|
PhysicsResidenceDisposition.Unchanged,
|
||||||
|
request.Position,
|
||||||
|
request.Orientation,
|
||||||
|
request.CellId,
|
||||||
|
request.CellLocalPosition,
|
||||||
|
CrossCellIds: ImmutableArray<uint>.Empty,
|
||||||
|
CollidedObjectIds: ImmutableArray<uint>.Empty);
|
||||||
|
|
||||||
|
internal static bool AcceptNoSlidePlacement(
|
||||||
|
Vector3 resolvedPosition,
|
||||||
|
Vector3 requestedPosition,
|
||||||
|
uint resolvedCellId,
|
||||||
|
uint adjustedCellId)
|
||||||
|
{
|
||||||
|
Vector3 displacement = resolvedPosition - requestedPosition;
|
||||||
|
return displacement.X <= 0.0500000007f
|
||||||
|
&& displacement.Y <= 0.0500000007f
|
||||||
|
&& resolvedCellId == adjustedCellId;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #111: the walkable floor Z of <paramref name="cellId"/>'s PHYSICS
|
/// #111: the walkable floor Z of <paramref name="cellId"/>'s PHYSICS
|
||||||
/// polygons under the world XY, nearest to <paramref name="referenceZ"/>.
|
/// polygons under the world XY, nearest to <paramref name="referenceZ"/>.
|
||||||
|
|
|
||||||
155
src/AcDream.Core/Physics/PhysicsSetPosition.cs
Normal file
155
src/AcDream.Core/Physics/PhysicsSetPosition.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>SetPositionError</c> (<c>acclient.h</c>, enum 491). This is
|
||||||
|
/// deliberately independent of <see cref="PhysicsResidenceDisposition"/>:
|
||||||
|
/// losing the destination cell is a successful SetPosition operation whose
|
||||||
|
/// residence is deferred, not a placement error.
|
||||||
|
/// </summary>
|
||||||
|
internal enum PhysicsSetPositionError
|
||||||
|
{
|
||||||
|
Ok = 0,
|
||||||
|
GeneralFailure = 1,
|
||||||
|
NoValidPosition = 2,
|
||||||
|
NoCell = 3,
|
||||||
|
Collided = 4,
|
||||||
|
InvalidArguments = 0x100,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum PhysicsResidenceDisposition
|
||||||
|
{
|
||||||
|
Committed,
|
||||||
|
DeferredCell,
|
||||||
|
Unchanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shadow-list operation performed by retail's SetPosition commit tail.
|
||||||
|
/// Recalculate delegates to the canonical live shadow-shape owner (the
|
||||||
|
/// PhysicsBSP/bounding-box path cannot be reconstructed from placement
|
||||||
|
/// spheres); Replace consumes <see cref="PhysicsSetPositionResult.CrossCellIds"/>;
|
||||||
|
/// Preserve intentionally leaves the existing list untouched.
|
||||||
|
/// </summary>
|
||||||
|
internal enum PhysicsShadowCommitAction
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Recalculate,
|
||||||
|
Replace,
|
||||||
|
Preserve,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct PhysicsSetPositionCollisionReport(
|
||||||
|
bool ContactPlaneValid,
|
||||||
|
Plane ContactPlane,
|
||||||
|
uint ContactPlaneCellId,
|
||||||
|
bool ContactPlaneIsWater,
|
||||||
|
bool LastKnownContactPlaneValid,
|
||||||
|
Plane LastKnownContactPlane,
|
||||||
|
uint LastKnownContactPlaneCellId,
|
||||||
|
bool LastKnownContactPlaneIsWater,
|
||||||
|
bool SlidingNormalValid,
|
||||||
|
Vector3 SlidingNormal,
|
||||||
|
bool CollisionNormalValid,
|
||||||
|
Vector3 CollisionNormal,
|
||||||
|
bool CollidedWithEnvironment,
|
||||||
|
int FramesStationaryFall,
|
||||||
|
Vector3 AdjustOffset,
|
||||||
|
uint? LastCollidedObjectId,
|
||||||
|
ImmutableArray<uint> CollidedObjectIds);
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
internal enum PhysicsSetPositionFlags : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Placement = 0x001,
|
||||||
|
Teleport = 0x002,
|
||||||
|
Restore = 0x004,
|
||||||
|
Slide = 0x010,
|
||||||
|
DoNotCreateCells = 0x020,
|
||||||
|
Scatter = 0x100,
|
||||||
|
RandomScatter = 0x200,
|
||||||
|
Line = 0x400,
|
||||||
|
SendPositionEvent = 0x1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The three retail weenie classifications which bypass placement collision
|
||||||
|
/// in <c>CPhysicsObj::SetPositionInternal</c> after AdjustPosition succeeds.
|
||||||
|
/// Runtime derives this from the canonical object record; callers cannot use a
|
||||||
|
/// free boolean to force ordinary objects through geometry.
|
||||||
|
/// </summary>
|
||||||
|
internal enum PhysicsPlacementClass
|
||||||
|
{
|
||||||
|
Ordinary,
|
||||||
|
Hook,
|
||||||
|
Storage,
|
||||||
|
Corpse,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Complete immutable input to retail <c>CPhysicsObj::SetPosition</c>. World
|
||||||
|
/// position feeds acdream's flat collision representation; cell-local position
|
||||||
|
/// is retail's <c>Position.frame.origin</c> and is the only input to
|
||||||
|
/// <c>LandDefs::adjust_to_outside</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct PhysicsSetPositionRequest(
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Orientation,
|
||||||
|
uint CellId,
|
||||||
|
Vector3 CellLocalPosition,
|
||||||
|
ImmutableArray<FlatCollisionSphere> Spheres,
|
||||||
|
float Scale,
|
||||||
|
float StepUpHeight,
|
||||||
|
float StepDownHeight,
|
||||||
|
PhysicsStateFlags MoverPhysicsState = PhysicsStateFlags.None,
|
||||||
|
ObjectInfoState MoverFlags = ObjectInfoState.None,
|
||||||
|
uint MovingEntityId = 0u,
|
||||||
|
PhysicsPlacementClass PlacementClass = PhysicsPlacementClass.Ordinary,
|
||||||
|
PhysicsSetPositionFlags Flags = PhysicsSetPositionFlags.Placement,
|
||||||
|
Vector3 Line = default,
|
||||||
|
float ScatterRadiusX = 0f,
|
||||||
|
float ScatterRadiusY = 0f,
|
||||||
|
uint ScatterAttempts = 0u,
|
||||||
|
// Null models retail's distinct `this->cell == nullptr` state. The
|
||||||
|
// retained Position cell id may equal the destination while the object is
|
||||||
|
// still in the lost-cell list; that wake must change_cell and reflood.
|
||||||
|
uint? CurrentCellId = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable commit packet produced by the pure placement transaction. Runtime
|
||||||
|
/// installs this packet atomically into the canonical body/spatial owner before
|
||||||
|
/// publishing one presentation delta.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct PhysicsSetPositionResult(
|
||||||
|
PhysicsSetPositionError Error,
|
||||||
|
PhysicsResidenceDisposition Residence,
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Orientation,
|
||||||
|
uint CellId,
|
||||||
|
Vector3 CellLocalPosition,
|
||||||
|
bool InContact = false,
|
||||||
|
bool OnWalkable = false,
|
||||||
|
Plane ContactPlane = default,
|
||||||
|
uint ContactPlaneCellId = 0u,
|
||||||
|
bool ContactPlaneIsWater = false,
|
||||||
|
bool SlidingNormalValid = false,
|
||||||
|
Vector3 SlidingNormal = default,
|
||||||
|
bool CollisionNormalValid = false,
|
||||||
|
Vector3 CollisionNormal = default,
|
||||||
|
int FramesStationaryFall = 0,
|
||||||
|
bool CollidedWithEnvironment = false,
|
||||||
|
bool CollisionHandlerResult = false,
|
||||||
|
bool CellChanged = false,
|
||||||
|
PhysicsShadowCommitAction ShadowAction = PhysicsShadowCommitAction.None,
|
||||||
|
ImmutableArray<uint> CrossCellIds = default,
|
||||||
|
ImmutableArray<uint> CollidedObjectIds = default)
|
||||||
|
{
|
||||||
|
internal bool IsSuccessful => Error == PhysicsSetPositionError.Ok;
|
||||||
|
internal bool IsCommitted =>
|
||||||
|
IsSuccessful && Residence == PhysicsResidenceDisposition.Committed;
|
||||||
|
internal bool IsDeferred =>
|
||||||
|
IsSuccessful && Residence == PhysicsResidenceDisposition.DeferredCell;
|
||||||
|
}
|
||||||
|
|
@ -1575,6 +1575,79 @@ public sealed class Transition
|
||||||
return transitionState == TransitionState.OK;
|
return transitionState == TransitionState.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>CTransition::find_valid_position</c> (0x0050C310). Placement
|
||||||
|
/// insertions use the complete outer <c>find_placement_position</c>
|
||||||
|
/// transaction; ordinary movement uses the transitional sweep.
|
||||||
|
/// </summary>
|
||||||
|
internal bool FindValidPosition(PhysicsEngine engine) =>
|
||||||
|
SpherePath.InsertType == InsertType.Transition
|
||||||
|
? FindTransitionalPosition(engine)
|
||||||
|
: FindPlacementPosition(engine);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>CTransition::find_placement_position</c> (0x0050C170):
|
||||||
|
/// INITIAL_PLACEMENT insertion, other-cell validation, the inner compass
|
||||||
|
/// search, optional placement step-down, and final validation.
|
||||||
|
/// </summary>
|
||||||
|
internal bool FindPlacementPosition(PhysicsEngine engine)
|
||||||
|
{
|
||||||
|
SpherePath sp = SpherePath;
|
||||||
|
sp.SetCheckPos(sp.CurPos, sp.CurCellId);
|
||||||
|
sp.InsertType = InsertType.InitialPlacement;
|
||||||
|
|
||||||
|
TransitionState initial = ValidatePlacement(
|
||||||
|
engine,
|
||||||
|
InitialPlacementInsert(engine),
|
||||||
|
retryPlacement: true);
|
||||||
|
if (initial != TransitionState.OK)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
sp.InsertType = InsertType.Placement;
|
||||||
|
if (!FindPlacementPos(engine))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ObjectInfo.StepDown)
|
||||||
|
{
|
||||||
|
const float placementWalkableAllowance = 0.0871556997f;
|
||||||
|
float stepDownHeight = ObjectInfo.StepDownHeight;
|
||||||
|
sp.WalkableAllowance = placementWalkableAllowance;
|
||||||
|
sp.SaveCheckPos();
|
||||||
|
InsertType savedInsert = sp.InsertType;
|
||||||
|
sp.InsertType = InsertType.Transition;
|
||||||
|
|
||||||
|
(float probeHeight, int probeCount) = GetStepDownProbePlan(
|
||||||
|
sp.NumSphere,
|
||||||
|
sp.GlobalSphere[0].Radius,
|
||||||
|
stepDownHeight);
|
||||||
|
bool stepped = DoStepDown(
|
||||||
|
probeHeight,
|
||||||
|
placementWalkableAllowance,
|
||||||
|
engine);
|
||||||
|
if (!stepped && probeCount > 1)
|
||||||
|
{
|
||||||
|
stepped = DoStepDown(
|
||||||
|
probeHeight,
|
||||||
|
placementWalkableAllowance,
|
||||||
|
engine);
|
||||||
|
}
|
||||||
|
if (!stepped)
|
||||||
|
{
|
||||||
|
sp.RestoreCheckPos();
|
||||||
|
CollisionInfo.ContactPlaneValid = false;
|
||||||
|
CollisionInfo.ContactPlaneIsWater = false;
|
||||||
|
}
|
||||||
|
sp.InsertType = savedInsert;
|
||||||
|
sp.ClearWalkable();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidatePlacement(
|
||||||
|
engine,
|
||||||
|
TransitionState.OK,
|
||||||
|
retryPlacement: true)
|
||||||
|
== TransitionState.OK;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail <c>CTransition::find_placement_pos</c> (0x0050BA50).
|
/// Retail <c>CTransition::find_placement_pos</c> (0x0050BA50).
|
||||||
/// Tests the requested position first, then searches concentric rings up
|
/// Tests the requested position first, then searches concentric rings up
|
||||||
|
|
@ -1613,15 +1686,19 @@ public sealed class Transition
|
||||||
sphereRadius = 0.48f;
|
sphereRadius = 0.48f;
|
||||||
}
|
}
|
||||||
|
|
||||||
float stepCountExact = 4f / sphereRadius;
|
double stepCountExact = 4d / (double)sphereRadius;
|
||||||
if (fakeSphere)
|
if (fakeSphere)
|
||||||
stepCountExact *= 0.5f;
|
stepCountExact *= 0.5d;
|
||||||
if (stepCountExact <= 1f)
|
if (stepCountExact <= 1d)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
int stepCount = (int)MathF.Ceiling(stepCountExact);
|
int stepCount = (int)Math.Ceiling(stepCountExact);
|
||||||
float distancePerStep = adjustRadius / stepCount;
|
float distancePerStep = (float)((double)adjustRadius / stepCount);
|
||||||
float radiansPerStep = MathF.PI * distancePerStep / sphereRadius;
|
// Retail stores the literal 3.14159989f here; do not substitute the
|
||||||
|
// BCL's more precise PI because it changes late compass samples.
|
||||||
|
float radiansPerStep = (float)(
|
||||||
|
((double)distancePerStep / sphereRadius)
|
||||||
|
* 3.14159989f);
|
||||||
float totalDistance = 0f;
|
float totalDistance = 0f;
|
||||||
float totalRadians = 0f;
|
float totalRadians = 0f;
|
||||||
|
|
||||||
|
|
@ -1630,23 +1707,25 @@ public sealed class Transition
|
||||||
totalDistance += distancePerStep;
|
totalDistance += distancePerStep;
|
||||||
totalRadians += radiansPerStep;
|
totalRadians += radiansPerStep;
|
||||||
|
|
||||||
int sampleCount = (int)MathF.Ceiling(totalRadians) * 2;
|
int sampleCount = (int)Math.Ceiling((double)totalRadians) * 2;
|
||||||
float headingStep = 360f / sampleCount;
|
float headingStep = (float)(360d / sampleCount);
|
||||||
|
|
||||||
for (int sample = 0; sample < sampleCount; sample++)
|
for (int sample = 0; sample < sampleCount; sample++)
|
||||||
{
|
{
|
||||||
sp.SetCheckPos(sp.CurPos, sp.CurCellId);
|
sp.SetCheckPos(sp.CurPos, sp.CurCellId);
|
||||||
|
|
||||||
// Frame::set_heading/get_vector_heading: 0 degrees is +Y,
|
// Frame::set_heading/get_vector_heading: 0 degrees is +Y,
|
||||||
// 90 degrees is +X in AC's compass convention.
|
// 90 degrees is +X in AC's compass convention. Retail promotes
|
||||||
float headingRadians = headingStep * sample * (MathF.PI / 180f);
|
// the float heading to x87 precision, multiplies by this exact
|
||||||
var offset = new Vector3(
|
// degree-to-radian constant, then rounds sin/cos back to float.
|
||||||
MathF.Sin(headingRadians) * totalDistance,
|
float heading = headingStep * sample;
|
||||||
MathF.Cos(headingRadians) * totalDistance,
|
Vector3 offset = GetPlacementCompassOffset(
|
||||||
0f);
|
heading,
|
||||||
|
totalDistance);
|
||||||
sp.GlobalOffset = AdjustOffset(offset);
|
sp.GlobalOffset = AdjustOffset(offset);
|
||||||
|
|
||||||
if (sp.GlobalOffset.Length() < PhysicsGlobals.EPSILON)
|
if (sp.GlobalOffset.LengthSquared()
|
||||||
|
< PhysicsGlobals.EpsilonSq)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
sp.AddOffsetToCheckPos(sp.GlobalOffset);
|
sp.AddOffsetToCheckPos(sp.GlobalOffset);
|
||||||
|
|
@ -1664,7 +1743,20 @@ public sealed class Transition
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TransitionState ValidatePlacementTransition(TransitionState transitionState)
|
internal static Vector3 GetPlacementCompassOffset(
|
||||||
|
float headingDegrees,
|
||||||
|
float distance)
|
||||||
|
{
|
||||||
|
const double DegreesToRadians = 0.017453292519943295d;
|
||||||
|
double radians = (double)headingDegrees * DegreesToRadians;
|
||||||
|
return new Vector3(
|
||||||
|
(float)Math.Sin(radians) * distance,
|
||||||
|
(float)Math.Cos(radians) * distance,
|
||||||
|
0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransitionState ValidatePlacementTransition(
|
||||||
|
TransitionState transitionState)
|
||||||
{
|
{
|
||||||
var sp = SpherePath;
|
var sp = SpherePath;
|
||||||
if (sp.CheckCellId == 0)
|
if (sp.CheckCellId == 0)
|
||||||
|
|
@ -1682,7 +1774,9 @@ public sealed class Transition
|
||||||
sp.GlobalCurrCenter[i].Radius = sp.LocalSphere[i].Radius;
|
sp.GlobalCurrCenter[i].Radius = sp.LocalSphere[i].Radius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (sp.PlacementAllowsSliding)
|
else if (transitionState > TransitionState.OK
|
||||||
|
&& transitionState <= TransitionState.Slid
|
||||||
|
&& sp.PlacementAllowsSliding)
|
||||||
{
|
{
|
||||||
// COLLISIONINFO::init at retail 0x0050B052. Placement probes are
|
// COLLISIONINFO::init at retail 0x0050B052. Placement probes are
|
||||||
// independent; a failed compass sample must not bias the next one.
|
// independent; a failed compass sample must not bias the next one.
|
||||||
|
|
@ -1692,6 +1786,58 @@ public sealed class Transition
|
||||||
return transitionState;
|
return transitionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal TransitionState ValidatePlacementTransitionForTest(
|
||||||
|
TransitionState transitionState) =>
|
||||||
|
ValidatePlacementTransition(transitionState);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>CTransition::validate_placement</c> (0x0050B210), used only
|
||||||
|
/// by the outer placement transaction. Adjusted/Slid receives one
|
||||||
|
/// placement_insert retry when requested; Collided never retries and this
|
||||||
|
/// validator never clears CollisionInfo.
|
||||||
|
/// </summary>
|
||||||
|
private TransitionState ValidatePlacement(
|
||||||
|
PhysicsEngine engine,
|
||||||
|
TransitionState transitionState,
|
||||||
|
bool retryPlacement)
|
||||||
|
{
|
||||||
|
SpherePath sp = SpherePath;
|
||||||
|
if (sp.CheckCellId == 0u)
|
||||||
|
return TransitionState.Collided;
|
||||||
|
|
||||||
|
if (transitionState == TransitionState.OK)
|
||||||
|
{
|
||||||
|
sp.CurPos = sp.CheckPos;
|
||||||
|
sp.CurCellId = sp.CheckCellId;
|
||||||
|
sp.CurOrientation = sp.CheckOrientation;
|
||||||
|
for (int i = 0; i < sp.NumSphere; i++)
|
||||||
|
{
|
||||||
|
sp.GlobalCurrCenter[i].Origin =
|
||||||
|
Vector3.Transform(
|
||||||
|
sp.LocalSphere[i].Origin,
|
||||||
|
sp.CurOrientation)
|
||||||
|
+ sp.CurPos;
|
||||||
|
sp.GlobalCurrCenter[i].Radius = sp.LocalSphere[i].Radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((transitionState is TransitionState.Adjusted
|
||||||
|
or TransitionState.Slid)
|
||||||
|
&& retryPlacement)
|
||||||
|
{
|
||||||
|
return ValidatePlacement(
|
||||||
|
engine,
|
||||||
|
PlacementInsert(engine),
|
||||||
|
retryPlacement: false);
|
||||||
|
}
|
||||||
|
return transitionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal TransitionState ValidatePlacementForTest(
|
||||||
|
PhysicsEngine engine,
|
||||||
|
TransitionState transitionState,
|
||||||
|
bool retryPlacement) =>
|
||||||
|
ValidatePlacement(engine, transitionState, retryPlacement);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Per-step collision check
|
// Per-step collision check
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
@ -1749,10 +1895,10 @@ public sealed class Transition
|
||||||
float diameter = sphereRadius * 2f;
|
float diameter = sphereRadius * 2f;
|
||||||
float probeHeight = requestedHeight;
|
float probeHeight = requestedHeight;
|
||||||
|
|
||||||
if (numSpheres < 2 && diameter < probeHeight)
|
if (numSpheres < 2 && diameter <= probeHeight)
|
||||||
probeHeight = sphereRadius * 0.5f;
|
probeHeight = sphereRadius * 0.5f;
|
||||||
|
|
||||||
if (diameter >= probeHeight)
|
if (diameter > probeHeight)
|
||||||
return (probeHeight, 1);
|
return (probeHeight, 1);
|
||||||
|
|
||||||
return (probeHeight * 0.5f, 2);
|
return (probeHeight * 0.5f, 2);
|
||||||
|
|
@ -2176,6 +2322,54 @@ public sealed class Transition
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The initial half of retail <c>find_placement_position</c>. This is
|
||||||
|
/// deliberately not <see cref="TransitionalInsert"/>: initial placement
|
||||||
|
/// performs one primary insert and, only on OK, the other-cell pass. It
|
||||||
|
/// never enters transitional collide/step/edge response branches.
|
||||||
|
/// </summary>
|
||||||
|
private TransitionState InitialPlacementInsert(PhysicsEngine engine)
|
||||||
|
{
|
||||||
|
SpherePath sp = SpherePath;
|
||||||
|
if (sp.CheckCellId == 0u)
|
||||||
|
return TransitionState.Collided;
|
||||||
|
|
||||||
|
TransitionState result = InsertIntoCell(
|
||||||
|
engine,
|
||||||
|
sp.CheckCellId,
|
||||||
|
numAttempts: 3);
|
||||||
|
if (result == TransitionState.OK)
|
||||||
|
{
|
||||||
|
result = RunCheckOtherCellsAndAdvance(
|
||||||
|
engine,
|
||||||
|
sp.GlobalSphere[0].Origin,
|
||||||
|
sp.GlobalSphere[0].Radius);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>CTransition::placement_insert</c> (0x0050B1D0), used by
|
||||||
|
/// <c>validate_placement</c> for exactly one Adjusted/Slid retry.
|
||||||
|
/// </summary>
|
||||||
|
private TransitionState PlacementInsert(PhysicsEngine engine)
|
||||||
|
{
|
||||||
|
SpherePath sp = SpherePath;
|
||||||
|
if (sp.CheckCellId == 0u)
|
||||||
|
return TransitionState.Collided;
|
||||||
|
|
||||||
|
TransitionState result = InsertIntoCell(
|
||||||
|
engine,
|
||||||
|
sp.CheckCellId,
|
||||||
|
numAttempts: 3);
|
||||||
|
return result == TransitionState.OK
|
||||||
|
? RunCheckOtherCellsAndAdvance(
|
||||||
|
engine,
|
||||||
|
sp.GlobalSphere[0].Origin,
|
||||||
|
sp.GlobalSphere[0].Radius)
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Primary-cell virtual <c>find_collisions</c> composition. A non-OK
|
/// Primary-cell virtual <c>find_collisions</c> composition. A non-OK
|
||||||
/// response terminates this pass, so an inner retry always restarts from
|
/// response terminates this pass, so an inner retry always restarts from
|
||||||
|
|
|
||||||
1041
tests/AcDream.Core.Tests/Physics/PhysicsSetPositionTests.cs
Normal file
1041
tests/AcDream.Core.Tests/Physics/PhysicsSetPositionTests.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue