feat(explore): a commandlet -- 91x faster, and the first real PICTURE
Every experiment in this project cost a build + full-suite cycle. The
landing task took 52 of them over ~9 hours. And nobody could SEE the world:
every visual judgement all session was made on density SLICES -- diagrams,
not pictures. That is how a floor heaving 1.4 m every two metres got
described as "roughness visible but modest" while Jahni spotted it in
thirty seconds by walking it.
-run=VoxelForgeExplore, editor-only module, three modes, one build many runs:
render raymarched PNGs, fixed 0.25-voxel step + 10 bisection iterations
(NOT sphere tracing -- the op stack yields density, not a signed
distance, so a long step tunnels through walls). Three views, each
with a 1.76 m human marker in frame, because every scale mistake
here came from numbers with no body beside them.
walk an agent using the EXISTING VF player-fit stencil, never its own
movement model -- two definitions of "can a player stand here"
would drift and we could not tell which was lying.
export OBJ through the canonical UVoxelMarchingCubesMesher. No second
mesher; a forked meshing invariant is the bug this project keeps
paying for. 23.2 MB, 106,762 verts, 200,842 tris for 128^3.
⭐ THE NUMBER THAT JUSTIFIES IT: the full automation run takes 4,396 s
(73m16s). The commandlet does all three modes in 48.1 s wall, 5.9 s of
which is engine startup. ~91x faster. And I had guessed the suite cost
~90 s -- it is 73 MINUTES. The iteration loop was far more expensive than
I knew.
First real walk metrics on the Maze (seed 0, slot 4):
distance 214 m · tortuosity 4.98 · dead ends 0.935/100 m
narrow-gap events 0 · reachable fit fraction 3.01%
result NotConnectedAtThisResolution
Tortuosity ~5 is a genuinely winding route, which is right for a maze.
3% reachable is not.
Editor Development build clean. SHIPPING build clean with the receipt
containing VoxelForge and NOT VoxelForgeEditor -- the instrument cannot
reach a packaged game. Automation 32/32, 0 failed.
⚠️ Codex initially shipped this UNBUILT, and correctly: I omitted the
## Validation section from that spec, so it fell back to CLAUDE.md's
"NEVER build or compile". 100+ compile errors from three causes -- bare
TCondensedJsonPrintPolicy and TJsonWriter needing their template
arguments, and a missing UObject/Package.h. That include is the SECOND
miss this session, so CODEMAP §6 now carries the gotcha.
⚠️ Unrelated, found in passing: VoxelForge.uplugin does not declare
RealtimeMeshComponent although the runtime module uses it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
1 parent
413c15de24
commit
bd547c35c4
9 files changed
+2327
-24
No files matched your search
@@ -972,6 +972,41 @@ generic biome field), not just SurfaceWorld. Empty `Biomes[]` ⇒ all-zero colou
|
|||||||
- **Status:** C++ ✅ BUILT & WORKING (ticked 2026-07-27). The master material graph is still
|
- **Status:** C++ ✅ BUILT & WORKING (ticked 2026-07-27). The master material graph is still
|
||||||
editor-side work and is deliberately NOT ticked — that half is Jahni's, not the code's.
|
editor-side work and is deliberately NOT ticked — that half is Jahni's, not the code's.
|
||||||
|
|
||||||
|
### 8.16 Explorer commandlet — one authoritative field, three consumers
|
||||||
|
`-run=VoxelForgeExplore` lives in the separate `VoxelForgeEditor` module, so the commandlet and its
|
||||||
|
PNG/JSON/OBJ writers are editor/commandlet-only and cannot enter a Game or Shipping link. It creates
|
||||||
|
only transient settings, definitions, manager, generator, diff layer, and mesher objects; no authored
|
||||||
|
asset, live manager, generator, or diff layer is mutated. The transient world keeps
|
||||||
|
`WorldRadiusVoxels=0` and `InterStrateGapChunks=0`, preserving the lateral-region gate.
|
||||||
|
|
||||||
|
- **Render.** The renderer samples `UVoxelGenerator::GetDensityAt` at a fixed 0.25-voxel default
|
||||||
|
step (overrideable down to 0.125), detects the MC density sign crossing, and refines it with ten
|
||||||
|
bisection iterations. It never treats density as an SDF and never sphere-traces. Each of three
|
||||||
|
deterministic views overlays a projected 1.76 m human-height marker; the JSON records the step,
|
||||||
|
metre conversion, refinement count, and marker projection status.
|
||||||
|
- **Walk.** `VF_MeasurePlayerFitWalkWithSampler` builds the existing step-1 capsule/floor-fit stencil
|
||||||
|
and uses the same six-neighbour/full-resolution route gate as the existing diagnostics. Its
|
||||||
|
deterministic depth-first agent walk counts metres travelled (including branch backtracking),
|
||||||
|
dead ends per 100 m, the arrival component's reachable fit volume, and traversed edges touching a
|
||||||
|
narrow-gap proxy below 1.5× the 0.68 m capsule width. The proxy is explicitly axis-aligned air span;
|
||||||
|
it is not a second player-fit or movement definition. The explorer passes the shared measurement
|
||||||
|
`MaxCells` refusal at a 32,000,000-cell default cap, with a conservative 20-byte/cell estimate
|
||||||
|
against a documented 768 MiB working-memory budget. Reachable fit volume is explicitly the bounded
|
||||||
|
fitted route window because `WorldRadiusVoxels=0` deliberately has no finite whole-world volume.
|
||||||
|
- **Export.** `UVoxelMarchingCubesMesher::GenerateMesh` is the only mesher. Because that canonical
|
||||||
|
entry point deliberately clamps one call to a 32³ tile, a 128³ request is four-by-four-by-four
|
||||||
|
canonical calls whose returned vertices/triangles are streamed with an index offset; no second
|
||||||
|
mesher or density evaluator exists. The bounded result is metre-space OBJ with source winding,
|
||||||
|
voxel-space UVs, normals, and ground/ceiling material groups, accompanied by a manifest containing seed,
|
||||||
|
archetype, slot, bounds, and player dimensions. Tile skirts are disabled at the export boundary
|
||||||
|
because they are a render seam aid that would extend beyond the manifest box. A conservative
|
||||||
|
preflight cap is checked before the canonical mesher is called.
|
||||||
|
|
||||||
|
The report and export manifest use fixed-order writers and serialize twice in-process to assert the
|
||||||
|
deterministic contract. Timings are console diagnostics only; putting wall-clock durations in the report would
|
||||||
|
break byte identity. The browser viewer remains a separate display-only consumer: it must load this
|
||||||
|
OBJ/manifest and never evaluate density or generate geometry.
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
+14
-1
@@ -112,9 +112,10 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
### 3.1 Module & build
|
### 3.1 Module & build
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `../../VoxelForge.uplugin` | Plugin manifest. One Runtime module `VoxelForge`. Beta. |
|
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
|
||||||
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
|
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
|
||||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||||
|
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. |
|
||||||
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. The former ambiguous `Cave Bail Not Op Stack` is split into `Sole Slot`, `Boundary Tile`, `No Layout`, and late `Recheck` counters, so each increment names one guard/context. |
|
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. The former ambiguous `Cave Bail Not Op Stack` is split into `Sole Slot`, `Boundary Tile`, `No Layout`, and late `Recheck` counters, so each increment names one guard/context. |
|
||||||
|
|
||||||
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
||||||
@@ -400,6 +401,16 @@ No UObject state, cache, actor, world, or PIE is required.
|
|||||||
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. |
|
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. |
|
||||||
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
||||||
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
||||||
|
| `FVoxelPlayerFitWalkReport` / `VF_MeasurePlayerFitWalkWithSampler` | Editor-only experiential instrument over the existing exact `VF_` capsule/floor-fit mask: deterministic graph-walk distance, dead ends, reachable fit volume, and a labelled narrow-gap proxy; no second movement model. |
|
||||||
|
|
||||||
|
`VoxelForgeExploreCommandlet` builds a transient, fixed-definition world and calls the production
|
||||||
|
`UVoxelGenerator::GetDensityAt` and `UVoxelMarchingCubesMesher::GenerateMesh` paths. `render` uses
|
||||||
|
fixed density samples (default 0.25 voxel) with bisection at sign crossings instead of sphere tracing;
|
||||||
|
`walk` uses `VF_MeasurePlayerFitWalkWithSampler`; `export` calls the canonical 32³ mesher in a
|
||||||
|
deterministic tile grid (four tiles per axis for the 128³ default), streams only its returned tile
|
||||||
|
arrays into metre-space OBJ plus `manifest.json` (canonical UVs remain voxel-space), and never retains the full region mesh. Output JSON/manifest are emitted by fixed-order writers and compared with a second
|
||||||
|
serialization before it is written. The commandlet keeps `WorldRadiusVoxels=0`, does not touch an
|
||||||
|
authored asset or diff layer, and refuses bounded-grid/export requests before allocating them.
|
||||||
|
|
||||||
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
||||||
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
||||||
@@ -794,6 +805,8 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
are dropped in `ProcessPendingChunks`. Always carry the epoch through new async paths.
|
are dropped in `ProcessPendingChunks`. Always carry the epoch through new async paths.
|
||||||
- **Generated code** under `Intermediate/` and `Binaries/` is build output — never edit.
|
- **Generated code** under `Intermediate/` and `Binaries/` is build output — never edit.
|
||||||
`*.generated.h` / `*.gen.cpp` are UHT output for the `UCLASS`/`USTRUCT` above.
|
`*.generated.h` / `*.gen.cpp` are UHT output for the `UCLASS`/`USTRUCT` above.
|
||||||
|
- **`UPackage` completeness:** files that call `NewObject(..., GetTransientPackage(), ...)` must include
|
||||||
|
`UObject/Package.h` directly; files that do not include `VoxelForgeTestFixture.h` do not get it transitively.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1338,6 +1338,37 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildPlayerFitConnectivityMetrics(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
FVoxelStrateMetrics& OutMetrics,
|
||||||
|
TArray<uint8>& OutPlayerFitMask)
|
||||||
|
{
|
||||||
|
OutMetrics = FVoxelStrateMetrics();
|
||||||
|
OutMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
||||||
|
OutMetrics.SampledMinZ = Grid.SampledMinZ;
|
||||||
|
OutMetrics.SampledMaxZ = Grid.SampledMaxZ;
|
||||||
|
OutMetrics.SampledNumX = Grid.NumX;
|
||||||
|
OutMetrics.SampledNumY = Grid.NumY;
|
||||||
|
OutMetrics.SampledNumZ = Grid.NumZ;
|
||||||
|
OutMetrics.SampledMinX = Grid.MinX;
|
||||||
|
OutMetrics.SampledMaxX = Grid.MaxX;
|
||||||
|
OutMetrics.SampledMinY = Grid.MinY;
|
||||||
|
OutMetrics.SampledMaxY = Grid.MaxY;
|
||||||
|
OutMetrics.NumSampled = Grid.CellCount;
|
||||||
|
for (const uint8 bAir : Grid.Air)
|
||||||
|
{
|
||||||
|
OutMetrics.NumAir += bAir != 0u ? 1 : 0;
|
||||||
|
}
|
||||||
|
OutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - OutMetrics.NumAir;
|
||||||
|
OutMetrics.AirFraction = OutMetrics.NumSampled > 0
|
||||||
|
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
|
||||||
|
/ static_cast<double>(OutMetrics.NumSampled))
|
||||||
|
: 0.0f;
|
||||||
|
BuildPlayerFitMetrics(Grid, Settings, OutMetrics, &OutPlayerFitMask);
|
||||||
|
OutMetrics.bValid = OutMetrics.NumSampled > 0;
|
||||||
|
}
|
||||||
|
|
||||||
float MedianFloat(TArray<float>& Values)
|
float MedianFloat(TArray<float>& Values)
|
||||||
{
|
{
|
||||||
if (Values.Num() == 0) return 0.0f;
|
if (Values.Num() == 0) return 0.0f;
|
||||||
@@ -1948,7 +1979,6 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
bOutStartSnapped = false;
|
bOutStartSnapped = false;
|
||||||
bOutGoalSnapped = false;
|
bOutGoalSnapped = false;
|
||||||
OutNumRouteRetries = 0;
|
OutNumRouteRetries = 0;
|
||||||
|
|
||||||
const EEndpointCellResult StartCell = ResolveEndpointCell(
|
const EEndpointCellResult StartCell = ResolveEndpointCell(
|
||||||
Grid, AVoxel, OutStart, bOutStartSnapped, EligibilityMask);
|
Grid, AVoxel, OutStart, bOutStartSnapped, EligibilityMask);
|
||||||
const EEndpointCellResult GoalCell = ResolveEndpointCell(
|
const EEndpointCellResult GoalCell = ResolveEndpointCell(
|
||||||
@@ -2243,28 +2273,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
// Manhattan feature transform here: those values belong to the separate legacy
|
// Manhattan feature transform here: those values belong to the separate legacy
|
||||||
// measurement pass, and recomputing them for every fine candidate made the gate
|
// measurement pass, and recomputing them for every fine candidate made the gate
|
||||||
// needlessly expensive without changing its answer.
|
// needlessly expensive without changing its answer.
|
||||||
PlayerMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
BuildPlayerFitConnectivityMetrics(Grid, Settings, PlayerMetrics, PlayerFitMask);
|
||||||
PlayerMetrics.SampledMinZ = Grid.SampledMinZ;
|
|
||||||
PlayerMetrics.SampledMaxZ = Grid.SampledMaxZ;
|
|
||||||
PlayerMetrics.SampledNumX = Grid.NumX;
|
|
||||||
PlayerMetrics.SampledNumY = Grid.NumY;
|
|
||||||
PlayerMetrics.SampledNumZ = Grid.NumZ;
|
|
||||||
PlayerMetrics.SampledMinX = Grid.MinX;
|
|
||||||
PlayerMetrics.SampledMaxX = Grid.MaxX;
|
|
||||||
PlayerMetrics.SampledMinY = Grid.MinY;
|
|
||||||
PlayerMetrics.SampledMaxY = Grid.MaxY;
|
|
||||||
PlayerMetrics.NumSampled = Grid.CellCount;
|
|
||||||
for (const uint8 bAir : Grid.Air)
|
|
||||||
{
|
|
||||||
PlayerMetrics.NumAir += bAir != 0u ? 1 : 0;
|
|
||||||
}
|
|
||||||
PlayerMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - PlayerMetrics.NumAir;
|
|
||||||
PlayerMetrics.AirFraction = PlayerMetrics.NumSampled > 0
|
|
||||||
? static_cast<float>(static_cast<double>(PlayerMetrics.NumAir)
|
|
||||||
/ static_cast<double>(PlayerMetrics.NumSampled))
|
|
||||||
: 0.0f;
|
|
||||||
BuildPlayerFitMetrics(Grid, Settings, PlayerMetrics, &PlayerFitMask);
|
|
||||||
PlayerMetrics.bValid = PlayerMetrics.NumSampled > 0;
|
|
||||||
if (OutPlayerMetrics != nullptr)
|
if (OutPlayerMetrics != nullptr)
|
||||||
{
|
{
|
||||||
*OutPlayerMetrics = PlayerMetrics;
|
*OutPlayerMetrics = PlayerMetrics;
|
||||||
@@ -2314,6 +2323,227 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 GetSixNeighbour(const FSampleGrid& Grid, int32 Cell, int32 Direction)
|
||||||
|
{
|
||||||
|
int32 X = 0;
|
||||||
|
int32 Y = 0;
|
||||||
|
int32 Z = 0;
|
||||||
|
DecodeIndex(Grid, Cell, X, Y, Z);
|
||||||
|
switch (Direction)
|
||||||
|
{
|
||||||
|
case 0: return X + 1 < Grid.NumX ? Grid.Index(X + 1, Y, Z) : INDEX_NONE;
|
||||||
|
case 1: return X > 0 ? Grid.Index(X - 1, Y, Z) : INDEX_NONE;
|
||||||
|
case 2: return Y + 1 < Grid.NumY ? Grid.Index(X, Y + 1, Z) : INDEX_NONE;
|
||||||
|
case 3: return Y > 0 ? Grid.Index(X, Y - 1, Z) : INDEX_NONE;
|
||||||
|
case 4: return Z + 1 < Grid.NumZ ? Grid.Index(X, Y, Z + 1) : INDEX_NONE;
|
||||||
|
case 5: return Z > 0 ? Grid.Index(X, Y, Z - 1) : INDEX_NONE;
|
||||||
|
default: return INDEX_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsEligiblePlayerFitCell(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
int32 Cell)
|
||||||
|
{
|
||||||
|
return Cell >= 0
|
||||||
|
&& Cell < Grid.CellCount
|
||||||
|
&& Grid.Air[Cell] != 0u
|
||||||
|
&& PlayerFitMask[Cell] != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 CountPlayerFitNeighbours(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
int32 Cell)
|
||||||
|
{
|
||||||
|
int32 Count = 0;
|
||||||
|
for (int32 Direction = 0; Direction < 6; ++Direction)
|
||||||
|
{
|
||||||
|
if (IsEligiblePlayerFitCell(
|
||||||
|
Grid, PlayerFitMask, GetSixNeighbour(Grid, Cell, Direction)))
|
||||||
|
{
|
||||||
|
++Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 LocalAirSpanVoxels(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
int32 Cell,
|
||||||
|
int32 Axis,
|
||||||
|
int32 MaxRelevantSpanVoxels)
|
||||||
|
{
|
||||||
|
if (Cell < 0 || Cell >= Grid.CellCount || Grid.Air[Cell] == 0u)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 CentreX = 0;
|
||||||
|
int32 CentreY = 0;
|
||||||
|
int32 CentreZ = 0;
|
||||||
|
DecodeIndex(Grid, Cell, CentreX, CentreY, CentreZ);
|
||||||
|
int32 Span = 1;
|
||||||
|
bool bTouchesWindowBoundary = false;
|
||||||
|
for (const int32 Sign : { -1, 1 })
|
||||||
|
{
|
||||||
|
int32 Offset = 1;
|
||||||
|
for (;; ++Offset)
|
||||||
|
{
|
||||||
|
if (Span >= MaxRelevantSpanVoxels)
|
||||||
|
{
|
||||||
|
return Span;
|
||||||
|
}
|
||||||
|
const int32 X = Axis == 0 ? CentreX + Sign * Offset : CentreX;
|
||||||
|
const int32 Y = Axis == 1 ? CentreY + Sign * Offset : CentreY;
|
||||||
|
const int32 Z = CentreZ;
|
||||||
|
if (X < 0 || X >= Grid.NumX || Y < 0 || Y >= Grid.NumY)
|
||||||
|
{
|
||||||
|
bTouchesWindowBoundary = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++Span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A window edge is a lower bound, not a solid wall. Do not label a corridor narrow merely
|
||||||
|
// because the fitted diagnostic box clipped the air span before reaching rock.
|
||||||
|
return bTouchesWindowBoundary ? MAX_int32 : Span * Grid.SampleStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
float LocalWidestHorizontalAirSpanVoxels(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
int32 Cell,
|
||||||
|
float NarrowGapThresholdVoxels)
|
||||||
|
{
|
||||||
|
const int32 MaxRelevantSpanVoxels = FMath::Max(
|
||||||
|
1,
|
||||||
|
FMath::CeilToInt(NarrowGapThresholdVoxels));
|
||||||
|
return static_cast<float>(FMath::Max(
|
||||||
|
LocalAirSpanVoxels(Grid, Cell, 0, MaxRelevantSpanVoxels),
|
||||||
|
LocalAirSpanVoxels(Grid, Cell, 1, MaxRelevantSpanVoxels)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WalkPlayerFitGraph(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
int32 Start,
|
||||||
|
int32 Goal,
|
||||||
|
float NarrowGapThresholdVoxels,
|
||||||
|
FVoxelPlayerFitWalkReport& InOutReport)
|
||||||
|
{
|
||||||
|
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Start)
|
||||||
|
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, Goal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<uint8> Visited;
|
||||||
|
Visited.Init(0u, Grid.CellCount);
|
||||||
|
TArray<uint8> NarrowCell;
|
||||||
|
NarrowCell.Init(255u, Grid.CellCount);
|
||||||
|
TArray<int32> Stack;
|
||||||
|
TArray<int32> NextDirection;
|
||||||
|
// Reserve the full bounded capacity up front. TArray growth slack is otherwise allowed
|
||||||
|
// to exceed the walker's stated memory budget while the DFS discovers a large component.
|
||||||
|
Stack.Reserve(Grid.CellCount);
|
||||||
|
NextDirection.Reserve(Grid.CellCount);
|
||||||
|
Stack.Add(Start);
|
||||||
|
NextDirection.Add(0);
|
||||||
|
Visited[Start] = 1u;
|
||||||
|
|
||||||
|
auto RecordTraversal = [&](int32 From, int32 To)
|
||||||
|
{
|
||||||
|
++InOutReport.AgentGraphTraversals;
|
||||||
|
auto IsNarrowCell = [&](int32 Cell)
|
||||||
|
{
|
||||||
|
if (NarrowCell[Cell] == 255u)
|
||||||
|
{
|
||||||
|
NarrowCell[Cell] = LocalWidestHorizontalAirSpanVoxels(
|
||||||
|
Grid, Cell, NarrowGapThresholdVoxels)
|
||||||
|
< NarrowGapThresholdVoxels ? 1u : 0u;
|
||||||
|
}
|
||||||
|
return NarrowCell[Cell] != 0u;
|
||||||
|
};
|
||||||
|
const bool bNarrow = IsNarrowCell(From) || IsNarrowCell(To);
|
||||||
|
if (bNarrow)
|
||||||
|
{
|
||||||
|
++InOutReport.NarrowGapTraversals;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while (Stack.Num() > 0)
|
||||||
|
{
|
||||||
|
const int32 Current = Stack.Last();
|
||||||
|
if (Current == Goal)
|
||||||
|
{
|
||||||
|
InOutReport.bCanReachDeparture = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bAdvanced = false;
|
||||||
|
while (NextDirection.Last() < 6)
|
||||||
|
{
|
||||||
|
const int32 Direction = NextDirection.Last()++;
|
||||||
|
const int32 Neighbour = GetSixNeighbour(Grid, Current, Direction);
|
||||||
|
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Neighbour)
|
||||||
|
|| Visited[Neighbour] != 0u)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Visited[Neighbour] = 1u;
|
||||||
|
RecordTraversal(Current, Neighbour);
|
||||||
|
Stack.Add(Neighbour);
|
||||||
|
NextDirection.Add(0);
|
||||||
|
bAdvanced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bAdvanced)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A degree-one pose is an actual topological dead end. A higher-degree pose with all
|
||||||
|
// branches already visited is merely a DFS backtrack through a loop/junction.
|
||||||
|
if (Current != Start && Current != Goal
|
||||||
|
&& CountPlayerFitNeighbours(Grid, PlayerFitMask, Current) <= 1)
|
||||||
|
{
|
||||||
|
++InOutReport.DeadEndsEncountered;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack.Pop();
|
||||||
|
NextDirection.Pop();
|
||||||
|
if (Stack.Num() > 0)
|
||||||
|
{
|
||||||
|
RecordTraversal(Current, Stack.Last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DistanceMeters = static_cast<float>(InOutReport.AgentGraphTraversals)
|
||||||
|
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
InOutReport.DistanceTravelledMeters = DistanceMeters;
|
||||||
|
InOutReport.NarrowGapFraction = InOutReport.AgentGraphTraversals > 0
|
||||||
|
? static_cast<float>(static_cast<double>(InOutReport.NarrowGapTraversals)
|
||||||
|
/ static_cast<double>(InOutReport.AgentGraphTraversals))
|
||||||
|
: 0.0f;
|
||||||
|
InOutReport.NarrowGapEventsPer100m = DistanceMeters > KINDA_SMALL_NUMBER
|
||||||
|
? static_cast<float>(static_cast<double>(InOutReport.NarrowGapTraversals)
|
||||||
|
* 100.0 / static_cast<double>(DistanceMeters))
|
||||||
|
: 0.0f;
|
||||||
|
InOutReport.DeadEndsPer100m = DistanceMeters > KINDA_SMALL_NUMBER
|
||||||
|
? static_cast<float>(static_cast<double>(InOutReport.DeadEndsEncountered)
|
||||||
|
* 100.0 / static_cast<double>(DistanceMeters))
|
||||||
|
: 0.0f;
|
||||||
|
InOutReport.DeadEndsEncountered = FMath::Max<int64>(
|
||||||
|
InOutReport.DeadEndsEncountered, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FVoxelStrateMetrics VF_MeasureStrate(
|
FVoxelStrateMetrics VF_MeasureStrate(
|
||||||
@@ -2587,4 +2817,182 @@ FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VF_MeasurePlayerFitWalkWithSampler(
|
||||||
|
const IVoxelStrateDensitySampler& Sampler,
|
||||||
|
int32 StrateBottomWorldZ,
|
||||||
|
int32 StrateTopWorldZ,
|
||||||
|
float BoundarySealThickness,
|
||||||
|
const FVector& AVoxel,
|
||||||
|
const FVector& BVoxel,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
FVoxelPlayerFitWalkReport& OutReport)
|
||||||
|
{
|
||||||
|
using namespace VoxelStrateMeasurePrivate;
|
||||||
|
|
||||||
|
OutReport = FVoxelPlayerFitWalkReport();
|
||||||
|
OutReport.CapsuleRadiusMeters = Settings.PlayerCapsuleRadiusVoxels
|
||||||
|
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
|
||||||
|
OutReport.CapsuleHeightMeters = 2.0f * Settings.PlayerCapsuleHalfHeightVoxels
|
||||||
|
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
OutReport.NarrowGapThresholdMeters = 1.5f * OutReport.CapsuleWidthMeters;
|
||||||
|
OutReport.NarrowGapDefinition = TEXT(
|
||||||
|
"widest axis-aligned horizontal air span at each traversed fit-graph cell");
|
||||||
|
|
||||||
|
const bool bFiniteEndpoints = FMath::IsFinite(AVoxel.X)
|
||||||
|
&& FMath::IsFinite(AVoxel.Y)
|
||||||
|
&& FMath::IsFinite(AVoxel.Z)
|
||||||
|
&& FMath::IsFinite(BVoxel.X)
|
||||||
|
&& FMath::IsFinite(BVoxel.Y)
|
||||||
|
&& FMath::IsFinite(BVoxel.Z);
|
||||||
|
if (!bFiniteEndpoints)
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = TEXT("Arrival and departure points must be finite.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Settings.MaxRouteRetries < 0)
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = TEXT("MaxRouteRetries cannot be negative.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Settings.SampleStep != 1)
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = TEXT(
|
||||||
|
"Player-fit walk refused: exact capsule fit requires SampleStep=1.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FSampleGrid Grid;
|
||||||
|
FString RefusalReason;
|
||||||
|
if (!BuildSampleGrid(
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
&Sampler,
|
||||||
|
INDEX_NONE,
|
||||||
|
StrateBottomWorldZ,
|
||||||
|
StrateTopWorldZ,
|
||||||
|
BoundarySealThickness,
|
||||||
|
true,
|
||||||
|
Settings,
|
||||||
|
true,
|
||||||
|
Grid,
|
||||||
|
RefusalReason))
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = RefusalReason;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<uint8> PlayerFitMask;
|
||||||
|
int64 NumPlayerFitCells = 0;
|
||||||
|
if (!BuildPlayerFitMask(
|
||||||
|
Grid,
|
||||||
|
Settings,
|
||||||
|
PlayerFitMask,
|
||||||
|
NumPlayerFitCells,
|
||||||
|
RefusalReason))
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = RefusalReason;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 Start = INDEX_NONE;
|
||||||
|
int32 Goal = INDEX_NONE;
|
||||||
|
int32 NumRouteRetries = 0;
|
||||||
|
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
|
||||||
|
nullptr,
|
||||||
|
&Sampler,
|
||||||
|
Grid,
|
||||||
|
AVoxel,
|
||||||
|
BVoxel,
|
||||||
|
Start,
|
||||||
|
Goal,
|
||||||
|
OutReport.bStartSnapped,
|
||||||
|
OutReport.bGoalSnapped,
|
||||||
|
NumRouteRetries,
|
||||||
|
Settings.MaxRouteRetries,
|
||||||
|
&PlayerFitMask);
|
||||||
|
|
||||||
|
FVoxelConnectivityDiagnostics Diagnostics;
|
||||||
|
PopulateConnectivityDiagnostics(
|
||||||
|
Grid,
|
||||||
|
Start,
|
||||||
|
Goal,
|
||||||
|
Result,
|
||||||
|
OutReport.bStartSnapped,
|
||||||
|
OutReport.bGoalSnapped,
|
||||||
|
NumRouteRetries,
|
||||||
|
Diagnostics,
|
||||||
|
&PlayerFitMask);
|
||||||
|
|
||||||
|
OutReport.bValid = true;
|
||||||
|
OutReport.Result = Result;
|
||||||
|
OutReport.NumRouteRetries = NumRouteRetries;
|
||||||
|
OutReport.SampledNumX = Grid.NumX;
|
||||||
|
OutReport.SampledNumY = Grid.NumY;
|
||||||
|
OutReport.SampledNumZ = Grid.NumZ;
|
||||||
|
OutReport.SampledMinZ = Grid.SampledMinZ;
|
||||||
|
OutReport.SampledMaxZ = Grid.SampledMaxZ;
|
||||||
|
OutReport.SampledMinX = Grid.MinX;
|
||||||
|
OutReport.SampledMaxX = Grid.MaxX;
|
||||||
|
OutReport.SampledMinY = Grid.MinY;
|
||||||
|
OutReport.SampledMaxY = Grid.MaxY;
|
||||||
|
OutReport.PlayerFitVolumeCells = NumPlayerFitCells;
|
||||||
|
OutReport.ReachablePlayerFitCells = FMath::Max<int64>(
|
||||||
|
Diagnostics.StartComponentCells, 0);
|
||||||
|
OutReport.ReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
|
||||||
|
? static_cast<float>(static_cast<double>(OutReport.ReachablePlayerFitCells)
|
||||||
|
/ static_cast<double>(OutReport.PlayerFitVolumeCells))
|
||||||
|
: 0.0f;
|
||||||
|
OutReport.StraightLineMeters = FVector::Dist(AVoxel, BVoxel)
|
||||||
|
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
|
||||||
|
if (Start >= 0 && Goal >= 0)
|
||||||
|
{
|
||||||
|
const float NarrowGapThresholdVoxels = OutReport.NarrowGapThresholdMeters
|
||||||
|
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
WalkPlayerFitGraph(
|
||||||
|
Grid,
|
||||||
|
PlayerFitMask,
|
||||||
|
Start,
|
||||||
|
Goal,
|
||||||
|
NarrowGapThresholdVoxels,
|
||||||
|
OutReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutReport.bCanReachDeparture = Result == EVoxelConnectivityResult::Connected;
|
||||||
|
OutReport.Tortuosity = OutReport.StraightLineMeters > KINDA_SMALL_NUMBER
|
||||||
|
? OutReport.DistanceTravelledMeters / OutReport.StraightLineMeters
|
||||||
|
: 0.0f;
|
||||||
|
OutReport.DeadEndsEncountered = FMath::Max<int64>(
|
||||||
|
OutReport.DeadEndsEncountered, 0);
|
||||||
|
OutReport.NarrowGapEventsPer100m = OutReport.DistanceTravelledMeters
|
||||||
|
> KINDA_SMALL_NUMBER
|
||||||
|
? static_cast<float>(static_cast<double>(OutReport.NarrowGapTraversals)
|
||||||
|
* 100.0 / static_cast<double>(OutReport.DistanceTravelledMeters))
|
||||||
|
: 0.0f;
|
||||||
|
OutReport.DeadEndsPer100m = OutReport.DistanceTravelledMeters
|
||||||
|
> KINDA_SMALL_NUMBER
|
||||||
|
? static_cast<float>(static_cast<double>(OutReport.DeadEndsEncountered)
|
||||||
|
* 100.0 / static_cast<double>(OutReport.DistanceTravelledMeters))
|
||||||
|
: 0.0f;
|
||||||
|
|
||||||
|
if (Result == EVoxelConnectivityResult::StartCellNotPlayerFit)
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = TEXT("arrival endpoint is not a player-fitting floor cell");
|
||||||
|
}
|
||||||
|
else if (Result == EVoxelConnectivityResult::GoalCellNotPlayerFit)
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = TEXT("departure endpoint is not a player-fitting floor cell");
|
||||||
|
}
|
||||||
|
else if (Result == EVoxelConnectivityResult::OutOfWindow)
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = TEXT("arrival or departure point is outside the fitted walk window");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OutReport.RefusalReason = Diagnostics.RefusalReason;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // WITH_EDITOR — measurement never ships
|
#endif // WITH_EDITOR — measurement never ships
|
||||||
@@ -255,6 +255,61 @@ struct VOXELFORGE_API FVoxelConnectivityDiagnostics
|
|||||||
float GoalToStartComponentDistanceCells = -1.0f;
|
float GoalToStartComponentDistanceCells = -1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experiential traversal facts over the exact VF player-fit mask.
|
||||||
|
*
|
||||||
|
* This is intentionally a report, not a new movement implementation. The mask is built by the
|
||||||
|
* same direct capsule stencil used by VF_DiagnosePlayerFitConnectivityWithSampler; the explorer's
|
||||||
|
* deterministic graph walk only instruments that already-resolved six-neighbour pose graph.
|
||||||
|
* Distances are metres so the result can be read beside a human-scale screenshot.
|
||||||
|
*/
|
||||||
|
struct VOXELFORGE_API FVoxelPlayerFitWalkReport
|
||||||
|
{
|
||||||
|
bool bValid = false;
|
||||||
|
FString RefusalReason;
|
||||||
|
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
|
||||||
|
bool bCanReachDeparture = false;
|
||||||
|
bool bStartSnapped = false;
|
||||||
|
bool bGoalSnapped = false;
|
||||||
|
int32 NumRouteRetries = 0;
|
||||||
|
|
||||||
|
int64 PlayerFitVolumeCells = 0;
|
||||||
|
int64 ReachablePlayerFitCells = 0;
|
||||||
|
float ReachablePlayerFitFraction = 0.0f;
|
||||||
|
int32 SampledNumX = 0;
|
||||||
|
int32 SampledNumY = 0;
|
||||||
|
int32 SampledNumZ = 0;
|
||||||
|
int32 SampledMinZ = 0;
|
||||||
|
int32 SampledMaxZ = 0;
|
||||||
|
float SampledMinX = 0.0f;
|
||||||
|
float SampledMaxX = 0.0f;
|
||||||
|
float SampledMinY = 0.0f;
|
||||||
|
float SampledMaxY = 0.0f;
|
||||||
|
|
||||||
|
// The agent follows a fixed-order depth-first walk over the already-resolved fit graph. A
|
||||||
|
// branch edge is counted twice when the agent backtracks, which makes this an instrument of
|
||||||
|
// traversal experience rather than a shortest-path length.
|
||||||
|
int64 AgentGraphTraversals = 0;
|
||||||
|
int64 DeadEndsEncountered = 0;
|
||||||
|
float DeadEndsPer100m = 0.0f;
|
||||||
|
float DistanceTravelledMeters = 0.0f;
|
||||||
|
float StraightLineMeters = 0.0f;
|
||||||
|
float Tortuosity = 0.0f;
|
||||||
|
|
||||||
|
// A narrow-gap event is a traversed graph edge touching a route cell whose widest
|
||||||
|
// axis-aligned horizontal air span is below the threshold. This deliberately reports the
|
||||||
|
// operational proxy instead of pretending the voxel mask contains an oriented corridor width.
|
||||||
|
int64 NarrowGapTraversals = 0;
|
||||||
|
float NarrowGapFraction = 0.0f;
|
||||||
|
float NarrowGapThresholdMeters = 0.0f;
|
||||||
|
float NarrowGapEventsPer100m = 0.0f;
|
||||||
|
|
||||||
|
float CapsuleRadiusMeters = 0.0f;
|
||||||
|
float CapsuleWidthMeters = 0.0f;
|
||||||
|
float CapsuleHeightMeters = 0.0f;
|
||||||
|
FString NarrowGapDefinition;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measure one strate in actor-space voxel coordinates.
|
* Measure one strate in actor-space voxel coordinates.
|
||||||
*
|
*
|
||||||
@@ -395,3 +450,18 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWit
|
|||||||
const FVector& BVoxel,
|
const FVector& BVoxel,
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
FVoxelStrateMetrics* OutPlayerMetrics = nullptr);
|
FVoxelStrateMetrics* OutPlayerMetrics = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrument a deterministic agent over the same exact player-fit graph used by the route gate.
|
||||||
|
* The return value is false only when the bounded fine grid/stencil could not be built; a valid
|
||||||
|
* report may still say NotConnectedAtThisResolution or another explicit connectivity result.
|
||||||
|
*/
|
||||||
|
VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
|
||||||
|
const IVoxelStrateDensitySampler& Sampler,
|
||||||
|
int32 StrateBottomWorldZ,
|
||||||
|
int32 StrateTopWorldZ,
|
||||||
|
float BoundarySealThickness,
|
||||||
|
const FVector& AVoxel,
|
||||||
|
const FVector& BVoxel,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
FVoxelPlayerFitWalkReport& OutReport);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
class FVoxelForgeEditorModule final : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void StartupModule() override {}
|
||||||
|
virtual void ShutdownModule() override {}
|
||||||
|
};
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FVoxelForgeEditorModule, VoxelForgeEditor)
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Commandlets/Commandlet.h"
|
||||||
|
#include "VoxelForgeExploreCommandlet.generated.h"
|
||||||
|
|
||||||
|
/** Headless, editor-only explorer for authoritative density, traversal, and mesh output. */
|
||||||
|
UCLASS()
|
||||||
|
class VOXELFORGEEDITOR_API UVoxelForgeExploreCommandlet : public UCommandlet
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UVoxelForgeExploreCommandlet();
|
||||||
|
|
||||||
|
virtual int32 Main(const FString& Params) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Editor-only commandlets and offline explorers. This module is not linked by Game/Shipping.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class VoxelForgeEditor : ModuleRules
|
||||||
|
{
|
||||||
|
public VoxelForgeEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
// The commandlet's PNG shading, metre conversions, and JSON fields are part of its
|
||||||
|
// deterministic artifact contract, so keep the editor-side arithmetic on the same precise
|
||||||
|
// floating-point model as the authoritative Runtime module.
|
||||||
|
FPSemantics = FPSemanticsMode.Precise;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Json",
|
||||||
|
"ImageWrapper",
|
||||||
|
"VoxelForge",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
"Name": "VoxelForge",
|
"Name": "VoxelForge",
|
||||||
"Type": "Runtime",
|
"Type": "Runtime",
|
||||||
"LoadingPhase": "Default"
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "VoxelForgeEditor",
|
||||||
|
"Type": "Editor",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user