Files
Fr0zkaandClaude Opus 5.5 1abee91721 refactor: resolved metre layout is the runtime authority (L41, migration step 3); stamp covers plugin sources; R8
UVoxelStrateManager::Initialize resolves the layout width to integer micrometres before passages or
scheduling; each slot stores physical [bottom, top) bounds, transition mode/width and passage settings;
chunk indices remain a floor-based compatibility projection. The Core adapter derives interiors and L29
bands from those resolved values. verify: full-tier cost baseline is HEAD (or --baseline), tier timeout
7200 s with 1800 s per process; the source stamp now includes the plugin Source tree compiled on Linux
(it previously missed VoxelStrateManager.cpp and friends). Owner hashes identical to L37; fast, medium
and full PASS on a clean rebuild. Unreal manager/adapter edits are uncompiled on Windows.
Research R8: inventory of unit-bearing UPROPERTYs and a custom-version asset migration plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-23 19:36:51 +02:00

18 KiB
Raw Permalink Blame History

R8 — metre authoring in Unreal assets

Read-only source and UE documentation review. No source, build, or test changes.

1. Reflected spatial-property inventory

Conversion notation: n is the old serialized number. Use double precision and integer micrometres at the adapter boundary. owner voxel = 0.125 m (VOXEL_SIZE); Core voxel = that world's saved CoreVoxelSizeCm / 100 (default 0.25 m); legacy runtime chunk = 32 owner voxels = 4 m; vertical layout chunk = the captured old StrateLayoutChunkSizeMeters (8 m default in current manager code). Frequencies are cycles per old voxel: f_new_per_m = f_old / oldVoxelMeters. Inverse-frequency fallbacks must be converted with the same route.

