188 lines
8.3 KiB
Markdown
188 lines
8.3 KiB
Markdown
# M2 Placement Transform Resolver
|
|
|
|
## Metadata
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Status | Implemented |
|
|
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-TRANSFORM-001` |
|
|
| Owners | Pure ADT M2 placement basis and model-specific origin compensation |
|
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 |
|
|
| Profiles/capabilities | Existing static M2 paths; four calibrated model exceptions |
|
|
|
|
## Purpose
|
|
|
|
Resolve the unscaled Godot-space orientation and optional local origin offset
|
|
used by the pure placement grouper, placeholder rendering and instance creation.
|
|
|
|
## Non-goals
|
|
|
|
- Convert WoW/server/ADT coordinates or own placement positions.
|
|
- Apply final scale clamps, build transforms, group placements or create Nodes.
|
|
- Add, generalize or configure model-specific corrections.
|
|
- Change M2 materials, animation, caches, visibility or quality profiles.
|
|
- Claim general build-12340 placement parity.
|
|
|
|
## Context and boundaries
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver]
|
|
Resolver --> Basis[Unscaled Basis]
|
|
Resolver --> Offset[Local origin offset]
|
|
Basis --> Group[M2PlacementGrouper]
|
|
Basis --> Placeholder[Placeholder MultiMesh path]
|
|
Basis --> Instance[Instance path]
|
|
Offset --> Group
|
|
Offset --> Placeholder
|
|
Offset --> Instance
|
|
Group --> Loader[StreamingWorldLoader-owned queues/build]
|
|
```
|
|
|
|
Allowed dependencies are `Vector3`, `Basis`, scalar math and path-string
|
|
normalization. Node, SceneTree, RenderingServer, ResourceLoader, tasks, caches,
|
|
files, gameplay, network and editor UI are forbidden.
|
|
|
|
## Public API
|
|
|
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
|
|---|---|---|---|---|
|
|
| `resolve_basis(rotation_radians, relative_m2_path)` | Pure query | Return unscaled regular or calibrated placement orientation | Worker/main thread; call-local result | Unknown paths use `Basis.from_euler` |
|
|
| `resolve_origin_offset(rotation_radians, relative_m2_path, scale_value)` | Pure query | Compensate the measured tall-waterfall twist anchor | Worker/main thread; call-local result | Regular/unanchored paths return zero; scale clamps to `0.0001` for compensation |
|
|
|
|
## Inputs and outputs
|
|
|
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
|
|---|---|---|---|---|---|
|
|
| Input | Godot Euler rotation in radians | ADT loader placement record | Resolver | Scalar copy | One call |
|
|
| Input | Relative M2 path | ADT MMDX/name table | Resolver allowlist | Caller string | One call |
|
|
| Input | Raw placement scale | ADT placement record | Offset compensation | Scalar copy | One call |
|
|
| Output | Unscaled `Basis` | Resolver | Grouper/placeholder/instance adapter | Value copy | One call |
|
|
| Output | Local `Vector3` offset | Resolver | Grouper/placeholder/instance adapter | Value copy | One call |
|
|
|
|
Callers preserve their historical basis scale minima: grouping/instance use
|
|
`0.0001`, while placeholders use `0.01`.
|
|
|
|
## Data flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
BasisCall[resolve_basis] --> ModelName[Normalize separators/case/basename]
|
|
ModelName --> Waterfall{Waterfall sheet?}
|
|
Waterfall -->|yes| WaterfallBasis[+90 degree world yaw * Euler * local-axis twist]
|
|
Waterfall -->|no| Cliff{Elwynn cliff rock?}
|
|
Cliff -->|yes| CliffBasis[Corrected Y/BACK/RIGHT composition]
|
|
Cliff -->|no| Regular[Basis.from_euler]
|
|
OffsetCall[resolve_origin_offset] --> Anchored{Tall waterfall anchor?}
|
|
Anchored -->|no| Zero[Vector3.ZERO]
|
|
Anchored -->|yes| Compensation[(base anchor - twisted anchor) * clamped scale]
|
|
```
|
|
|
|
## Lifecycle/state
|
|
|
|
The resolver is stateless. It may be shared between the main thread and worker
|
|
calls because each result is computed solely from value inputs. Construction,
|
|
world reset and shutdown require no clear, cancellation or resource release.
|
|
|
|
## Main sequence
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller as Grouper or StreamingWorldLoader
|
|
participant Resolver as M2PlacementTransformResolver
|
|
participant Consumer as Grouper/placeholder/instance path
|
|
Caller->>Resolver: resolve_basis(rotation, path)
|
|
Resolver-->>Caller: unscaled Basis
|
|
Caller->>Resolver: resolve_origin_offset(rotation, path, scale)
|
|
Resolver-->>Caller: local offset
|
|
Caller->>Consumer: compose historical scaled Transform3D
|
|
```
|
|
|
|
## Ownership, threading and resources
|
|
|
|
- The resolver retains no mutable state or engine resources.
|
|
- The grouper owns worker-path final transforms; the loader owns placement records,
|
|
tasks, queues, caches and MultiMesh/Node/Mesh/material/RID creation and cleanup.
|
|
- Pure methods are safe for grouper worker use and main-thread consumers.
|
|
- Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver.
|
|
|
|
## Errors, cancellation and recovery
|
|
|
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
|
|---|---|---|---|---|
|
|
| Empty/unknown path | Allowlist miss | Regular Euler basis and zero offset | Contract fixture | Expected fallback |
|
|
| Mixed case/backslashes/MDX | Normalization | Same calibrated model match | Contract fixture | None required |
|
|
| Zero/negative scale for anchored offset | Numeric clamp | Use `0.0001` compensation scale | Contract fixture | Caller may supply valid ADT scale later |
|
|
| World reset/shutdown | No retained state | No action | Existing shutdown verifier | New calls remain independent |
|
|
|
|
There is no asynchronous operation or cancellation inside the resolver.
|
|
|
|
## Configuration and capabilities
|
|
|
|
The resolver introduces no settings. The preserved constants and allowlists are:
|
|
|
|
- world yaw `PI` for `ElwynnCliffRock01/02`;
|
|
- world yaw `PI/2` for `NewWaterfall` and `ElwynnTallWaterfall01`;
|
|
- measured local axes/twist signs for both waterfall sheets;
|
|
- measured tall-waterfall anchor `(-2.667799, 89.62273, 0.00129)`.
|
|
|
|
## Persistence, cache and migration
|
|
|
|
No persistence, schema or cache format participates. Existing ADT/M2 cache
|
|
versions and rebuild instructions remain unchanged.
|
|
|
|
## Diagnostics and observability
|
|
|
|
The resolver emits no logs. The contract verifier compares formulas/constants,
|
|
checks two direct loader consumers plus the grouper consumer and records bounded
|
|
synthetic timing. Visual
|
|
and asset-backed evidence remains in the renderer checkpoint workflow.
|
|
|
|
## Verification
|
|
|
|
- `verify_m2_placement_transform_resolver.gd`: regular fallback, path
|
|
normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale
|
|
compensation, three consumers, source boundary and 10,000-pair timing.
|
|
- M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown,
|
|
scheduler, streaming and coordinate regressions remain required.
|
|
- Fidelity evidence is exact extraction of existing calibrated formulas. The
|
|
private asset/camera comparison that motivated them is not reproduced here.
|
|
|
|
## Extension points
|
|
|
|
- `M2PlacementGrouper` composes this resolver for the worker grouping path.
|
|
- New model corrections require separate measured fidelity evidence; this module
|
|
is not a generic override registry.
|
|
|
|
## Capability status
|
|
|
|
| Capability | Status | Evidence | Gap/next step |
|
|
|---|---|---|---|
|
|
| Consistent transform rules across three M2 paths | Implemented | Formula/source/timing contract | Asset-backed visual recheck pending |
|
|
| General ADT M2 placement parity | Partial | Existing checkpoint notes | Broader fixture coverage required |
|
|
|
|
## Known gaps and risks
|
|
|
|
- Four model-specific exceptions are empirical compatibility rules.
|
|
- Most placements use raw Godot Euler behavior; independent client-wide
|
|
transform validation is incomplete.
|
|
- Callers intentionally retain different final scale clamps.
|
|
- Grouping, build, cache and render ownership remain in the streamer.
|
|
|
|
## Source map
|
|
|
|
| Path | Responsibility |
|
|
|---|---|
|
|
| `src/render/m2/m2_placement_transform_resolver.gd` | Pure basis/offset rules and calibrated allowlists |
|
|
| `src/render/m2/m2_placement_grouper.gd` | Grouping adapter and local Transform3D ownership |
|
|
| `src/scenes/streaming/streaming_world_loader.gd` | Two direct adapters and renderer resource ownership |
|
|
| `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression |
|
|
|
|
## Related decisions and references
|
|
|
|
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
|
|
- [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md)
|
|
- [`world-renderer.md`](world-renderer.md)
|
|
- [`../../RENDER.md`](../../RENDER.md)
|
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|