Merge pull request #422 from EVEngine/codex/tileworld-creator-core-port

feat(procgen): port TileWorldCreator graph runtime
This commit is contained in:
西风逍遥游 authored and GitHub committed 2026-09-19 01:31:14 +08:00
commit 829d8367bd
32 files changed
+5221 -512

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# TileWorldCreator 4 核心功能移植到 EVEngine
## 范围
本移植以 `TileWorldCreator 4 v4.3.5.unitypackage` 中的运行时算法、图组合、网格构建和配置语义为
行为参考,采用 EVEngine 原生 `Grid2D`、`PointSet`、`PointGraph`、`MeshBuild`、Result 与模块句柄。
“核心功能完成”不包括 Unity Inspector/EditorWindow 外观、Unity Prefab/ScriptableObject 二进制兼容、
Unity Renderer/Collider 组件或示例美术资产;这些是宿主编辑器与资产格式,不是生成算法本身。
## 功能对应
- Dot Grid → `generate.dot_grid`
- Cellular Automata → `generate.cellular`
- Pathfinding → `grid.path`
- BSP Dungeon → `generate.registry` + `algorithm=dungeon.bsp`;完整 Roguelike 地牢使用
`algorithm=level.roguelike`,不在 GridGraph 内维护第二套地牢算法
- Random Walk Dungeon → `generate.random_walk`
- Maze → `generate.maze`
- Height Texture → `Grid2D.detail` 输入与 `select.detail_range`
- Random Noise → `generate.random_noise`
- Shapes → `generate.shape`
- Checkerboard → `generate.checkerboard`
- Poisson Disc Sampling → `generate.poisson`
- Add / Boolean / Subtract → `grid.union`、`grid.intersect`、`grid.subtract`
- Smooth / Expand / Shrink / Invert → 同名 `grid.*` 节点
- Select / Select by neighbour / Select by rule → `select.random`、`select.neighbors`、`select.rule`
- Select islands / Find position on islands → `select.islands`、`select.island_centers`
- Push to paint positions → 图输出的拥有型 `Grid2D`;由调用方显式绑定为下游输入,不使用隐式全局画布
- Tile preset / neighbour rules → `grid.autotile` 把八邻域 mask 写入 detail;
`resolveTilePreset` 按 4.3.5 原表精确解析 14 类标准网格瓦片、旋转、X 镜像和 9 位配置
- Object build layer → `ObjectBuildLayer` 把 PointGraph 输出转换为带 `asset`、父子关系和完整变换的
`PointSet`,支持确定性加权选择、随机变换、朝向层、place-on-top 高度属性和子对象规则
- Persistent build configuration → `BuildLayerStack` 按插入顺序执行启用的 Tiles/Objects 层,使用
`EVPCG_BUILD_LAYERS 1` 与嵌入式 `EVPCG_OBJECT_LAYER 1` 格式原子回读,失败不发布部分 artifacts
- Incremental build → `IncrementalBuildExecutor` 按固定格子簇缓存 BuildLayerStack artifacts;哈希包含层定义、
格子一圈 halo、完整点字段和强类型属性,输出有序 upsert/removal delta,任一簇失败则旧缓存原样保留
- Tiles build layer / mesh combiner → `mesh.grid_tiles`、`mesh.merge`、`mesh.transform`
- Grid mesh collider generator → 增量示例从每簇 `MeshBuild` 创建 `World3D` 静态三角网格碰撞体;
更新采用先建后换,removal 同步销毁,且 physics provider 缺席时可观察地退化为纯渲染
- Runtime manager / configuration → Procgen 模块拥有的代际句柄、图 revision、版本 1 定义格式和统一 Result
## 混合调用契约
标准路径为:
`GridGraph -> convert.grid_to_points -> point.subgraph(PointGraph) -> MeshGraph.point.input -> mesh.instance_points`
网格铺砖路径为:
`GridGraph -> grid.autotile -> MeshGraph.grid.input -> mesh.grid_tiles -> MeshBuild/GeneratedArtifact`
跨域只传递拥有型值或显式句柄。连接建立时检查端口类型;修改从节点向下游失效缓存;图实例仅允许
所属线程访问且不可重入。Grid/Mesh 定义只持久化拓扑与标量参数,场景输入在恢复后重新绑定。
## 确定性与失败语义
随机生成器使用图参数中的命名 seed;相同版本、尺寸、参数和 seed 产生相同网格。图执行成本与访问
格子数、输出顶点数或“源网格顶点数 × 点数量”成线性关系。无效操作、参数、类型、断开的必需输入、
环、尺寸不匹配和损坏资产均返回结构化 Result,不使用 `bool + lastError`。
## 验证入口
- `examples/tileworld-graph-dungeon`:可直接运行的资产无关 3D 示例,覆盖注册表 Roguelike 地牢、
`select.semantic` 分层、autotile、随机选择、
Grid→PointGraph、版本化 BuildLayerStack、PointSet→MeshGraph 实例化,以及边界墙、导航和装饰层;
地面与对象场景缓存直接消费 8×8 格子簇的 upsert/removal delta,不再额外执行完整 build stack。
同一 delta 还驱动每簇静态三角网格碰撞体的替换/回收。运行时会选取可行走端点、执行
`grid.path`,并验证 MeshBuild 上传、空对象簇、热重载和换 seed 重建。
- `test/procgen_grid_graph.cpp`:生成器、选择器、寻路、autotile、TilePreset 精确配置、Grid→PointGraph、Grid/Point→Mesh、
类型拒绝、环、缓存失效和两种定义格式回读。
- `test/procgen_object_build_layer.cpp`:加权资产、确定性随机变换、朝向、place-on-top、子对象和失败路径。
- `test/procgen_build_layer_stack.cpp`:层顺序/启用状态、混合 artifact、版本往返、未知字段和损坏配置原子拒绝。
- `test/procgen_incremental_build.cpp`:稳定缓存、簇内与边界脏区传播、删除 delta、全局坐标网格分片、
参数拒绝以及失败时事务回滚。
- `test/procgen_result_contract.cpp`:Squirrel 创建拥有型 Grid2D 并直接绑定 GridGraph,随后执行
GridGraph/MeshGraph 并取得拥有型 MeshBuild,覆盖跨图代理的生命周期与类型契约。
- `src/modules/procgen/GridGraphAlgorithms.cpp`:独立确定性算法实现。
+64
View File
@@ -2980,3 +2980,67 @@ Created、1 为 Removed。新增项按本次扫描顺序排列,随后删除项
spawner 计数,`updateRuleFraction` 更新当前规则的小数进度;`getProgress` 使用
`(totalCompleted + fraction) / totalCount`。`getTitle`、`getSubtitle`、`getStatus` 提供 UI 所需快照,
`requestCancel` 发布取消请求,`clear` 隐藏并清空状态。所有更新先验证候选,非法计数或进度不修改旧值。
## GridGraph、PointGraph 与 MeshGraph 混合编排
`GridGraph` 是面向语义格子的确定性数据流图。内建掩码生成节点覆盖填充、随机噪声、棋盘、点阵、
形状、元胞自动机、随机游走、迷宫和 Poisson 撒点;`generate.registry` 则直接复用现有
`GeneratorRegistry`,以 `algorithm` 字符串选择 `dungeon.bsp`、`level.roguelike`、WFC 等成熟
生成器,并把节点上的整数、浮点和字符串参数投影为 `Params`。地牢算法只保留注册表中的一份实现。
修改与选择节点覆盖布尔组合、反转、膨胀、收缩、平滑、随机/边缘/邻居/规则/岛屿/岛心/
细节范围/语义选择、寻路与八邻域 autotile。固定 seed 的生成结果可复现。
`convert.grid_to_points` 是显式的类型边界:它将匹配语义的格子中心转换为 `PointSet`,并写入
`cell_x`、`cell_y`、`semantic` 和 `detail` 属性。`point.subgraph` 随后可把这组点送入独立的
`PointGraph`,用于筛选、变换、采样和组合。
`MeshGraph` 接受 `Grid2D`、`PointSet` 与 `MeshBuild` 三种强类型输入。`mesh.grid_tiles` 从格子
构造顶面和暴露侧墙,并按
`group/semantic/tile-kind/rN/(normal|mx)/reference-configuration` 建立命名三角形组。
其中标准网格 mask 会映射到 TileWorldCreator 4.3.5 的 3×3 配置表,直接携带瓦片类别、
0/90/180/270 度朝向和 X 镜像语义;原版未归类的配置 350 保持为 `none`。
`eve.ProcgenObjectBuildLayer()` 是 Objects Build Layer 的原生对应物。它接收 PointGraph 产生的
`PointSet`,按稳定点身份和层 seed 确定性选择加权 `asset`,并应用层偏移、位置散布、Euler
旋转、统一或非统一缩放。`setOrientation` 可按北、东、南、西优先级朝向另一点层;
`setPlaceOnTop` 读取输入点的 `surface_highest_y` 或 `surface_lowest_y`。`addChild` 生成带
`object_role=child` 与 `parent_index` 的子对象点。结果仍是普通 PointSet,可继续送入 PointGraph、
`mesh.instance_points` 或实例发布接口。
对象层 API 速查:`clearAssets` 清空加权资产;`setLayerOffset`、`setLayerScale` 设置层级变换;
`setPositionRadius` 设置平面散布半径;`setRandomRotation`、`setRandomScale` 设置确定性随机范围;
`disableOrientation` 关闭朝向层;`clearChildRules` 清空子对象规则;`buildOriented` 使用显式朝向点集
执行对象层。GridGraph 与 MeshGraph 的运行时输入分别通过 `setNodeGrid` 绑定;GridGraph 使用
`setNodePointSubgraph` 绑定拥有明确输入/输出节点的 PointGraph 子图。
`eve.ProcgenBuildLayerStack()` 将 Tiles 与 Objects 定义放进同一个有序执行器。`addTileLayer` 和
`addObjectLayer` 保存值副本,`setEnabled` 控制参与执行的层;`execute` 只在全部启用层成功后返回
`ProcgenBuildLayerExecution`,再通过 `getType/getMesh/getPoints` 获取具名 artifact。配置使用
`EVPCG_BUILD_LAYERS 1`,对象层使用嵌入式 `EVPCG_OBJECT_LAYER 1`;反序列化拒绝未知记录和尾随字段,
并在完整校验成功后原子替换旧配置。
层栈可用 `getLayerId`、`getLayerType` 和 `isLayerEnabled` 查询定义;`executeOriented` 为需要朝向
点集的对象层执行完整栈。增量执行器的 `getCachedClusterCount` 返回当前成功提交的活动簇数量,
失败构建不会改变该计数。
`eve.ProcgenIncrementalBuildExecutor()` 为 BuildLayerStack 增加跨次构建缓存。`update` 接收簇边长(格子数)
和格子世界尺寸,返回 `ProcgenIncrementalBuildDelta`;每项通过 `getClusterX/getClusterZ/isRemoved` 标识
场景侧应更新或删除的簇,非删除项用 `getArtifacts` 取得该簇的完整层结果。格子哈希带一格 halo,
因此边界 autotile 改动会同时使相邻簇失效;点按半开世界坐标范围归属一个簇。任一脏簇构建失败时,
内部旧缓存不变。带朝向层时使用 `updateOriented`;`getCachedArtifacts` 可按簇坐标读取当前快照。
场景侧应以 `clusterX:clusterZ` 作为派生缓存键:普通 delta 替换该簇的渲染/碰撞 artifact,removed
delta 先从场景隐藏或移除旧对象再删除缓存项。空 PointSet 是合法簇结果,不应尝试上传空实例网格。
示例中的物理消费者按同一键维护静态三角网格刚体:先成功创建替换体再销毁旧体,避免失败时留下
无碰撞窗口;裁剪掉 `physics` 模块时明确退化为纯渲染路径,不影响生成结果与缓存权威状态。
`mesh.instance_points` 把任意 `MeshBuild` 实例化到点集;`mesh.merge` 与 `mesh.transform` 完成
组合和变换。输出仍是现有 `MeshBuild`,可继续交给统一发布、碰撞、材质和场景生命周期。
三个图各自拥有缓存,不共享跨帧可变裸指针。连接时检查端口类型,Grid 输出不能直接连到
Point 或 Mesh 输入,必须经过显式转换或输入绑定。图拓扑、参数或输入变化会从修改点向下游
失效缓存,相同输入重复执行会复用结果。
脚本通过 `procgen.newGridGraph()` 与 `procgen.newMeshGraph()` 获取模块拥有的句柄;所有失败均为
统一 Result 投影。两种图定义都支持版本化 `serializeDefinition()` / `deserializeDefinition()`;
运行时输入值刻意不写入定义,加载后由调用方重新绑定,避免资产与场景实例形成第二份权威状态。
TileWorldCreator 4 运行时核心能力的逐项对应和边界见
[`TileWorldCreator4核心移植.md`](../../dev/TileWorldCreator4核心移植.md)。
+1
View File
@@ -121,6 +121,7 @@ Windows 本地直接跑同样的命令即可:脚本自身按 UTF-8 写 stdout/
|---|---|
| [procgen](procgen/README.md) | 六种地图算法(BSP / Cellular / Drunkard / Maze / 地形 / WFC)+ 纹理配方 |
| [procgen-script-pipeline](procgen-script-pipeline/README.md) | 纯脚本 PointSet 组合、确定性 seed、事务式 hot reload |
| [tileworld-graph-dungeon](tileworld-graph-dungeon/README.md) | TileWorldCreator 风格 GridGraph + PointGraph + MeshGraph 混合地牢、分层构建、增量簇更新与可选物理碰撞 |
| [pcg-biome](pcg-biome/README.md) | UE PCG 风格空间数据、多层运行时 Cell、时间预算与 Scene 实例批次 |
| [pcg-runtime-orchestration](pcg-runtime-orchestration/README.md) | Pcg 运行时编排的数值契约:运行时盖章器 + 生成进度 + 任务队列(无 GPU 场景,等价脚本级单测) |
| [pcg-location-system](pcg-location-system/README.md) | 位置书签:相机位姿 + 玩家位姿 + 控制器 + 场景名的命名书签存取与取景 |
@@ -0,0 +1,37 @@
# TileWorld Graph Dungeon
Asset-free 3D example for the TileWorldCreator-style graph port.
The live path is:
`GridGraph(GeneratorRegistry(level.roguelike) -> semantic layers -> autotile -> random selection -> grid-to-points)`
`-> PointGraph(transform)`
`-> BuildLayerStack + IncrementalBuildExecutor(cluster delta)`
`-> MeshGraph(grid tiles + point instancing)`
`-> MeshBuild -> Vulkan renderables`.
The example uses four independent build layers:
- autotile-mask floor groups with per-group material tints;
- a raised boundary-wall layer extracted from the roguelike generator's wall-autotile masks;
- a gold navigation layer produced by `grid.path` between deterministic endpoints;
- sparse procedural rock instances generated through PointGraph.
The example executes the stack through an 8x8-cell incremental cluster cache.
Its scene cache consumes each upsert/removal delta to replace or hide only the
affected floor and prop renderables, then repeats the same update to prove the
stable delta is empty. Each live cluster also owns a static triangle-mesh
collider derived from the exact same floor artifact. Collider replacement is
construct-before-destroy, and removal destroys the matching body. When the
`physics` module is trimmed, this optional consumer is visibly reported as
`physics=disabled` and rendering continues. The example does not execute a
second full-stack preview build.
- `R`: regenerate with the next deterministic seed.
- `Space`: pause/resume the camera orbit.
Run from the repository root:
```powershell
make run/win32-debug GAME=examples/tileworld-graph-dungeon
```
@@ -0,0 +1,7 @@
config <- {
width = 1100,
height = 720,
title = "TileWorld Graph Dungeon",
debug = false,
hotReload = true
};
+354
View File
@@ -0,0 +1,354 @@
// TileWorldCreator-style runtime example implemented with EVEngine-native graphs.
// R regenerates with the next deterministic seed. Space toggles the camera orbit.
persist twSeed = 435
persist twCamera = null
persist twIncrementalBuild = null
persist twClusterParts = {}
persist twPhysicsWorld = null
persist twWalls = null
persist twPath = null
persist twYaw = -0.72
persist twRotate = true
persist twPreviousKeys = {}
const TW_WIDTH = 25;
const TW_HEIGHT = 19;
const TW_CELL = 2.0;
function twRequire(result, context) {
if (!result.ok) throw context + ": " + result.status.summary;
return result.value;
}
function twPressed(key) {
local down = keyboard.isDown(key);
local previous = key in twPreviousKeys ? twPreviousKeys[key] : false;
twPreviousKeys[key] <- down;
return down && !previous;
}
function twBuildRock() {
local params = twRequire(procgen.newParams(), "rock parameters");
params.setSeed(twSeed + 991);
params.setInt("subdivisions", 0);
params.setString("baseShape", "block");
params.setFloat("radius", 0.32);
params.setFloat("variation", 0.18);
params.setFloat("flattening", 0.25);
params.setFloat("angularity", 0.65);
return twRequire(procgen.buildMesh("mesh.rock", params), "rock mesh");
}
function twBuildClusterCollider(meshBuild) {
if (twPhysicsWorld == null) return null;
local vertices = [];
local indices = [];
for (local i = 0; i < meshBuild.getVertexCount(); ++i) {
vertices.append(meshBuild.getPositionX(i));
vertices.append(meshBuild.getPositionY(i));
vertices.append(meshBuild.getPositionZ(i));
}
for (local i = 0; i < meshBuild.getIndexCount(); ++i)
indices.append(meshBuild.getIndex(i));
if (vertices.len() < 9 || indices.len() < 3) return null;
// Construct-before-destroy keeps the previous collision authoritative if
// validation or Box3D mesh creation rejects the replacement.
local body = twPhysicsWorld.newBody("static", 0.0, 0.0, 0.0);
try {
body.newTriangleMeshShape(vertices, indices);
} catch (error) {
twPhysicsWorld.destroyBody(body);
throw error;
}
return body;
}
function twFirstOccupied(grid) {
for (local y = 0; y < grid.getHeight(); ++y)
for (local x = 0; x < grid.getWidth(); ++x)
if (grid.getCell(x, y) != 0) return [x, y];
return null;
}
function twLastOccupied(grid) {
for (local y = grid.getHeight() - 1; y >= 0; --y)
for (local x = grid.getWidth() - 1; x >= 0; --x)
if (grid.getCell(x, y) != 0) return [x, y];
return null;
}
function twBuild() {
if (twPhysicsWorld == null && has_module("physics"))
twPhysicsWorld = physics.newWorld3D(0.0, 0.0, 0.0, false);
// PointGraph is nested in GridGraph: selected grid cells become elevated points.
local pointGraph = twRequire(procgen.newPointGraph(), "point graph");
if (!pointGraph.addNode("input", "input")) throw "PointGraph input node failed";
if (!pointGraph.addNode("raise", "transform")) throw "PointGraph transform node failed";
if (!pointGraph.connect("input", "raise", 0)) throw "PointGraph connection failed";
if (!pointGraph.setNodeFloat("raise", "y", 0.62)) throw "PointGraph parameter failed";
local gridGraph = twRequire(procgen.newGridGraph(), "grid graph");
// Reuse EVEngine's mature semantic dungeon generator as a graph source.
twRequire(gridGraph.addNode("rooms", "generate.registry"), "registry generator node");
twRequire(gridGraph.setNodeString("rooms", "algorithm", "level.roguelike"), "generator algorithm");
twRequire(gridGraph.setNodeInt("rooms", "width", TW_WIDTH), "dungeon width");
twRequire(gridGraph.setNodeInt("rooms", "height", TW_HEIGHT), "dungeon height");
twRequire(gridGraph.setNodeInt("rooms", "seed", twSeed), "dungeon seed");
twRequire(gridGraph.setNodeInt("rooms", "roomCount", 8), "room budget");
twRequire(gridGraph.setNodeInt("rooms", "roomMin", 4), "minimum room size");
twRequire(gridGraph.setNodeInt("rooms", "roomMax", 7), "maximum room size");
twRequire(gridGraph.setNodeString("rooms", "layoutStyle", "clustered"), "room layout");
twRequire(gridGraph.setNodeString("rooms", "connectionStyle", "nearest"), "room connections");
twRequire(gridGraph.setNodeString("rooms", "corridorStyle", "l"), "corridor style");
twRequire(gridGraph.setNodeString("rooms", "floorPattern", "brick"), "floor pattern");
twRequire(gridGraph.setNodeFloat("rooms", "propDensity", 0.14), "room prop density");
twRequire(gridGraph.setNodeFloat("rooms", "corridorLightDensity", 0.04), "corridor lights");
twRequire(gridGraph.addNode("floor", "select.semantic"), "floor semantic selector");
twRequire(gridGraph.addNode("corridors", "select.semantic"), "corridor semantic selector");
twRequire(gridGraph.addNode("walls", "select.semantic"), "wall semantic selector");
twRequire(gridGraph.setNodeInt("floor", "semantic", 2), "floor semantic");
twRequire(gridGraph.setNodeInt("corridors", "semantic", 3), "corridor semantic");
twRequire(gridGraph.setNodeInt("walls", "semantic", 1), "wall semantic");
twRequire(gridGraph.connect("rooms", "floor", 0), "floor extraction");
twRequire(gridGraph.connect("rooms", "corridors", 0), "corridor extraction");
twRequire(gridGraph.connect("rooms", "walls", 0), "wall extraction");
twRequire(gridGraph.addNode("wall_edges", "select.detail_range"), "wall edge selector");
twRequire(gridGraph.setNodeInt("wall_edges", "min", 1), "wall edge minimum mask");
twRequire(gridGraph.setNodeInt("wall_edges", "max", 255), "wall edge maximum mask");
twRequire(gridGraph.connect("walls", "wall_edges", 0), "wall edge connection");
twRequire(gridGraph.addNode("walkable", "grid.union"), "walkable union node");
twRequire(gridGraph.connect("floor", "walkable", 0), "floor union connection");
twRequire(gridGraph.connect("corridors", "walkable", 1), "corridor union connection");
twRequire(gridGraph.addNode("autotile", "grid.autotile"), "autotile node");
twRequire(gridGraph.connect("walkable", "autotile", 0), "autotile connection");
twRequire(gridGraph.addNode("scatter", "select.random"), "scatter node");
twRequire(gridGraph.setNodeFloat("scatter", "weight", 0.055), "scatter weight");
twRequire(gridGraph.setNodeInt("scatter", "seed", twSeed + 17), "scatter seed");
twRequire(gridGraph.connect("autotile", "scatter", 0), "scatter connection");
twRequire(gridGraph.addNode("points", "convert.grid_to_points"), "point conversion node");
twRequire(gridGraph.setNodeFloat("points", "cellSize", TW_CELL), "point cell size");
twRequire(gridGraph.connect("scatter", "points", 0), "point conversion connection");
twRequire(gridGraph.addNode("point_pass", "point.subgraph"), "PointGraph bridge node");
twRequire(gridGraph.connect("points", "point_pass", 0), "PointGraph bridge connection");
twRequire(gridGraph.setNodePointSubgraph("point_pass", pointGraph, "input", "raise"),
"PointGraph bridge binding");
local level = twRequire(gridGraph.execute("rooms"), "semantic dungeon execution");
local grid = twRequire(gridGraph.execute("autotile"), "grid execution");
local border = twRequire(gridGraph.execute("wall_edges"), "wall execution");
local points = twRequire(gridGraph.execute("point_pass"), "point execution");
// Deterministic Objects Build Layer: weighted assets, random transforms and
// child placements remain ordinary PointSet data for downstream graphs.
local objectLayer = eve.ProcgenObjectBuildLayer();
twRequire(objectLayer.addAsset("dungeon/rock_a", 3.0), "rock A asset");
twRequire(objectLayer.addAsset("dungeon/rock_b", 1.0), "rock B asset");
objectLayer.setSeed(twSeed);
twRequire(objectLayer.setPositionRadius(0.22), "object scatter radius");
twRequire(objectLayer.setRandomRotation(0.0, 0.0, -180.0, 180.0, 0.0, 0.0),
"object rotation range");
twRequire(objectLayer.setRandomScale(0.72, 1.18, 0.72, 1.18, 0.72, 1.18, true),
"object scale range");
twRequire(objectLayer.addChild("dungeon/rubble", 1, 0.48, 0.25, 0.5, -180.0, 180.0),
"rubble child rule");
local buildStack = eve.ProcgenBuildLayerStack();
twRequire(buildStack.addTileLayer("floor", true, TW_CELL, 0.34, "dungeon"), "floor build layer");
twRequire(buildStack.addObjectLayer("props", true, objectLayer), "object build layer");
local persistedStack = buildStack.serializeDefinition();
local restoredStack = eve.ProcgenBuildLayerStack();
twRequire(restoredStack.deserializeDefinition(persistedStack), "build layer restore");
if (twIncrementalBuild == null) twIncrementalBuild = eve.ProcgenIncrementalBuildExecutor();
local clusterDelta = twRequire(twIncrementalBuild.update(restoredStack, grid, points, 8, TW_CELL),
"incremental cluster build");
local stableDelta = twRequire(twIncrementalBuild.update(restoredStack, grid, points, 8, TW_CELL),
"stable incremental cluster build");
// Scene-side cluster cache consumes only upserts/removals from the executor.
// No full BuildLayerStack execution is needed for the visible floor/props.
local rockBuild = twBuildRock();
for (local i = 0; i < clusterDelta.getCount(); ++i) {
local clusterX = clusterDelta.getClusterX(i);
local clusterZ = clusterDelta.getClusterZ(i);
local key = clusterX + ":" + clusterZ;
if (clusterDelta.isRemoved(i)) {
if (key in twClusterParts) {
twClusterParts[key].floor.setVisible(false);
twClusterParts[key].props.setVisible(false);
if (twPhysicsWorld != null && "collider" in twClusterParts[key] &&
twClusterParts[key].collider != null)
twPhysicsWorld.destroyBody(twClusterParts[key].collider);
delete twClusterParts[key];
}
continue;
}
local artifacts = twRequire(clusterDelta.getArtifacts(i), "cluster artifacts");
local floorBuild = twRequire(artifacts.getMesh(0), "cluster floor artifact");
local objectPoints = twRequire(artifacts.getPoints(1), "cluster object artifact");
local floorMesh = twRequire(procgen.uploadMesh(floorBuild, gfx), "cluster floor upload");
if (!(key in twClusterParts))
twClusterParts[key] <- {
floor = eve.Renderable3D(), props = eve.Renderable3D(), collider = null
};
local part = twClusterParts[key];
if (!("collider" in part)) part.collider <- null;
local replacementCollider = twBuildClusterCollider(floorBuild);
if (twPhysicsWorld != null && part.collider != null)
twPhysicsWorld.destroyBody(part.collider);
part.collider = replacementCollider;
part.floor.setMesh(floorMesh);
part.floor.setVisible(true);
part.floor.setTint(0.31 + 0.025 * ((clusterX + clusterZ) & 1), 0.43, 0.53, 1.0);
part.floor.setRoughness(0.94);
part.floor.setMetallic(0.02);
part.floor.setCastShadow(true);
part.floor.setReceiveShadow(true);
if (objectPoints.getCount() == 0) {
part.props.setVisible(false);
} else {
local propGraph = twRequire(procgen.newMeshGraph(), "cluster prop mesh graph");
twRequire(propGraph.addNode("rock", "mesh.input"), "cluster rock input");
twRequire(propGraph.addNode("points", "point.input"), "cluster point input");
twRequire(propGraph.addNode("instances", "mesh.instance_points"), "cluster instance node");
twRequire(propGraph.setNodeMesh("rock", rockBuild), "cluster rock binding");
twRequire(propGraph.setNodePoints("points", objectPoints), "cluster point binding");
twRequire(propGraph.setNodeFloat("instances", "scale", 1.0), "cluster instance scale");
twRequire(propGraph.connect("rock", "instances", 0), "cluster rock connection");
twRequire(propGraph.connect("points", "instances", 1), "cluster point connection");
local propBuild = twRequire(propGraph.execute("instances"), "cluster instance execution");
local propMesh = twRequire(procgen.uploadMesh(propBuild, gfx), "cluster prop upload");
part.props.setMesh(propMesh);
part.props.setVisible(true);
part.props.setTint(0.80, 0.48, 0.19, 1.0);
part.props.setRoughness(0.78);
part.props.setMetallic(0.08);
part.props.setCastShadow(true);
part.props.setReceiveShadow(true);
}
}
// Feed explicit owned Grid2D endpoint masks into GridGraph. This exercises the
// public newGrid -> GridGraph mixed-composition path, not an internal shortcut.
local startCell = null;
local targetCell = null;
for (local i = 0; i < level.getObjectCount(); ++i) {
local role = level.getObjectType(i);
if (role == "spawn") startCell = [level.getObjectX(i).tointeger(), level.getObjectY(i).tointeger()];
if (role == "stairs") targetCell = [level.getObjectX(i).tointeger(), level.getObjectY(i).tointeger()];
}
if (startCell == null) startCell = twFirstOccupied(grid);
if (targetCell == null) targetCell = twLastOccupied(grid);
if (startCell == null || targetCell == null ||
(startCell[0] == targetCell[0] && startCell[1] == targetCell[1]))
throw "generated layout has no distinct navigable endpoint pair";
local startSelection = twRequire(procgen.newGrid(TW_WIDTH, TW_HEIGHT), "route start grid");
local targetSelection = twRequire(procgen.newGrid(TW_WIDTH, TW_HEIGHT), "route target grid");
twRequire(startSelection.fill(0), "route start clear");
twRequire(targetSelection.fill(0), "route target clear");
twRequire(startSelection.setCell(startCell[0], startCell[1], 1), "route start cell");
twRequire(targetSelection.setCell(targetCell[0], targetCell[1], 1), "route target cell");
twRequire(gridGraph.addNode("route_start", "grid.input"), "route start input");
twRequire(gridGraph.addNode("route_target", "grid.input"), "route target input");
twRequire(gridGraph.setNodeGrid("route_start", startSelection), "route start binding");
twRequire(gridGraph.setNodeGrid("route_target", targetSelection), "route target binding");
twRequire(gridGraph.addNode("route", "grid.path"), "path node");
twRequire(gridGraph.connect("autotile", "route", 0), "navigation connection");
twRequire(gridGraph.connect("route_start", "route", 1), "start connection");
twRequire(gridGraph.connect("route_target", "route", 2), "target connection");
local route = twRequire(gridGraph.execute("route"), "path execution");
local routeCells = 0;
for (local y = 0; y < TW_HEIGHT; ++y)
for (local x = 0; x < TW_WIDTH; ++x)
if (route.getCell(x, y) != 0) routeCells += 1;
if (routeCells < 2) throw "path output is unexpectedly short";
// A second build layer raises only the selected boundary cells into walls.
local wallGraph = twRequire(procgen.newMeshGraph(), "wall mesh graph");
twRequire(wallGraph.addNode("grid", "grid.input"), "wall grid input");
twRequire(wallGraph.addNode("walls", "mesh.grid_tiles"), "wall mesh node");
twRequire(wallGraph.setNodeGrid("grid", border), "wall grid binding");
twRequire(wallGraph.setNodeFloat("walls", "cellSize", TW_CELL), "wall cell size");
twRequire(wallGraph.setNodeFloat("walls", "height", 1.65), "wall height");
twRequire(wallGraph.setNodeString("walls", "group", "walls"), "wall group");
twRequire(wallGraph.connect("grid", "walls", 0), "wall connection");
local wallBuild = twRequire(wallGraph.execute("walls"), "wall mesh execution");
local wallMesh = twRequire(procgen.uploadMesh(wallBuild, gfx), "wall mesh upload");
// Navigation is another independent build layer laid just above the floor.
local pathGraph = twRequire(procgen.newMeshGraph(), "path mesh graph");
twRequire(pathGraph.addNode("grid", "grid.input"), "path grid input");
twRequire(pathGraph.addNode("path", "mesh.grid_tiles"), "path mesh node");
twRequire(pathGraph.setNodeGrid("grid", route), "path grid binding");
twRequire(pathGraph.setNodeFloat("path", "cellSize", TW_CELL), "path cell size");
twRequire(pathGraph.setNodeFloat("path", "height", 0.43), "path height");
twRequire(pathGraph.setNodeString("path", "group", "navigation"), "path group");
twRequire(pathGraph.connect("grid", "path", 0), "path mesh connection");
local pathBuild = twRequire(pathGraph.execute("path"), "path mesh execution");
local pathMesh = twRequire(procgen.uploadMesh(pathBuild, gfx), "path mesh upload");
if (twWalls == null) twWalls = eve.Renderable3D();
twWalls.setMesh(wallMesh);
twWalls.setTint(0.18, 0.24, 0.31, 1.0);
twWalls.setRoughness(0.88);
twWalls.setMetallic(0.06);
twWalls.setCastShadow(true);
twWalls.setReceiveShadow(true);
if (twPath == null) twPath = eve.Renderable3D();
twPath.setMesh(pathMesh);
twPath.setTint(0.92, 0.56, 0.12, 1.0);
twPath.setRoughness(0.72);
twPath.setMetallic(0.04);
twPath.setCastShadow(false);
twPath.setReceiveShadow(true);
print("tileworld-graph-dungeon: seed=" + twSeed +
" cells=" + grid.getWidth() + "x" + grid.getHeight() +
" dungeonObjects=" + level.getObjectCount() +
" liveClusters=" + twClusterParts.len() +
" wallTriangles=" + (wallBuild.getIndexCount() / 3) +
" routeCells=" + routeCells +
" props=" + points.getCount() +
" colliderBodies=" + (twPhysicsWorld == null ? 0 : twPhysicsWorld.getBodyCount()) +
" physics=" + (twPhysicsWorld == null ? "disabled" : "clustered") +
" dirtyClusters=" + clusterDelta.getCount() +
" stableClusters=" + stableDelta.getCount() + "\n");
}
if (twCamera == null) {
twCamera = eve.Camera3D();
twCamera.setActive(true);
}
local twCenterX = TW_WIDTH * TW_CELL * 0.5;
local twCenterZ = TW_HEIGHT * TW_CELL * 0.5;
twCamera.setEye(twCenterX + cos(twYaw) * 76.0, 58.0,
twCenterZ + sin(twYaw) * 76.0);
twCamera.setTarget(twCenterX, 0.0, twCenterZ);
twCamera.setUp(0.0, 1.0, 0.0);
twCamera.setFov(42.0);
twCamera.setClipPlanes(0.1, 300.0);
twCamera.setAmbient(0.31, 0.34, 0.39);
gfx.setDirectionalLight(-0.55, -1.0, -0.35, 1.65, 1.48, 1.24);
gfx.setBackgroundColor(0.035, 0.055, 0.085, 1.0);
if (twClusterParts.len() == 0 || twWalls == null || twPath == null) twBuild();
eve_update = function(dt) {
if (twPressed("r") || twPressed("R")) {
twSeed = (twSeed + 1).tointeger();
twBuild();
}
if (twPressed("space")) twRotate = !twRotate;
if (twRotate) twYaw = (twYaw + dt * 0.10).tofloat();
twCamera.setEye(twCenterX + cos(twYaw) * 76.0, 58.0,
twCenterZ + sin(twYaw) * 76.0);
};
eve_render = function() {
gfx.clear();
gfx.render3D();
};
+249
View File
@@ -0,0 +1,249 @@
#include "procgen/BuildLayerStack.h"
#include "common/Diagnostic.h"
#include "procgen/MeshGraph.h"
#include <cmath>
#include <iomanip>
#include <limits>
#include <sstream>
namespace eve::procgen {
namespace {
template <class T>
Result<T> fail(DiagnosticCode code, std::string message, std::string path = {}) {
return Result<T>::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, "procgen.buildLayers"));
}
std::string hexEncode(std::string_view value) {
constexpr char digits[] = "0123456789abcdef";
std::string output;
output.reserve(value.size() * 2);
for (const unsigned char byte : value) {
output.push_back(digits[byte >> 4U]);
output.push_back(digits[byte & 15U]);
}
return output;
}
Result<std::string> hexDecode(std::string_view value) {
if (value.size() % 2 != 0) return fail<std::string>(DiagnosticCode::ParseError, "odd embedded definition size");
const auto nibble = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
std::string output(value.size() / 2, '\0');
for (std::size_t index = 0; index < value.size(); index += 2) {
const int high = nibble(value[index]);
const int low = nibble(value[index + 1]);
if (high < 0 || low < 0) return fail<std::string>(DiagnosticCode::ParseError, "invalid embedded definition");
output[index / 2] = char((high << 4) | low);
}
return Result<std::string>::success(std::move(output));
}
} // namespace
int BuildLayerExecution::getCount() const noexcept { return int(artifacts_.size()); }
std::string BuildLayerExecution::getId(int index) const {
return index >= 0 && index < int(artifacts_.size()) ? artifacts_[std::size_t(index)].id : std::string();
}
std::string BuildLayerExecution::getType(int index) const {
if (index < 0 || index >= int(artifacts_.size())) return {};
return std::holds_alternative<MeshBuild>(artifacts_[std::size_t(index)].value) ? "mesh" : "points";
}
Result<MeshBuild> BuildLayerExecution::getMesh(int index) const {
if (index < 0 || index >= int(artifacts_.size()))
return fail<MeshBuild>(DiagnosticCode::InvalidArgument, "artifact index is out of range");
const auto* value = std::get_if<MeshBuild>(&artifacts_[std::size_t(index)].value);
if (!value) return fail<MeshBuild>(DiagnosticCode::TypeMismatch, "artifact is not a mesh");
return Result<MeshBuild>::success(*value);
}
Result<PointSet> BuildLayerExecution::getPoints(int index) const {
if (index < 0 || index >= int(artifacts_.size()))
return fail<PointSet>(DiagnosticCode::InvalidArgument, "artifact index is out of range");
const auto* value = std::get_if<PointSet>(&artifacts_[std::size_t(index)].value);
if (!value) return fail<PointSet>(DiagnosticCode::TypeMismatch, "artifact is not a point set");
return Result<PointSet>::success(*value);
}
bool BuildLayerStack::contains(std::string_view id) const {
for (const auto& layer : layers_)
if (layer.id == id) return true;
return false;
}
Result<void> BuildLayerStack::addTileLayer(std::string id, bool enabled, float cellSize, float height,
std::string group) {
if (id.empty()) return fail<void>(DiagnosticCode::InvalidArgument, "layer id is empty");
if (contains(id)) return fail<void>(DiagnosticCode::Conflict, "duplicate layer id", id);
if (!std::isfinite(cellSize) || cellSize <= 0.f || !std::isfinite(height))
return fail<void>(DiagnosticCode::InvalidArgument, "tile dimensions are invalid", id);
layers_.push_back({std::move(id), enabled, TileLayer{cellSize, height, std::move(group)}});
return Result<void>::success();
}
Result<void> BuildLayerStack::addObjectLayer(std::string id, bool enabled, const ObjectBuildLayer& layer) {
if (id.empty()) return fail<void>(DiagnosticCode::InvalidArgument, "layer id is empty");
if (contains(id)) return fail<void>(DiagnosticCode::Conflict, "duplicate layer id", id);
layers_.push_back({std::move(id), enabled, layer});
return Result<void>::success();
}
Result<void> BuildLayerStack::setEnabled(std::string_view id, bool enabled) {
for (auto& layer : layers_) {
if (layer.id != id) continue;
layer.enabled = enabled;
return Result<void>::success();
}
return fail<void>(DiagnosticCode::NotFound, "unknown layer", std::string(id));
}
void BuildLayerStack::clear() { layers_.clear(); }
int BuildLayerStack::getLayerCount() const noexcept { return int(layers_.size()); }
std::string BuildLayerStack::getLayerId(int index) const {
return index >= 0 && index < int(layers_.size()) ? layers_[std::size_t(index)].id : std::string();
}
std::string BuildLayerStack::getLayerType(int index) const {
if (index < 0 || index >= int(layers_.size())) return {};
return std::holds_alternative<TileLayer>(layers_[std::size_t(index)].definition) ? "tiles" : "objects";
}
bool BuildLayerStack::isLayerEnabled(int index) const noexcept {
return index >= 0 && index < int(layers_.size()) && layers_[std::size_t(index)].enabled;
}
Result<BuildLayerExecution> BuildLayerStack::execute(const Grid2D& grid, const PointSet& points,
const PointSet* orientation) const {
return executeRegion(grid, points,
{0, 0, grid.getWidth(), grid.getHeight(), -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity()},
orientation);
}
Result<BuildLayerExecution> BuildLayerStack::executeRegion(const Grid2D& grid, const PointSet& points,
const BuildLayerRegion& region,
const PointSet* orientation) const {
if (region.minCellX < 0 || region.minCellY < 0 || region.maxCellX < region.minCellX ||
region.maxCellY < region.minCellY || region.maxCellX > grid.getWidth() || region.maxCellY > grid.getHeight() ||
!(region.minWorldX < region.maxWorldX) || !(region.minWorldZ < region.maxWorldZ))
return fail<BuildLayerExecution>(DiagnosticCode::InvalidArgument, "invalid build-layer region");
PointSet regionPoints;
regionPoints.reserve(points.points().size());
for (std::size_t index = 0; index < points.points().size(); ++index) {
const auto& point = points.points()[index];
if (point.x < region.minWorldX || point.x >= region.maxWorldX || point.z < region.minWorldZ ||
point.z >= region.maxWorldZ)
continue;
auto appended = regionPoints.appendPointFrom(points, index);
if (!appended.ok()) return Result<BuildLayerExecution>::failure(appended.status());
}
BuildLayerExecution execution;
execution.artifacts_.reserve(layers_.size());
for (const auto& layer : layers_) {
if (!layer.enabled) continue;
if (const auto* tile = std::get_if<TileLayer>(&layer.definition)) {
MeshGraph graph;
auto result = graph.addNode("grid", "grid.input");
if (result.ok()) result = graph.addNode("tiles", "mesh.grid_tiles");
if (result.ok()) result = graph.setNodeGrid("grid", grid);
if (result.ok()) result = graph.setNodeFloat("tiles", "cellSize", tile->cellSize);
if (result.ok()) result = graph.setNodeFloat("tiles", "height", tile->height);
if (result.ok()) result = graph.setNodeFloat("tiles", "minX", float(region.minCellX));
if (result.ok()) result = graph.setNodeFloat("tiles", "minY", float(region.minCellY));
if (result.ok()) result = graph.setNodeFloat("tiles", "maxX", float(region.maxCellX));
if (result.ok()) result = graph.setNodeFloat("tiles", "maxY", float(region.maxCellY));
if (result.ok()) result = graph.setNodeString("tiles", "group", tile->group);
if (result.ok()) result = graph.connect("grid", "tiles");
if (!result.ok()) return Result<BuildLayerExecution>::failure(result.status());
auto mesh = graph.execute("tiles");
if (!mesh.ok()) return Result<BuildLayerExecution>::failure(mesh.status());
execution.artifacts_.push_back({layer.id, std::move(mesh).takeValue()});
} else {
auto built = std::get<ObjectBuildLayer>(layer.definition).build(regionPoints, orientation);
if (!built.ok()) return Result<BuildLayerExecution>::failure(built.status());
execution.artifacts_.push_back({layer.id, std::move(built).takeValue()});
}
}
return Result<BuildLayerExecution>::success(std::move(execution));
}
std::string BuildLayerStack::serializeDefinition() const {
std::ostringstream out;
out << "EVPCG_BUILD_LAYERS 1\n" << std::setprecision(9);
for (const auto& layer : layers_) {
if (const auto* tile = std::get_if<TileLayer>(&layer.definition)) {
out << "TILE " << std::quoted(layer.id) << ' ' << int(layer.enabled) << ' ' << tile->cellSize << ' '
<< tile->height << ' ' << std::quoted(tile->group) << '\n';
} else {
const auto& object = std::get<ObjectBuildLayer>(layer.definition);
out << "OBJECT " << std::quoted(layer.id) << ' ' << int(layer.enabled) << ' '
<< hexEncode(object.serializeDefinition()) << '\n';
}
}
out << "END\n";
return out.str();
}
Result<void> BuildLayerStack::deserializeDefinition(std::string_view definition) {
BuildLayerStack replacement;
std::istringstream input{std::string(definition)};
std::string magic;
int version = 0;
if (!(input >> magic >> version) || magic != "EVPCG_BUILD_LAYERS" || version != 1)
return fail<void>(DiagnosticCode::ParseError, "invalid build-layer header");
std::string line;
std::getline(input, line);
bool ended = false;
while (std::getline(input, line)) {
if (line.empty()) continue;
std::istringstream record(line);
std::string kind;
record >> kind;
if (kind == "END") {
ended = true;
break;
}
std::string id;
int enabled = 0;
if (kind == "TILE") {
float cellSize = 0.f, height = 0.f;
std::string group;
if (!(record >> std::quoted(id) >> enabled >> cellSize >> height >> std::quoted(group)) ||
(enabled != 0 && enabled != 1))
return fail<void>(DiagnosticCode::ParseError, "invalid TILE record");
auto applied = replacement.addTileLayer(std::move(id), enabled != 0, cellSize, height, std::move(group));
if (!applied.ok()) return applied;
} else if (kind == "OBJECT") {
std::string encoded;
if (!(record >> std::quoted(id) >> enabled >> encoded) || (enabled != 0 && enabled != 1))
return fail<void>(DiagnosticCode::ParseError, "invalid OBJECT record");
auto decoded = hexDecode(encoded);
if (!decoded.ok()) return Result<void>::failure(decoded.status());
ObjectBuildLayer object;
auto applied = object.deserializeDefinition(decoded.value());
if (!applied.ok()) return applied;
auto added = replacement.addObjectLayer(std::move(id), enabled != 0, object);
if (!added.ok()) return added;
} else {
return fail<void>(DiagnosticCode::ParseError, "unknown build-layer record: " + kind);
}
record >> std::ws;
if (!record.eof()) return fail<void>(DiagnosticCode::ParseError, "trailing build-layer record data");
}
if (!ended) return fail<void>(DiagnosticCode::ParseError, "build-layer END record is missing");
*this = std::move(replacement);
return Result<void>::success();
}
} // namespace eve::procgen
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include "common/Result.h"
#include "procgen/Grid2D.h"
#include "procgen/MeshBuild.h"
#include "procgen/ObjectBuildLayer.h"
#include "procgen/PointSet.h"
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace eve::procgen {
/** @brief One named build-layer artifact in configured execution order. */
struct BuildLayerArtifact {
std::string id;
std::variant<MeshBuild, PointSet> value;
};
/** @brief Half-open grid and world-space bounds used for one incremental cluster build. */
struct BuildLayerRegion {
int minCellX = 0;
int minCellY = 0;
int maxCellX = 0;
int maxCellY = 0;
float minWorldX = 0.f;
float minWorldZ = 0.f;
float maxWorldX = 0.f;
float maxWorldZ = 0.f;
};
/** @brief Immutable-by-convention result of one complete build-stack execution. */
class BuildLayerExecution {
public:
[[nodiscard]] int getCount() const noexcept;
[[nodiscard]] std::string getId(int index) const;
[[nodiscard]] std::string getType(int index) const;
/** @brief Copy a mesh artifact, or return a typed failure for a bad index/type. */
[[nodiscard]] Result<MeshBuild> getMesh(int index) const;
/** @brief Copy a point artifact, or return a typed failure for a bad index/type. */
[[nodiscard]] Result<PointSet> getPoints(int index) const;
private:
friend class BuildLayerStack;
std::vector<BuildLayerArtifact> artifacts_;
};
/**
* @brief Ordered, versioned executor for procedural tile and object build layers.
*
* The stack owns all definitions. Execution creates a new result and publishes no
* partial artifacts if a layer fails. No scene, renderer or physics state is owned.
* @thread Affine; mutate and execute on the owning thread.
*/
class BuildLayerStack {
public:
/** @brief Append a grid-to-mesh tile layer. Layer ids are unique. */
[[nodiscard]] Result<void> addTileLayer(std::string id, bool enabled, float cellSize, float height,
std::string group);
/** @brief Append an object layer by value. Layer ids are unique. */
[[nodiscard]] Result<void> addObjectLayer(std::string id, bool enabled, const ObjectBuildLayer& layer);
[[nodiscard]] Result<void> setEnabled(std::string_view id, bool enabled);
void clear();
[[nodiscard]] int getLayerCount() const noexcept;
[[nodiscard]] std::string getLayerId(int index) const;
[[nodiscard]] std::string getLayerType(int index) const;
[[nodiscard]] bool isLayerEnabled(int index) const noexcept;
/**
* @brief Execute every enabled layer in stable insertion order.
* @param grid Source for tile layers.
* @param points Source for object layers.
* @param orientation Optional orientation layer shared by object definitions that enable orientation.
* @return Complete ordered artifacts, or the first structured failure with no visible partial result.
* @cost Sum of enabled tile mesh construction and object layer costs.
*/
[[nodiscard]] Result<BuildLayerExecution> execute(const Grid2D& grid, const PointSet& points,
const PointSet* orientation = nullptr) const;
/** @brief Execute enabled layers for one half-open cluster region. */
[[nodiscard]] Result<BuildLayerExecution> executeRegion(const Grid2D& grid, const PointSet& points,
const BuildLayerRegion& region,
const PointSet* orientation = nullptr) const;
/** @brief Serialize ordered definitions using EVPCG_BUILD_LAYERS schema version 1. */
[[nodiscard]] std::string serializeDefinition() const;
/** @brief Atomically restore version 1; unknown fields and malformed embedded layers are rejected. */
[[nodiscard]] Result<void> deserializeDefinition(std::string_view definition);
private:
struct TileLayer {
float cellSize = 1.f;
float height = 0.f;
std::string group;
};
struct Layer {
std::string id;
bool enabled = true;
std::variant<TileLayer, ObjectBuildLayer> definition;
};
[[nodiscard]] bool contains(std::string_view id) const;
std::vector<Layer> layers_;
};
} // namespace eve::procgen
+667
View File
@@ -0,0 +1,667 @@
#include "procgen/GridGraph.h"
#include "common/Diagnostic.h"
#include "procgen/GridGraphAlgorithms.h"
#include "procgen/GeneratorRegistry.h"
#include "procgen/Params.h"
#include "procgen/PointGraph.h"
#include "procgen/Semantic.h"
#include "procgen/algorithms/RoguelikeGenerator.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <queue>
#include <sstream>
#include <unordered_set>
namespace eve::procgen {
namespace {
struct OperationSpec {
const char* id;
int inputs;
GridGraphValueType output;
};
const std::vector<OperationSpec>& specs() {
static const std::vector<OperationSpec> value = {
{"grid.input", 0, GridGraphValueType::Grid},
{"grid.union", 2, GridGraphValueType::Grid},
{"grid.intersect", 2, GridGraphValueType::Grid},
{"grid.subtract", 2, GridGraphValueType::Grid},
{"grid.invert", 1, GridGraphValueType::Grid},
{"grid.expand", 1, GridGraphValueType::Grid},
{"grid.shrink", 1, GridGraphValueType::Grid},
{"grid.smooth", 1, GridGraphValueType::Grid},
{"grid.autotile", 1, GridGraphValueType::Grid},
{"generate.fill", 0, GridGraphValueType::Grid},
{"generate.random_noise", 0, GridGraphValueType::Grid},
{"generate.checkerboard", 0, GridGraphValueType::Grid},
{"generate.dot_grid", 0, GridGraphValueType::Grid},
{"generate.shape", 0, GridGraphValueType::Grid},
{"generate.cellular", 0, GridGraphValueType::Grid},
{"generate.random_walk", 0, GridGraphValueType::Grid},
{"generate.maze", 0, GridGraphValueType::Grid},
{"generate.poisson", 0, GridGraphValueType::Grid},
{"generate.registry", 0, GridGraphValueType::Grid},
{"select.random", 1, GridGraphValueType::Grid},
{"select.border", 1, GridGraphValueType::Grid},
{"select.fill", 1, GridGraphValueType::Grid},
{"select.neighbors", 1, GridGraphValueType::Grid},
{"select.rule", 1, GridGraphValueType::Grid},
{"select.islands", 1, GridGraphValueType::Grid},
{"select.island_centers", 1, GridGraphValueType::Grid},
{"select.detail_range", 1, GridGraphValueType::Grid},
{"select.semantic", 1, GridGraphValueType::Grid},
{"grid.path", 3, GridGraphValueType::Grid},
{"convert.grid_to_points", 1, GridGraphValueType::PointSet},
{"point.subgraph", 1, GridGraphValueType::PointSet},
};
return value;
}
const OperationSpec* specFor(std::string_view id) {
const auto found =
std::find_if(specs().begin(), specs().end(), [id](const OperationSpec& spec) { return id == spec.id; });
return found == specs().end() ? nullptr : &*found;
}
template <class T>
Result<T> fail(DiagnosticCode code, std::string message, std::string path = {}) {
return Result<T>::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, "procgen.gridGraph"));
}
Result<void> failVoid(DiagnosticCode code, std::string message, std::string path = {}) {
return Result<void>::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, "procgen.gridGraph"));
}
bool sameSize(const Grid2D& a, const Grid2D& b) {
return a.getWidth() == b.getWidth() && a.getHeight() == b.getHeight();
}
bool occupied(const Grid2D& grid, int x, int y) { return grid.getCell(x, y) != int(Semantic::Empty); }
Grid2D combine(const Grid2D& a, const Grid2D& b, std::string_view operation) {
Grid2D out = a;
for (int y = 0; y < a.getHeight(); ++y) {
for (int x = 0; x < a.getWidth(); ++x) {
const bool left = occupied(a, x, y);
const bool right = occupied(b, x, y);
bool keep = false;
if (operation == "grid.union") keep = left || right;
if (operation == "grid.intersect") keep = left && right;
if (operation == "grid.subtract") keep = left && !right;
if (!keep) {
out.setCell(x, y, int(Semantic::Empty));
out.setDetail(x, y, 0);
} else if (!left) {
out.setCell(x, y, b.getCell(x, y));
out.setDetail(x, y, b.getDetail(x, y));
}
}
}
return out;
}
Grid2D morphology(const Grid2D& input, int radius, bool expand) {
Grid2D out = input;
radius = std::max(1, radius);
for (int y = 0; y < input.getHeight(); ++y) {
for (int x = 0; x < input.getWidth(); ++x) {
bool result = expand ? occupied(input, x, y) : true;
for (int oy = -radius; oy <= radius && (expand ? !result : result); ++oy) {
for (int ox = -radius; ox <= radius; ++ox) {
const bool sample = occupied(input, x + ox, y + oy);
result = expand ? (result || sample) : (result && sample);
if (expand ? result : !result) break;
}
}
if (result) {
if (!occupied(out, x, y)) out.setCell(x, y, int(Semantic::Floor));
} else {
out.setCell(x, y, int(Semantic::Empty));
out.setDetail(x, y, 0);
}
}
}
return out;
}
std::uint64_t hashText(std::string_view text) {
std::uint64_t hash = 1469598103934665603ull;
for (const unsigned char value : text) {
hash ^= value;
hash *= 1099511628211ull;
}
return hash == 0 ? 1 : hash;
}
Result<PointSet> gridToPoints(const Grid2D& grid, int semantic, float cellSize, float originX, float originZ,
std::string_view nodeId) {
if (!std::isfinite(cellSize) || cellSize <= 0.f)
return fail<PointSet>(DiagnosticCode::InvalidArgument, "cellSize must be finite and positive",
std::string(nodeId));
PointSet points;
points.reserve(grid.cells().size());
std::uint64_t ordinal = 0;
for (int y = 0; y < grid.getHeight(); ++y) {
for (int x = 0; x < grid.getWidth(); ++x) {
const int cell = grid.getCell(x, y);
if (cell == int(Semantic::Empty) || (semantic >= 0 && cell != semantic)) continue;
const int index =
points.add(originX + (float(x) + 0.5f) * cellSize, 0.f, originZ + (float(y) + 0.5f) * cellSize);
auto idResult = points.trySetPointId(index, derivePointId(hashText(nodeId), ++ordinal));
if (!idResult.ok()) return Result<PointSet>::failure(idResult.status());
auto xResult = points.trySetIntAttribute(index, "cell_x", x);
if (!xResult.ok()) return Result<PointSet>::failure(xResult.status());
auto yResult = points.trySetIntAttribute(index, "cell_y", y);
if (!yResult.ok()) return Result<PointSet>::failure(yResult.status());
auto semanticResult = points.trySetIntAttribute(index, "semantic", cell);
if (!semanticResult.ok()) return Result<PointSet>::failure(semanticResult.status());
auto detailResult = points.trySetIntAttribute(index, "detail", grid.getDetail(x, y));
if (!detailResult.ok()) return Result<PointSet>::failure(detailResult.status());
}
}
return Result<PointSet>::success(std::move(points));
}
} // namespace
Result<void> GridGraph::addNode(std::string id, std::string operation) {
if (id.empty()) return failVoid(DiagnosticCode::InvalidArgument, "node id is empty");
if (!specFor(operation)) return failVoid(DiagnosticCode::NotFound, "unknown operation: " + operation, id);
if (nodes_.contains(id)) return failVoid(DiagnosticCode::Conflict, "duplicate node id: " + id, id);
Node node;
node.id = id;
node.operation = std::move(operation);
nodes_.emplace(id, std::move(node));
order_.push_back(std::move(id));
++revision_;
return Result<void>::success();
}
Result<void> GridGraph::connect(std::string_view fromId, std::string_view toId, int inputIndex) {
const auto from = nodes_.find(std::string(fromId));
const auto to = nodes_.find(std::string(toId));
if (from == nodes_.end() || to == nodes_.end())
return failVoid(DiagnosticCode::NotFound, "connection references an unknown node", std::string(toId));
const OperationSpec* target = specFor(to->second.operation);
if (!target || inputIndex < 0 || inputIndex >= target->inputs)
return failVoid(DiagnosticCode::InvalidArgument, "input slot is outside the operation contract",
std::string(toId));
const auto sourceType = operationOutputType(from->second.operation);
if (!sourceType.ok()) return Result<void>::failure(sourceType.status());
const GridGraphValueType expected =
to->second.operation == "point.subgraph" ? GridGraphValueType::PointSet : GridGraphValueType::Grid;
if (sourceType.value() != expected)
return failVoid(DiagnosticCode::TypeMismatch, "typed graph ports are incompatible", std::string(toId));
to->second.inputs[inputIndex] = std::string(fromId);
invalidateFrom(toId);
++revision_;
return Result<void>::success();
}
Result<void> GridGraph::setNodeGrid(std::string_view id, const Grid2D& grid) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
if (found->second.operation != "grid.input")
return failVoid(DiagnosticCode::TypeMismatch, "node is not a grid.input", std::string(id));
found->second.inputGrid = grid;
found->second.hasInputGrid = true;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> GridGraph::setNodeInt(std::string_view id, std::string key, int value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
const bool declared =
found->second.operation.starts_with("generate.") || found->second.operation.starts_with("select.") ||
found->second.operation == "grid.path" ||
((found->second.operation == "grid.expand" || found->second.operation == "grid.shrink") && key == "radius") ||
(found->second.operation == "grid.smooth" && key == "threshold") ||
((found->second.operation == "grid.invert" || found->second.operation == "convert.grid_to_points") &&
key == "semantic");
if (!declared)
return failVoid(DiagnosticCode::InvalidArgument, "unknown integer parameter: " + key, std::string(id));
found->second.ints[std::move(key)] = value;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> GridGraph::setNodeFloat(std::string_view id, std::string key, float value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
if (!found->second.operation.starts_with("generate.") && !found->second.operation.starts_with("select.") &&
(found->second.operation != "convert.grid_to_points" ||
(key != "cellSize" && key != "originX" && key != "originZ")))
return failVoid(DiagnosticCode::InvalidArgument, "unknown float parameter: " + key, std::string(id));
found->second.floats[std::move(key)] = value;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> GridGraph::setNodeString(std::string_view id, std::string key, std::string value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
const bool declared = (found->second.operation == "select.rule" && key == "rule") ||
found->second.operation == "generate.registry";
if (!declared)
return failVoid(DiagnosticCode::InvalidArgument, "unknown string parameter: " + key, std::string(id));
found->second.strings[std::move(key)] = std::move(value);
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> GridGraph::setNodePointSubgraph(std::string_view id, const PointGraph& graph, std::string inputNode,
std::string outputNode) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
if (found->second.operation != "point.subgraph")
return failVoid(DiagnosticCode::TypeMismatch, "node is not a point.subgraph", std::string(id));
if (inputNode.empty() || outputNode.empty())
return failVoid(DiagnosticCode::InvalidArgument, "subgraph ports must be non-empty", std::string(id));
found->second.pointSubgraph = std::make_shared<PointGraph>(graph);
found->second.pointInputNode = std::move(inputNode);
found->second.pointOutputNode = std::move(outputNode);
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
ResultRef<const GridGraphValue> GridGraph::evaluate(std::string_view id, std::unordered_map<std::string, int>& states) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end())
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::NotFound, "unknown node",
std::string(id));
Node& node = found->second;
if (node.cacheValid) return ResultRef<const GridGraphValue>::success(std::cref(node.cache));
if (states[node.id] == 1)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::Conflict, "graph contains a cycle",
node.id);
states[node.id] = 1;
const OperationSpec* spec = specFor(node.operation);
std::vector<std::reference_wrapper<const GridGraphValue>> inputs;
for (int i = 0; i < spec->inputs; ++i) {
if (node.inputs[i].empty())
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::NotFound,
"required input is disconnected", node.id);
auto input = evaluate(node.inputs[i], states);
if (!input.ok()) return ResultRef<const GridGraphValue>::failure(input.status());
inputs.push_back(input.value());
}
if (node.operation == "grid.input") {
if (!node.hasInputGrid)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::NotFound,
"grid.input has no bound value", node.id);
node.cache = node.inputGrid;
} else if (node.operation == "generate.registry") {
const auto algorithm = node.strings.find("algorithm");
if (algorithm == node.strings.end() || algorithm->second.empty())
return fail<std::reference_wrapper<const GridGraphValue>>(
DiagnosticCode::InvalidArgument, "generate.registry requires a non-empty algorithm parameter",
node.id);
Params params;
params.setSize(node.ints.contains("width") ? node.ints["width"] : 32,
node.ints.contains("height") ? node.ints["height"] : 32);
params.setSeed(std::uint32_t(node.ints.contains("seed") ? node.ints["seed"] : 1));
for (const auto& [key, value] : node.ints)
if (key != "width" && key != "height" && key != "seed") params.setInt(key, value);
for (const auto& [key, value] : node.floats) params.setFloat(key, value);
for (const auto& [key, value] : node.strings)
if (key != "algorithm") params.setString(key, value);
auto& registry = GeneratorRegistry::instance();
registry.registerBuiltins();
if (!registry.has(algorithm->second))
return fail<std::reference_wrapper<const GridGraphValue>>(
DiagnosticCode::NotFound, "registered generator was not found: " + algorithm->second, node.id);
Grid2D generated;
std::string error;
if (!registry.generate(algorithm->second, params, generated, error))
return fail<std::reference_wrapper<const GridGraphValue>>(
DiagnosticCode::Failed, error.empty() ? "registered generator failed" : std::move(error), node.id);
node.cache = std::move(generated);
} else if (node.operation.starts_with("generate.")) {
gridgraph::GenerateSettings settings;
settings.width = node.ints.contains("width") ? node.ints["width"] : 32;
settings.height = node.ints.contains("height") ? node.ints["height"] : 32;
settings.semantic = node.ints.contains("semantic") ? node.ints["semantic"] : int(Semantic::Floor);
settings.seed = std::uint64_t(node.ints.contains("seed") ? node.ints["seed"] : 1);
settings.a = node.ints.contains("a") ? node.ints["a"] : 1;
settings.b = node.ints.contains("b") ? node.ints["b"] : 0;
settings.c = node.ints.contains("c") ? node.ints["c"] : 1;
settings.x = node.floats.contains("x") ? node.floats["x"] : 0.5f;
settings.y = node.floats.contains("y") ? node.floats["y"] : 0.f;
auto generated = gridgraph::generate(node.operation, settings);
if (!generated.ok()) return ResultRef<const GridGraphValue>::failure(generated.status());
node.cache = std::move(generated).takeValue();
} else if (node.operation == "select.semantic") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
Grid2D out;
out.resize(source->getWidth(), source->getHeight());
const int semantic = node.ints.contains("semantic") ? node.ints["semantic"] : int(Semantic::Floor);
for (int y = 0; y < source->getHeight(); ++y)
for (int x = 0; x < source->getWidth(); ++x)
if (source->getCell(x, y) == semantic) {
out.setCell(x, y, semantic);
out.setDetail(x, y, source->getDetail(x, y));
}
for (const auto& [key, value] : source->metadata()) out.setMeta(key, value);
node.cache = std::move(out);
} else if (node.operation == "select.detail_range") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
Grid2D out;
out.resize(source->getWidth(), source->getHeight());
const int minimum = node.ints.contains("min") ? node.ints["min"] : 0;
const int maximum = node.ints.contains("max") ? node.ints["max"] : 255;
for (int y = 0; y < source->getHeight(); ++y)
for (int x = 0; x < source->getWidth(); ++x)
if (occupied(*source, x, y) && source->getDetail(x, y) >= minimum &&
source->getDetail(x, y) <= maximum) {
out.setCell(x, y, source->getCell(x, y));
out.setDetail(x, y, source->getDetail(x, y));
}
node.cache = std::move(out);
} else if (node.operation.starts_with("select.")) {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
auto selected = gridgraph::select(*source, node.operation, node.ints.contains("mode") ? node.ints["mode"] : 0,
node.ints.contains("count") ? node.ints["count"] : 0,
node.floats.contains("weight") ? node.floats["weight"] : 0.5f,
std::uint64_t(node.ints.contains("seed") ? node.ints["seed"] : 1),
node.strings.contains("rule") ? node.strings["rule"] : "*********");
if (!selected.ok()) return ResultRef<const GridGraphValue>::failure(selected.status());
node.cache = std::move(selected).takeValue();
} else if (node.operation == "grid.path") {
const auto* navigation = std::get_if<Grid2D>(&inputs[0].get());
const auto* starts = std::get_if<Grid2D>(&inputs[1].get());
const auto* targets = std::get_if<Grid2D>(&inputs[2].get());
if (!navigation || !starts || !targets)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"three grid inputs required", node.id);
auto path = gridgraph::findPath(*navigation, *starts, *targets,
node.ints.contains("semantic") ? node.ints["semantic"] : int(Semantic::Floor));
if (!path.ok()) return ResultRef<const GridGraphValue>::failure(path.status());
node.cache = std::move(path).takeValue();
} else if (node.operation == "grid.union" || node.operation == "grid.intersect" ||
node.operation == "grid.subtract") {
const auto* first = std::get_if<Grid2D>(&inputs[0].get());
const auto* second = std::get_if<Grid2D>(&inputs[1].get());
if (!first || !second || !sameSize(*first, *second))
return fail<std::reference_wrapper<const GridGraphValue>>(
DiagnosticCode::TypeMismatch, "grid inputs must have equal dimensions", node.id);
node.cache = combine(*first, *second, node.operation);
} else if (node.operation == "grid.invert") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
Grid2D out = *source;
const int semantic = node.ints.contains("semantic") ? node.ints["semantic"] : int(Semantic::Floor);
for (int y = 0; y < out.getHeight(); ++y)
for (int x = 0; x < out.getWidth(); ++x)
out.setCell(x, y, occupied(*source, x, y) ? int(Semantic::Empty) : semantic);
node.cache = std::move(out);
} else if (node.operation == "grid.expand" || node.operation == "grid.shrink") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
node.cache = morphology(*source, node.ints.contains("radius") ? node.ints["radius"] : 1,
node.operation == "grid.expand");
} else if (node.operation == "grid.smooth") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
Grid2D out = *source;
const int threshold = node.ints.contains("threshold") ? node.ints["threshold"] : 4;
for (int y = 0; y < source->getHeight(); ++y) {
for (int x = 0; x < source->getWidth(); ++x) {
int neighbors = 0;
for (int oy = -1; oy <= 1; ++oy)
for (int ox = -1; ox <= 1; ++ox)
if ((ox != 0 || oy != 0) && occupied(*source, x + ox, y + oy)) ++neighbors;
out.setCell(x, y, neighbors >= threshold ? int(Semantic::Floor) : int(Semantic::Empty));
}
}
node.cache = std::move(out);
} else if (node.operation == "grid.autotile") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
Grid2D out = *source;
auto autotiled = autotileOccupiedGridInPlace(out);
if (!autotiled.ok())
return ResultRef<const GridGraphValue>::failure(autotiled.status());
node.cache = std::move(out);
} else if (node.operation == "convert.grid_to_points") {
const auto* source = std::get_if<Grid2D>(&inputs[0].get());
if (!source)
return fail<std::reference_wrapper<const GridGraphValue>>(DiagnosticCode::TypeMismatch,
"grid input required", node.id);
auto points = gridToPoints(*source, node.ints.contains("semantic") ? node.ints["semantic"] : -1,
node.floats.contains("cellSize") ? node.floats["cellSize"] : 1.f,
node.floats.contains("originX") ? node.floats["originX"] : 0.f,
node.floats.contains("originZ") ? node.floats["originZ"] : 0.f, node.id);
if (!points.ok()) return ResultRef<const GridGraphValue>::failure(points.status());
node.cache = std::move(points).takeValue();
} else if (node.operation == "point.subgraph") {
const auto* source = std::get_if<PointSet>(&inputs[0].get());
if (!source || !node.pointSubgraph)
return fail<std::reference_wrapper<const GridGraphValue>>(
DiagnosticCode::TypeMismatch, "bound PointGraph and PointSet input required", node.id);
PointSet copy = *source;
if (!node.pointSubgraph->setNodePoints(node.pointInputNode, &copy))
return fail<std::reference_wrapper<const GridGraphValue>>(
DiagnosticCode::InvalidArgument, "nested PointGraph input binding failed", node.id);
auto output = node.pointSubgraph->executeResult(node.pointOutputNode);
if (!output.ok()) return ResultRef<const GridGraphValue>::failure(output.status());
node.cache = std::move(output).takeValue();
}
states[node.id] = 2;
node.cacheValid = true;
return ResultRef<const GridGraphValue>::success(std::cref(node.cache));
}
Result<GridGraphValue> GridGraph::execute(std::string_view outputId) {
std::unordered_map<std::string, int> states;
auto result = evaluate(outputId, states);
if (!result.ok()) return Result<GridGraphValue>::failure(result.status());
return Result<GridGraphValue>::success(result.value().get());
}
Result<void> GridGraph::validate(std::string_view outputId) const {
std::unordered_map<std::string, int> states;
const auto visit = [&](const auto& self, std::string_view id) -> Result<void> {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
if (states[found->first] == 1)
return failVoid(DiagnosticCode::Conflict, "graph contains a cycle", found->first);
if (states[found->first] == 2) return Result<void>::success();
states[found->first] = 1;
const OperationSpec* spec = specFor(found->second.operation);
for (int index = 0; index < spec->inputs; ++index) {
if (found->second.inputs[index].empty())
return failVoid(DiagnosticCode::NotFound, "required input is disconnected", found->first);
auto input = self(self, found->second.inputs[index]);
if (!input.ok()) return input;
}
if (found->second.operation == "grid.input" && !found->second.hasInputGrid)
return failVoid(DiagnosticCode::NotFound, "grid.input has no bound value", found->first);
if (found->second.operation == "point.subgraph" &&
(!found->second.pointSubgraph || found->second.pointInputNode.empty() ||
found->second.pointOutputNode.empty()))
return failVoid(DiagnosticCode::NotFound, "point.subgraph has no complete binding", found->first);
states[found->first] = 2;
return Result<void>::success();
};
return visit(visit, outputId);
}
void GridGraph::clearCache() {
for (auto& [id, node] : nodes_) node.cacheValid = false;
}
void GridGraph::invalidateFrom(std::string_view id) {
std::queue<std::string> pending;
std::unordered_set<std::string> visited;
pending.push(std::string(id));
while (!pending.empty()) {
const std::string current = pending.front();
pending.pop();
if (!visited.insert(current).second) continue;
const auto found = nodes_.find(current);
if (found != nodes_.end()) found->second.cacheValid = false;
for (const auto& [otherId, node] : nodes_)
if (node.inputs[0] == current || node.inputs[1] == current || node.inputs[2] == current)
pending.push(otherId);
}
}
int GridGraph::operationCount() { return int(specs().size()); }
std::string GridGraph::operationId(int index) {
return index >= 0 && index < int(specs().size()) ? specs()[std::size_t(index)].id : std::string();
}
Result<GridGraphValueType> GridGraph::operationOutputType(std::string_view operation) {
const OperationSpec* spec = specFor(operation);
if (!spec)
return fail<GridGraphValueType>(DiagnosticCode::NotFound, "unknown operation: " + std::string(operation));
return Result<GridGraphValueType>::success(spec->output);
}
std::string GridGraph::serializeDefinition() const {
std::ostringstream out;
out << "EVPCG_GRID_GRAPH 1\n" << std::setprecision(9);
for (const auto& id : order_) {
const auto& node = nodes_.at(id);
out << "NODE " << std::quoted(id) << ' ' << std::quoted(node.operation) << '\n';
std::vector<std::string> keys;
for (const auto& [key, value] : node.ints) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const auto& key : keys)
out << "INT " << std::quoted(id) << ' ' << std::quoted(key) << ' ' << node.ints.at(key) << '\n';
keys.clear();
for (const auto& [key, value] : node.floats) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const auto& key : keys)
out << "FLOAT " << std::quoted(id) << ' ' << std::quoted(key) << ' ' << node.floats.at(key) << '\n';
keys.clear();
for (const auto& [key, value] : node.strings) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const auto& key : keys)
out << "STRING " << std::quoted(id) << ' ' << std::quoted(key) << ' ' << std::quoted(node.strings.at(key))
<< '\n';
}
for (const auto& id : order_) {
const auto& node = nodes_.at(id);
for (int input = 0; input < 3; ++input)
if (!node.inputs[input].empty())
out << "EDGE " << std::quoted(node.inputs[input]) << ' ' << std::quoted(id) << ' ' << input << '\n';
}
out << "END\n";
return out.str();
}
Result<void> GridGraph::deserializeDefinition(std::string_view definition) {
GridGraph replacement;
std::istringstream input{std::string(definition)};
std::string magic;
int version = 0;
if (!(input >> magic >> version) || magic != "EVPCG_GRID_GRAPH" || version != 1)
return failVoid(DiagnosticCode::ParseError, "invalid grid graph header");
std::string line;
std::getline(input, line);
bool ended = false;
while (std::getline(input, line)) {
if (line.empty()) continue;
std::istringstream record(line);
std::string kind;
record >> kind;
if (kind == "END") {
ended = true;
break;
}
std::string id;
if (kind == "NODE") {
std::string operation;
if (!(record >> std::quoted(id) >> std::quoted(operation)))
return failVoid(DiagnosticCode::ParseError, "invalid NODE record");
auto result = replacement.addNode(std::move(id), std::move(operation));
if (!result.ok()) return result;
} else if (kind == "EDGE") {
std::string target;
int slot = -1;
if (!(record >> std::quoted(id) >> std::quoted(target) >> slot))
return failVoid(DiagnosticCode::ParseError, "invalid EDGE record");
auto result = replacement.connect(id, target, slot);
if (!result.ok()) return result;
} else if (kind == "INT") {
std::string key;
int value = 0;
if (!(record >> std::quoted(id) >> std::quoted(key) >> value))
return failVoid(DiagnosticCode::ParseError, "invalid INT record");
auto result = replacement.setNodeInt(id, std::move(key), value);
if (!result.ok()) return result;
} else if (kind == "FLOAT") {
std::string key;
float value = 0.f;
if (!(record >> std::quoted(id) >> std::quoted(key) >> value))
return failVoid(DiagnosticCode::ParseError, "invalid FLOAT record");
auto result = replacement.setNodeFloat(id, std::move(key), value);
if (!result.ok()) return result;
} else if (kind == "STRING") {
std::string key;
std::string value;
if (!(record >> std::quoted(id) >> std::quoted(key) >> std::quoted(value)))
return failVoid(DiagnosticCode::ParseError, "invalid STRING record");
auto result = replacement.setNodeString(id, std::move(key), std::move(value));
if (!result.ok()) return result;
} else {
return failVoid(DiagnosticCode::ParseError, "unknown grid graph record: " + kind);
}
record >> std::ws;
if (!record.eof()) return failVoid(DiagnosticCode::ParseError, "trailing grid graph record data");
}
if (!ended) return failVoid(DiagnosticCode::ParseError, "grid graph END record is missing");
std::unordered_map<std::string, int> states;
const auto visit = [&](const auto& self, const std::string& id) -> Result<void> {
if (states[id] == 2) return Result<void>::success();
if (states[id] == 1) return failVoid(DiagnosticCode::Conflict, "cycle in serialized graph", id);
states[id] = 1;
const auto found = replacement.nodes_.find(id);
if (found == replacement.nodes_.end()) return failVoid(DiagnosticCode::NotFound, "unknown node", id);
for (const auto& dependency : found->second.inputs) {
if (dependency.empty()) continue;
auto result = self(self, dependency);
if (!result.ok()) return result;
}
states[id] = 2;
return Result<void>::success();
};
for (const auto& id : replacement.order_) {
auto result = visit(visit, id);
if (!result.ok()) return result;
}
*this = std::move(replacement);
return Result<void>::success();
}
} // namespace eve::procgen
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include "common/BorrowedRef.h"
#include "procgen/Grid2D.h"
#include "procgen/PointSet.h"
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>
namespace eve::procgen {
class PointGraph;
/** @brief Strong runtime value passed between typed GridGraph ports. */
using GridGraphValue = std::variant<Grid2D, PointSet>;
/** @brief Declared output kind of a GridGraph operation. */
enum class GridGraphValueType : std::uint8_t { Grid, PointSet };
/**
* @brief Deterministic data-flow graph for grid generation and explicit grid-to-point composition.
*
* Grid nodes own dense intermediate values rather than ECS entities. Mutations invalidate the
* affected node and its downstream caches. `point.subgraph` is the first typed cross-graph bridge:
* it consumes a PointSet and executes an independently owned PointGraph instance.
*
* @thread Graph-owning thread only.
* @reentrant Not reentrant for one graph instance.
*/
class GridGraph {
public:
/** @brief Add a reflected operation under a stable, non-empty node id. */
[[nodiscard]] Result<void> addNode(std::string id, std::string operation);
/** @brief Connect one node output to a typed input slot. */
[[nodiscard]] Result<void> connect(std::string_view fromId, std::string_view toId, int inputIndex = 0);
/** @brief Assign an owning copy to a `grid.input` node. */
[[nodiscard]] Result<void> setNodeGrid(std::string_view id, const Grid2D& grid);
/** @brief Set one integer parameter declared by the node operation. */
[[nodiscard]] Result<void> setNodeInt(std::string_view id, std::string key, int value);
/** @brief Set one floating-point parameter declared by the node operation. */
[[nodiscard]] Result<void> setNodeFloat(std::string_view id, std::string key, float value);
/** @brief Set one string parameter declared by the node operation. */
[[nodiscard]] Result<void> setNodeString(std::string_view id, std::string key, std::string value);
/**
* @brief Bind an owning PointGraph copy to a `point.subgraph` bridge.
* @param inputNode Nested `input` node receiving the upstream PointSet.
* @param outputNode Nested output node to evaluate.
*/
[[nodiscard]] Result<void> setNodePointSubgraph(std::string_view id, const PointGraph& graph, std::string inputNode,
std::string outputNode);
/**
* @brief Validate and evaluate one output into an independent owning value.
* @return Grid2D or PointSet according to the selected node's reflected output type.
* @cost Linear in visited cells for local grid operations; nested PointGraph cost is additive.
*/
[[nodiscard]] Result<GridGraphValue> execute(std::string_view outputId);
/** @brief Validate topology, required inputs and cross-graph bindings without evaluating values. */
[[nodiscard]] Result<void> validate(std::string_view outputId) const;
/** @brief Remove all cached node values while retaining topology and parameters. */
void clearCache();
/** @brief Monotonic authoring revision; successful mutations increment it. */
[[nodiscard]] std::uint64_t revision() const noexcept { return revision_; }
/** @brief Serialize topology and scalar parameters using the versioned EVPCG grid schema. */
[[nodiscard]] std::string serializeDefinition() const;
/** @brief Atomically replace this graph from a versioned definition. */
[[nodiscard]] Result<void> deserializeDefinition(std::string_view definition);
/** @brief Return the number of reflected operations. */
[[nodiscard]] static int operationCount();
/** @brief Return a reflected operation id, or empty for an invalid index. */
[[nodiscard]] static std::string operationId(int index);
/** @brief Return a reflected operation's output kind. */
[[nodiscard]] static Result<GridGraphValueType> operationOutputType(std::string_view operation);
private:
struct Node {
std::string id;
std::string operation;
std::string inputs[3];
std::unordered_map<std::string, int> ints;
std::unordered_map<std::string, float> floats;
std::unordered_map<std::string, std::string> strings;
Grid2D inputGrid;
bool hasInputGrid = false;
std::shared_ptr<PointGraph> pointSubgraph;
std::string pointInputNode;
std::string pointOutputNode;
GridGraphValue cache;
bool cacheValid = false;
};
[[nodiscard]] ResultRef<const GridGraphValue> evaluate(std::string_view id,
std::unordered_map<std::string, int>& states);
void invalidateFrom(std::string_view id);
std::unordered_map<std::string, Node> nodes_;
std::vector<std::string> order_;
std::uint64_t revision_ = 0;
};
} // namespace eve::procgen
+339
View File
@@ -0,0 +1,339 @@
#include "procgen/GridGraphAlgorithms.h"
#include "common/Diagnostic.h"
#include "procgen/Semantic.h"
#include <algorithm>
#include <cmath>
#include <queue>
#include <random>
#include <sstream>
namespace eve::procgen::gridgraph {
namespace {
template <class T>
Result<T> failure(std::string message) {
return Result<T>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, std::move(message), {}, {}, "procgen.gridGraph"));
}
bool occupied(const Grid2D& grid, int x, int y) { return grid.getCell(x, y) != int(Semantic::Empty); }
Grid2D makeGrid(const GenerateSettings& settings) {
Grid2D grid;
grid.resize(settings.width, settings.height);
return grid;
}
void setOccupied(Grid2D& grid, int x, int y, int semantic) {
if (x >= 0 && y >= 0 && x < grid.getWidth() && y < grid.getHeight()) grid.setCell(x, y, semantic);
}
std::vector<std::pair<int, int>> cells(const Grid2D& grid) {
std::vector<std::pair<int, int>> result;
for (int y = 0; y < grid.getHeight(); ++y)
for (int x = 0; x < grid.getWidth(); ++x)
if (occupied(grid, x, y)) result.emplace_back(x, y);
return result;
}
std::vector<std::vector<std::pair<int, int>>> islands(const Grid2D& grid) {
std::vector<std::vector<std::pair<int, int>>> result;
std::vector<unsigned char> visited(std::size_t(grid.getWidth() * grid.getHeight()));
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int y = 0; y < grid.getHeight(); ++y) {
for (int x = 0; x < grid.getWidth(); ++x) {
const int start = y * grid.getWidth() + x;
if (!occupied(grid, x, y) || visited[std::size_t(start)]) continue;
result.emplace_back();
std::queue<std::pair<int, int>> pending;
pending.emplace(x, y);
visited[std::size_t(start)] = 1;
while (!pending.empty()) {
const auto current = pending.front();
pending.pop();
result.back().push_back(current);
for (int direction = 0; direction < 4; ++direction) {
const int nx = current.first + dx[direction];
const int ny = current.second + dy[direction];
if (nx < 0 || ny < 0 || nx >= grid.getWidth() || ny >= grid.getHeight()) continue;
const int index = ny * grid.getWidth() + nx;
if (occupied(grid, nx, ny) && !visited[std::size_t(index)]) {
visited[std::size_t(index)] = 1;
pending.emplace(nx, ny);
}
}
}
}
}
return result;
}
Grid2D cellular(const GenerateSettings& settings) {
Grid2D grid = makeGrid(settings);
std::mt19937_64 rng(settings.seed);
std::uniform_real_distribution<float> probability(0.f, 1.f);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x)
if (probability(rng) < settings.x) grid.setCell(x, y, settings.semantic);
for (int step = 0; step < settings.a; ++step) {
Grid2D next = grid;
for (int y = 0; y < settings.height; ++y) {
for (int x = 0; x < settings.width; ++x) {
int neighbors = 0;
for (int oy = -1; oy <= 1; ++oy)
for (int ox = -1; ox <= 1; ++ox)
if ((ox != 0 || oy != 0) && occupied(grid, x + ox, y + oy)) ++neighbors;
next.setCell(
x, y,
neighbors > 4 ? settings.semantic : (neighbors < 4 ? int(Semantic::Empty) : grid.getCell(x, y)));
}
}
grid = std::move(next);
}
return grid;
}
Grid2D randomWalk(const GenerateSettings& settings) {
Grid2D grid = makeGrid(settings);
std::mt19937_64 rng(settings.seed);
std::uniform_int_distribution<int> startX(0, settings.width - 1);
std::uniform_int_distribution<int> startY(0, settings.height - 1);
std::uniform_int_distribution<int> direction(0, 3);
int x = settings.b != 0 ? std::clamp(settings.b, 0, settings.width - 1) : startX(rng);
int y = settings.c != 0 ? std::clamp(settings.c, 0, settings.height - 1) : startY(rng);
const int iterations = std::max(1, settings.a);
const int length = std::max(1, int(settings.x));
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int iteration = 0; iteration < iterations; ++iteration) {
for (int step = 0; step < length; ++step) {
setOccupied(grid, x, y, settings.semantic);
const int d = direction(rng);
x = std::clamp(x + dx[d], 0, settings.width - 1);
y = std::clamp(y + dy[d], 0, settings.height - 1);
}
if (settings.y > 0.5f) {
x = startX(rng);
y = startY(rng);
}
}
return grid;
}
Grid2D maze(const GenerateSettings& settings) {
Grid2D grid = makeGrid(settings);
if (settings.width < 3 || settings.height < 3) return grid;
std::mt19937_64 rng(settings.seed);
std::vector<std::pair<int, int>> stack{{1, 1}};
setOccupied(grid, 1, 1, settings.semantic);
constexpr int dx[] = {-2, 2, 0, 0};
constexpr int dy[] = {0, 0, -2, 2};
while (!stack.empty()) {
const auto current = stack.back();
std::vector<int> candidates;
for (int d = 0; d < 4; ++d) {
const int nx = current.first + dx[d];
const int ny = current.second + dy[d];
if (nx > 0 && ny > 0 && nx < settings.width - 1 && ny < settings.height - 1 && !occupied(grid, nx, ny))
candidates.push_back(d);
}
if (candidates.empty()) {
stack.pop_back();
continue;
}
std::shuffle(candidates.begin(), candidates.end(), rng);
const int d = candidates.front();
const int nx = current.first + dx[d];
const int ny = current.second + dy[d];
setOccupied(grid, current.first + dx[d] / 2, current.second + dy[d] / 2, settings.semantic);
setOccupied(grid, nx, ny, settings.semantic);
stack.emplace_back(nx, ny);
}
return grid;
}
} // namespace
Result<Grid2D> generate(std::string_view operation, const GenerateSettings& settings) {
if (settings.width <= 0 || settings.height <= 0) return failure<Grid2D>("generator dimensions must be positive");
Grid2D grid = makeGrid(settings);
std::mt19937_64 rng(settings.seed);
if (operation == "generate.fill") {
grid.fill(settings.semantic);
} else if (operation == "generate.random_noise") {
std::uniform_real_distribution<float> probability(0.f, 1.f);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x)
if (probability(rng) < settings.x) grid.setCell(x, y, settings.semantic);
} else if (operation == "generate.checkerboard" || operation == "generate.dot_grid") {
const int spacing = std::max(1, settings.a);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x)
if (operation == "generate.checkerboard" ? ((x / spacing + y / spacing) % 2 == 0)
: (x % spacing == 0 && y % spacing == 0))
grid.setCell(x, y, settings.semantic);
} else if (operation == "generate.shape") {
const int cx = settings.a;
const int cy = settings.b;
const int radius = std::max(1, settings.c);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x) {
bool inside = settings.x < 0.5f ? ((x - cx) * (x - cx) + (y - cy) * (y - cy) <= radius * radius)
: (std::abs(x - cx) <= radius && std::abs(y - cy) <= radius);
if (inside) grid.setCell(x, y, settings.semantic);
}
} else if (operation == "generate.cellular") {
grid = cellular(settings);
} else if (operation == "generate.random_walk") {
grid = randomWalk(settings);
} else if (operation == "generate.maze") {
grid = maze(settings);
} else if (operation == "generate.poisson") {
const float radius = std::max(1.f, settings.x);
std::vector<std::pair<int, int>> accepted;
std::vector<std::pair<int, int>> candidates;
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x) candidates.emplace_back(x, y);
std::shuffle(candidates.begin(), candidates.end(), rng);
for (const auto& candidate : candidates) {
const bool clear = std::none_of(accepted.begin(), accepted.end(), [&](const auto& point) {
const float dx = float(point.first - candidate.first);
const float dy = float(point.second - candidate.second);
return dx * dx + dy * dy < radius * radius;
});
if (clear) {
accepted.push_back(candidate);
grid.setCell(candidate.first, candidate.second, settings.semantic);
}
}
} else {
return failure<Grid2D>("unsupported generator: " + std::string(operation));
}
return Result<Grid2D>::success(std::move(grid));
}
Result<Grid2D> select(const Grid2D& input, std::string_view operation, int mode, int count, float weight,
std::uint64_t seed, std::string_view rule) {
Grid2D out;
out.resize(input.getWidth(), input.getHeight());
auto active = cells(input);
if (operation == "select.random") {
std::mt19937_64 rng(seed);
std::shuffle(active.begin(), active.end(), rng);
const int amount = count > 0 ? std::min(count, int(active.size()))
: int(std::round(std::clamp(weight, 0.f, 1.f) * float(active.size())));
active.resize(std::size_t(amount));
for (const auto& point : active)
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
} else if (operation == "select.border" || operation == "select.fill") {
for (const auto& point : active) {
bool border = false;
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int d = 0; d < 4; ++d) border = border || !occupied(input, point.first + dx[d], point.second + dy[d]);
if ((operation == "select.border" && border) || (operation == "select.fill" && !border))
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
} else if (operation == "select.neighbors") {
for (const auto& point : active) {
int neighbors = 0;
for (int oy = -1; oy <= 1; ++oy)
for (int ox = -1; ox <= 1; ++ox)
if ((ox != 0 || oy != 0) && occupied(input, point.first + ox, point.second + oy)) ++neighbors;
if ((mode == 0 && neighbors == count) || (mode == 1 && neighbors >= count) ||
(mode == 2 && neighbors <= count))
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
} else if (operation == "select.rule") {
if (rule.size() != 9) return failure<Grid2D>("rule must contain exactly nine characters (0/1/*)");
for (const auto& point : active) {
bool match = true;
for (int oy = -1; oy <= 1 && match; ++oy)
for (int ox = -1; ox <= 1; ++ox) {
const char expected = rule[std::size_t((oy + 1) * 3 + ox + 1)];
if (expected != '*' &&
(occupied(input, point.first + ox, point.second + oy) != (expected == '1'))) {
match = false;
break;
}
}
if (match) out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
} else if (operation == "select.islands" || operation == "select.island_centers") {
for (const auto& island : islands(input)) {
const bool keep = mode == 0 ? int(island.size()) < count
: (mode == 1 ? int(island.size()) > count : int(island.size()) == count);
if (!keep) continue;
if (operation == "select.island_centers") {
long long sx = 0;
long long sy = 0;
for (const auto& point : island) {
sx += point.first;
sy += point.second;
}
const int x = int(std::llround(double(sx) / double(island.size())));
const int y = int(std::llround(double(sy) / double(island.size())));
out.setCell(x, y, input.getCell(island.front().first, island.front().second));
} else {
for (const auto& point : island)
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
}
} else {
return failure<Grid2D>("unsupported selector: " + std::string(operation));
}
return Result<Grid2D>::success(std::move(out));
}
Result<Grid2D> findPath(const Grid2D& navigation, const Grid2D& starts, const Grid2D& targets, int semantic) {
if (navigation.getWidth() != starts.getWidth() || navigation.getHeight() != starts.getHeight() ||
navigation.getWidth() != targets.getWidth() || navigation.getHeight() != targets.getHeight())
return failure<Grid2D>("pathfinding grids must have equal dimensions");
const auto startCells = cells(starts);
const auto targetCells = cells(targets);
if (startCells.empty() || targetCells.empty())
return failure<Grid2D>("pathfinding requires start and target cells");
const int width = navigation.getWidth();
const int height = navigation.getHeight();
std::vector<int> previous(std::size_t(width * height), -1);
std::queue<int> pending;
const int start = startCells.front().second * width + startCells.front().first;
pending.push(start);
previous[std::size_t(start)] = start;
int destination = -1;
while (!pending.empty() && destination < 0) {
const int current = pending.front();
pending.pop();
const int x = current % width;
const int y = current / width;
if (occupied(targets, x, y)) {
destination = current;
break;
}
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int d = 0; d < 4; ++d) {
const int nx = x + dx[d];
const int ny = y + dy[d];
if (nx < 0 || ny < 0 || nx >= width || ny >= height || !occupied(navigation, nx, ny)) continue;
const int next = ny * width + nx;
if (previous[std::size_t(next)] < 0) {
previous[std::size_t(next)] = current;
pending.push(next);
}
}
}
if (destination < 0) return failure<Grid2D>("no path found");
Grid2D out;
out.resize(width, height);
for (int current = destination;; current = previous[std::size_t(current)]) {
out.setCell(current % width, current / width, semantic);
if (current == start) break;
}
return Result<Grid2D>::success(std::move(out));
}
} // namespace eve::procgen::gridgraph
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "common/Result.h"
#include "procgen/Grid2D.h"
#include <cstdint>
#include <string_view>
#include <vector>
namespace eve::procgen::gridgraph {
struct GenerateSettings {
int width = 32;
int height = 32;
int semantic = 1;
std::uint64_t seed = 1;
int a = 0;
int b = 0;
int c = 0;
float x = 0.f;
float y = 0.f;
};
[[nodiscard]] Result<Grid2D> generate(std::string_view operation, const GenerateSettings& settings);
[[nodiscard]] Result<Grid2D> select(const Grid2D& input, std::string_view operation, int mode, int count, float weight,
std::uint64_t seed, std::string_view rule);
[[nodiscard]] Result<Grid2D> findPath(const Grid2D& navigation, const Grid2D& starts, const Grid2D& targets,
int semantic);
} // namespace eve::procgen::gridgraph
+391
View File
@@ -0,0 +1,391 @@
#include "procgen/GridMeshGraphScript.h"
#include "common/SquirrelBinding.h"
#include "common/SquirrelOwnership.h"
#include "procgen/BuildLayerStack.h"
#include "procgen/GridGraph.h"
#include "procgen/IncrementalBuild.h"
#include "procgen/MeshGraph.h"
#include "procgen/ObjectBuildLayer.h"
#include "procgen/PointGraph.h"
#include <simplesquirrel/simplesquirrel.hpp>
#include <functional>
namespace eve::procgen {
void exposeGridMeshGraphs(ssq::Table& table) {
auto grid = table.addClass<GridGraph>("ProcgenGridGraph",
std::function<GridGraph*()>([]() -> GridGraph* { return nullptr; }), true);
const HSQUIRRELVM gridVm = grid.getHandle();
grid.addFunc("addNode", [gridVm](GridGraph* self, const std::string& id, const std::string& operation) {
return eve::script::projectResult(gridVm, self->addNode(id, operation));
});
grid.addFunc("connect", [gridVm](GridGraph* self, const std::string& from, const std::string& to, int slot) {
return eve::script::projectResult(gridVm, self->connect(from, to, slot));
});
grid.addFunc("setNodeGrid", [gridVm](GridGraph* self, const std::string& id, Grid2D* value) {
if (!value)
return eve::script::projectResult(
gridVm, Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "grid is null")));
return eve::script::projectResult(gridVm, self->setNodeGrid(id, *value));
});
grid.addFunc("setNodeInt", [gridVm](GridGraph* self, const std::string& id, const std::string& key, int value) {
return eve::script::projectResult(gridVm, self->setNodeInt(id, key, value));
});
grid.addFunc("setNodeFloat", [gridVm](GridGraph* self, const std::string& id, const std::string& key, float value) {
return eve::script::projectResult(gridVm, self->setNodeFloat(id, key, value));
});
grid.addFunc("setNodeString",
[gridVm](GridGraph* self, const std::string& id, const std::string& key, const std::string& value) {
return eve::script::projectResult(gridVm, self->setNodeString(id, key, value));
});
grid.addFunc("setNodePointSubgraph", [gridVm](GridGraph* self, const std::string& id, PointGraph* nested,
const std::string& input, const std::string& output) {
if (!nested)
return eve::script::projectResult(gridVm, Result<void>::failure(Diagnostic::error(
DiagnosticCode::InvalidArgument, "point graph is null")));
return eve::script::projectResult(gridVm, self->setNodePointSubgraph(id, *nested, input, output));
});
grid.addFunc("execute", [gridVm](GridGraph* self, const std::string& output) -> ssq::Table {
auto result = self->execute(output);
if (!result.ok()) return eve::script::projectStatusResult(gridVm, result.status(), false, false);
auto value = std::move(result).takeValue();
ssq::Object object;
if (auto* outputGrid = std::get_if<Grid2D>(&value)) {
auto instance =
eve::script::makeOwnedSquirrelInstance<Grid2D>(gridVm, std::make_unique<Grid2D>(*outputGrid));
if (!instance.ok()) return eve::script::projectStatusResult(gridVm, instance.status(), false, false);
object = std::move(instance).takeValue();
} else {
auto instance = eve::script::makeOwnedSquirrelInstance<PointSet>(
gridVm, std::make_unique<PointSet>(std::get<PointSet>(std::move(value))));
if (!instance.ok()) return eve::script::projectStatusResult(gridVm, instance.status(), false, false);
object = std::move(instance).takeValue();
}
auto projected = eve::script::projectStatusResult(gridVm, Status::success(), true, true);
projected.set("value", std::move(object));
return projected;
});
grid.addFunc("validate", [gridVm](GridGraph* self, const std::string& output) {
return eve::script::projectResult(gridVm, self->validate(output));
});
grid.addFunc("clearCache", &GridGraph::clearCache);
grid.addFunc("getRevision", [](GridGraph* self) { return self->revision(); });
grid.addFunc("serializeDefinition", &GridGraph::serializeDefinition);
grid.addFunc("deserializeDefinition", [gridVm](GridGraph* self, const std::string& definition) {
return eve::script::projectResult(gridVm, self->deserializeDefinition(definition));
});
auto mesh = table.addClass<MeshGraph>("ProcgenMeshGraph",
std::function<MeshGraph*()>([]() -> MeshGraph* { return nullptr; }), true);
const HSQUIRRELVM meshVm = mesh.getHandle();
mesh.addFunc("addNode", [meshVm](MeshGraph* self, const std::string& id, const std::string& operation) {
return eve::script::projectResult(meshVm, self->addNode(id, operation));
});
mesh.addFunc("connect", [meshVm](MeshGraph* self, const std::string& from, const std::string& to, int slot) {
return eve::script::projectResult(meshVm, self->connect(from, to, slot));
});
mesh.addFunc("setNodeGrid", [meshVm](MeshGraph* self, const std::string& id, Grid2D* value) {
if (!value)
return eve::script::projectResult(
meshVm, Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "grid is null")));
return eve::script::projectResult(meshVm, self->setNodeGrid(id, *value));
});
mesh.addFunc("setNodePoints", [meshVm](MeshGraph* self, const std::string& id, PointSet* value) {
if (!value)
return eve::script::projectResult(
meshVm, Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "points are null")));
return eve::script::projectResult(meshVm, self->setNodePoints(id, *value));
});
mesh.addFunc("setNodeMesh", [meshVm](MeshGraph* self, const std::string& id, MeshBuild* value) {
if (!value)
return eve::script::projectResult(
meshVm, Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh is null")));
return eve::script::projectResult(meshVm, self->setNodeMesh(id, *value));
});
mesh.addFunc("setNodeFloat", [meshVm](MeshGraph* self, const std::string& id, const std::string& key, float value) {
return eve::script::projectResult(meshVm, self->setNodeFloat(id, key, value));
});
mesh.addFunc("setNodeString",
[meshVm](MeshGraph* self, const std::string& id, const std::string& key, const std::string& value) {
return eve::script::projectResult(meshVm, self->setNodeString(id, key, value));
});
mesh.addFunc("execute", [meshVm](MeshGraph* self, const std::string& output) -> ssq::Table {
auto result = self->execute(output);
if (!result.ok()) return eve::script::projectStatusResult(meshVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<MeshBuild>(
meshVm, std::make_unique<MeshBuild>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(meshVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(meshVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
mesh.addFunc("validate", [meshVm](MeshGraph* self, const std::string& output) {
return eve::script::projectResult(meshVm, self->validate(output));
});
mesh.addFunc("clearCache", &MeshGraph::clearCache);
mesh.addFunc("getRevision", [](MeshGraph* self) { return self->revision(); });
mesh.addFunc("serializeDefinition", &MeshGraph::serializeDefinition);
mesh.addFunc("deserializeDefinition", [meshVm](MeshGraph* self, const std::string& definition) {
return eve::script::projectResult(meshVm, self->deserializeDefinition(definition));
});
auto objects = table.addClass<ObjectBuildLayer>("ProcgenObjectBuildLayer", ssq::Class::Ctor<ObjectBuildLayer()>());
const HSQUIRRELVM objectsVm = objects.getHandle();
objects.addFunc("addAsset", [objectsVm](ObjectBuildLayer* self, const std::string& asset, float weight) {
return eve::script::projectResult(objectsVm, self->addAsset(asset, weight));
});
objects.addFunc("clearAssets", &ObjectBuildLayer::clearAssets);
objects.addFunc("setSeed", [](ObjectBuildLayer* self, std::uint32_t seed) { self->setSeed(seed); });
objects.addFunc("setLayerOffset",
[](ObjectBuildLayer* self, float x, float y, float z) { self->setLayerOffset(x, y, z); });
objects.addFunc("setLayerScale", [objectsVm](ObjectBuildLayer* self, float x, float y, float z) {
return eve::script::projectResult(objectsVm, self->setLayerScale(x, y, z));
});
objects.addFunc("setPositionRadius", [objectsVm](ObjectBuildLayer* self, float radius) {
return eve::script::projectResult(objectsVm, self->setPositionRadius(radius));
});
objects.addFunc("setRandomRotation", [objectsVm](ObjectBuildLayer* self, float minPitch, float maxPitch,
float minYaw, float maxYaw, float minRoll, float maxRoll) {
return eve::script::projectResult(
objectsVm, self->setRandomRotation(minPitch, maxPitch, minYaw, maxYaw, minRoll, maxRoll));
});
objects.addFunc("setRandomScale", [objectsVm](ObjectBuildLayer* self, float minX, float maxX, float minY,
float maxY, float minZ, float maxZ, bool uniform) {
return eve::script::projectResult(objectsVm, self->setRandomScale(minX, maxX, minY, maxY, minZ, maxZ, uniform));
});
objects.addFunc("setOrientation",
[objectsVm](ObjectBuildLayer* self, float cellSize, float yawOffset, bool invert) {
return eve::script::projectResult(objectsVm, self->setOrientation(cellSize, yawOffset, invert));
});
objects.addFunc("disableOrientation", [](ObjectBuildLayer* self) { self->disableOrientation(); });
objects.addFunc("setPlaceOnTop", [](ObjectBuildLayer* self, bool enabled, bool useLowest, float topOffset) {
self->setPlaceOnTop(enabled, useLowest, topOffset);
});
objects.addFunc("addChild", [objectsVm](ObjectBuildLayer* self, const std::string& asset, int count, float radius,
float minScale, float maxScale, float minYaw, float maxYaw) {
return eve::script::projectResult(objectsVm,
self->addChild(asset, count, radius, minScale, maxScale, minYaw, maxYaw));
});
objects.addFunc("clearChildRules", &ObjectBuildLayer::clearChildRules);
objects.addFunc("serializeDefinition", &ObjectBuildLayer::serializeDefinition);
objects.addFunc("deserializeDefinition", [objectsVm](ObjectBuildLayer* self, const std::string& definition) {
return eve::script::projectResult(objectsVm, self->deserializeDefinition(definition));
});
objects.addFunc("build", [objectsVm](ObjectBuildLayer* self, PointSet* source) -> ssq::Table {
if (!source)
return eve::script::projectStatusResult(
objectsVm,
Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "source points are null"))
.status(),
false, false);
auto result = self->build(*source);
if (!result.ok()) return eve::script::projectStatusResult(objectsVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<PointSet>(
objectsVm, std::make_unique<PointSet>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(objectsVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(objectsVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
objects.addFunc("buildOriented",
[objectsVm](ObjectBuildLayer* self, PointSet* source, PointSet* orientation) -> ssq::Table {
if (!source || !orientation)
return eve::script::projectStatusResult(
objectsVm,
Result<void>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, "source or orientation is null"))
.status(),
false, false);
auto result = self->build(*source, orientation);
if (!result.ok())
return eve::script::projectStatusResult(objectsVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<PointSet>(
objectsVm, std::make_unique<PointSet>(std::move(result).takeValue()));
if (!instance.ok())
return eve::script::projectStatusResult(objectsVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(objectsVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
auto execution = table.addClass<BuildLayerExecution>(
"ProcgenBuildLayerExecution",
std::function<BuildLayerExecution*()>([]() -> BuildLayerExecution* { return nullptr; }), true);
const HSQUIRRELVM executionVm = execution.getHandle();
execution.addFunc("getCount", [](BuildLayerExecution* self) { return self->getCount(); });
execution.addFunc("getId", &BuildLayerExecution::getId);
execution.addFunc("getType", &BuildLayerExecution::getType);
execution.addFunc("getMesh", [executionVm](BuildLayerExecution* self, int index) -> ssq::Table {
auto result = self->getMesh(index);
if (!result.ok()) return eve::script::projectStatusResult(executionVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<MeshBuild>(
executionVm, std::make_unique<MeshBuild>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(executionVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(executionVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
execution.addFunc("getPoints", [executionVm](BuildLayerExecution* self, int index) -> ssq::Table {
auto result = self->getPoints(index);
if (!result.ok()) return eve::script::projectStatusResult(executionVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<PointSet>(
executionVm, std::make_unique<PointSet>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(executionVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(executionVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
auto stack = table.addClass<BuildLayerStack>("ProcgenBuildLayerStack", ssq::Class::Ctor<BuildLayerStack()>());
const HSQUIRRELVM stackVm = stack.getHandle();
stack.addFunc("addTileLayer", [stackVm](BuildLayerStack* self, const std::string& id, bool enabled, float cellSize,
float height, const std::string& group) {
return eve::script::projectResult(stackVm, self->addTileLayer(id, enabled, cellSize, height, group));
});
stack.addFunc("addObjectLayer", [stackVm](BuildLayerStack* self, const std::string& id, bool enabled,
ObjectBuildLayer* layer) {
if (!layer)
return eve::script::projectResult(stackVm, Result<void>::failure(Diagnostic::error(
DiagnosticCode::InvalidArgument, "object layer is null")));
return eve::script::projectResult(stackVm, self->addObjectLayer(id, enabled, *layer));
});
stack.addFunc("setEnabled", [stackVm](BuildLayerStack* self, const std::string& id, bool enabled) {
return eve::script::projectResult(stackVm, self->setEnabled(id, enabled));
});
stack.addFunc("clear", &BuildLayerStack::clear);
stack.addFunc("getLayerCount", [](BuildLayerStack* self) { return self->getLayerCount(); });
stack.addFunc("getLayerId", &BuildLayerStack::getLayerId);
stack.addFunc("getLayerType", &BuildLayerStack::getLayerType);
stack.addFunc("isLayerEnabled", [](BuildLayerStack* self, int index) { return self->isLayerEnabled(index); });
stack.addFunc("serializeDefinition", &BuildLayerStack::serializeDefinition);
stack.addFunc("deserializeDefinition", [stackVm](BuildLayerStack* self, const std::string& definition) {
return eve::script::projectResult(stackVm, self->deserializeDefinition(definition));
});
stack.addFunc("execute", [stackVm](BuildLayerStack* self, Grid2D* grid, PointSet* points) -> ssq::Table {
if (!grid || !points)
return eve::script::projectStatusResult(
stackVm,
Result<void>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, "grid or points input is null"))
.status(),
false, false);
auto result = self->execute(*grid, *points);
if (!result.ok()) return eve::script::projectStatusResult(stackVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<BuildLayerExecution>(
stackVm, std::make_unique<BuildLayerExecution>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(stackVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(stackVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
stack.addFunc(
"executeOriented",
[stackVm](BuildLayerStack* self, Grid2D* grid, PointSet* points, PointSet* orientation) -> ssq::Table {
if (!grid || !points || !orientation)
return eve::script::projectStatusResult(
stackVm,
Result<void>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, "build-layer input is null"))
.status(),
false, false);
auto result = self->execute(*grid, *points, orientation);
if (!result.ok()) return eve::script::projectStatusResult(stackVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<BuildLayerExecution>(
stackVm, std::make_unique<BuildLayerExecution>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(stackVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(stackVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
auto delta = table.addClass<IncrementalBuildDelta>(
"ProcgenIncrementalBuildDelta",
std::function<IncrementalBuildDelta*()>([]() -> IncrementalBuildDelta* { return nullptr; }), true);
const HSQUIRRELVM deltaVm = delta.getHandle();
delta.addFunc("getCount", [](IncrementalBuildDelta* self) { return self->getCount(); });
delta.addFunc("getClusterX", [](IncrementalBuildDelta* self, int index) { return self->getClusterX(index); });
delta.addFunc("getClusterZ", [](IncrementalBuildDelta* self, int index) { return self->getClusterZ(index); });
delta.addFunc("isRemoved", [](IncrementalBuildDelta* self, int index) { return self->isRemoved(index); });
delta.addFunc("getArtifacts", [deltaVm](IncrementalBuildDelta* self, int index) -> ssq::Table {
auto result = self->getArtifacts(index);
if (!result.ok()) return eve::script::projectStatusResult(deltaVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<BuildLayerExecution>(
deltaVm, std::make_unique<BuildLayerExecution>(std::move(result).takeValue()));
if (!instance.ok()) return eve::script::projectStatusResult(deltaVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(deltaVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
auto incremental = table.addClass<IncrementalBuildExecutor>("ProcgenIncrementalBuildExecutor",
ssq::Class::Ctor<IncrementalBuildExecutor()>());
const HSQUIRRELVM incrementalVm = incremental.getHandle();
incremental.addFunc("clear", &IncrementalBuildExecutor::clear);
incremental.addFunc("getCachedClusterCount",
[](IncrementalBuildExecutor* self) { return self->getCachedClusterCount(); });
incremental.addFunc("getCachedArtifacts",
[incrementalVm](IncrementalBuildExecutor* self, int x, int z) -> ssq::Table {
auto result = self->getCachedArtifacts(x, z);
if (!result.ok())
return eve::script::projectStatusResult(incrementalVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<BuildLayerExecution>(
incrementalVm,
std::make_unique<BuildLayerExecution>(std::move(result).takeValue()));
if (!instance.ok())
return eve::script::projectStatusResult(incrementalVm, instance.status(), false, false);
auto projected =
eve::script::projectStatusResult(incrementalVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
incremental.addFunc(
"update",
[incrementalVm](IncrementalBuildExecutor* self, BuildLayerStack* buildStack, Grid2D* grid, PointSet* points,
int clusterSizeCells, float cellSizeWorld) -> ssq::Table {
if (!buildStack || !grid || !points)
return eve::script::projectStatusResult(
incrementalVm,
Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
"incremental build input is null"))
.status(),
false, false);
auto result = self->update(*buildStack, *grid, *points, clusterSizeCells, cellSizeWorld);
if (!result.ok())
return eve::script::projectStatusResult(incrementalVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<IncrementalBuildDelta>(
incrementalVm, std::make_unique<IncrementalBuildDelta>(std::move(result).takeValue()));
if (!instance.ok())
return eve::script::projectStatusResult(incrementalVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(incrementalVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
incremental.addFunc(
"updateOriented",
[incrementalVm](IncrementalBuildExecutor* self, BuildLayerStack* buildStack, Grid2D* grid, PointSet* points,
int clusterSizeCells, float cellSizeWorld, PointSet* orientation) -> ssq::Table {
if (!buildStack || !grid || !points || !orientation)
return eve::script::projectStatusResult(
incrementalVm,
Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
"incremental build input is null"))
.status(),
false, false);
auto result =
self->update(*buildStack, *grid, *points, clusterSizeCells, cellSizeWorld, orientation);
if (!result.ok())
return eve::script::projectStatusResult(incrementalVm, result.status(), false, false);
auto instance = eve::script::makeOwnedSquirrelInstance<IncrementalBuildDelta>(
incrementalVm, std::make_unique<IncrementalBuildDelta>(std::move(result).takeValue()));
if (!instance.ok())
return eve::script::projectStatusResult(incrementalVm, instance.status(), false, false);
auto projected = eve::script::projectStatusResult(incrementalVm, Status::success(), true, true);
projected.set("value", std::move(instance).takeValue());
return projected;
});
}
} // namespace eve::procgen
+11
View File
@@ -0,0 +1,11 @@
#pragma once
namespace ssq {
class Table;
}
namespace eve::procgen {
void exposeGridMeshGraphs(ssq::Table& table);
} // namespace eve::procgen
+228
View File
@@ -0,0 +1,228 @@
#include "procgen/IncrementalBuild.h"
#include "common/Diagnostic.h"
#include "procgen/Semantic.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <set>
#include <string_view>
namespace eve::procgen {
namespace {
template <class T>
Result<T> fail(DiagnosticCode code, std::string message) {
return Result<T>::failure(Diagnostic::error(code, std::move(message), {}, {}, "procgen.incrementalBuild"));
}
std::uint64_t keyFor(int x, int z) {
return (std::uint64_t(std::uint32_t(x)) << 32U) | std::uint32_t(z);
}
int keyX(std::uint64_t key) { return std::int32_t(key >> 32U); }
int keyZ(std::uint64_t key) { return std::int32_t(key & 0xffffffffU); }
void hashBytes(std::uint64_t& hash, const void* bytes, std::size_t count) {
const auto* data = static_cast<const unsigned char*>(bytes);
for (std::size_t index = 0; index < count; ++index) {
hash ^= data[index];
hash *= 1099511628211ULL;
}
}
template <class T>
void hashValue(std::uint64_t& hash, const T& value) {
hashBytes(hash, &value, sizeof(value));
}
void hashString(std::uint64_t& hash, std::string_view value) {
hashBytes(hash, value.data(), value.size());
const unsigned char separator = 0xff;
hashBytes(hash, &separator, 1);
}
void hashPoint(std::uint64_t& hash, const PointSet& points, std::size_t row) {
const auto& point = points.points()[row];
hashValue(hash, point.id);
hashValue(hash, point.x);
hashValue(hash, point.y);
hashValue(hash, point.z);
hashValue(hash, point.normalX);
hashValue(hash, point.normalY);
hashValue(hash, point.normalZ);
hashValue(hash, point.pitch);
hashValue(hash, point.yaw);
hashValue(hash, point.roll);
hashValue(hash, point.scaleX);
hashValue(hash, point.scaleY);
hashValue(hash, point.scaleZ);
hashValue(hash, point.density);
hashValue(hash, point.seed);
hashValue(hash, point.boundsMinX);
hashValue(hash, point.boundsMinY);
hashValue(hash, point.boundsMinZ);
hashValue(hash, point.boundsMaxX);
hashValue(hash, point.boundsMaxY);
hashValue(hash, point.boundsMaxZ);
hashValue(hash, point.colorR);
hashValue(hash, point.colorG);
hashValue(hash, point.colorB);
hashValue(hash, point.colorA);
hashValue(hash, point.steepness);
const auto& attributes = points.attributes();
for (std::size_t column = 0; column < attributes.columnCount(); ++column) {
const auto name = attributes.columnName(column);
hashString(hash, name);
const auto type = attributes.typeOf(name);
if (!type || !attributes.has(row, name)) {
const std::uint8_t absent = 0;
hashValue(hash, absent);
continue;
}
const auto typeValue = std::uint8_t(*type) + 1;
hashValue(hash, typeValue);
switch (*type) {
case ProcgenAttributeType::Float: hashValue(hash, *attributes.getFloat(row, name)); break;
case ProcgenAttributeType::Int: hashValue(hash, *attributes.getInt(row, name)); break;
case ProcgenAttributeType::Bool: hashValue(hash, *attributes.getBool(row, name)); break;
case ProcgenAttributeType::Vector: {
const auto value = *attributes.getVector(row, name);
hashValue(hash, value.x);
hashValue(hash, value.y);
hashValue(hash, value.z);
break;
}
case ProcgenAttributeType::String: hashString(hash, *attributes.getString(row, name)); break;
}
}
}
int floorDivision(float value, float divisor) { return int(std::floor(value / divisor)); }
bool gridContent(const Grid2D& grid, int clusterX, int clusterZ, int size) {
const int minX = std::max(0, clusterX * size);
const int minY = std::max(0, clusterZ * size);
const int maxX = std::min(grid.getWidth(), (clusterX + 1) * size);
const int maxY = std::min(grid.getHeight(), (clusterZ + 1) * size);
for (int y = minY; y < maxY; ++y)
for (int x = minX; x < maxX; ++x)
if (grid.getCell(x, y) != int(Semantic::Empty)) return true;
return false;
}
std::uint64_t clusterHash(const BuildLayerStack& stack, const Grid2D& grid, const PointSet& points,
const PointSet* orientation, int clusterX, int clusterZ, int size, float worldSize) {
std::uint64_t hash = 1469598103934665603ULL;
const auto definition = stack.serializeDefinition();
hashString(hash, definition);
const int minX = std::max(0, clusterX * size - 1);
const int minY = std::max(0, clusterZ * size - 1);
const int maxX = std::min(grid.getWidth(), (clusterX + 1) * size + 1);
const int maxY = std::min(grid.getHeight(), (clusterZ + 1) * size + 1);
for (int y = minY; y < maxY; ++y) {
for (int x = minX; x < maxX; ++x) {
const int cell = grid.getCell(x, y);
const int detail = grid.getDetail(x, y);
hashValue(hash, cell);
hashValue(hash, detail);
}
}
for (std::size_t index = 0; index < points.points().size(); ++index) {
const auto& point = points.points()[index];
if (floorDivision(point.x, worldSize) == clusterX && floorDivision(point.z, worldSize) == clusterZ)
hashPoint(hash, points, index);
}
if (orientation) {
for (std::size_t index = 0; index < orientation->points().size(); ++index) hashPoint(hash, *orientation, index);
}
return hash;
}
} // namespace
int IncrementalBuildDelta::getCount() const noexcept { return int(changes_.size()); }
int IncrementalBuildDelta::getClusterX(int index) const noexcept {
return index >= 0 && index < int(changes_.size()) ? changes_[std::size_t(index)].x : 0;
}
int IncrementalBuildDelta::getClusterZ(int index) const noexcept {
return index >= 0 && index < int(changes_.size()) ? changes_[std::size_t(index)].z : 0;
}
bool IncrementalBuildDelta::isRemoved(int index) const noexcept {
return index >= 0 && index < int(changes_.size()) && changes_[std::size_t(index)].removed;
}
Result<BuildLayerExecution> IncrementalBuildDelta::getArtifacts(int index) const {
if (index < 0 || index >= int(changes_.size()))
return fail<BuildLayerExecution>(DiagnosticCode::InvalidArgument, "delta index is out of range");
if (changes_[std::size_t(index)].removed)
return fail<BuildLayerExecution>(DiagnosticCode::NotFound, "removed cluster has no artifacts");
return Result<BuildLayerExecution>::success(changes_[std::size_t(index)].artifacts);
}
Result<IncrementalBuildDelta> IncrementalBuildExecutor::update(const BuildLayerStack& stack, const Grid2D& grid,
const PointSet& points, int clusterSizeCells,
float cellSizeWorld, const PointSet* orientation) {
if (clusterSizeCells <= 0 || !std::isfinite(cellSizeWorld) || cellSizeWorld <= 0.f)
return fail<IncrementalBuildDelta>(DiagnosticCode::InvalidArgument, "cluster and cell sizes must be positive");
const float worldSize = float(clusterSizeCells) * cellSizeWorld;
std::set<std::pair<int, int>> candidates;
for (const auto& [key, record] : cache_) candidates.emplace(keyX(key), keyZ(key));
for (int y = 0; y < grid.getHeight(); ++y)
for (int x = 0; x < grid.getWidth(); ++x)
if (grid.getCell(x, y) != int(Semantic::Empty)) candidates.emplace(x / clusterSizeCells, y / clusterSizeCells);
for (const auto& point : points.points())
candidates.emplace(floorDivision(point.x, worldSize), floorDivision(point.z, worldSize));
IncrementalBuildDelta delta;
std::vector<std::uint64_t> removals;
std::vector<std::pair<std::uint64_t, Record>> upserts;
for (const auto& [clusterX, clusterZ] : candidates) {
const auto key = keyFor(clusterX, clusterZ);
bool active = gridContent(grid, clusterX, clusterZ, clusterSizeCells);
if (!active) {
for (const auto& point : points.points()) {
if (floorDivision(point.x, worldSize) == clusterX && floorDivision(point.z, worldSize) == clusterZ) {
active = true;
break;
}
}
}
const auto previous = cache_.find(key);
if (!active) {
if (previous != cache_.end()) {
removals.push_back(key);
delta.changes_.push_back({clusterX, clusterZ, true, {}});
}
continue;
}
const auto hash = clusterHash(stack, grid, points, orientation, clusterX, clusterZ, clusterSizeCells, worldSize);
if (previous != cache_.end() && previous->second.hash == hash) continue;
const int minCellX = std::clamp(clusterX * clusterSizeCells, 0, grid.getWidth());
const int minCellY = std::clamp(clusterZ * clusterSizeCells, 0, grid.getHeight());
const int maxCellX = std::clamp((clusterX + 1) * clusterSizeCells, minCellX, grid.getWidth());
const int maxCellY = std::clamp((clusterZ + 1) * clusterSizeCells, minCellY, grid.getHeight());
BuildLayerRegion region{minCellX, minCellY, maxCellX, maxCellY, float(clusterX) * worldSize,
float(clusterZ) * worldSize, float(clusterX + 1) * worldSize,
float(clusterZ + 1) * worldSize};
auto built = stack.executeRegion(grid, points, region, orientation);
if (!built.ok()) return Result<IncrementalBuildDelta>::failure(built.status());
Record record{hash, std::move(built).takeValue()};
delta.changes_.push_back({clusterX, clusterZ, false, record.artifacts});
upserts.emplace_back(key, std::move(record));
}
for (const auto key : removals) cache_.erase(key);
for (auto& [key, record] : upserts) cache_.insert_or_assign(key, std::move(record));
return Result<IncrementalBuildDelta>::success(std::move(delta));
}
void IncrementalBuildExecutor::clear() { cache_.clear(); }
int IncrementalBuildExecutor::getCachedClusterCount() const noexcept { return int(cache_.size()); }
Result<BuildLayerExecution> IncrementalBuildExecutor::getCachedArtifacts(int clusterX, int clusterZ) const {
const auto found = cache_.find(keyFor(clusterX, clusterZ));
if (found == cache_.end()) return fail<BuildLayerExecution>(DiagnosticCode::NotFound, "cluster is not cached");
return Result<BuildLayerExecution>::success(found->second.artifacts);
}
} // namespace eve::procgen
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include "common/Result.h"
#include "procgen/BuildLayerStack.h"
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace eve::procgen {
/** @brief One cache mutation emitted by an incremental cluster rebuild. */
struct BuildClusterChange {
int x = 0;
int z = 0;
bool removed = false;
BuildLayerExecution artifacts;
};
/** @brief Ordered changes required to synchronize a scene-side cluster cache. */
class IncrementalBuildDelta {
public:
[[nodiscard]] int getCount() const noexcept;
[[nodiscard]] int getClusterX(int index) const noexcept;
[[nodiscard]] int getClusterZ(int index) const noexcept;
[[nodiscard]] bool isRemoved(int index) const noexcept;
[[nodiscard]] Result<BuildLayerExecution> getArtifacts(int index) const;
private:
friend class IncrementalBuildExecutor;
std::vector<BuildClusterChange> changes_;
};
/**
* @brief Derived cluster cache that incrementally executes a BuildLayerStack.
*
* Grid hashes include a one-cell halo so tile-side geometry is invalidated when a
* neighboring cluster changes. Point ownership follows the source point's half-open
* world-space cluster. Updates are transactional and preserve the old cache on failure.
* @thread Affine; update and query on the owning thread.
*/
class IncrementalBuildExecutor {
public:
/**
* @brief Rebuild only clusters whose layer definition, grid halo, point rows or orientation changed.
* @param stack Authoritative ordered layer definitions.
* @param grid Authoritative tile source.
* @param points Authoritative object placement source.
* @param clusterSizeCells Positive square cluster extent in grid cells.
* @param cellSizeWorld Positive world-space size of one grid cell.
* @param orientation Optional object orientation layer; changes invalidate all active clusters.
* @return Deterministically ordered upsert/removal delta. Empty means the cache was already current.
* @cost O(grid cells + points + dirty cluster build cost); unchanged artifacts are copied only by cache ownership.
*/
[[nodiscard]] Result<IncrementalBuildDelta> update(const BuildLayerStack& stack, const Grid2D& grid,
const PointSet& points, int clusterSizeCells,
float cellSizeWorld, const PointSet* orientation = nullptr);
void clear();
[[nodiscard]] int getCachedClusterCount() const noexcept;
[[nodiscard]] Result<BuildLayerExecution> getCachedArtifacts(int clusterX, int clusterZ) const;
private:
struct Record {
std::uint64_t hash = 0;
BuildLayerExecution artifacts;
};
std::unordered_map<std::uint64_t, Record> cache_;
};
} // namespace eve::procgen
+417
View File
@@ -0,0 +1,417 @@
#include "procgen/MeshGraph.h"
#include "common/Diagnostic.h"
#include "procgen/Semantic.h"
#include "procgen/TilePreset.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <queue>
#include <sstream>
#include <unordered_set>
namespace eve::procgen {
namespace {
enum class ValueType { Grid, Points, Mesh };
struct Spec {
const char* id;
int inputs;
ValueType input[2];
ValueType output;
};
constexpr Spec specs[] = {
{"grid.input", 0, {}, ValueType::Grid},
{"point.input", 0, {}, ValueType::Points},
{"mesh.input", 0, {}, ValueType::Mesh},
{"mesh.grid_tiles", 1, {ValueType::Grid}, ValueType::Mesh},
{"mesh.instance_points", 2, {ValueType::Mesh, ValueType::Points}, ValueType::Mesh},
{"mesh.merge", 2, {ValueType::Mesh, ValueType::Mesh}, ValueType::Mesh},
{"mesh.transform", 1, {ValueType::Mesh}, ValueType::Mesh},
};
const Spec* specFor(std::string_view id) {
const auto found =
std::find_if(std::begin(specs), std::end(specs), [id](const Spec& spec) { return id == spec.id; });
return found == std::end(specs) ? nullptr : found;
}
template <class T>
Result<T> failure(DiagnosticCode code, std::string message, std::string path = {}) {
return Result<T>::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, "procgen.meshGraph"));
}
Result<void> failureVoid(DiagnosticCode code, std::string message, std::string path = {}) {
return Result<void>::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, "procgen.meshGraph"));
}
ValueType typeOf(const MeshGraphValue& value) {
if (std::holds_alternative<Grid2D>(value)) return ValueType::Grid;
if (std::holds_alternative<PointSet>(value)) return ValueType::Points;
return ValueType::Mesh;
}
bool acceptsFloat(std::string_view operation, std::string_view key) {
if (operation == "mesh.grid_tiles")
return key == "cellSize" || key == "height" || key == "minX" || key == "minY" || key == "maxX" ||
key == "maxY";
if (operation == "mesh.instance_points") return key == "scale";
if (operation == "mesh.transform")
return key == "x" || key == "y" || key == "z" || key == "yaw" || key == "sx" || key == "sy" || key == "sz";
return false;
}
bool acceptsString(std::string_view operation, std::string_view key) {
return operation == "mesh.grid_tiles" && key == "group";
}
void addQuad(MeshBuild& mesh, float ax, float ay, float az, float bx, float by, float bz, float cx, float cy, float cz,
float dx, float dy, float dz, float nx, float ny, float nz) {
const auto base = std::uint32_t(mesh.getVertexCount());
mesh.addVertex(ax, ay, az, nx, ny, nz, 0.f, 0.f);
mesh.addVertex(bx, by, bz, nx, ny, nz, 1.f, 0.f);
mesh.addVertex(cx, cy, cz, nx, ny, nz, 1.f, 1.f);
mesh.addVertex(dx, dy, dz, nx, ny, nz, 0.f, 1.f);
mesh.addTriangle(base, base + 2, base + 1);
mesh.addTriangle(base, base + 3, base + 2);
}
MeshBuild buildTiles(const Grid2D& grid, float cellSize, float height, std::string_view group, int minX, int minY,
int maxX, int maxY) {
MeshBuild mesh;
minX = std::clamp(minX, 0, grid.getWidth());
minY = std::clamp(minY, 0, grid.getHeight());
maxX = std::clamp(maxX, minX, grid.getWidth());
maxY = std::clamp(maxY, minY, grid.getHeight());
for (int y = minY; y < maxY; ++y) {
for (int x = minX; x < maxX; ++x) {
if (grid.getCell(x, y) == int(Semantic::Empty)) continue;
const auto preset = resolveTilePreset(std::uint8_t(grid.getDetail(x, y)));
const std::string cellGroup =
(group.empty() ? std::string("tiles") : std::string(group)) + "/" + std::to_string(grid.getCell(x, y)) +
"/" + std::string(tilePresetKindName(preset.kind)) + "/r" + std::to_string(preset.rotationDegrees) +
(preset.mirrorX ? "/mx/" : "/normal/") + std::to_string(preset.configuration);
mesh.setActiveGroup(cellGroup);
const float x0 = float(x) * cellSize;
const float x1 = x0 + cellSize;
const float z0 = float(y) * cellSize;
const float z1 = z0 + cellSize;
addQuad(mesh, x0, height, z0, x1, height, z0, x1, height, z1, x0, height, z1, 0.f, 1.f, 0.f);
if (grid.getCell(x - 1, y) == int(Semantic::Empty))
addQuad(mesh, x0, 0.f, z1, x0, 0.f, z0, x0, height, z0, x0, height, z1, -1.f, 0.f, 0.f);
if (grid.getCell(x + 1, y) == int(Semantic::Empty))
addQuad(mesh, x1, 0.f, z0, x1, 0.f, z1, x1, height, z1, x1, height, z0, 1.f, 0.f, 0.f);
if (grid.getCell(x, y - 1) == int(Semantic::Empty))
addQuad(mesh, x0, 0.f, z0, x1, 0.f, z0, x1, height, z0, x0, height, z0, 0.f, 0.f, -1.f);
if (grid.getCell(x, y + 1) == int(Semantic::Empty))
addQuad(mesh, x1, 0.f, z1, x0, 0.f, z1, x0, height, z1, x1, height, z1, 0.f, 0.f, 1.f);
}
}
mesh.setMeta("source", "mesh.grid_tiles");
return mesh;
}
} // namespace
Result<void> MeshGraph::addNode(std::string id, std::string operation) {
if (id.empty()) return failureVoid(DiagnosticCode::InvalidArgument, "node id is empty");
if (!specFor(operation)) return failureVoid(DiagnosticCode::NotFound, "unknown operation: " + operation, id);
if (nodes_.contains(id)) return failureVoid(DiagnosticCode::Conflict, "duplicate node id", id);
Node node;
node.id = id;
node.operation = std::move(operation);
nodes_.emplace(id, std::move(node));
order_.push_back(std::move(id));
++revision_;
return Result<void>::success();
}
Result<void> MeshGraph::connect(std::string_view fromId, std::string_view toId, int inputIndex) {
const auto from = nodes_.find(std::string(fromId));
const auto to = nodes_.find(std::string(toId));
if (from == nodes_.end() || to == nodes_.end())
return failureVoid(DiagnosticCode::NotFound, "connection references an unknown node", std::string(toId));
const Spec* source = specFor(from->second.operation);
const Spec* target = specFor(to->second.operation);
if (inputIndex < 0 || inputIndex >= target->inputs)
return failureVoid(DiagnosticCode::InvalidArgument, "input slot is outside the operation contract",
std::string(toId));
if (source->output != target->input[inputIndex])
return failureVoid(DiagnosticCode::TypeMismatch, "typed graph ports are incompatible", std::string(toId));
to->second.inputs[inputIndex] = std::string(fromId);
invalidateFrom(toId);
++revision_;
return Result<void>::success();
}
Result<void> MeshGraph::setNodeGrid(std::string_view id, const Grid2D& value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end() || found->second.operation != "grid.input")
return failureVoid(DiagnosticCode::TypeMismatch, "node is not grid.input", std::string(id));
found->second.value = value;
found->second.hasValue = true;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> MeshGraph::setNodePoints(std::string_view id, const PointSet& value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end() || found->second.operation != "point.input")
return failureVoid(DiagnosticCode::TypeMismatch, "node is not point.input", std::string(id));
found->second.value = value;
found->second.hasValue = true;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> MeshGraph::setNodeMesh(std::string_view id, const MeshBuild& value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end() || found->second.operation != "mesh.input")
return failureVoid(DiagnosticCode::TypeMismatch, "node is not mesh.input", std::string(id));
found->second.value = value;
found->second.hasValue = true;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> MeshGraph::setNodeFloat(std::string_view id, std::string key, float value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failureVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
if (!acceptsFloat(found->second.operation, key))
return failureVoid(DiagnosticCode::InvalidArgument, "parameter is not declared by this operation: " + key,
std::string(id));
if (!std::isfinite(value))
return failureVoid(DiagnosticCode::InvalidArgument, "mesh parameter must be finite", std::string(id));
found->second.floats[std::move(key)] = value;
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<void> MeshGraph::setNodeString(std::string_view id, std::string key, std::string value) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end()) return failureVoid(DiagnosticCode::NotFound, "unknown node", std::string(id));
if (!acceptsString(found->second.operation, key))
return failureVoid(DiagnosticCode::InvalidArgument, "parameter is not declared by this operation: " + key,
std::string(id));
found->second.strings[std::move(key)] = std::move(value);
invalidateFrom(id);
++revision_;
return Result<void>::success();
}
Result<MeshGraphValue> MeshGraph::evaluate(std::string_view id, std::unordered_map<std::string, int>& states) {
const auto found = nodes_.find(std::string(id));
if (found == nodes_.end())
return failure<MeshGraphValue>(DiagnosticCode::NotFound, "unknown node", std::string(id));
Node& node = found->second;
if (node.cacheValid) return Result<MeshGraphValue>::success(node.cache);
if (states[node.id] == 1)
return failure<MeshGraphValue>(DiagnosticCode::Conflict, "graph contains a cycle", node.id);
states[node.id] = 1;
const Spec* spec = specFor(node.operation);
std::vector<MeshGraphValue> inputs;
for (int input = 0; input < spec->inputs; ++input) {
if (node.inputs[input].empty())
return failure<MeshGraphValue>(DiagnosticCode::NotFound, "required input is disconnected", node.id);
auto value = evaluate(node.inputs[input], states);
if (!value.ok()) return value;
if (typeOf(value.value()) != spec->input[input])
return failure<MeshGraphValue>(DiagnosticCode::TypeMismatch, "runtime port type mismatch", node.id);
inputs.push_back(std::move(value).takeValue());
}
if (spec->inputs == 0) {
if (!node.hasValue)
return failure<MeshGraphValue>(DiagnosticCode::NotFound, "input node has no value", node.id);
node.cache = node.value;
} else if (node.operation == "mesh.grid_tiles") {
const auto& grid = std::get<Grid2D>(inputs[0]);
node.cache = buildTiles(
grid, node.floats.contains("cellSize") ? node.floats["cellSize"] : 1.f,
node.floats.contains("height") ? node.floats["height"] : 1.f,
node.strings.contains("group") ? node.strings["group"] : "tiles",
node.floats.contains("minX") ? int(node.floats["minX"]) : 0,
node.floats.contains("minY") ? int(node.floats["minY"]) : 0,
node.floats.contains("maxX") ? int(node.floats["maxX"]) : grid.getWidth(),
node.floats.contains("maxY") ? int(node.floats["maxY"]) : grid.getHeight());
} else if (node.operation == "mesh.merge") {
MeshBuild result = std::get<MeshBuild>(inputs[0]);
const MeshBuild& other = std::get<MeshBuild>(inputs[1]);
if (!result.appendTransformed(&other, 0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f))
return failure<MeshGraphValue>(DiagnosticCode::Failed, "mesh merge failed", node.id);
node.cache = std::move(result);
} else if (node.operation == "mesh.transform") {
MeshBuild result;
const MeshBuild& source = std::get<MeshBuild>(inputs[0]);
if (!result.appendTransformed(&source, node.floats["x"], node.floats["y"], node.floats["z"], node.floats["yaw"],
node.floats.contains("sx") ? node.floats["sx"] : 1.f,
node.floats.contains("sy") ? node.floats["sy"] : 1.f,
node.floats.contains("sz") ? node.floats["sz"] : 1.f))
return failure<MeshGraphValue>(DiagnosticCode::InvalidArgument, "mesh transform failed", node.id);
node.cache = std::move(result);
} else if (node.operation == "mesh.instance_points") {
MeshBuild result;
const MeshBuild& source = std::get<MeshBuild>(inputs[0]);
const PointSet& points = std::get<PointSet>(inputs[1]);
const float scale = node.floats.contains("scale") ? node.floats["scale"] : 1.f;
for (int index = 0; index < points.getCount(); ++index)
if (!result.appendTransformed(&source, points.getX(index), points.getY(index), points.getZ(index), 0.f,
scale, scale, scale))
return failure<MeshGraphValue>(DiagnosticCode::InvalidArgument, "point instancing failed", node.id);
node.cache = std::move(result);
}
states[node.id] = 2;
node.cacheValid = true;
return Result<MeshGraphValue>::success(node.cache);
}
Result<MeshBuild> MeshGraph::execute(std::string_view outputId) {
std::unordered_map<std::string, int> states;
auto value = evaluate(outputId, states);
if (!value.ok()) return Result<MeshBuild>::failure(value.status());
const auto* mesh = std::get_if<MeshBuild>(&value.value());
if (!mesh)
return failure<MeshBuild>(DiagnosticCode::TypeMismatch, "output node is not a mesh", std::string(outputId));
return Result<MeshBuild>::success(*mesh);
}
Result<void> MeshGraph::validate(std::string_view outputId) const {
MeshGraph copy = *this;
auto result = copy.execute(outputId);
if (!result.ok()) return Result<void>::failure(result.status());
return Result<void>::success();
}
void MeshGraph::clearCache() {
for (auto& [id, node] : nodes_) node.cacheValid = false;
}
void MeshGraph::invalidateFrom(std::string_view id) {
std::queue<std::string> pending;
std::unordered_set<std::string> visited;
pending.push(std::string(id));
while (!pending.empty()) {
const std::string current = pending.front();
pending.pop();
if (!visited.insert(current).second) continue;
const auto found = nodes_.find(current);
if (found != nodes_.end()) found->second.cacheValid = false;
for (const auto& [otherId, node] : nodes_)
if (node.inputs[0] == current || node.inputs[1] == current) pending.push(otherId);
}
}
std::string MeshGraph::serializeDefinition() const {
std::ostringstream out;
out << "EVPCG_MESH_GRAPH 1\n" << std::setprecision(9);
for (const auto& id : order_) {
const auto& node = nodes_.at(id);
out << "NODE " << std::quoted(id) << ' ' << std::quoted(node.operation) << '\n';
std::vector<std::string> keys;
for (const auto& [key, value] : node.floats) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const auto& key : keys)
out << "FLOAT " << std::quoted(id) << ' ' << std::quoted(key) << ' ' << node.floats.at(key) << '\n';
keys.clear();
for (const auto& [key, value] : node.strings) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const auto& key : keys)
out << "STRING " << std::quoted(id) << ' ' << std::quoted(key) << ' ' << std::quoted(node.strings.at(key))
<< '\n';
}
for (const auto& id : order_) {
const auto& node = nodes_.at(id);
for (int input = 0; input < 2; ++input)
if (!node.inputs[input].empty())
out << "EDGE " << std::quoted(node.inputs[input]) << ' ' << std::quoted(id) << ' ' << input << '\n';
}
out << "END\n";
return out.str();
}
Result<void> MeshGraph::deserializeDefinition(std::string_view definition) {
MeshGraph replacement;
std::istringstream input{std::string(definition)};
std::string magic;
int version = 0;
if (!(input >> magic >> version) || magic != "EVPCG_MESH_GRAPH" || version != 1)
return failureVoid(DiagnosticCode::ParseError, "invalid mesh graph header");
std::string line;
std::getline(input, line);
bool ended = false;
while (std::getline(input, line)) {
if (line.empty()) continue;
std::istringstream record(line);
std::string kind;
record >> kind;
if (kind == "END") {
ended = true;
break;
}
std::string id;
auto result = [&]() -> Result<void> {
if (kind == "NODE") {
std::string operation;
if (!(record >> std::quoted(id) >> std::quoted(operation)))
return failureVoid(DiagnosticCode::ParseError, "invalid NODE record");
return replacement.addNode(std::move(id), std::move(operation));
}
if (kind == "EDGE") {
std::string target;
int slot = -1;
if (!(record >> std::quoted(id) >> std::quoted(target) >> slot))
return failureVoid(DiagnosticCode::ParseError, "invalid EDGE record");
return replacement.connect(id, target, slot);
}
if (kind == "FLOAT") {
std::string key;
float value = 0.f;
if (!(record >> std::quoted(id) >> std::quoted(key) >> value))
return failureVoid(DiagnosticCode::ParseError, "invalid FLOAT record");
return replacement.setNodeFloat(id, std::move(key), value);
}
if (kind == "STRING") {
std::string key;
std::string value;
if (!(record >> std::quoted(id) >> std::quoted(key) >> std::quoted(value)))
return failureVoid(DiagnosticCode::ParseError, "invalid STRING record");
return replacement.setNodeString(id, std::move(key), std::move(value));
}
return failureVoid(DiagnosticCode::ParseError, "unknown mesh graph record: " + kind);
}();
if (!result.ok()) return result;
record >> std::ws;
if (!record.eof()) return failureVoid(DiagnosticCode::ParseError, "trailing mesh graph record data");
}
if (!ended) return failureVoid(DiagnosticCode::ParseError, "mesh graph END record is missing");
std::unordered_map<std::string, int> states;
const auto visit = [&](const auto& self, const std::string& id) -> Result<void> {
if (states[id] == 2) return Result<void>::success();
if (states[id] == 1) return failureVoid(DiagnosticCode::Conflict, "cycle in serialized graph", id);
states[id] = 1;
const auto found = replacement.nodes_.find(id);
if (found == replacement.nodes_.end()) return failureVoid(DiagnosticCode::NotFound, "unknown node", id);
for (const auto& dependency : found->second.inputs) {
if (dependency.empty()) continue;
auto result = self(self, dependency);
if (!result.ok()) return result;
}
states[id] = 2;
return Result<void>::success();
};
for (const auto& id : replacement.order_) {
auto result = visit(visit, id);
if (!result.ok()) return result;
}
*this = std::move(replacement);
return Result<void>::success();
}
} // namespace eve::procgen
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "common/Result.h"
#include "procgen/Grid2D.h"
#include "procgen/MeshBuild.h"
#include "procgen/PointSet.h"
#include <cstdint>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>
namespace eve::procgen {
/** @brief Strong values accepted by MeshGraph ports. */
using MeshGraphValue = std::variant<Grid2D, PointSet, MeshBuild>;
/** @brief Typed procedural mesh graph composing grid, point and topology domains. */
class MeshGraph {
public:
[[nodiscard]] Result<void> addNode(std::string id, std::string operation);
[[nodiscard]] Result<void> connect(std::string_view fromId, std::string_view toId, int inputIndex = 0);
[[nodiscard]] Result<void> setNodeGrid(std::string_view id, const Grid2D& value);
[[nodiscard]] Result<void> setNodePoints(std::string_view id, const PointSet& value);
[[nodiscard]] Result<void> setNodeMesh(std::string_view id, const MeshBuild& value);
[[nodiscard]] Result<void> setNodeFloat(std::string_view id, std::string key, float value);
[[nodiscard]] Result<void> setNodeString(std::string_view id, std::string key, std::string value);
/** @cost Linear in emitted vertices and indices; point instancing multiplies source mesh cost by point count. */
[[nodiscard]] Result<MeshBuild> execute(std::string_view outputId);
[[nodiscard]] Result<void> validate(std::string_view outputId) const;
void clearCache();
[[nodiscard]] std::uint64_t revision() const noexcept { return revision_; }
/** @brief Serialize topology and scalar parameters using the versioned EVPCG mesh schema. */
[[nodiscard]] std::string serializeDefinition() const;
/** @brief Atomically replace this graph from a versioned definition. Runtime input values are not persisted. */
[[nodiscard]] Result<void> deserializeDefinition(std::string_view definition);
private:
struct Node {
std::string id;
std::string operation;
std::string inputs[2];
std::unordered_map<std::string, float> floats;
std::unordered_map<std::string, std::string> strings;
MeshGraphValue value;
bool hasValue = false;
MeshGraphValue cache;
bool cacheValid = false;
};
[[nodiscard]] Result<MeshGraphValue> evaluate(std::string_view id, std::unordered_map<std::string, int>& states);
void invalidateFrom(std::string_view id);
std::unordered_map<std::string, Node> nodes_;
std::vector<std::string> order_;
std::uint64_t revision_ = 0;
};
} // namespace eve::procgen
+419
View File
@@ -0,0 +1,419 @@
#include "procgen/ObjectBuildLayer.h"
#include "common/Diagnostic.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <limits>
#include <sstream>
namespace eve::procgen {
namespace {
template <class T>
Result<T> fail(std::string message, std::string path = {}) {
return Result<T>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::move(message), std::move(path),
{}, "procgen.objectBuild"));
}
bool finiteRange(float minimum, float maximum) {
return std::isfinite(minimum) && std::isfinite(maximum) && minimum <= maximum;
}
Result<void> validateRange(const ObjectTransformRange& range) {
if (!finiteRange(range.minPitch, range.maxPitch) || !finiteRange(range.minYaw, range.maxYaw) ||
!finiteRange(range.minRoll, range.maxRoll) || !finiteRange(range.minScaleX, range.maxScaleX) ||
!finiteRange(range.minScaleY, range.maxScaleY) || !finiteRange(range.minScaleZ, range.maxScaleZ))
return fail<void>("random transform ranges must be finite and ordered", "transform");
if (range.minScaleX == 0.f || range.maxScaleX == 0.f || range.minScaleY == 0.f || range.maxScaleY == 0.f ||
range.minScaleZ == 0.f || range.maxScaleZ == 0.f)
return fail<void>("random scale bounds must be non-zero", "transform.scale");
return Result<void>::success();
}
std::uint64_t mix(std::uint64_t value) {
value += 0x9e3779b97f4a7c15ULL;
value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
return value ^ (value >> 31U);
}
struct RandomStream {
std::uint64_t state;
std::uint64_t next() {
state = mix(state);
return state;
}
float unit() { return float(next() >> 40U) * (1.f / 16777216.f); }
float between(float minimum, float maximum) { return minimum + (maximum - minimum) * unit(); }
};
std::uint64_t pointKey(const ProcgenPoint& point, std::size_t index, std::uint32_t seed) {
if (point.id != 0) return mix(point.id ^ (std::uint64_t(seed) << 32U));
const auto qx = std::int64_t(std::llround(double(point.x) * 1000.0));
const auto qy = std::int64_t(std::llround(double(point.y) * 1000.0));
const auto qz = std::int64_t(std::llround(double(point.z) * 1000.0));
return mix(std::uint64_t(qx) ^ (mix(std::uint64_t(qy)) << 1U) ^ (mix(std::uint64_t(qz)) << 2U) ^
std::uint64_t(index) ^ (std::uint64_t(seed) << 32U));
}
float orientedYaw(const ProcgenPoint& point, const PointSet& orientation, float cellSize, bool invert) {
struct Direction {
float x;
float z;
float yaw;
};
const Direction cardinal[] = {
{0.f, 1.f, invert ? 90.f : 270.f}, {1.f, 0.f, 0.f}, {0.f, -1.f, invert ? 270.f : 90.f}, {-1.f, 0.f, 180.f}};
const float tolerance = std::max(0.0001f, cellSize * 0.01f);
const auto matches = [&](float x, float z) {
return std::any_of(orientation.points().begin(), orientation.points().end(), [&](const ProcgenPoint& other) {
return std::abs(other.x - x) <= tolerance && std::abs(other.z - z) <= tolerance;
});
};
for (const auto& direction : cardinal)
if (matches(point.x + direction.x * cellSize, point.z + direction.z * cellSize)) return direction.yaw;
const Direction diagonal[] = {{1.f, 1.f, 90.f}, {1.f, -1.f, 0.f}, {-1.f, -1.f, 180.f}, {-1.f, 1.f, 180.f}};
for (const auto& direction : diagonal)
if (matches(point.x + direction.x * cellSize, point.z + direction.z * cellSize)) return direction.yaw;
return 0.f;
}
void applyRandomTransform(ProcgenPoint& point, const ObjectTransformRange& range, RandomStream& random) {
point.pitch += random.between(range.minPitch, range.maxPitch);
point.yaw += random.between(range.minYaw, range.maxYaw);
point.roll += random.between(range.minRoll, range.maxRoll);
if (range.uniformScale) {
const float scale = random.between(range.minScaleX, range.maxScaleX);
point.scaleX *= scale;
point.scaleY *= scale;
point.scaleZ *= scale;
} else {
point.scaleX *= random.between(range.minScaleX, range.maxScaleX);
point.scaleY *= random.between(range.minScaleY, range.maxScaleY);
point.scaleZ *= random.between(range.minScaleZ, range.maxScaleZ);
}
}
Result<void> tagPoint(PointSet& output, int index, const std::string& asset, std::string role, std::int64_t parent) {
auto result = output.trySetStringAttribute(index, "asset", asset);
if (!result.ok()) return result;
result = output.trySetStringAttribute(index, "object_role", role);
if (!result.ok()) return result;
return output.trySetIntAttribute(index, "parent_index", parent);
}
} // namespace
Result<void> ObjectBuildLayer::addAsset(std::string asset, float weight) {
if (asset.empty()) return fail<void>("asset id is empty", "asset");
if (!std::isfinite(weight) || weight <= 0.f) return fail<void>("asset weight must be finite and positive", asset);
assets_.push_back({std::move(asset), weight});
return Result<void>::success();
}
void ObjectBuildLayer::clearAssets() { assets_.clear(); }
void ObjectBuildLayer::setSeed(std::uint32_t seed) noexcept { seed_ = seed == 0 ? 1 : seed; }
void ObjectBuildLayer::setLayerOffset(float x, float y, float z) noexcept {
offsetX_ = x;
offsetY_ = y;
offsetZ_ = z;
}
Result<void> ObjectBuildLayer::setLayerScale(float x, float y, float z) {
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || x == 0.f || y == 0.f || z == 0.f)
return fail<void>("layer scale must be finite and non-zero", "scale");
scaleX_ = x;
scaleY_ = y;
scaleZ_ = z;
return Result<void>::success();
}
Result<void> ObjectBuildLayer::setPositionRadius(float radius) {
if (!std::isfinite(radius) || radius < 0.f) return fail<void>("position radius must be finite and non-negative");
positionRadius_ = radius;
return Result<void>::success();
}
Result<void> ObjectBuildLayer::setRandomTransform(const ObjectTransformRange& range) {
auto valid = validateRange(range);
if (!valid.ok()) return valid;
transform_ = range;
return Result<void>::success();
}
Result<void> ObjectBuildLayer::setRandomRotation(float minPitch, float maxPitch, float minYaw, float maxYaw,
float minRoll, float maxRoll) {
auto candidate = transform_;
candidate.minPitch = minPitch;
candidate.maxPitch = maxPitch;
candidate.minYaw = minYaw;
candidate.maxYaw = maxYaw;
candidate.minRoll = minRoll;
candidate.maxRoll = maxRoll;
return setRandomTransform(candidate);
}
Result<void> ObjectBuildLayer::setRandomScale(float minX, float maxX, float minY, float maxY, float minZ, float maxZ,
bool uniform) {
auto candidate = transform_;
candidate.minScaleX = minX;
candidate.maxScaleX = maxX;
candidate.minScaleY = minY;
candidate.maxScaleY = maxY;
candidate.minScaleZ = minZ;
candidate.maxScaleZ = maxZ;
candidate.uniformScale = uniform;
return setRandomTransform(candidate);
}
Result<void> ObjectBuildLayer::setOrientation(float cellSize, float yawOffset, bool invert) {
if (!std::isfinite(cellSize) || cellSize <= 0.f) return fail<void>("orientation cell size must be positive");
if (!std::isfinite(yawOffset)) return fail<void>("orientation yaw offset must be finite");
orient_ = true;
orientationCellSize_ = cellSize;
orientationYawOffset_ = yawOffset;
invertOrientation_ = invert;
return Result<void>::success();
}
void ObjectBuildLayer::disableOrientation() noexcept { orient_ = false; }
void ObjectBuildLayer::setPlaceOnTop(bool enabled, bool useLowest, float topOffset) noexcept {
placeOnTop_ = enabled;
useLowest_ = useLowest;
topOffset_ = std::isfinite(topOffset) ? topOffset : 0.f;
}
Result<void> ObjectBuildLayer::addChildRule(std::string asset, int count, float radius,
const ObjectTransformRange& range) {
if (asset.empty()) return fail<void>("child asset id is empty", "child.asset");
if (count < 0) return fail<void>("child count must be non-negative", asset);
if (!std::isfinite(radius) || radius < 0.f)
return fail<void>("child radius must be finite and non-negative", asset);
auto valid = validateRange(range);
if (!valid.ok()) return valid;
children_.push_back({std::move(asset), count, radius, range});
return Result<void>::success();
}
Result<void> ObjectBuildLayer::addChild(std::string asset, int count, float radius, float minScale, float maxScale,
float minYaw, float maxYaw) {
ObjectTransformRange range;
range.minScaleX = minScale;
range.maxScaleX = maxScale;
range.minScaleY = minScale;
range.maxScaleY = maxScale;
range.minScaleZ = minScale;
range.maxScaleZ = maxScale;
range.uniformScale = true;
range.minYaw = minYaw;
range.maxYaw = maxYaw;
return addChildRule(std::move(asset), count, radius, range);
}
void ObjectBuildLayer::clearChildRules() { children_.clear(); }
Result<PointSet> ObjectBuildLayer::build(const PointSet& source, const PointSet* orientation) const {
if (assets_.empty()) return fail<PointSet>("object build layer has no assets", "assets");
if (orient_ && !orientation) return fail<PointSet>("orientation is enabled but no orientation layer was supplied");
float totalWeight = 0.f;
for (const auto& asset : assets_) totalWeight += asset.weight;
if (!std::isfinite(totalWeight) || totalWeight <= 0.f) return fail<PointSet>("total asset weight is invalid");
std::size_t childCount = 0;
for (const auto& child : children_) childCount += std::size_t(child.count);
PointSet output;
output.reserve(source.points().size() * (childCount + 1));
for (std::size_t sourceIndex = 0; sourceIndex < source.points().size(); ++sourceIndex) {
const auto& input = source.points()[sourceIndex];
RandomStream random{pointKey(input, sourceIndex, seed_)};
float selection = random.unit() * totalWeight;
const Asset* selected = &assets_.back();
for (const auto& asset : assets_) {
selection -= asset.weight;
if (selection < 0.f) {
selected = &asset;
break;
}
}
auto appended = output.appendPointFrom(source, sourceIndex);
if (!appended.ok()) return Result<PointSet>::failure(appended.status());
const int parentIndex = appended.value();
auto& parent = output.mutablePoint(std::size_t(parentIndex));
if (positionRadius_ > 0.f) {
const float angle = random.unit() * 6.283185307179586f;
const float radius = std::sqrt(random.unit()) * positionRadius_;
parent.x += std::cos(angle) * radius;
parent.z += std::sin(angle) * radius;
}
parent.x += offsetX_;
parent.y += offsetY_;
parent.z += offsetZ_;
if (placeOnTop_) {
const char* attribute = useLowest_ ? "surface_lowest_y" : "surface_highest_y";
if (source.hasFloatAttribute(int(sourceIndex), attribute))
parent.y = source.getFloatAttribute(int(sourceIndex), attribute, parent.y) + topOffset_ + offsetY_;
}
applyRandomTransform(parent, transform_, random);
parent.scaleX *= scaleX_;
parent.scaleY *= scaleY_;
parent.scaleZ *= scaleZ_;
if (orient_)
parent.yaw = orientedYaw(input, *orientation, orientationCellSize_, invertOrientation_) +
orientationYawOffset_ + random.between(transform_.minYaw, transform_.maxYaw);
auto tagged = tagPoint(output, parentIndex, selected->id, "parent", -1);
if (!tagged.ok()) return Result<PointSet>::failure(tagged.status());
for (std::size_t ruleIndex = 0; ruleIndex < children_.size(); ++ruleIndex) {
const auto& rule = children_[ruleIndex];
RandomStream childRandom{mix(pointKey(input, sourceIndex, seed_) ^ (std::uint64_t(ruleIndex + 1) << 32U))};
for (int childOrdinal = 0; childOrdinal < rule.count; ++childOrdinal) {
ProcgenPoint child = parent;
child.id = derivePointId(parent.id != 0 ? parent.id : pointKey(input, sourceIndex, seed_),
std::uint64_t(ruleIndex) * 1000000ULL + std::uint64_t(childOrdinal + 1));
const float angle = childRandom.unit() * 6.283185307179586f;
const float radius = std::sqrt(childRandom.unit()) * rule.radius;
child.x += std::cos(angle) * radius;
child.z += std::sin(angle) * radius;
child.pitch = 0.f;
child.yaw = 0.f;
child.roll = 0.f;
child.scaleX = 1.f;
child.scaleY = 1.f;
child.scaleZ = 1.f;
applyRandomTransform(child, rule.transform, childRandom);
const int childIndex = output.appendPoint(child);
tagged = tagPoint(output, childIndex, rule.asset, "child", parentIndex);
if (!tagged.ok()) return Result<PointSet>::failure(tagged.status());
}
}
}
return Result<PointSet>::success(std::move(output));
}
std::string ObjectBuildLayer::serializeDefinition() const {
std::ostringstream out;
out << "EVPCG_OBJECT_LAYER 1\n" << std::setprecision(9);
out << "SEED " << seed_ << '\n';
out << "OFFSET " << offsetX_ << ' ' << offsetY_ << ' ' << offsetZ_ << '\n';
out << "SCALE " << scaleX_ << ' ' << scaleY_ << ' ' << scaleZ_ << '\n';
out << "POSITION_RADIUS " << positionRadius_ << '\n';
out << "RANDOM " << transform_.minPitch << ' ' << transform_.maxPitch << ' ' << transform_.minYaw << ' '
<< transform_.maxYaw << ' ' << transform_.minRoll << ' ' << transform_.maxRoll << ' ' << transform_.minScaleX
<< ' ' << transform_.maxScaleX << ' ' << transform_.minScaleY << ' ' << transform_.maxScaleY << ' '
<< transform_.minScaleZ << ' ' << transform_.maxScaleZ << ' ' << int(transform_.uniformScale) << '\n';
out << "ORIENTATION " << int(orient_) << ' ' << orientationCellSize_ << ' ' << orientationYawOffset_ << ' '
<< int(invertOrientation_) << '\n';
out << "PLACE " << int(placeOnTop_) << ' ' << int(useLowest_) << ' ' << topOffset_ << '\n';
for (const auto& asset : assets_) out << "ASSET " << std::quoted(asset.id) << ' ' << asset.weight << '\n';
for (const auto& child : children_) {
const auto& t = child.transform;
out << "CHILD " << std::quoted(child.asset) << ' ' << child.count << ' ' << child.radius << ' ' << t.minPitch
<< ' ' << t.maxPitch << ' ' << t.minYaw << ' ' << t.maxYaw << ' ' << t.minRoll << ' ' << t.maxRoll << ' '
<< t.minScaleX << ' ' << t.maxScaleX << ' ' << t.minScaleY << ' ' << t.maxScaleY << ' ' << t.minScaleZ
<< ' ' << t.maxScaleZ << ' ' << int(t.uniformScale) << '\n';
}
out << "END\n";
return out.str();
}
Result<void> ObjectBuildLayer::deserializeDefinition(std::string_view definition) {
ObjectBuildLayer replacement;
std::istringstream input{std::string(definition)};
std::string magic;
int version = 0;
if (!(input >> magic >> version) || magic != "EVPCG_OBJECT_LAYER" || version != 1)
return fail<void>("invalid object layer header");
std::string line;
std::getline(input, line);
bool ended = false;
while (std::getline(input, line)) {
if (line.empty()) continue;
std::istringstream record(line);
std::string kind;
record >> kind;
if (kind == "END") {
ended = true;
break;
}
if (kind == "SEED") {
std::uint32_t seed = 0;
if (!(record >> seed)) return fail<void>("invalid SEED record");
replacement.setSeed(seed);
} else if (kind == "OFFSET") {
float x = 0.f, y = 0.f, z = 0.f;
if (!(record >> x >> y >> z) || !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
return fail<void>("invalid OFFSET record");
replacement.setLayerOffset(x, y, z);
} else if (kind == "SCALE") {
float x = 0.f, y = 0.f, z = 0.f;
if (!(record >> x >> y >> z)) return fail<void>("invalid SCALE record");
auto applied = replacement.setLayerScale(x, y, z);
if (!applied.ok()) return applied;
} else if (kind == "POSITION_RADIUS") {
float radius = 0.f;
if (!(record >> radius)) return fail<void>("invalid POSITION_RADIUS record");
auto applied = replacement.setPositionRadius(radius);
if (!applied.ok()) return applied;
} else if (kind == "RANDOM") {
ObjectTransformRange range;
int uniform = 0;
if (!(record >> range.minPitch >> range.maxPitch >> range.minYaw >> range.maxYaw >> range.minRoll >>
range.maxRoll >> range.minScaleX >> range.maxScaleX >> range.minScaleY >> range.maxScaleY >>
range.minScaleZ >> range.maxScaleZ >> uniform) ||
(uniform != 0 && uniform != 1))
return fail<void>("invalid RANDOM record");
range.uniformScale = uniform != 0;
auto applied = replacement.setRandomTransform(range);
if (!applied.ok()) return applied;
} else if (kind == "ORIENTATION") {
int enabled = 0, invert = 0;
float cell = 0.f, offset = 0.f;
if (!(record >> enabled >> cell >> offset >> invert) || (enabled != 0 && enabled != 1) ||
(invert != 0 && invert != 1))
return fail<void>("invalid ORIENTATION record");
if (enabled) {
auto applied = replacement.setOrientation(cell, offset, invert != 0);
if (!applied.ok()) return applied;
}
} else if (kind == "PLACE") {
int enabled = 0, lowest = 0;
float offset = 0.f;
if (!(record >> enabled >> lowest >> offset) || (enabled != 0 && enabled != 1) ||
(lowest != 0 && lowest != 1) || !std::isfinite(offset))
return fail<void>("invalid PLACE record");
replacement.setPlaceOnTop(enabled != 0, lowest != 0, offset);
} else if (kind == "ASSET") {
std::string asset;
float weight = 0.f;
if (!(record >> std::quoted(asset) >> weight)) return fail<void>("invalid ASSET record");
auto applied = replacement.addAsset(std::move(asset), weight);
if (!applied.ok()) return applied;
} else if (kind == "CHILD") {
std::string asset;
int count = 0, uniform = 0;
float radius = 0.f;
ObjectTransformRange range;
if (!(record >> std::quoted(asset) >> count >> radius >> range.minPitch >> range.maxPitch >> range.minYaw >>
range.maxYaw >> range.minRoll >> range.maxRoll >> range.minScaleX >> range.maxScaleX >>
range.minScaleY >> range.maxScaleY >> range.minScaleZ >> range.maxScaleZ >> uniform) ||
(uniform != 0 && uniform != 1))
return fail<void>("invalid CHILD record");
range.uniformScale = uniform != 0;
auto applied = replacement.addChildRule(std::move(asset), count, radius, range);
if (!applied.ok()) return applied;
} else {
return fail<void>("unknown object layer record: " + kind);
}
record >> std::ws;
if (!record.eof()) return fail<void>("trailing object layer record data");
}
if (!ended) return fail<void>("object layer END record is missing");
*this = std::move(replacement);
return Result<void>::success();
}
} // namespace eve::procgen
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include "common/Result.h"
#include "procgen/PointSet.h"
#include <cstdint>
#include <string>
#include <vector>
namespace eve::procgen {
/** @brief Random transform interval used by one object build rule. */
struct ObjectTransformRange {
float minPitch = 0.f;
float maxPitch = 0.f;
float minYaw = 0.f;
float maxYaw = 0.f;
float minRoll = 0.f;
float maxRoll = 0.f;
float minScaleX = 1.f;
float maxScaleX = 1.f;
float minScaleY = 1.f;
float maxScaleY = 1.f;
float minScaleZ = 1.f;
float maxScaleZ = 1.f;
bool uniformScale = false;
};
/**
* @brief Deterministic TileWorldCreator-style object build layer.
*
* Input and output are owned values. Each source point emits one parent object point;
* child rules emit additional points carrying `asset`, `object_role` and `parent_index`
* attributes. The layer never creates scene objects or retains input pointers.
* @thread Affine; configure and build on the owning thread.
*/
class ObjectBuildLayer {
public:
/** @brief Add a weighted asset candidate. Zero and negative weights are rejected. */
[[nodiscard]] Result<void> addAsset(std::string asset, float weight);
void clearAssets();
/** @brief Set the named deterministic random stream seed. */
void setSeed(std::uint32_t seed) noexcept;
/** @brief Set fixed layer translation applied after source transforms. */
void setLayerOffset(float x, float y, float z) noexcept;
/** @brief Set fixed component-wise scale multiplier. Values must be non-zero. */
[[nodiscard]] Result<void> setLayerScale(float x, float y, float z);
/** @brief Set circular XZ position jitter radius. */
[[nodiscard]] Result<void> setPositionRadius(float radius);
/** @brief Configure independent random Euler and scale intervals for parent objects. */
[[nodiscard]] Result<void> setRandomTransform(const ObjectTransformRange& range);
/** @brief Update only the parent Euler randomization interval. */
[[nodiscard]] Result<void> setRandomRotation(float minPitch, float maxPitch, float minYaw, float maxYaw,
float minRoll, float maxRoll);
/** @brief Update only the parent scale randomization interval. */
[[nodiscard]] Result<void> setRandomScale(float minX, float maxX, float minY, float maxY, float minZ, float maxZ,
bool uniform);
/** @brief Enable orientation toward the first matching N/E/S/W then diagonal orientation point. */
[[nodiscard]] Result<void> setOrientation(float cellSize, float yawOffset, bool invert);
void disableOrientation() noexcept;
/** @brief Project output Y to `surface_lowest_y` or `surface_highest_y`, plus offset, when present. */
void setPlaceOnTop(bool enabled, bool useLowest, float topOffset) noexcept;
/**
* @brief Add a child-spawn rule with its own deterministic transform stream.
* @param asset Asset identifier written to emitted child points.
* @param count Children emitted per parent.
* @param radius Circular XZ scatter radius around the parent.
* @param range Random rotation and scale interval.
*/
[[nodiscard]] Result<void> addChildRule(std::string asset, int count, float radius,
const ObjectTransformRange& range);
/** @brief Script-friendly child rule using uniform scale and yaw ranges. */
[[nodiscard]] Result<void> addChild(std::string asset, int count, float radius, float minScale, float maxScale,
float minYaw, float maxYaw);
void clearChildRules();
/**
* @brief Build attributed object points from source points.
* @param source Placement points; their attributes and transforms are preserved for parents.
* @param orientation Optional point layer used to derive cardinal orientation.
* @return Complete result, or a diagnostic without mutating the layer or either input.
* @cost O(P*A + P*C + P*O) where P is source points, A assets, C emitted children,
* and O orientation points when orientation is enabled.
*/
[[nodiscard]] Result<PointSet> build(const PointSet& source, const PointSet* orientation = nullptr) const;
/** @brief Serialize all layer settings using the versioned EVPCG object-layer schema. */
[[nodiscard]] std::string serializeDefinition() const;
/** @brief Atomically replace settings from schema version 1; unknown records are rejected. */
[[nodiscard]] Result<void> deserializeDefinition(std::string_view definition);
private:
struct Asset {
std::string id;
float weight = 1.f;
};
struct ChildRule {
std::string asset;
int count = 0;
float radius = 0.f;
ObjectTransformRange transform;
};
std::vector<Asset> assets_;
std::vector<ChildRule> children_;
ObjectTransformRange transform_;
std::uint32_t seed_ = 1;
float offsetX_ = 0.f;
float offsetY_ = 0.f;
float offsetZ_ = 0.f;
float scaleX_ = 1.f;
float scaleY_ = 1.f;
float scaleZ_ = 1.f;
float positionRadius_ = 0.f;
bool orient_ = false;
bool invertOrientation_ = false;
float orientationCellSize_ = 1.f;
float orientationYawOffset_ = 0.f;
bool placeOnTop_ = false;
bool useLowest_ = false;
float topOffset_ = 0.f;
};
} // namespace eve::procgen
File diff suppressed because it is too large. Load diff
+187 -175
View File
@@ -4,7 +4,9 @@
#include "common/SquirrelOwnership.h"
#include "procgen/Biome.h"
#include "procgen/Grid2D.h"
#include "procgen/GridGraph.h"
#include "procgen/MeshBuild.h"
#include "procgen/MeshGraph.h"
#include "procgen/OutputSpec.h"
#include "procgen/Palette.h"
#include "procgen/ParamSchema.h"
@@ -18,13 +20,13 @@
#include "procgen/algorithms/LSystem.h"
#include "procgen/core/ProcgenCore.h"
#include "procgen/heightmap/Heightmap.h"
#include "procgen/heightmap/TerrainSampler.h"
#include "procgen/heightmap/TerrainPipeline.h"
#include "procgen/heightmap/TerrainMesh.h"
#include "procgen/mesh/MeshModifierGraph.h"
#include "procgen/mesh/MeshDeformationSession.h"
#include "procgen/mesh/DynamicMeshUvPaintSession.h"
#include "procgen/spline/SplinePath.h"
#include "procgen/heightmap/TerrainPipeline.h"
#include "procgen/heightmap/TerrainSampler.h"
#include "procgen/texture/CloudField.h"
#include "procgen/texture/CloudShadow.h"
@@ -95,6 +97,10 @@ struct ProcgenMeshDeformationSessionHandleTag {};
struct ProcgenDynamicMeshUvPaintSessionHandleTag {};
/** @brief Handle domain for module-owned 3D spline paths. */
struct ProcgenSplinePathHandleTag {};
/** @brief Handle domain for module-owned grid graphs. */
struct ProcgenGridGraphHandleTag {};
/** @brief Handle domain for module-owned mesh graphs. */
struct ProcgenMeshGraphHandleTag {};
/** @brief Handle domain for module-owned biome rules. */
struct ProcgenBiomeRulesHandleTag {};
/** @brief Handle domain for module-owned shape grammars. */
@@ -122,6 +128,8 @@ using ProcgenMeshDeformationSessionHandleRef = eve::script::RuntimeHandleRef<Pro
using ProcgenDynamicMeshUvPaintSessionHandleRef =
eve::script::RuntimeHandleRef<ProcgenDynamicMeshUvPaintSessionHandleTag>;
using ProcgenSplinePathHandleRef = eve::script::RuntimeHandleRef<ProcgenSplinePathHandleTag>;
using ProcgenGridGraphHandleRef = eve::script::RuntimeHandleRef<ProcgenGridGraphHandleTag>;
using ProcgenMeshGraphHandleRef = eve::script::RuntimeHandleRef<ProcgenMeshGraphHandleTag>;
using ProcgenBiomeRulesHandleRef = eve::script::RuntimeHandleRef<ProcgenBiomeRulesHandleTag>;
using ProcgenShapeGrammarHandleRef = eve::script::RuntimeHandleRef<ProcgenShapeGrammarHandleTag>;
using ProcgenLSystemHandleRef = eve::script::RuntimeHandleRef<ProcgenLSystemHandleTag>;
@@ -219,64 +227,67 @@ public:
float translateZ, float yawDegrees,
float scaleX, float scaleY, float scaleZ);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> transformPoints3DHandle(
ProcgenPointSetHandleRef input, float translateX, float translateY, float translateZ,
float pitchDegrees, float yawDegrees, float rollDegrees, float scaleX, float scaleY,
float scaleZ);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> copyPointsHandle(
ProcgenPointSetHandleRef source, ProcgenPointSetHandleRef targets,
bool inheritTargetAttributes);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> remapDensityHandle(
ProcgenPointSetHandleRef input, float inputMin, float inputMax, float outputMin,
float outputMax, bool clampOutput);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> mathFloatAttributeHandle(
ProcgenPointSetHandleRef input, const std::string& attribute,
const std::string& outputAttribute, const std::string& operation, float operand,
float defaultValue);
ProcgenPointSetHandleRef input, float translateX, float translateY, float translateZ, float pitchDegrees,
float yawDegrees, float rollDegrees, float scaleX, float scaleY, float scaleZ);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> copyPointsHandle(ProcgenPointSetHandleRef source,
ProcgenPointSetHandleRef targets,
bool inheritTargetAttributes);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> remapDensityHandle(ProcgenPointSetHandleRef input,
float inputMin, float inputMax,
float outputMin, float outputMax,
bool clampOutput);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> mathFloatAttributeHandle(ProcgenPointSetHandleRef input,
const std::string& attribute,
const std::string& outputAttribute,
const std::string& operation,
float operand, float defaultValue);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> filterFloatAttributeHandle(ProcgenPointSetHandleRef input,
const std::string &name,
const std::string& name,
float minValue, float maxValue,
bool invert);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> filterStringAttributeHandle(ProcgenPointSetHandleRef input,
const std::string &name,
const std::string &value,
const std::string& name,
const std::string& value,
bool invert);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> densityCullHandle(ProcgenPointSetHandleRef input, uint32_t seed,
float multiplier);
/** @brief Project points through the optional world-query capability. */
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> projectToWorldHandle(
ProcgenPointSetHandleRef input, float maxY, float minY, std::uint64_t maskBits,
bool keepUnmatched);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> projectToWorldHandle(ProcgenPointSetHandleRef input, float maxY,
float minY, std::uint64_t maskBits,
bool keepUnmatched);
// --- Spatial data and composable PCG domains ---
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> pointDataHandle(ProcgenPointSetHandleRef points);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> boxVolumeHandle(float minX, float minY, float minZ,
float maxX, float maxY, float maxZ);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> sphereVolumeHandle(float x, float y, float z, float radius);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> polygonVolumeHandle(
ProcgenPointSetHandleRef controlPoints, float minY, float maxY);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> polygonVolumeHandle(ProcgenPointSetHandleRef controlPoints,
float minY, float maxY);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> splineDataHandle(ProcgenPointSetHandleRef controlPoints,
float radius);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> heightfieldDataHandle(ProcgenHeightmapHandleRef heightmap,
float originX, float originZ,
float cellSize, float heightScale);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> textureMaskDataHandle(
ProcgenHeightmapHandleRef values, float originX, float originZ, float cellSize,
float minValue, float maxValue, float minY, float maxY);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> meshSurfaceDataHandle(
ProcgenMeshBuildHandleRef mesh, float tolerance);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> textureMaskDataHandle(ProcgenHeightmapHandleRef values,
float originX, float originZ,
float cellSize, float minValue,
float maxValue, float minY,
float maxY);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> meshSurfaceDataHandle(ProcgenMeshBuildHandleRef mesh,
float tolerance);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> unionSpatialHandle(ProcgenSpatialDataHandleRef left,
ProcgenSpatialDataHandleRef right);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> intersectSpatialHandle(ProcgenSpatialDataHandleRef left,
ProcgenSpatialDataHandleRef right);
[[nodiscard]] eve::Result<ProcgenSpatialDataHandleRef> differenceSpatialHandle(ProcgenSpatialDataHandleRef left,
ProcgenSpatialDataHandleRef right);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> sampleSpatialHandle(ProcgenSpatialDataHandleRef spatial,
float spacing, uint32_t seed, float jitter);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> filterSpatialHandle(ProcgenPointSetHandleRef input,
ProcgenSpatialDataHandleRef spatial,
bool invert);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> projectToSpatialHandle(ProcgenPointSetHandleRef input,
ProcgenSpatialDataHandleRef spatial);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> sampleSpatialHandle(ProcgenSpatialDataHandleRef spatial,
float spacing, uint32_t seed, float jitter);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> filterSpatialHandle(ProcgenPointSetHandleRef input,
ProcgenSpatialDataHandleRef spatial,
bool invert);
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> projectToSpatialHandle(ProcgenPointSetHandleRef input,
ProcgenSpatialDataHandleRef spatial);
[[nodiscard]] eve::Result<ProcgenRuntimeGenerationHandleRef> newRuntimeGenerationHandle(uint32_t worldSeed);
[[nodiscard]] eve::Result<ProcgenPointGraphHandleRef> newPointGraphHandle();
@@ -288,6 +299,8 @@ public:
[[nodiscard]] eve::Result<ProcgenDynamicMeshUvPaintSessionHandleRef> newDynamicMeshUvPaintSessionHandle();
/** @brief Allocate a module-owned 3D spline path. */
[[nodiscard]] eve::Result<ProcgenSplinePathHandleRef> newSplinePathHandle();
[[nodiscard]] eve::Result<ProcgenGridGraphHandleRef> newGridGraphHandle();
[[nodiscard]] eve::Result<ProcgenMeshGraphHandleRef> newMeshGraphHandle();
[[nodiscard]] eve::Result<ProcgenBiomeRulesHandleRef> newBiomeRulesHandle();
[[nodiscard]] eve::Result<ProcgenShapeGrammarHandleRef> newShapeGrammarHandle();
[[nodiscard]] eve::Result<ProcgenLSystemHandleRef> newLSystemHandle();
@@ -306,6 +319,8 @@ public:
ProcgenDynamicMeshUvPaintSessionHandleRef reference) noexcept;
/** @brief Resolve a spline path for the current synchronous call only. */
[[nodiscard]] eve::script::Borrowed<SplinePath> resolveSplinePath(ProcgenSplinePathHandleRef reference) noexcept;
[[nodiscard]] eve::script::Borrowed<GridGraph> resolveGridGraph(ProcgenGridGraphHandleRef reference) noexcept;
[[nodiscard]] eve::script::Borrowed<MeshGraph> resolveMeshGraph(ProcgenMeshGraphHandleRef reference) noexcept;
[[nodiscard]] eve::script::Borrowed<BiomeRules> resolveBiomeRules(ProcgenBiomeRulesHandleRef reference) noexcept;
[[nodiscard]] eve::script::Borrowed<ShapeGrammar> resolveShapeGrammar(
ProcgenShapeGrammarHandleRef reference) noexcept;
@@ -321,6 +336,8 @@ public:
[[nodiscard]] eve::Result<void> release(ProcgenDynamicMeshUvPaintSessionHandleRef reference);
/** @brief Release a module-owned spline path. */
[[nodiscard]] eve::Result<void> release(ProcgenSplinePathHandleRef reference);
[[nodiscard]] eve::Result<void> release(ProcgenGridGraphHandleRef reference);
[[nodiscard]] eve::Result<void> release(ProcgenMeshGraphHandleRef reference);
[[nodiscard]] eve::Result<void> release(ProcgenBiomeRulesHandleRef reference);
[[nodiscard]] eve::Result<void> release(ProcgenShapeGrammarHandleRef reference);
[[nodiscard]] eve::Result<void> release(ProcgenLSystemHandleRef reference);
@@ -335,18 +352,20 @@ public:
[[nodiscard]] bool isStale(ProcgenDynamicMeshUvPaintSessionHandleRef reference) const noexcept;
/** @brief Report whether a spline path handle is stale. */
[[nodiscard]] bool isStale(ProcgenSplinePathHandleRef reference) const noexcept;
[[nodiscard]] bool isStale(ProcgenGridGraphHandleRef reference) const noexcept;
[[nodiscard]] bool isStale(ProcgenMeshGraphHandleRef reference) const noexcept;
[[nodiscard]] bool isStale(ProcgenBiomeRulesHandleRef reference) const noexcept;
[[nodiscard]] bool isStale(ProcgenShapeGrammarHandleRef reference) const noexcept;
[[nodiscard]] bool isStale(ProcgenLSystemHandleRef reference) const noexcept;
[[nodiscard]] eve::Result<void> publishInstances(const std::string &batchId, ProcgenPointSetHandleRef points,
const std::string &assetAttribute,
const std::string &defaultAsset);
[[nodiscard]] eve::Result<void> removeInstances(const std::string &batchId);
[[nodiscard]] eve::Result<void> publishCellInstances(const std::string &prefix, const ProcgenCellRequest &request,
[[nodiscard]] eve::Result<void> publishInstances(const std::string& batchId, ProcgenPointSetHandleRef points,
const std::string& assetAttribute,
const std::string& defaultAsset);
[[nodiscard]] eve::Result<void> removeInstances(const std::string& batchId);
[[nodiscard]] eve::Result<void> publishCellInstances(const std::string& prefix, const ProcgenCellRequest& request,
ProcgenPointSetHandleRef points,
const std::string &assetAttribute,
const std::string &defaultAsset);
const std::string& assetAttribute,
const std::string& defaultAsset);
/**
* @brief Replace a Scene cell batch from a complete RuntimeGeneration snapshot at an explicit revision.
* @param prefix Non-empty namespace used by publishCellInstances.
@@ -359,7 +378,7 @@ public:
* @thread Synchronous on the Scene-owning thread; no input references are retained.
* @reentrant Not reentrant for the same cell batch.
*/
[[nodiscard]] eve::Result<uint64_t> publishCellSnapshot(const std::string& prefix,
[[nodiscard]] eve::Result<uint64_t> publishCellSnapshot(const std::string& prefix,
const ProcgenCellRequest& request,
ProcgenPointSetHandleRef points, uint64_t targetRevision,
const std::string& assetAttribute,
@@ -444,91 +463,91 @@ public:
[[nodiscard]] eve::Result<uint64_t> completeCellCleanupAtomic(
const std::string& prefix, const std::vector<RuntimeGeneration*>& runtimes,
const std::vector<const ProcgenCellRequest*>& requests);
int getPublishedInstanceCount(const std::string& batchId) const;
int getPublishedCreatedCount(const std::string& batchId) const;
int getPublishedReusedCount(const std::string &batchId) const;
int getPublishedRemovedCount(const std::string &batchId) const;
uint32_t deriveSeed(uint32_t parent, const std::string &scope) const;
int getPublishedInstanceCount(const std::string& batchId) const;
int getPublishedCreatedCount(const std::string& batchId) const;
int getPublishedReusedCount(const std::string& batchId) const;
int getPublishedRemovedCount(const std::string& batchId) const;
uint32_t deriveSeed(uint32_t parent, const std::string& scope) const;
// --- Atomic script rebuilds ---
/** @brief Starts a module-owned rebuild context and returns its handle. */
[[nodiscard]] static eve::Result<ProcgenContextHandleRef> beginSystemHandle(const std::string &name, uint32_t seed);
[[nodiscard]] static eve::Result<ProcgenContextHandleRef> beginSystemHandle(const std::string& name, uint32_t seed);
/** @brief Starts a cached module-owned rebuild context and returns its handle. */
[[nodiscard]] static eve::Result<ProcgenContextHandleRef> beginCachedSystemHandle(const std::string &name,
[[nodiscard]] static eve::Result<ProcgenContextHandleRef> beginCachedSystemHandle(const std::string& name,
uint32_t seed,
const std::string &buildKey);
const std::string& buildKey);
/** @brief Resolves a rebuild context as a non-owning observation. */
[[nodiscard]] static eve::script::Borrowed<ProcgenContext> resolve(ProcgenContextHandleRef reference) noexcept;
/** @brief Releases a module-owned rebuild context. */
[[nodiscard]] static eve::Result<void> release(ProcgenContextHandleRef reference);
/** @brief Reports whether a rebuild context reference is stale. */
[[nodiscard]] static bool isStale(ProcgenContextHandleRef reference) noexcept;
[[nodiscard]] eve::Result<void> commitSystem(ProcgenContextHandleRef context);
[[nodiscard]] eve::Result<void> abortSystem(ProcgenContextHandleRef context);
[[nodiscard]] eve::Result<void> removeSystem(const std::string &name);
bool hasSystem(const std::string& name) const;
uint64_t getSystemRevision(const std::string& name) const;
uint32_t getSystemSeed(const std::string& name) const;
std::string getSystemBuildKey(const std::string& name) const;
int getSystemOutputCount(const std::string& name) const;
std::string getSystemOutputName(const std::string &name, int index) const;
int getSystemDebugStageCount(const std::string& name) const;
std::string getSystemDebugStageName(const std::string& name, int index) const;
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> getSystemOutputHandle(const std::string &name,
const std::string &outputName) const;
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> getSystemDebugStageHandle(const std::string &name,
const std::string &stageName) const;
[[nodiscard]] static bool isStale(ProcgenContextHandleRef reference) noexcept;
[[nodiscard]] eve::Result<void> commitSystem(ProcgenContextHandleRef context);
[[nodiscard]] eve::Result<void> abortSystem(ProcgenContextHandleRef context);
[[nodiscard]] eve::Result<void> removeSystem(const std::string& name);
bool hasSystem(const std::string& name) const;
uint64_t getSystemRevision(const std::string& name) const;
uint32_t getSystemSeed(const std::string& name) const;
std::string getSystemBuildKey(const std::string& name) const;
int getSystemOutputCount(const std::string& name) const;
std::string getSystemOutputName(const std::string& name, int index) const;
int getSystemDebugStageCount(const std::string& name) const;
std::string getSystemDebugStageName(const std::string& name, int index) const;
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> getSystemOutputHandle(const std::string& name,
const std::string& outputName) const;
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> getSystemDebugStageHandle(const std::string& name,
const std::string& stageName) const;
[[nodiscard]] eve::Result<ProcgenPointSetHandleRef> getPreviousSystemDebugStageHandle(
const std::string &name, const std::string &stageName) const;
const std::string& name, const std::string& stageName) const;
/** @brief Revision number of the snapshot retained for hot-reload comparison. */
uint64_t getPreviousSystemRevision(const std::string& name) const;
std::string getSystemDebugReport(const std::string& name) const;
uint64_t getPreviousSystemRevision(const std::string& name) const;
std::string getSystemDebugReport(const std::string& name) const;
/** @brief Human-readable point-count changes between the current and previous commits. */
std::string getSystemDebugDiffReport(const std::string& name) const;
std::string getSystemDebugDiffReport(const std::string& name) const;
// --- Phase A: maps ---
[[nodiscard]] eve::Result<ProcgenGridHandleRef> generateHandle(const std::string &algorithmId,
[[nodiscard]] eve::Result<ProcgenGridHandleRef> generateHandle(const std::string& algorithmId,
ProcgenParamsHandleRef params);
[[nodiscard]] eve::Result<void> generateTo(const std::string &algorithmId, ProcgenParamsHandleRef params,
[[nodiscard]] eve::Result<void> generateTo(const std::string& algorithmId, ProcgenParamsHandleRef params,
ProcgenOutputHandleRef output);
[[nodiscard]] eve::Result<void> applyToLayer(ProcgenGridHandleRef grid, const std::string &palette,
map::TileLayer &layer);
[[nodiscard]] eve::Result<void> applyToLayer(ProcgenGridHandleRef grid, const std::string& palette,
map::TileLayer& layer);
void setPaletteGid(const std::string &palette, const std::string &semantic, int gid);
int getPaletteGid(const std::string &palette, const std::string &semantic) const;
void setPaletteGid(const std::string& palette, const std::string& semantic, int gid);
int getPaletteGid(const std::string& palette, const std::string& semantic) const;
int getAlgorithmCount() const;
std::string getAlgorithmId(int index) const;
bool hasAlgorithm(const std::string &algorithmId) const;
bool hasAlgorithm(const std::string& algorithmId) const;
/** @brief Returns an owning copy of an algorithm schema or a diagnostic. */
[[nodiscard]] eve::Result<RecipeDescriptor> getAlgorithmSchema(const std::string &algorithmId) const;
[[nodiscard]] eve::Result<RecipeDescriptor> getAlgorithmSchema(const std::string& algorithmId) const;
/** @brief Human-readable algorithm label from its schema.
* @param algorithmId Algorithm id. @return Display name or empty text. */
std::string getAlgorithmDisplayName(const std::string &algorithmId) const;
std::string getAlgorithmDisplayName(const std::string& algorithmId) const;
/** @brief Algorithm category from its schema.
* @param algorithmId Algorithm id. @return Category or empty text. */
std::string getAlgorithmCategory(const std::string &algorithmId) const;
std::string getAlgorithmCategory(const std::string& algorithmId) const;
/** @brief Number of reflected parameters for one algorithm.
* @param algorithmId Algorithm id. @return Parameter count. */
int getAlgorithmParamCount(const std::string &algorithmId) const;
int getAlgorithmParamCount(const std::string& algorithmId) const;
/** @brief Stable parameter key at an index.
* @param algorithmId Algorithm id. @param index Parameter index. @return Key or empty text. */
std::string getAlgorithmParamKey(const std::string &algorithmId, int index) const;
std::string getAlgorithmParamKey(const std::string& algorithmId, int index) const;
/** @brief Human-readable parameter label at an index.
* @param algorithmId Algorithm id. @param index Parameter index. @return Label or empty text. */
std::string getAlgorithmParamLabel(const std::string &algorithmId, int index) const;
std::string getAlgorithmParamLabel(const std::string& algorithmId, int index) const;
/** @brief Parameter help text at an index.
* @param algorithmId Algorithm id. @param index Parameter index. @return Help text or empty text. */
std::string getAlgorithmParamDescription(const std::string &algorithmId, int index) const;
std::string getAlgorithmParamDescription(const std::string& algorithmId, int index) const;
/** @brief Parameter group at an index.
* @param algorithmId Algorithm id. @param index Parameter index. @return Group or empty text. */
std::string getAlgorithmParamCategory(const std::string &algorithmId, int index) const;
std::string getAlgorithmParamCategory(const std::string& algorithmId, int index) const;
/** @brief Parameter kind as int, float, bool, string or choice.
* @param algorithmId Algorithm id. @param index Parameter index. @return Kind or empty text. */
std::string getAlgorithmParamKind(const std::string &algorithmId, int index) const;
std::string getAlgorithmParamKind(const std::string& algorithmId, int index) const;
/** @brief Schema default encoded as text for lossless dynamic UI transport.
* @param algorithmId Algorithm id. @param index Parameter index. @return Encoded default. */
std::string getAlgorithmParamDefault(const std::string &algorithmId, int index) const;
std::string getAlgorithmParamDefault(const std::string& algorithmId, int index) const;
/** @brief Return true when a numeric minimum is declared.
* @param algorithmId Algorithm id. @param index Parameter index. @return Whether it exists. */
bool algorithmParamHasMinimum(const std::string &algorithmId, int index) const;
@@ -537,28 +556,27 @@ public:
bool algorithmParamHasMaximum(const std::string &algorithmId, int index) const;
/** @brief Numeric minimum.
* @param algorithmId Algorithm id. @param index Parameter index. @return Minimum or zero. */
float getAlgorithmParamMinimum(const std::string &algorithmId, int index) const;
float getAlgorithmParamMinimum(const std::string& algorithmId, int index) const;
/** @brief Numeric maximum.
* @param algorithmId Algorithm id. @param index Parameter index. @return Maximum or zero. */
float getAlgorithmParamMaximum(const std::string &algorithmId, int index) const;
float getAlgorithmParamMaximum(const std::string& algorithmId, int index) const;
/** @brief Suggested numeric editing step.
* @param algorithmId Algorithm id. @param index Parameter index. @return Step or zero. */
float getAlgorithmParamStep(const std::string &algorithmId, int index) const;
float getAlgorithmParamStep(const std::string& algorithmId, int index) const;
/** @brief Return true when a parameter should be hidden by compact inspectors.
* @param algorithmId Algorithm id. @param index Parameter index. @return Whether it is advanced. */
bool isAlgorithmParamAdvanced(const std::string &algorithmId, int index) const;
bool isAlgorithmParamAdvanced(const std::string& algorithmId, int index) const;
/** @brief Number of allowed values for a choice parameter.
* @param algorithmId Algorithm id. @param index Parameter index. @return Choice count. */
int getAlgorithmParamChoiceCount(const std::string &algorithmId, int index) const;
int getAlgorithmParamChoiceCount(const std::string& algorithmId, int index) const;
/** @brief Choice value at an index.
* @param algorithmId Algorithm id. @param paramIndex Parameter index.
* @param choiceIndex Choice index. @return Choice or empty text. */
std::string getAlgorithmParamChoice(const std::string &algorithmId, int paramIndex,
int choiceIndex) const;
std::string getAlgorithmParamChoice(const std::string& algorithmId, int paramIndex, int choiceIndex) const;
/** @brief Fill missing algorithm-specific values from schema defaults.
* @param algorithmId Algorithm id. @param params Parameters to update.
* @return Whether the schema exists. */
[[nodiscard]] eve::Result<void> applyAlgorithmDefaults(const std::string &algorithmId,
[[nodiscard]] eve::Result<void> applyAlgorithmDefaults(const std::string& algorithmId,
ProcgenParamsHandleRef params) const;
/**
@@ -573,7 +591,7 @@ public:
[[nodiscard]] eve::Result<std::string> gridToJson(ProcgenGridHandleRef grid) const;
// --- Phase B: textures ---
[[nodiscard]] eve::Result<ProcgenImageHandleRef> generateImageHandle(const std::string &recipeId,
[[nodiscard]] eve::Result<ProcgenImageHandleRef> generateImageHandle(const std::string& recipeId,
ProcgenParamsHandleRef params);
/**
* @brief Resolves a generated image as a non-owning observation.
@@ -602,7 +620,7 @@ public:
*/
[[nodiscard]] static bool isStale(ProcgenImageHandleRef reference) noexcept;
[[nodiscard]] eve::Result<ProcgenNormalImageHandleRef> generateNormalImageHandle(const std::string &recipeId,
[[nodiscard]] eve::Result<ProcgenNormalImageHandleRef> generateNormalImageHandle(const std::string& recipeId,
ProcgenParamsHandleRef params);
/**
* @brief Resolves a generated normal image as a non-owning observation.
@@ -644,17 +662,17 @@ public:
* @thread Graphics/Procgen owner thread; no synchronization is provided.
* @reentrancy Does not invoke user callbacks.
*/
[[nodiscard]] eve::script::Borrowed<graphics::Texture> generateTextureBorrowed(const std::string &recipeId,
[[nodiscard]] eve::script::Borrowed<graphics::Texture> generateTextureBorrowed(const std::string& recipeId,
ProcgenParamsHandleRef params,
graphics::Graphics *gfx);
graphics::Graphics* gfx);
int getTextureRecipeCount() const;
std::string getTextureRecipeId(int index) const;
bool hasTextureRecipe(const std::string &recipeId) const;
bool hasTextureRecipe(const std::string& recipeId) const;
/** @brief Returns an owning copy of a texture schema or a diagnostic. */
[[nodiscard]] eve::Result<RecipeDescriptor> getTextureRecipeSchema(const std::string &recipeId) const;
[[nodiscard]] eve::Result<RecipeDescriptor> getTextureRecipeSchema(const std::string& recipeId) const;
/** @brief Fill missing texture parameters in a module-owned parameter object. */
[[nodiscard]] eve::Result<void> applyTextureRecipeDefaults(const std::string &recipeId,
[[nodiscard]] eve::Result<void> applyTextureRecipeDefaults(const std::string& recipeId,
ProcgenParamsHandleRef params) const;
// --- Phase E: dynamic clouds + cloud shadows ---
@@ -685,7 +703,7 @@ public:
* (call PbrTextureSet::destroy()). Recipes: pbr.soil/stone/rock/marble/water/
* ripple/wood/cloth/ornament/spot/zebra/wall/cement/mud/sky_cloud.
*/
[[nodiscard]] eve::Result<ProcgenPbrMaterialHandleRef> generatePbrMaterialHandle(const std::string &recipeId,
[[nodiscard]] eve::Result<ProcgenPbrMaterialHandleRef> generatePbrMaterialHandle(const std::string& recipeId,
ProcgenParamsHandleRef params);
[[nodiscard]] eve::script::Borrowed<PbrTextureSet> resolvePbrMaterial(ProcgenPbrMaterialHandleRef) noexcept;
[[nodiscard]] eve::Result<void> releasePbrMaterial(ProcgenPbrMaterialHandleRef);
@@ -693,11 +711,11 @@ public:
int getPbrRecipeCount() const;
std::string getPbrRecipeId(int index) const;
bool hasPbrRecipe(const std::string &recipeId) const;
bool hasPbrRecipe(const std::string& recipeId) const;
/** @brief Returns an owning copy of a PBR schema or a diagnostic. */
[[nodiscard]] eve::Result<RecipeDescriptor> getPbrRecipeSchema(const std::string &recipeId) const;
[[nodiscard]] eve::Result<RecipeDescriptor> getPbrRecipeSchema(const std::string& recipeId) const;
/** @brief Fill missing PBR parameters in a module-owned parameter object. */
[[nodiscard]] eve::Result<void> applyPbrRecipeDefaults(const std::string &recipeId,
[[nodiscard]] eve::Result<void> applyPbrRecipeDefaults(const std::string& recipeId,
ProcgenParamsHandleRef params) const;
// --- Mesh recipes (Marching Cubes, …) ---
@@ -706,7 +724,7 @@ public:
* Recipes include mesh.marchingcubes, mesh.hexplanet, mesh.castle and the
* registered vegetation, building and linear-structure recipes.
*/
[[nodiscard]] eve::Result<ProcgenMeshBuildHandleRef> buildMeshHandle(const std::string &recipeId,
[[nodiscard]] eve::Result<ProcgenMeshBuildHandleRef> buildMeshHandle(const std::string& recipeId,
ProcgenParamsHandleRef params);
[[nodiscard]] eve::script::Borrowed<MeshBuild> resolveMeshBuild(ProcgenMeshBuildHandleRef) noexcept;
[[nodiscard]] eve::Result<void> releaseMeshBuild(ProcgenMeshBuildHandleRef);
@@ -718,7 +736,7 @@ public:
* @param id Non-nil identity for this artifact instance.
* @return Generated CPU artifact or a structured diagnostic.
*/
[[nodiscard]] eve::Result<GeneratedArtifact> buildArtifact(const std::string &recipeId,
[[nodiscard]] eve::Result<GeneratedArtifact> buildArtifact(const std::string& recipeId,
ProcgenParamsHandleRef params, ArtifactId id);
/**
* @brief Build then transactionally publish through requested optional adapters.
@@ -729,11 +747,11 @@ public:
* @return Publication receipt or an Unsupported/validation/adapter diagnostic.
* @remarks Must run on the main/simulation thread. A failure leaves the store unchanged.
*/
[[nodiscard]] eve::Result<ArtifactPublishReceipt> publishArtifact(const std::string &recipeId,
[[nodiscard]] eve::Result<ArtifactPublishReceipt> publishArtifact(const std::string& recipeId,
ProcgenParamsHandleRef params, ArtifactId id,
ArtifactPublishOptions options = {});
/** @brief Borrow the compatibility facade's published CPU artifact store. */
[[nodiscard]] const ArtifactStore &artifactStore() const noexcept { return artifactStore_; }
[[nodiscard]] const ArtifactStore& artifactStore() const noexcept { return artifactStore_; }
/** @brief Upload an existing/composed CPU mesh to Graphics. */
/** @brief Upload a CPU mesh and return a Graphics-owned borrowed mesh. */
[[nodiscard]] eve::script::Borrowed<graphics::Mesh> uploadMeshBorrowed(const MeshBuild &mesh,
@@ -773,17 +791,17 @@ public:
graphics::Graphics& gfx, float worldDiameter,
float verticalFovDegrees);
/** @brief Build + upload to GPU Mesh (owned by Graphics). */
[[nodiscard]] eve::script::Borrowed<graphics::Mesh> generateMeshBorrowed(const std::string &recipeId,
[[nodiscard]] eve::script::Borrowed<graphics::Mesh> generateMeshBorrowed(const std::string& recipeId,
ProcgenParamsHandleRef params,
graphics::Graphics *gfx);
graphics::Graphics* gfx);
int getMeshRecipeCount() const;
std::string getMeshRecipeId(int index) const;
bool hasMeshRecipe(const std::string &recipeId) const;
bool hasMeshRecipe(const std::string& recipeId) const;
/** @brief Returns an owning copy of a mesh schema or a diagnostic. */
[[nodiscard]] eve::Result<RecipeDescriptor> getMeshRecipeSchema(const std::string &recipeId) const;
[[nodiscard]] eve::Result<RecipeDescriptor> getMeshRecipeSchema(const std::string& recipeId) const;
/** @brief Fill missing mesh parameters in a module-owned parameter object. */
[[nodiscard]] eve::Result<void> applyMeshRecipeDefaults(const std::string &recipeId,
[[nodiscard]] eve::Result<void> applyMeshRecipeDefaults(const std::string& recipeId,
ProcgenParamsHandleRef params) const;
// --- Phase D: terrain height sampling ---
@@ -802,100 +820,92 @@ public:
[[nodiscard]] eve::Result<void> releaseHeightmap(ProcgenHeightmapHandleRef);
[[nodiscard]] bool isHeightmapStale(ProcgenHeightmapHandleRef) const noexcept;
/** @brief Compatibility facade that classifies a heightmap into a semantic Grid2D. */
bool heightmapToGrid(Heightmap *heightmap, Params *params, Grid2D *out);
bool heightmapToGrid(Heightmap* heightmap, Params* params, Grid2D* out);
/** @brief Compatibility facade that applies mass-conserving thermal erosion in place. */
bool erodeTerrainThermal(Heightmap *heightmap, int iterations, float talus, float strength);
bool erodeTerrainThermal(Heightmap* heightmap, int iterations, float talus, float strength);
/** @brief Compatibility facade that applies deterministic grid water/sediment erosion. */
bool erodeTerrainHydraulic(Heightmap *heightmap, int iterations, float rainfall,
float evaporation, float capacity, float erosion,
float deposition);
bool erodeTerrainHydraulic(Heightmap* heightmap, int iterations, float rainfall, float evaporation, float capacity,
float erosion, float deposition);
/** @brief Compatibility facade that cuts drainage-connected river valleys in place. */
bool erodeTerrainFluvial(Heightmap *heightmap, int iterations, float riverThreshold,
float incision, float maxDepth, float bankWidth);
bool erodeTerrainFluvial(Heightmap* heightmap, int iterations, float riverThreshold, float incision, float maxDepth,
float bankWidth);
/** @brief Compatibility facade with independent incision and spill-sill limits. */
bool erodeTerrainFluvialAdvanced(Heightmap *heightmap, int iterations, float riverThreshold,
float incision, float maxDepth, float bankWidth,
float maxBreachDepth);
bool erodeTerrainFluvialAdvanced(Heightmap* heightmap, int iterations, float riverThreshold, float incision,
float maxDepth, float bankWidth, float maxBreachDepth);
/** @brief Compatibility facade with explicit raster-to-physical scaling. */
bool erodeTerrainFluvialScaled(Heightmap *heightmap, int iterations, float riverThreshold,
float incision, float maxDepth, float bankWidth,
float maxBreachDepth, float coordinateScale);
bool erodeTerrainFluvialScaled(Heightmap* heightmap, int iterations, float riverThreshold, float incision,
float maxDepth, float bankWidth, float maxBreachDepth, float coordinateScale);
/** @brief Compatibility facade for detailed erosion. @ownership Caller owns the result. */
TerrainErosionMap *erodeTerrainFluvialDetailed(
Heightmap *heightmap, int iterations, float riverThreshold, float incision,
float maxDepth, float bankWidth, float maxBreachDepth, float coordinateScale);
TerrainErosionMap* erodeTerrainFluvialDetailed(Heightmap* heightmap, int iterations, float riverThreshold,
float incision, float maxDepth, float bankWidth,
float maxBreachDepth, float coordinateScale);
/** @brief Compatibility facade for terrain analysis. @ownership Caller owns the result. */
TerrainLayers *analyzeTerrain(Heightmap *heightmap, float riverThreshold, float seaLevel,
float latitude);
TerrainLayers* analyzeTerrain(Heightmap* heightmap, float riverThreshold, float seaLevel, float latitude);
/** @brief Compatibility facade for scaled analysis. @ownership Caller owns the result. */
TerrainLayers *analyzeTerrainScaled(Heightmap *heightmap, float riverThreshold, float seaLevel,
float latitude, float coordinateScale);
TerrainLayers* analyzeTerrainScaled(Heightmap* heightmap, float riverThreshold, float seaLevel, float latitude,
float coordinateScale);
/** @brief Compatibility facade for EVTR baking. @ownership Caller owns the result. */
data::ByteData *bakeTerrainAsset(Heightmap *heightmap, TerrainLayers *layers, int chunkSize);
data::ByteData* bakeTerrainAsset(Heightmap* heightmap, TerrainLayers* layers, int chunkSize);
/** @brief Compatibility facade for LOD chunks. @ownership Caller owns the result. */
TerrainMeshChunk *buildTerrainChunk(Heightmap *heightmap, TerrainLayers *layers,
int originX, int originY, int cellsX, int cellsY, int lod,
float cellSize, float heightScale, float skirtDepth);
TerrainMeshChunk* buildTerrainChunk(Heightmap* heightmap, TerrainLayers* layers, int originX, int originY,
int cellsX, int cellsY, int lod, float cellSize, float heightScale,
float skirtDepth);
/** @brief Select terrain LOD from measured screen-space geometric error. */
int selectTerrainLod(Heightmap *heightmap, int originX, int originY, int cellsX, int cellsY,
int maxLod, float cellSize, float heightScale, float cameraDistance,
float viewportHeight, float verticalFovDegrees, float targetPixelError);
int selectTerrainLod(Heightmap* heightmap, int originX, int originY, int cellsX, int cellsY, int maxLod,
float cellSize, float heightScale, float cameraDistance, float viewportHeight,
float verticalFovDegrees, float targetPixelError);
/** @brief Compatibility upload facade. @lifetime Returned mesh is owned by Graphics. */
graphics::Mesh *generateTerrainChunkMesh(TerrainMeshChunk *chunk, graphics::Graphics *gfx);
graphics::Mesh* generateTerrainChunkMesh(TerrainMeshChunk* chunk, graphics::Graphics* gfx);
/** @brief Compatibility river mesh facade. @lifetime Returned mesh is owned by Graphics. */
graphics::Mesh *generateTerrainRiverMesh(Heightmap *heightmap, TerrainLayers *layers,
graphics::Graphics *gfx, int originX, int originY,
int cellsX, int cellsY, float cellSize,
float heightScale, float minWidth, float maxWidth,
float heightOffset);
graphics::Mesh* generateTerrainRiverMesh(Heightmap* heightmap, TerrainLayers* layers, graphics::Graphics* gfx,
int originX, int originY, int cellsX, int cellsY, float cellSize,
float heightScale, float minWidth, float maxWidth, float heightOffset);
/** @brief Compatibility slope-banded river facade. @lifetime Returned mesh is owned by Graphics. */
graphics::Mesh *generateTerrainRiverMeshAdvanced(
Heightmap *heightmap, TerrainLayers *layers, graphics::Graphics *gfx,
int originX, int originY, int cellsX, int cellsY, float cellSize,
float heightScale, float minWidth, float maxWidth, float heightOffset,
float minSurfaceSlope, float maxSurfaceSlope);
graphics::Mesh* generateTerrainRiverMeshAdvanced(Heightmap* heightmap, TerrainLayers* layers,
graphics::Graphics* gfx, int originX, int originY, int cellsX,
int cellsY, float cellSize, float heightScale, float minWidth,
float maxWidth, float heightOffset, float minSurfaceSlope,
float maxSurfaceSlope);
/** @brief Compatibility lake mesh facade. @lifetime Returned mesh is owned by Graphics. */
graphics::Mesh *generateTerrainLakeMesh(Heightmap *heightmap, TerrainLayers *layers,
graphics::Graphics *gfx, int originX, int originY,
int cellsX, int cellsY, float cellSize,
float heightScale, float minimumDepth,
float heightOffset);
graphics::Mesh* generateTerrainLakeMesh(Heightmap* heightmap, TerrainLayers* layers, graphics::Graphics* gfx,
int originX, int originY, int cellsX, int cellsY, float cellSize,
float heightScale, float minimumDepth, float heightOffset);
/** @brief Compatibility splat facade. @ownership Caller owns the returned image. */
image::ImageData *generateTerrainSplatMap(TerrainMeshChunk *chunk);
image::ImageData* generateTerrainSplatMap(TerrainMeshChunk* chunk);
/** @brief Compatibility albedo facade. @ownership Caller owns the returned image. */
image::ImageData *generateTerrainAlbedoMap(TerrainMeshChunk *chunk);
image::ImageData* generateTerrainAlbedoMap(TerrainMeshChunk* chunk);
/** @brief Compatibility erosion facade. @ownership Caller owns the returned image. */
image::ImageData *generateTerrainErosionMap(TerrainErosionMap *erosion, float exposure);
image::ImageData* generateTerrainErosionMap(TerrainErosionMap* erosion, float exposure);
/** @brief Compatibility wear facade. @ownership Caller owns the returned image. */
image::ImageData *generateTerrainWearMap(TerrainErosionMap *erosion, float exposure);
image::ImageData* generateTerrainWearMap(TerrainErosionMap* erosion, float exposure);
/** @brief Compatibility deposition facade. @ownership Caller owns the returned image. */
image::ImageData *generateTerrainDepositionMap(TerrainErosionMap *erosion, float exposure);
image::ImageData* generateTerrainDepositionMap(TerrainErosionMap* erosion, float exposure);
/** @brief Compatibility material facade. @lifetime Returned shader is owned by Graphics. */
graphics::Shader *createTerrainMaterialShader(graphics::Graphics *gfx);
graphics::Shader* createTerrainMaterialShader(graphics::Graphics* gfx);
/** @brief Compatibility water facade. @lifetime Returned shader is owned by Graphics. */
graphics::Shader *createTerrainWaterShader(graphics::Graphics *gfx);
graphics::Shader* createTerrainWaterShader(graphics::Graphics* gfx);
[[nodiscard]] eve::Result<ProcgenHeightmapHandleRef> generateHeightmapHandle(ProcgenParamsHandleRef params);
/** @brief Classify a heightmap into a module-owned grid using params bands. */
[[nodiscard]] eve::Result<ProcgenGridHandleRef> heightmapToGrid(ProcgenHeightmapHandleRef heightmap,
ProcgenParamsHandleRef params);
PaletteTable &palettes() { return palettes_; }
PaletteTable& palettes() { return palettes_; }
private:
bool runGenerate(const std::string &algorithmId, const Params &params, Grid2D &out);
bool runGenerate(const std::string& algorithmId, const Params& params, Grid2D& out);
ArtifactId nextCompatibilityArtifactId() noexcept;
struct OwnershipState;
std::unique_ptr<OwnershipState> ownership_;
PaletteTable palettes_;
PaletteTable palettes_;
mutable std::string lastError_;
mutable std::vector<std::string> algorithmIdsCache_;
mutable std::vector<std::string> textureRecipeIdsCache_;
mutable std::vector<std::string> pbrRecipeIdsCache_;
mutable std::vector<std::string> meshRecipeIdsCache_;
std::unordered_map<std::string, ProcgenSystemSnapshot> systems_;
std::unordered_map<std::string, ProcgenSystemSnapshot> previousSystems_;
mutable std::vector<std::string> algorithmIdsCache_;
mutable std::vector<std::string> textureRecipeIdsCache_;
mutable std::vector<std::string> pbrRecipeIdsCache_;
mutable std::vector<std::string> meshRecipeIdsCache_;
std::unordered_map<std::string, ProcgenSystemSnapshot> systems_;
std::unordered_map<std::string, ProcgenSystemSnapshot> previousSystems_;
ArtifactStore artifactStore_;
std::uint64_t nextArtifactSequence_ = 1;
eve::script::RuntimeObjectRegistry<Params, ProcgenParamsHandleTag> params_;
@@ -910,6 +920,8 @@ private:
eve::script::RuntimeObjectRegistry<DynamicMeshUvPaintSession, ProcgenDynamicMeshUvPaintSessionHandleTag>
dynamicMeshUvPaintSessions_;
eve::script::RuntimeObjectRegistry<SplinePath, ProcgenSplinePathHandleTag> splinePaths_;
eve::script::RuntimeObjectRegistry<GridGraph, ProcgenGridGraphHandleTag> gridGraphs_;
eve::script::RuntimeObjectRegistry<MeshGraph, ProcgenMeshGraphHandleTag> meshGraphs_;
eve::script::RuntimeObjectRegistry<BiomeRules, ProcgenBiomeRulesHandleTag> biomeRules_;
eve::script::RuntimeObjectRegistry<ShapeGrammar, ProcgenShapeGrammarHandleTag> shapeGrammars_;
eve::script::RuntimeObjectRegistry<LSystem, ProcgenLSystemHandleTag> lsystems_;
+10
View File
@@ -1,5 +1,7 @@
#include "procgen/Procgen.h"
#include "procgen/GridGraph.h"
#include "procgen/MeshGraph.h"
#include "procgen/PointGraph.h"
#include "procgen/mesh/MeshModifierGraph.h"
@@ -25,4 +27,12 @@ eve::Result<ProcgenSplinePathHandleRef> Procgen::newSplinePathHandle() {
return splinePaths_.emplace(std::make_unique<SplinePath>());
}
eve::Result<ProcgenGridGraphHandleRef> Procgen::newGridGraphHandle() {
return gridGraphs_.emplace(std::make_unique<GridGraph>());
}
eve::Result<ProcgenMeshGraphHandleRef> Procgen::newMeshGraphHandle() {
return meshGraphs_.emplace(std::make_unique<MeshGraph>());
}
} // namespace eve::procgen
+126
View File
@@ -0,0 +1,126 @@
#include "procgen/TilePreset.h"
#include <algorithm>
#include <array>
namespace eve::procgen {
namespace {
template <std::size_t N>
bool contains(const std::array<std::uint16_t, N>& values, std::uint16_t value) noexcept {
return std::find(values.begin(), values.end(), value) != values.end();
}
constexpr std::array<std::uint16_t, 32> cornerFill = {27, 31, 54, 55, 91, 95, 118, 119, 216, 217, 220,
221, 283, 287, 310, 311, 347, 351, 374, 375, 432, 433,
436, 437, 472, 473, 476, 477, 496, 497, 500, 501};
constexpr std::array<std::uint16_t, 31> cornerWay = {26, 30, 50, 51, 90, 94, 114, 115, 152, 153, 156,
157, 176, 177, 180, 181, 240, 241, 244, 245, 282, 286,
306, 307, 346, 370, 371, 408, 409, 412, 413};
constexpr std::array<std::uint16_t, 32> edgeWay = {56, 57, 60, 61, 120, 121, 124, 125, 146, 147, 150,
151, 210, 211, 214, 215, 312, 313, 316, 317, 376, 377,
380, 381, 402, 403, 406, 407, 466, 467, 470, 471};
constexpr std::array<std::uint16_t, 16> edgeFill = {63, 127, 219, 223, 319, 383, 438, 439,
475, 479, 502, 503, 504, 505, 508, 509};
constexpr std::array<std::uint16_t, 16> threeWay = {58, 122, 154, 158, 178, 179, 184, 185,
188, 189, 242, 243, 314, 378, 410, 414};
constexpr std::array<std::uint16_t, 4> threeWayFill = {191, 251, 446, 506};
constexpr std::array<std::uint16_t, 64> deadEnd = {
18, 19, 22, 23, 24, 25, 28, 29, 48, 49, 52, 53, 82, 83, 86, 87, 88, 89, 92, 93, 112, 113,
116, 117, 144, 145, 148, 149, 208, 209, 212, 213, 274, 275, 278, 279, 280, 281, 284, 285, 304, 305, 308, 309,
338, 339, 342, 343, 344, 345, 348, 349, 368, 369, 372, 373, 400, 401, 404, 405, 464, 465, 468, 469};
constexpr std::array<std::uint16_t, 16> single = {16, 17, 20, 21, 80, 81, 84, 85,
272, 273, 276, 277, 336, 337, 340, 341};
constexpr std::array<std::uint16_t, 32> edgeCornerFill = {59, 62, 123, 126, 155, 159, 182, 183, 218, 222, 246,
247, 248, 249, 252, 253, 315, 318, 379, 382, 411, 415,
434, 435, 440, 441, 444, 445, 474, 478, 498, 499};
constexpr std::array<std::uint16_t, 4> threeCorner = {187, 190, 250, 442};
constexpr std::array<std::uint16_t, 2> doubleCorner = {254, 443};
constexpr std::array<std::uint16_t, 4> interiorCorner = {255, 447, 507, 510};
constexpr std::array<std::uint16_t, 15> rotationZero = {312, 120, 56, 24, 89, 280, 58, 152,
186, 95, 218, 222, 434, 474, 498};
constexpr std::array<std::uint16_t, 65> rotation90 = {
22, 23, 18, 19, 82, 83, 86, 87, 274, 275, 278, 279, 338, 339, 342, 343, 146, 147, 151, 210, 211, 214,
215, 403, 406, 407, 466, 467, 470, 471, 438, 439, 502, 503, 178, 179, 242, 243, 191, 54, 59, 123, 248, 249,
252, 253, 315, 379, 55, 118, 310, 311, 374, 375, 26, 30, 90, 94, 119, 282, 286, 346, 507, 254, 250};
constexpr std::array<std::uint16_t, 51> rotation180 = {48, 49, 52, 53, 112, 113, 116, 117, 304, 305, 308, 309, 368,
369, 372, 373, 50, 51, 114, 115, 306, 307, 370, 371, 504, 505,
508, 509, 432, 433, 436, 437, 496, 497, 500, 501, 187, 184, 185,
188, 189, 446, 155, 159, 182, 183, 246, 247, 411, 415, 255};
constexpr std::array<std::uint16_t, 53> rotation270 = {
144, 145, 148, 149, 208, 209, 212, 213, 400, 401, 404, 405, 464, 465, 468, 469, 154, 158,
410, 414, 506, 216, 217, 220, 472, 476, 473, 477, 176, 177, 180, 181, 221, 240, 241, 244,
245, 190, 150, 402, 223, 219, 475, 479, 62, 126, 318, 382, 440, 441, 444, 445, 447};
constexpr std::array<std::uint16_t, 16> mirrorX = {59, 123, 182, 183, 218, 222, 246, 247,
315, 379, 440, 441, 444, 445, 474, 478};
TilePresetKind classify(std::uint16_t c) noexcept {
if (contains(cornerWay, c)) return TilePresetKind::CornerWay;
if (contains(cornerFill, c)) return TilePresetKind::CornerFill;
if (contains(edgeWay, c)) return TilePresetKind::EdgeWay;
if (contains(edgeFill, c)) return TilePresetKind::EdgeFill;
if (c == 511) return TilePresetKind::Fill;
if (contains(single, c)) return TilePresetKind::Single;
if (contains(threeWay, c)) return TilePresetKind::ThreeWay;
if (contains(threeWayFill, c)) return TilePresetKind::ThreeWayFill;
if (contains(threeCorner, c)) return TilePresetKind::ThreeCorner;
if (contains(deadEnd, c)) return TilePresetKind::DeadEnd;
if (c == 186) return TilePresetKind::FourWay;
if (contains(edgeCornerFill, c)) return TilePresetKind::EdgeCornerFill;
if (contains(doubleCorner, c)) return TilePresetKind::DoubleCorner;
if (contains(interiorCorner, c)) return TilePresetKind::InteriorCorner;
return TilePresetKind::None;
}
} // namespace
std::uint16_t toTilePresetConfiguration(std::uint8_t mask) noexcept {
std::uint16_t c = 1u << 4u;
if (mask & (1u << 0u)) c |= 1u << 1u; // N -> top
if (mask & (1u << 1u)) c |= 1u << 5u; // E -> right
if (mask & (1u << 2u)) c |= 1u << 7u; // S -> bottom
if (mask & (1u << 3u)) c |= 1u << 3u; // W -> left
if (mask & (1u << 4u)) c |= 1u << 2u; // NE -> top-right
if (mask & (1u << 5u)) c |= 1u << 8u; // SE -> bottom-right
if (mask & (1u << 6u)) c |= 1u << 6u; // SW -> bottom-left
if (mask & (1u << 7u)) c |= 1u << 0u; // NW -> top-left
return c;
}
TilePresetVariant resolveTilePreset(std::uint8_t mask) noexcept {
const auto c = toTilePresetConfiguration(mask);
int rotation = 0;
if (contains(rotation90, c))
rotation = 90;
else if (contains(rotation180, c))
rotation = 180;
else if (contains(rotation270, c))
rotation = 270;
else if (contains(rotationZero, c))
rotation = 0;
return {classify(c), rotation, contains(mirrorX, c), c};
}
std::string_view tilePresetKindName(TilePresetKind kind) noexcept {
switch (kind) {
case TilePresetKind::DeadEnd: return "dead_end";
case TilePresetKind::Single: return "single";
case TilePresetKind::Fill: return "fill";
case TilePresetKind::CornerWay: return "corner_way";
case TilePresetKind::CornerFill: return "corner_fill";
case TilePresetKind::InteriorCorner: return "interior_corner";
case TilePresetKind::DoubleCorner: return "double_corner";
case TilePresetKind::EdgeWay: return "edge_way";
case TilePresetKind::EdgeFill: return "edge_fill";
case TilePresetKind::ThreeWay: return "three_way";
case TilePresetKind::ThreeWayFill: return "three_way_fill";
case TilePresetKind::EdgeCornerFill: return "edge_corner_fill";
case TilePresetKind::ThreeCorner: return "three_corner";
case TilePresetKind::FourWay: return "four_way";
case TilePresetKind::None: return "none";
}
return "none";
}
} // namespace eve::procgen
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <cstdint>
#include <string_view>
namespace eve::procgen {
/** @brief TileWorldCreator-compatible standard-grid tile categories. */
enum class TilePresetKind : std::uint8_t {
None,
DeadEnd,
Single,
Fill,
CornerWay,
CornerFill,
InteriorCorner,
DoubleCorner,
EdgeWay,
EdgeFill,
ThreeWay,
ThreeWayFill,
EdgeCornerFill,
ThreeCorner,
FourWay,
};
/** @brief Stable build semantics resolved from one eight-neighbour autotile mask. */
struct TilePresetVariant {
TilePresetKind kind = TilePresetKind::None;
/** Clockwise rotation in degrees, restricted to 0, 90, 180 or 270. */
int rotationDegrees = 0;
/** Mirror the selected tile mesh along its local X axis after rotation. */
bool mirrorX = false;
/** Original TileWorldCreator 3x3 configuration value, including the center bit. */
std::uint16_t configuration = 16;
};
/**
* @brief Convert EVEngine's N/E/S/W/NE/SE/SW/NW mask to the reference 3x3 configuration.
* @param neighbourMask Eight-neighbour occupancy mask produced by `grid.autotile`.
* @return Nine-bit row-major configuration with the occupied center bit set.
*/
[[nodiscard]] std::uint16_t toTilePresetConfiguration(std::uint8_t neighbourMask) noexcept;
/**
* @brief Resolve the exact TileWorldCreator 4.3.5 standard-grid category and transform tables.
* @param neighbourMask Eight-neighbour occupancy mask produced by `grid.autotile`.
* @return Deterministic tile category, rotation, mirror flag and reference configuration.
* @note Configuration 350 is intentionally `None`, matching the reference table's unclassified entry.
*/
[[nodiscard]] TilePresetVariant resolveTilePreset(std::uint8_t neighbourMask) noexcept;
/** @brief Stable lowercase identifier used in mesh build-layer group names. */
[[nodiscard]] std::string_view tilePresetKindName(TilePresetKind kind) noexcept;
} // namespace eve::procgen
@@ -734,6 +734,26 @@ bool autotileGridInPlace(Grid2D &grid) {
return grid.getWidth() > 0 && grid.getHeight() > 0;
}
eve::Result<void> autotileOccupiedGridInPlace(Grid2D &grid) {
if (grid.getWidth() <= 0 || grid.getHeight() <= 0)
return eve::Result<void>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::InvalidArgument, "autotile grid dimensions must be positive", {}, {},
"procgen.gridGraph"));
constexpr int dx[] = {0, 1, 0, -1, 1, 1, -1, -1};
constexpr int dy[] = {-1, 0, 1, 0, -1, 1, 1, -1};
for (int y = 0; y < grid.getHeight(); ++y) {
for (int x = 0; x < grid.getWidth(); ++x) {
int mask = 0;
if (uint32_t(grid.getCell(x, y)) != Semantic::Empty)
for (int direction = 0; direction < 8; ++direction)
if (uint32_t(grid.getCell(x + dx[direction], y + dy[direction])) != Semantic::Empty)
mask |= 1 << direction;
grid.setDetail(x, y, mask);
}
}
return eve::Result<void>::success();
}
uint32_t randomSeedValue() {
std::random_device rd;
const uint32_t v = rd();
@@ -1,5 +1,7 @@
#pragma once
#include "common/Result.h"
#include <cstdint>
namespace eve::procgen {
@@ -17,6 +19,14 @@ void registerRoguelikeGenerator(GeneratorRegistry &registry);
*/
bool autotileGridInPlace(Grid2D &grid);
/**
* @brief Write an eight-neighbour occupancy mask into every non-empty cell's detail value.
* @param grid Dense grid mutated in place; empty cells receive detail zero.
* @return Success after writing all masks; InvalidArgument for unusable dimensions.
* @thread Caller-owned grid only; not safe for concurrent mutation.
*/
[[nodiscard]] eve::Result<void> autotileOccupiedGridInPlace(Grid2D &grid);
/** @brief Produce a fresh seed suitable for regenerating a level (never 0). */
uint32_t randomSeedValue();
+69
View File
@@ -0,0 +1,69 @@
#include "procgen/BuildLayerStack.h"
#include "procgen/Semantic.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
using namespace eve::procgen;
TEST_CASE("procgen.buildLayers.executesOrderedTileAndObjectDefinitions") {
Grid2D grid;
grid.resize(2, 1);
grid.setCell(0, 0, int(Semantic::Floor));
grid.setCell(1, 0, int(Semantic::Floor));
PointSet points;
points.add(2.f, 0.f, 3.f);
ObjectBuildLayer objects;
REQUIRE(objects.addAsset("props/crate", 1.f).ok());
objects.setSeed(15);
BuildLayerStack stack;
REQUIRE(stack.addTileLayer("floor", true, 2.f, 0.5f, "floor").ok());
REQUIRE(stack.addObjectLayer("props", true, objects).ok());
REQUIRE(stack.addTileLayer("disabled", false, 1.f, 1.f, "disabled").ok());
CHECK_EQ(stack.getLayerCount(), 3);
CHECK_EQ(stack.getLayerId(1), "props");
CHECK_EQ(stack.getLayerType(0), "tiles");
CHECK(!stack.isLayerEnabled(2));
auto result = stack.execute(grid, points);
REQUIRE(result.ok());
CHECK_EQ(result.value().getCount(), 2);
CHECK_EQ(result.value().getId(0), "floor");
CHECK_EQ(result.value().getType(0), "mesh");
CHECK_EQ(result.value().getType(1), "points");
auto mesh = result.value().getMesh(0);
REQUIRE(mesh.ok());
CHECK_EQ(mesh.value().getIndexCount(), 48);
auto built = result.value().getPoints(1);
REQUIRE(built.ok());
CHECK_EQ(built.value().getStringAttribute(0, "asset", ""), "props/crate");
CHECK(!result.value().getPoints(0).ok());
CHECK(!result.value().getMesh(1).ok());
}
TEST_CASE("procgen.buildLayers.roundTripsAndRejectsDamageAtomically") {
ObjectBuildLayer objects;
REQUIRE(objects.addAsset("props/a", 2.f).ok());
REQUIRE(objects.addAsset("props/b", 1.f).ok());
REQUIRE(objects.setPositionRadius(0.4f).ok());
REQUIRE(objects.addChild("props/child", 2, 1.f, 0.5f, 1.5f, -20.f, 20.f).ok());
const auto objectDefinition = objects.serializeDefinition();
ObjectBuildLayer restoredObject;
REQUIRE(restoredObject.deserializeDefinition(objectDefinition).ok());
CHECK_EQ(restoredObject.serializeDefinition(), objectDefinition);
CHECK(!restoredObject.deserializeDefinition("EVPCG_OBJECT_LAYER 1\nUNKNOWN 1\nEND\n").ok());
CHECK_EQ(restoredObject.serializeDefinition(), objectDefinition);
BuildLayerStack stack;
REQUIRE(stack.addObjectLayer("objects", true, objects).ok());
REQUIRE(stack.addTileLayer("tiles", false, 1.25f, 0.3f, "terrain main").ok());
const auto definition = stack.serializeDefinition();
BuildLayerStack restored;
REQUIRE(restored.deserializeDefinition(definition).ok());
CHECK_EQ(restored.serializeDefinition(), definition);
CHECK(!restored.deserializeDefinition("EVPCG_BUILD_LAYERS 1\nOBJECT \"bad\" 1 xyz\nEND\n").ok());
CHECK_EQ(restored.serializeDefinition(), definition);
CHECK(!restored.addTileLayer("objects", true, 1.f, 0.f, "duplicate").ok());
CHECK(!restored.setEnabled("missing", true).ok());
}
+279
View File
@@ -0,0 +1,279 @@
#include "procgen/GeneratorRegistry.h"
#include "procgen/GridGraph.h"
#include "procgen/MeshGraph.h"
#include "procgen/Params.h"
#include "procgen/PointGraph.h"
#include "procgen/Semantic.h"
#include "procgen/TilePreset.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
using namespace eve::procgen;
TEST_CASE("procgen.tilePreset.matchesReferenceStandardGridTables") {
const auto isolated = resolveTilePreset(0);
CHECK_EQ(isolated.configuration, 16);
CHECK_EQ(isolated.kind, TilePresetKind::Single);
CHECK_EQ(isolated.rotationDegrees, 0);
CHECK(!isolated.mirrorX);
const auto north = resolveTilePreset(1u << 0u);
CHECK_EQ(north.configuration, 18);
CHECK_EQ(north.kind, TilePresetKind::DeadEnd);
CHECK_EQ(north.rotationDegrees, 90);
const auto east = resolveTilePreset(1u << 1u);
CHECK_EQ(east.configuration, 48);
CHECK_EQ(east.kind, TilePresetKind::DeadEnd);
CHECK_EQ(east.rotationDegrees, 180);
const auto asymmetric = resolveTilePreset(139);
CHECK_EQ(asymmetric.configuration, 59);
CHECK_EQ(asymmetric.kind, TilePresetKind::EdgeCornerFill);
CHECK_EQ(asymmetric.rotationDegrees, 90);
CHECK(asymmetric.mirrorX);
const auto filled = resolveTilePreset(255);
CHECK_EQ(filled.configuration, 511);
CHECK_EQ(filled.kind, TilePresetKind::Fill);
// The source 4.3.5 table omits configuration 350; preserve that compatibility edge case.
const auto unclassified = resolveTilePreset(121);
CHECK_EQ(unclassified.configuration, 350);
CHECK_EQ(unclassified.kind, TilePresetKind::None);
for (int mask = 0; mask <= 255; ++mask) {
const auto variant = resolveTilePreset(std::uint8_t(mask));
CHECK((variant.configuration & 16u) != 0u);
const bool validRotation = variant.rotationDegrees == 0 || variant.rotationDegrees == 90 ||
variant.rotationDegrees == 180 || variant.rotationDegrees == 270;
CHECK(validRotation);
}
}
TEST_CASE("procgen.gridGraph.composesGridAndPointGraphs") {
Grid2D source;
source.resize(5, 5);
source.setCell(2, 2, int(Semantic::Grass));
PointGraph points;
REQUIRE(points.addNode("input", "input"));
REQUIRE(points.addNode("move", "transform"));
REQUIRE(points.connect("input", "move"));
REQUIRE(points.setNodeFloat("move", "y", 3.f));
GridGraph graph;
REQUIRE(graph.addNode("source", "grid.input").ok());
REQUIRE(graph.addNode("grow", "grid.expand").ok());
REQUIRE(graph.addNode("points", "convert.grid_to_points").ok());
REQUIRE(graph.addNode("point_pass", "point.subgraph").ok());
REQUIRE(graph.connect("source", "grow").ok());
REQUIRE(graph.connect("grow", "points").ok());
REQUIRE(graph.connect("points", "point_pass").ok());
REQUIRE(graph.setNodeGrid("source", source).ok());
REQUIRE(graph.setNodeInt("grow", "radius", 1).ok());
REQUIRE(graph.setNodeFloat("points", "cellSize", 2.f).ok());
REQUIRE(graph.setNodePointSubgraph("point_pass", points, "input", "move").ok());
auto result = graph.execute("point_pass");
REQUIRE(result.ok());
const auto* output = std::get_if<PointSet>(&result.value());
REQUIRE(output != nullptr);
CHECK_EQ(output->getCount(), 9);
CHECK_EQ(output->getY(0), 3.f);
CHECK(output->hasIntAttribute(0, "cell_x"));
CHECK(output->hasIntAttribute(0, "semantic"));
auto again = graph.execute("point_pass");
REQUIRE(again.ok());
CHECK_EQ(std::get<PointSet>(again.value()).getCount(), 9);
}
TEST_CASE("procgen.gridGraph.rejectsTypeMismatchCyclesAndDimensionMismatch") {
GridGraph graph;
REQUIRE(graph.addNode("a", "grid.input").ok());
REQUIRE(graph.addNode("b", "grid.input").ok());
REQUIRE(graph.addNode("merge", "grid.union").ok());
REQUIRE(graph.addNode("points", "convert.grid_to_points").ok());
REQUIRE(graph.connect("a", "merge", 0).ok());
REQUIRE(graph.connect("b", "merge", 1).ok());
REQUIRE(graph.connect("merge", "points").ok());
auto badType = graph.connect("points", "merge", 0);
CHECK(!badType.ok());
Grid2D small;
small.resize(2, 2);
Grid2D large;
large.resize(3, 3);
REQUIRE(graph.setNodeGrid("a", small).ok());
REQUIRE(graph.setNodeGrid("b", large).ok());
auto mismatch = graph.execute("merge");
CHECK(!mismatch.ok());
GridGraph cycle;
REQUIRE(cycle.addNode("left", "grid.invert").ok());
REQUIRE(cycle.addNode("right", "grid.invert").ok());
REQUIRE(cycle.connect("left", "right").ok());
REQUIRE(cycle.connect("right", "left").ok());
auto cyclic = cycle.validate("left");
CHECK(!cyclic.ok());
}
TEST_CASE("procgen.gridGraph.coversGeneratorsSelectorsPathAndSerialization") {
GridGraph graph;
REQUIRE(graph.addNode("maze", "generate.maze").ok());
REQUIRE(graph.setNodeInt("maze", "width", 17).ok());
REQUIRE(graph.setNodeInt("maze", "height", 17).ok());
REQUIRE(graph.setNodeInt("maze", "seed", 42).ok());
REQUIRE(graph.addNode("border", "select.border").ok());
REQUIRE(graph.connect("maze", "border").ok());
auto first = graph.execute("border");
REQUIRE(first.ok());
const auto* selected = std::get_if<Grid2D>(&first.value());
REQUIRE(selected != nullptr);
CHECK_EQ(selected->getWidth(), 17);
GridGraph restored;
REQUIRE(restored.deserializeDefinition(graph.serializeDefinition()).ok());
auto second = restored.execute("border");
REQUIRE(second.ok());
CHECK_EQ(std::get<Grid2D>(second.value()).cells(), selected->cells());
Grid2D start;
start.resize(17, 17);
start.setCell(1, 1, int(Semantic::Floor));
Grid2D target;
target.resize(17, 17);
target.setCell(15, 15, int(Semantic::Floor));
REQUIRE(restored.addNode("start", "grid.input").ok());
REQUIRE(restored.addNode("target", "grid.input").ok());
REQUIRE(restored.addNode("path", "grid.path").ok());
REQUIRE(restored.setNodeGrid("start", start).ok());
REQUIRE(restored.setNodeGrid("target", target).ok());
REQUIRE(restored.connect("maze", "path", 0).ok());
REQUIRE(restored.connect("start", "path", 1).ok());
REQUIRE(restored.connect("target", "path", 2).ok());
CHECK(restored.execute("path").ok());
}
TEST_CASE("procgen.gridGraph.reusesRegisteredRoguelikeAndExtractsSemanticLayers") {
GeneratorRegistry::instance().registerBuiltins();
for (const int seed : {7, 42, 436}) {
Params params;
params.setSize(31, 23);
params.setSeed(std::uint32_t(seed));
params.setInt("roomCount", 9);
params.setString("layoutStyle", "clustered");
params.setString("connectionStyle", "nearest");
params.setString("corridorStyle", "l");
Grid2D expected;
std::string error;
REQUIRE(GeneratorRegistry::instance().generate("level.roguelike", params, expected, error));
GridGraph graph;
REQUIRE(graph.addNode("level", "generate.registry").ok());
REQUIRE(graph.setNodeString("level", "algorithm", "level.roguelike").ok());
REQUIRE(graph.setNodeInt("level", "width", 31).ok());
REQUIRE(graph.setNodeInt("level", "height", 23).ok());
REQUIRE(graph.setNodeInt("level", "seed", seed).ok());
REQUIRE(graph.setNodeInt("level", "roomCount", 9).ok());
REQUIRE(graph.setNodeString("level", "layoutStyle", "clustered").ok());
REQUIRE(graph.setNodeString("level", "connectionStyle", "nearest").ok());
REQUIRE(graph.setNodeString("level", "corridorStyle", "l").ok());
REQUIRE(graph.addNode("floor", "select.semantic").ok());
REQUIRE(graph.addNode("corridor", "select.semantic").ok());
REQUIRE(graph.setNodeInt("floor", "semantic", int(Semantic::Floor)).ok());
REQUIRE(graph.setNodeInt("corridor", "semantic", int(Semantic::Corridor)).ok());
REQUIRE(graph.connect("level", "floor").ok());
REQUIRE(graph.connect("level", "corridor").ok());
auto generated = graph.execute("level");
REQUIRE(generated.ok());
const auto& actual = std::get<Grid2D>(generated.value());
CHECK_EQ(actual.cells(), expected.cells());
CHECK_EQ(actual.detail(), expected.detail());
CHECK_EQ(actual.getObjectCount(), expected.getObjectCount());
auto floor = graph.execute("floor");
auto corridor = graph.execute("corridor");
REQUIRE(floor.ok());
REQUIRE(corridor.ok());
int selected = 0;
for (const auto cell : std::get<Grid2D>(floor.value()).cells()) {
const bool valid = cell == Semantic::Empty || cell == Semantic::Floor;
CHECK(valid);
if (cell == Semantic::Floor) ++selected;
}
for (const auto cell : std::get<Grid2D>(corridor.value()).cells()) {
const bool valid = cell == Semantic::Empty || cell == Semantic::Corridor;
CHECK(valid);
if (cell == Semantic::Corridor) ++selected;
}
CHECK(selected > 0);
GridGraph restored;
REQUIRE(restored.deserializeDefinition(graph.serializeDefinition()).ok());
auto restoredLevel = restored.execute("level");
REQUIRE(restoredLevel.ok());
CHECK_EQ(std::get<Grid2D>(restoredLevel.value()).cells(), expected.cells());
}
GridGraph missing;
REQUIRE(missing.addNode("level", "generate.registry").ok());
CHECK(!missing.execute("level").ok());
REQUIRE(missing.setNodeString("level", "algorithm", "missing.generator").ok());
CHECK(!missing.execute("level").ok());
}
TEST_CASE("procgen.meshGraph.composesGridPointAndMeshDomains") {
Grid2D grid;
grid.resize(2, 1);
grid.setCell(0, 0, int(Semantic::Floor));
grid.setCell(1, 0, int(Semantic::Floor));
GridGraph autotile;
REQUIRE(autotile.addNode("grid", "grid.input").ok());
REQUIRE(autotile.addNode("autotile", "grid.autotile").ok());
REQUIRE(autotile.setNodeGrid("grid", grid).ok());
REQUIRE(autotile.connect("grid", "autotile").ok());
auto autotiled = autotile.execute("autotile");
REQUIRE(autotiled.ok());
grid = std::get<Grid2D>(autotiled.value());
MeshGraph tiles;
REQUIRE(tiles.addNode("grid", "grid.input").ok());
REQUIRE(tiles.addNode("tiles", "mesh.grid_tiles").ok());
REQUIRE(tiles.setNodeGrid("grid", grid).ok());
REQUIRE(tiles.connect("grid", "tiles").ok());
auto tiled = tiles.execute("tiles");
REQUIRE(tiled.ok());
CHECK_EQ(tiled.value().getIndexCount(), 48);
CHECK_EQ(tiled.value().getGroupCount(), 2);
CHECK(tiled.value().getGroupName(0).find("/dead_end/r180/normal/48") != std::string::npos);
CHECK(tiled.value().getGroupName(1).find("/dead_end/r0/normal/24") != std::string::npos);
MeshGraph restoredTiles;
REQUIRE(restoredTiles.deserializeDefinition(tiles.serializeDefinition()).ok());
REQUIRE(restoredTiles.setNodeGrid("grid", grid).ok());
auto restoredMesh = restoredTiles.execute("tiles");
REQUIRE(restoredMesh.ok());
CHECK_EQ(restoredMesh.value().getIndexCount(), tiled.value().getIndexCount());
CHECK(!restoredTiles.setNodeFloat("tiles", "misspelled", 1.f).ok());
PointSet points;
points.add(0.f, 0.f, 0.f);
points.add(4.f, 0.f, 0.f);
MeshGraph instances;
REQUIRE(instances.addNode("mesh", "mesh.input").ok());
REQUIRE(instances.addNode("points", "point.input").ok());
REQUIRE(instances.addNode("instances", "mesh.instance_points").ok());
REQUIRE(instances.setNodeMesh("mesh", tiled.value()).ok());
REQUIRE(instances.setNodePoints("points", points).ok());
REQUIRE(instances.connect("mesh", "instances", 0).ok());
REQUIRE(instances.connect("points", "instances", 1).ok());
auto result = instances.execute("instances");
REQUIRE(result.ok());
CHECK_EQ(result.value().getIndexCount(), tiled.value().getIndexCount() * 2);
}
+118
View File
@@ -0,0 +1,118 @@
#include "procgen/IncrementalBuild.h"
#include "procgen/Semantic.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
using namespace eve::procgen;
namespace {
bool containsChange(const IncrementalBuildDelta& delta, int x, int z, bool removed) {
for (int index = 0; index < delta.getCount(); ++index)
if (delta.getClusterX(index) == x && delta.getClusterZ(index) == z && delta.isRemoved(index) == removed)
return true;
return false;
}
} // namespace
TEST_CASE("procgen.incrementalBuild.rebuildsOnlyDirtyClustersAndEmitsRemovals") {
Grid2D grid;
grid.resize(8, 2);
grid.setCell(1, 0, int(Semantic::Floor));
grid.setCell(5, 0, int(Semantic::Floor));
PointSet points;
const int leftPoint = points.add(1.5f, 0.f, 0.5f);
const int rightPoint = points.add(5.5f, 0.f, 0.5f);
ObjectBuildLayer objects;
REQUIRE(objects.addAsset("props/crate", 1.f).ok());
BuildLayerStack stack;
REQUIRE(stack.addTileLayer("floor", true, 1.f, 0.f, "floor").ok());
REQUIRE(stack.addObjectLayer("props", true, objects).ok());
IncrementalBuildExecutor incremental;
auto first = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(first.ok());
CHECK_EQ(first.value().getCount(), 2);
CHECK_EQ(incremental.getCachedClusterCount(), 2);
CHECK(containsChange(first.value(), 0, 0, false));
CHECK(containsChange(first.value(), 1, 0, false));
CHECK(incremental.getCachedArtifacts(0, 0).ok());
auto unchanged = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(unchanged.ok());
CHECK_EQ(unchanged.value().getCount(), 0);
grid.setDetail(1, 0, 7);
auto interior = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(interior.ok());
CHECK_EQ(interior.value().getCount(), 1);
CHECK(containsChange(interior.value(), 0, 0, false));
grid.setCell(3, 0, int(Semantic::Floor));
auto boundary = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(boundary.ok());
CHECK_EQ(boundary.value().getCount(), 2);
CHECK(containsChange(boundary.value(), 0, 0, false));
CHECK(containsChange(boundary.value(), 1, 0, false));
points.setPosition(rightPoint, 6.5f, 0.f, 0.5f);
auto pointEdit = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(pointEdit.ok());
CHECK_EQ(pointEdit.value().getCount(), 1);
CHECK(containsChange(pointEdit.value(), 1, 0, false));
grid.setCell(1, 0, int(Semantic::Empty));
grid.setCell(3, 0, int(Semantic::Empty));
points.setPosition(leftPoint, 5.75f, 0.f, 0.5f);
auto removed = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(removed.ok());
CHECK(containsChange(removed.value(), 0, 0, true));
CHECK_EQ(incremental.getCachedClusterCount(), 1);
CHECK(!incremental.getCachedArtifacts(0, 0).ok());
}
TEST_CASE("procgen.incrementalBuild.partitionsGlobalMeshAndPreservesCacheOnFailure") {
Grid2D grid;
grid.resize(8, 1);
for (int x = 0; x < 8; ++x) grid.setCell(x, 0, int(Semantic::Floor));
PointSet points;
points.add(1.f, 0.f, 0.f);
ObjectBuildLayer objects;
REQUIRE(objects.addAsset("props/crate", 1.f).ok());
BuildLayerStack stack;
REQUIRE(stack.addTileLayer("floor", true, 1.f, 0.f, "floor").ok());
REQUIRE(stack.addObjectLayer("props", true, objects).ok());
IncrementalBuildExecutor incremental;
auto delta = incremental.update(stack, grid, points, 4, 1.f);
REQUIRE(delta.ok());
REQUIRE_EQ(delta.value().getCount(), 2);
auto leftArtifacts = incremental.getCachedArtifacts(0, 0);
auto rightArtifacts = incremental.getCachedArtifacts(1, 0);
REQUIRE(leftArtifacts.ok());
REQUIRE(rightArtifacts.ok());
auto leftMesh = leftArtifacts.value().getMesh(0);
auto rightMesh = rightArtifacts.value().getMesh(0);
REQUIRE(leftMesh.ok());
REQUIRE(rightMesh.ok());
auto full = stack.execute(grid, points);
REQUIRE(full.ok());
auto fullMesh = full.value().getMesh(0);
REQUIRE(fullMesh.ok());
CHECK_EQ(leftMesh.value().getIndexCount() + rightMesh.value().getIndexCount(), fullMesh.value().getIndexCount());
CHECK(leftMesh.value().getPositionX(0) < 4.f);
CHECK(rightMesh.value().getPositionX(0) >= 4.f);
ObjectBuildLayer oriented;
REQUIRE(oriented.addAsset("props/oriented", 1.f).ok());
REQUIRE(oriented.setOrientation(1.f, 0.f, false).ok());
BuildLayerStack failingStack;
REQUIRE(failingStack.addObjectLayer("oriented", true, oriented).ok());
auto failed = incremental.update(failingStack, grid, points, 4, 1.f);
CHECK(!failed.ok());
CHECK_EQ(incremental.getCachedClusterCount(), 2);
CHECK(incremental.getCachedArtifacts(0, 0).ok());
CHECK(!incremental.update(stack, grid, points, 0, 1.f).ok());
CHECK(!incremental.update(stack, grid, points, 4, 0.f).ok());
}
+74
View File
@@ -0,0 +1,74 @@
#include "procgen/ObjectBuildLayer.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <set>
using namespace eve::procgen;
TEST_CASE("procgen.objectBuild.weightedTransformsOrientationChildrenAndPlacement") {
PointSet source;
for (int index = 0; index < 32; ++index) {
const int point = source.add(float(index), 2.f, 0.f);
REQUIRE(source.trySetPointId(point, std::uint64_t(index + 1)).ok());
REQUIRE(source.trySetFloatAttribute(point, "surface_highest_y", 10.f + float(index)).ok());
}
PointSet orientation;
orientation.add(0.f, 0.f, 1.f); // North has priority for the first source point.
ObjectBuildLayer layer;
REQUIRE(layer.addAsset("tree/oak", 3.f).ok());
REQUIRE(layer.addAsset("tree/birch", 1.f).ok());
layer.setSeed(77);
layer.setLayerOffset(1.f, 2.f, 3.f);
REQUIRE(layer.setLayerScale(2.f, 3.f, 4.f).ok());
REQUIRE(layer.setRandomRotation(5.f, 5.f, 7.f, 7.f, 9.f, 9.f).ok());
REQUIRE(layer.setRandomScale(0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, true).ok());
REQUIRE(layer.setOrientation(1.f, 10.f, false).ok());
layer.setPlaceOnTop(true, false, 0.25f);
REQUIRE(layer.addChild("grass/tuft", 2, 0.5f, 0.8f, 1.2f, -30.f, 30.f).ok());
auto first = layer.build(source, &orientation);
auto again = layer.build(source, &orientation);
REQUIRE(first.ok());
REQUIRE(again.ok());
CHECK_EQ(first.value().getCount(), 96);
CHECK_EQ(again.value().getCount(), first.value().getCount());
std::set<std::string> selectedAssets;
for (int index = 0; index < first.value().getCount(); ++index) {
CHECK_EQ(first.value().getX(index), again.value().getX(index));
CHECK_EQ(first.value().getY(index), again.value().getY(index));
CHECK_EQ(first.value().getZ(index), again.value().getZ(index));
CHECK_EQ(first.value().getYaw(index), again.value().getYaw(index));
CHECK_EQ(first.value().getStringAttribute(index, "asset", ""),
again.value().getStringAttribute(index, "asset", ""));
const auto role = first.value().getStringAttribute(index, "object_role", "");
if (role == "parent") selectedAssets.insert(first.value().getStringAttribute(index, "asset", ""));
}
CHECK_EQ(selectedAssets.size(), 2);
CHECK_EQ(first.value().getStringAttribute(0, "object_role", ""), "parent");
CHECK_EQ(first.value().getYaw(0), 287.f); // North 270 + offset 10 + fixed random yaw 7.
CHECK_EQ(first.value().getPitch(0), 5.f);
CHECK_EQ(first.value().getRoll(0), 9.f);
CHECK_EQ(first.value().getScaleX(0), 1.f);
CHECK_EQ(first.value().getScaleY(0), 1.5f);
CHECK_EQ(first.value().getScaleZ(0), 2.f);
CHECK_EQ(first.value().getY(0), 12.25f); // surface 10 + top 0.25 + layer Y 2.
CHECK_EQ(first.value().getStringAttribute(1, "object_role", ""), "child");
CHECK_EQ(first.value().getIntAttribute(1, "parent_index", -2), 0);
}
TEST_CASE("procgen.objectBuild.rejectsInvalidConfigurationWithoutPartialOutput") {
PointSet source;
source.add(0.f, 0.f, 0.f);
ObjectBuildLayer layer;
CHECK(!layer.build(source).ok());
CHECK(!layer.addAsset("", 1.f).ok());
CHECK(!layer.addAsset("tree", 0.f).ok());
CHECK(!layer.setLayerScale(1.f, 0.f, 1.f).ok());
CHECK(!layer.setPositionRadius(-1.f).ok());
CHECK(!layer.setOrientation(0.f, 0.f, false).ok());
CHECK(!layer.addChild("grass", -1, 1.f, 1.f, 1.f, 0.f, 0.f).ok());
}
+65
View File
@@ -172,8 +172,73 @@ TEST_CASE("procgen.result.squirrelExecutionAndSchedulerShareProjection") {
assert(graph.validateResult().ok);
local result = graph.executeResult("source");
assert(result.ok && result.hasValue && result.value.getCount() == 1);
local objectLayer = eve.ProcgenObjectBuildLayer();
assert(objectLayer.addAsset("props/rock", 1.0).ok);
objectLayer.setSeed(99);
assert(objectLayer.setRandomScale(0.8, 1.2, 0.8, 1.2, 0.8, 1.2, true).ok);
assert(objectLayer.addChild("props/rubble", 2, 0.5, 0.2, 0.4, -180.0, 180.0).ok);
local builtObjects = objectLayer.build(result.value);
assert(builtObjects.ok && builtObjects.value.getCount() == 3);
assert(builtObjects.value.getStringAttribute(0, "asset", "") == "props/rock");
assert(builtObjects.value.getStringAttribute(1, "object_role", "") == "child");
assert(builtObjects.value.getIntAttribute(1, "parent_index", -2) == 0);
local buildStack = eve.ProcgenBuildLayerStack();
assert(buildStack.addTileLayer("tiles", true, 1.0, 0.25, "script").ok);
assert(buildStack.addObjectLayer("objects", true, objectLayer).ok);
local stackDefinition = buildStack.serializeDefinition();
local restoredStack = eve.ProcgenBuildLayerStack();
assert(restoredStack.deserializeDefinition(stackDefinition).ok);
local stackGrid = procgen.newGrid(1, 1).value;
assert(stackGrid.setCell(0, 0, 2).ok);
local stackResult = restoredStack.execute(stackGrid, result.value);
assert(stackResult.ok && stackResult.value.getCount() == 2);
assert(stackResult.value.getType(0) == "mesh" && stackResult.value.getMesh(0).ok);
assert(stackResult.value.getType(1) == "points" && stackResult.value.getPoints(1).ok);
local incremental = eve.ProcgenIncrementalBuildExecutor();
local firstDelta = incremental.update(restoredStack, stackGrid, result.value, 1, 1.0);
assert(firstDelta.ok && firstDelta.value.getCount() == 1);
assert(firstDelta.value.getClusterX(0) == 0 && !firstDelta.value.isRemoved(0));
assert(firstDelta.value.getArtifacts(0).ok);
local stableDelta = incremental.update(restoredStack, stackGrid, result.value, 1, 1.0);
assert(stableDelta.ok && stableDelta.value.getCount() == 0);
assert(incremental.getCachedClusterCount() == 1);
assert(incremental.getCachedArtifacts(0, 0).ok);
local missing = graph.executeResult("missing");
assert(!missing.ok && missing.value == null && missing.diagnostics[0].path == "missing");
local gridGraph = procgen.newGridGraph().value;
assert(gridGraph.addNode("noise", "generate.random_noise").ok);
assert(gridGraph.setNodeInt("noise", "width", 4).ok);
assert(gridGraph.setNodeInt("noise", "height", 4).ok);
assert(gridGraph.setNodeInt("noise", "seed", 7).ok);
assert(gridGraph.setNodeFloat("noise", "x", 1.0).ok);
local generatedGrid = gridGraph.execute("noise");
assert(generatedGrid.ok && generatedGrid.value.getWidth() == 4);
local ownedGrid = procgen.newGrid(3, 2);
assert(ownedGrid.ok && ownedGrid.value.fill(0).ok);
assert(ownedGrid.value.setCell(1, 1, 1).ok);
local ownedInputGraph = procgen.newGridGraph().value;
assert(ownedInputGraph.addNode("input", "grid.input").ok);
assert(ownedInputGraph.setNodeGrid("input", ownedGrid.value).ok);
local ownedGridCopy = ownedInputGraph.execute("input");
assert(ownedGridCopy.ok && ownedGridCopy.value.getCell(1, 1) == 1);
local registryGraph = procgen.newGridGraph().value;
assert(registryGraph.addNode("level", "generate.registry").ok);
assert(registryGraph.setNodeString("level", "algorithm", "level.roguelike").ok);
assert(registryGraph.setNodeInt("level", "width", 25).ok);
assert(registryGraph.setNodeInt("level", "height", 19).ok);
assert(registryGraph.setNodeInt("level", "seed", 435).ok);
assert(registryGraph.addNode("floors", "select.semantic").ok);
assert(registryGraph.setNodeInt("floors", "semantic", 2).ok);
assert(registryGraph.connect("level", "floors", 0).ok);
local semanticFloors = registryGraph.execute("floors");
assert(semanticFloors.ok && semanticFloors.value.getWidth() == 25);
local meshGraph = procgen.newMeshGraph().value;
assert(meshGraph.addNode("grid", "grid.input").ok);
assert(meshGraph.addNode("tiles", "mesh.grid_tiles").ok);
assert(meshGraph.setNodeGrid("grid", generatedGrid.value).ok);
assert(meshGraph.connect("grid", "tiles", 0).ok);
local generatedMesh = meshGraph.execute("tiles");
assert(generatedMesh.ok && generatedMesh.value.getIndexCount() > 0);
local runtime = procgen.newRuntimeGeneration(19).value;
local empty = runtime.nextCleanupRequest();
assert(empty.ok && empty.value == null);