Reflected fields (source line) Current unit and use Convert to metres / losslessness
UVoxelStrateDefinition::StrateHeightInChunks ( VoxelStrateDefinition.h:75 ), TransitionBlendChunks (:103), UVoxelSettings::InterStrateGapChunks (VoxelSettings.h:420) Integer vertical layout chunks; manager assigns slot bounds and transition bands. heightM = count × captured old layoutChunkM; same for gap and transition width. Exact and yields the same chunk indices only when old effective width is known and new metres remain an integer multiple of it. Current manager uses StrateLayoutChunkSizeMeters (VoxelSettings.h:427, default 8 m); the strate comment says 4 m (VoxelStrateDefinition.h:71–73). This must be resolved from each owner's pre-update world, not guessed.
UVoxelSettings::StrateLayoutChunkSizeMeters (VoxelSettings.h:427) Already metres; physical vertical-layout chunk width, independent of Core tile/voxel width. Keep as metres. It is the basis for converting height, gap and transition values, not the 4 m legacy streaming chunk.
ViewDistanceXY/Up/Down (VoxelSettings.h:44,47,50), PawnCollisionRadiusChunks (:96), StrateViewMarginChunks (:111), RenderDistanceChunks (:183), DecorationRadiusChunks / DecorationNearRadiusChunks (:241,248); FStrateLandmark::SpacingChunks, StreamRadiusChunks, ExclusionRadiusChunks, SuppressRadiusChunks (VoxelStrateTypes.h:2298,2335,2344,2360) Legacy chunk counts controlling streaming extents, collision work, content grids and landmark placement. These routes define one chunk as 4 m (32 × 12.5 cm); landmark/content code multiplies by the runtime chunk width. m = n × 4. Exact for the old grid. Keep StrateLayoutChunkSizeMeters separate; it is not this chunk. If these knobs remain integer counts internally, recover round(m/4) only for the old 4 m grid.
WorldRadiusVoxels, EdgeSealThickness, OriginSpineRadius, MaxBrushRadius (VoxelSettings.h:222,226,440,465); decoration spacing/far spacing/march step/column depth (:263,271,277,289); FDecoSubCompanion::RadiusMinVox/RadiusMaxVox (VoxelStrateTypes.h:2127,2130), FDecoCompanion::RadiusMinVox/RadiusMaxVox (:2168,2171); landmark orb radius/falloff/max-shadow distance (:2383,2388,2393) Owner Runtime voxels at 12.5 cm. Used as radii, bounds, placement spacing/search steps, or light ranges. m = n × 0.125. Exact physical conversion for the 12.5 cm grid. The spacing/step fields need metre-aware sampling, not just renamed UI labels.
FStrateGenerationParams (VoxelStrateTypes.h: WormNetworkRange 458; RoomSpacing 479; MinRoomRadius 496; MaxRoomRadius 504; FloorReliefStrength 552; RoomMouthRiseBlendVoxels 571; OriginRoomRadius 593; TunnelMinRadius 615; TunnelMaxRadius 622; MaxTunnelLength 637; TunnelWarpStrength 649; TunnelFloorMaxLedgeHeight 708; SDFBlendRadius 739; CaveWarpStrength 762; SurfaceRoughness 786; DomainWarpStrength 815; FloorBias 840; BoundarySealThickness 1076) Owner-voxel distances/displacements used by cave morphology, warps, roughness, tunnel/room envelopes and seals. m = n × 0.125. Exact for physical lengths. TunnelFloorTerraceStepHeight (:702) is serialized but explicitly ignored; keep it as deprecated compatibility data instead of converting a dead control. TunnelEndpointZOffset (:678) and TunnelFloorGentleSlopeThreshold (:716) are ratios/degrees, not lengths.
FStrateGenerationParams frequencies (VoxelStrateTypes.h:418,560,771,793,822) WormFrequency, FloorReliefFrequency, CaveWarpFrequency, RoughnessFrequency, DomainWarpFrequency; inverse owner-voxel wavelength. cycles/m = f / 0.125 (multiply by 8). Exact change of units; resulting sampled output is byte-identical only if the evaluator uses the same operation/rounding order.
FSlabGenerationParams (VoxelStrateTypes.h: FloorRoughness 1259; ColumnMinRadius 1311; ColumnMaxRadius 1317; ColumnSpacing 1324; CeilingRoughness 1280; BoundarySealThickness 1334) and FMazeGenerationParams (:CellSize 1371; CorridorRadius 1377; SurfaceRoughness 1391; BoundarySealThickness 1395) Owner-voxel geometry: surfaces, formations, columns, cells, corridors and seals. m = n × 0.125, exact physically. Slab frequencies at :1266,1288 use cycles/m = f/0.125. Relative floor/ceiling heights, density and probabilities remain dimensionless.
FSurfaceGenerationParams geometry (VoxelStrateTypes.h: ElevationRange 1430; SurfaceRoughness 1452; HeightWarpStrength 1460; TerraceHeight 1490; LayerLineDepth 1509; LayerLineSpacing 1513; CliffSampleDist 1537; OverhangReach 1554; OverhangHeight 1560; BeachWidth 1588; CeilingRoughness 1599; CeilingUndulation 1613; CeilingRidgeStrength 1625; CeilingWarpStrength 1635; BoundarySealThickness 1643) Voxel lengths/displacements. Current Core SurfaceWorld adapter consumes these raw in Core-voxel units via WorldAssemblyFixed; the legacy owner path is 12.5 cm, while the current Core default is 25 cm. To preserve the current Core recipe, m = n × captured CoreVoxelSizeCm/100 (default n×0.25), not n×0.125. If preserving a legacy Runtime route instead, use that route's 0.125 m scale. This route distinction is essential.
FSurfaceGenerationParams frequencies (VoxelStrateTypes.h:1434,1443,1447,1464,1470,1565,1604,1617,1629,1639) Continent, mountain, detail, height-warp, relief, overhang, ceiling roughness/undulation/ridge/warp frequencies. Current SurfaceWorld adapter treats them as cycles per Core voxel. cycles/m = f / captured CoreVoxelMeters (×4 at 25 cm, ×8 at 12.5 cm). Use fallbackM = fallbackOldVoxels × CoreVoxelMeters when old frequency is zero. Exact unit conversion; see adapter caveat below.
FVerticalShaftParams (VoxelStrateTypes.h: ShaftSpacing 1672; radii 1680,1683; ConnectorRadius 1691; LedgeSpacing 1696; LedgeDepth 1700; SurfaceRoughness 1704; BoundarySealThickness 1708), FFloatingIslandParams (:IslandSpacing 1737; radii 1746,1749; roughness 1766; SDFBlendRadius 1771; seal 1775), FStrateDisturbanceParams (:ChasmSpacing/Radius 1808,1810; bridge spacing/radius 1817,1819; ridge spacing/height/thickness 1826,1828,1830; seal 1838) Owner voxels; FI comments explicitly define owner scale and current adapter converts FI dimensions by owner/Core ratio. m = n×0.125; exact. Disturbance and shaft spacing become metres too. Density, thickness ratio and jitter remain unitless.
FStratePassageConfig (VoxelStrateTypes.h: MouthRadius 1886; MidRadius 1889; ReachMin/Max 1895,1898; DistanceMin/Max 1904,1907; Wander 1913; VerticalWobble 1922; SpiralRadius 1927; CascadeLedge 1938) Owner voxels; current Core adapter explicitly maps these through OwnerVoxelsToCoreQ. m=n×0.125; exact. Connections, Segments, CascadeSteps are counts; SpiralTurns is turns, so keep those. The adapter's fixed 12-owner-voxel bore/wall reserves also need to become explicit 1.5 m / 0.5 m constants.
FBiomeMapParams (VoxelBiomeTypes.h: CellSize 37; WarpStrength 42; BorderBlend 51; frequencies 46,63,71) Widths and warp displacement are voxel units; current biome recipe adapter packs them as Core voxels. Climate/warp frequencies are inverse Core-voxel units on that route. Width/displacement: m=n×CoreVoxelMeters; frequency: cycles/m=f/CoreVoxelMeters. The legacy owner evaluator instead requires 0.125 m. ReliefContrast is unitless.
UVoxelTerrainOpDefinition (VoxelTerrainOpDefinition.h: TerraceStepHeight 91; LayerLineSpacing 112; RibbingSpacing 127; OverhangDepth 174; arch radii 195,201; column radii 218,224; pit radii/depth 241,247,253; chimney radii/height 270,276,282; dome radii 297,303; PinchStrength/PinchLength 328,334) Legacy owner-voxel distances and SDF radii/lengths; PinchStrength is a physical narrowing width in morphology. m=n×0.125. ScallopFrequency (:159) and OverhangFrequency (:179) become f/0.125 cycles/m. LayerLineDepth (:118) and RibbingDepth (:133) are density amplitudes, not distances; leave them outside metre conversion.
FPlacementProfile::SurfaceOffset, LocationOffset, CullDistance (VoxelStrateTypes.h:2068,2072,2097); UVoxelStrateDefinition::CeilingLayerZOffset/FloorLayerZOffset (VoxelStrateDefinition.h:316,329); AVoxelWorld::CoreVoxelSizeCm (VoxelWorld.h:304) Unreal centimetres. Placement vector is post-snap world offset; layer offsets are vertical offsets; Core voxel field is a world resolution (12.5–25 cm). Scalar/vector centimetres: /100 per component. Core resolution: CoreVoxelSizeMeters=cm/100. Exact at micrometre precision for current values. Do not bake this world-level value into a shared strate asset if different worlds use different Core sizes.
EditorBrushCenter, EditorBrushRadius, BiomePreviewWorldSize, BiomePreviewCenter (VoxelWorld.h:862,866,902,906) Brush center is Unreal cm; brush radius and preview window size/center are legacy owner-voxel coordinates. Center cm /100; other values/components ×0.125. BiomePreviewCenter is a coordinate, not a size, but its units still change.
UVoxelSeasonAsset mirrors (VoxelSeasonAsset.h:56,59) and VoxelSettings::WorldAssemblyManifestJson (VoxelSettings.h:392) Reflected mirror values are voxel-valued; the JSON is serialized manifest text, not a scalar property. Manifest schema has HeightInChunks, InterStrateGapChunks, OriginSpineRadius, WorldRadiusVoxels and per-strate HeightInChunks (VoxelSeasonManifest.h:88–91,164). Migrate the source JSON with its own schema-version step, using the same route-aware formulas; refresh mirrors and content hash. Do not multiply opaque JSON text or convert mirrors independently. CSV/DataTable import sources also need a separate migration.

ClipRadius (VoxelSettings.h:153) and AVoxelWorld::CoreClipRadius (VoxelWorld.h:308) are tile counts, SkirtCells (VoxelSettings.h:193) scales with LOD step, and CoarseTileCells/clip levels/region sizes are algorithmic counts. They have no single metre conversion that preserves current semantics across LODs; keep as counts or redesign the algorithm around a physical extent. Runtime-only FVoxelModification in VoxelDiffLayer.h is not reflected (UPROPERTY) and is not in this inventory.

Losslessness: 0.125/0.25 m voxels and 4/8 m chunks map exactly to integer micrometres. The Core recipe uses 1/256 Core-voxel fixed point; old values reproduce its old integer Q only when converted from the actual route and old world resolution. Arbitrary new metre values round to that Q grid, so they are not universally lossless. Use one llround at the adapter boundary and compare the resulting integer fields.

2. UE5 migration mechanism

Use a plugin FGuid custom version registered with a static FCustomVersionRegistration; migrate loaded properties in each owning UObject's Serialize. UE documents this GUID/version pattern and FArchive::UsingCustomVersion for object-level asset conversion (asset/package versioning, FCustomVersionRegistration, UsingCustomVersion, UObject::Serialize).

// Registered once in the plugin module; increment Version when a later migration is added.
Ar.UsingCustomVersion(FVoxelForgeAssetVersion::GUID);
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.CustomVer(FVoxelForgeAssetVersion::GUID) < MetersV1)
{
    HeightMeters = double(StrateHeightInChunks_DEPRECATED) * CapturedOldLayoutChunkMeters;
    TransitionWidthMeters = double(TransitionBlendChunks_DEPRECATED) * CapturedOldLayoutChunkMeters;
    // Convert each nested parameter by its recorded legacy route, then mark schema state.
}

Run this for settings, strate, biome, terrain-op, and world owners. A world-specific CoreVoxelSizeCm and old effective layout-chunk width must be available to the converter; do not infer either from current CDO defaults. Existing UVoxelStrateDefinition::PostLoad is useful for object cleanup, but PostLoad is not called for newly-created objects (UE PostLoad). PostEditChangeProperty is for editor reactions/previews after a user edit, not load migration; use it only to refresh validation/UI when a new metre field changes (property-change event API).

For one transition release, keep each old reflected property as a deprecated _DEPRECATED field with the old type and old CDO default, and add the new ...Meters property with meta=(Units="m"). On rename, use exact PropertyRedirects from the old serialized name to its deprecated holding property, then copy/convert in Serialize; redirects remap names/values on load but do not change units (Core Redirects). Keep deprecated BP references compilable until graphs are updated. Epic notes metadata is editor-only, so runtime conversion must use the actual numeric field, not Units metadata (metadata specifiers). Keep the old CDO defaults for this release: old assets may omit values equal to their old CDO default. Convert old-version assets even when the new field has its default; do not mistake that for already migrated data.

For USTRUCT rows in DataTables, add a version-aware PostSerialize(const FArchive&) via TStructOpsTypeTraits::WithPostSerialize, or run the same conversion in a one-time editor asset upgrader. Reimported CSV/JSON is external source data and will overwrite the table, so migrate that source too. Keep redirects and deprecated fields through a full resave/reference audit, then remove them in a later version.

3. Owner checklist

  • Back up the Windows project/source control state. Before updating, record each world's old CoreVoxelSizeCm, effective layout-chunk metres, settings, seed and exported ResolvedLayoutSnapshotV3 (strata bounds, gaps, transition widths, passage Q values and hash). This resolves the code's 4 m comment / current 8 m default conflict.
  • Update the plugin, open and compile affected Blueprints, then load/save every referenced Settings, Strate, Biome, TerrainOp, Season, world/level and DataTable asset. Check conversion logs and save every dirty package. An asset never loaded and saved still has its old on-disk values; cooking can consume an in-memory conversion but is not a source-asset resave.
  • Replace old Blueprint property nodes and fix deprecation/redirect warnings. Convert CSV/JSON sources before reimport; verify Season manifest schema and content hash refreshed.
  • Re-export the same worlds with the same seed. Compare integer micrometre bounds, transition fields, passage fixed-point values, recipe Q/wavelength fields and ResolvedLayoutSnapshotV3 stable identity against the pre-update export; inspect a clean cook/reopen for missing assets and redirect warnings.

4. Core adapter mapping

  • AttachValidatedWorldAssembly currently resolves layout chunk micrometres once (VoxelForgeCoreAdapter.cpp:818–822); snapshot interiors and transition width use chunk indices × that width (:1098–1108). Feed it metre-valued bounds/gap/transition as integer micrometres. For converted legacy assets, convert back to the same chunk indices and preserve the same integer fields/hash. Reject or extend the Core snapshot schema for heights that are not integral layout chunks; the current manager/resolved layout is chunk-indexed.
  • Replace WorldAssemblyFixed(value) (:67–71, currently round(value×256) voxels) with MetersToCoreQ(m) = round(m×1,000,000×256/CoreVoxelMicrometres). Replace WorldAssemblyWavelength(f, fallbackVoxels) (:78–83) with a metre-frequency form: MetersToCoreQ(f>0 ? 1/f_cyclesPerMeter : fallbackMeters). For old SurfaceWorld and biome recipes, n Core voxels → n×CoreVoxelMeters, so current Q remains n×256; old frequency f becomes f/CoreVoxelMeters, preserving its wavelength Q.
  • For Floating Islands and passages, current code uses the 12.5 cm owner/Core ratio (:492–524, :1061–1085). Convert old owner values once to n×0.125m, then call MetersToCoreQ; at 25 cm Core this yields the same n×128 Q as the existing ratio. Do not multiply by the ratio a second time. Convert hard-coded 12/4 owner-voxel bore/reserve constants to 1.5/0.5 m.
  • Set resolved voxelSizeMicrometres from round(voxelSizeMeters×1e6) (:1098). Current runtime still has one actor-level Core sampling size and writes that same size for every strate; until Core sampling supports mixed per-strate resolutions, validate that every resolved strate value equals the world's CoreVoxelSizeCm/100 (normally 0.25 m) rather than publishing conflicting metadata.
  • Exact parity means the converted adapter emits the same old integer Q, micrometre intervals and canonical V3 layout identity for each old asset. Keep the old schema path/rounding for converted assets until that comparison passes.

Owner question: Can you capture each world's pre-update layout export, including its effective layout chunk width and CoreVoxelSizeCm (especially any world not at 25 cm)?