mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
ROADMAP.md
This commit is contained in:
@@ -302,6 +302,7 @@ set(WORKFLOW_SOURCES
|
||||
|
||||
set(FRAME_WORKFLOW_SOURCES
|
||||
src/services/impl/workflow_frame_begin_step.cpp
|
||||
src/services/impl/workflow_frame_bullet_physics_step.cpp
|
||||
src/services/impl/workflow_frame_physics_step.cpp
|
||||
src/services/impl/workflow_frame_scene_step.cpp
|
||||
src/services/impl/workflow_frame_render_step.cpp
|
||||
|
||||
@@ -2,44 +2,192 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "SDL3CPP Workflow v1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string"
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/legacyWorkflow"
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"plugin"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
{
|
||||
"$ref": "#/definitions/n8nWorkflow"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"legacyWorkflow": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string"
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/legacyStep"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"steps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"legacyStep": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"plugin"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugin": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugin": {
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"n8nWorkflow": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string"
|
||||
},
|
||||
"nodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/n8nNode"
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"$ref": "#/definitions/n8nConnections"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"nodes"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"n8nNode": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugin": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"typeVersion": {
|
||||
"type": "integer"
|
||||
},
|
||||
"position": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"plugin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"n8nConnections": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"main": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/n8nConnectionEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"n8nConnectionEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"steps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "boot.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_config",
|
||||
"plugin": "config.load",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"path": "config.path"
|
||||
},
|
||||
@@ -14,6 +15,7 @@
|
||||
{
|
||||
"id": "validate_version",
|
||||
"plugin": "config.version.validate",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path"
|
||||
@@ -25,6 +27,7 @@
|
||||
{
|
||||
"id": "migrate_version",
|
||||
"plugin": "config.migrate",
|
||||
"position": [520, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path",
|
||||
@@ -38,6 +41,7 @@
|
||||
{
|
||||
"id": "validate_schema",
|
||||
"plugin": "config.schema.validate",
|
||||
"position": [780, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path"
|
||||
@@ -46,6 +50,7 @@
|
||||
{
|
||||
"id": "build_runtime_config",
|
||||
"plugin": "runtime.config.build",
|
||||
"position": [1040, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path"
|
||||
@@ -54,5 +59,35 @@
|
||||
"runtime": "config.runtime"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"load_config": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_version", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"validate_version": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "migrate_version", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"migrate_version": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_schema", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"validate_schema": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "build_runtime_config", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "frame.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "begin_frame",
|
||||
"plugin": "frame.begin",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta",
|
||||
"elapsed": "frame.elapsed"
|
||||
@@ -12,6 +13,7 @@
|
||||
{
|
||||
"id": "step_physics",
|
||||
"plugin": "frame.physics",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -19,6 +21,7 @@
|
||||
{
|
||||
"id": "update_scene",
|
||||
"plugin": "frame.scene",
|
||||
"position": [520, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -26,17 +29,51 @@
|
||||
{
|
||||
"id": "render_frame",
|
||||
"plugin": "frame.render",
|
||||
"position": [780, 0],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "update_audio",
|
||||
"plugin": "frame.audio"
|
||||
"plugin": "frame.audio",
|
||||
"position": [1040, -120]
|
||||
},
|
||||
{
|
||||
"id": "dispatch_gui",
|
||||
"plugin": "frame.gui"
|
||||
"plugin": "frame.gui",
|
||||
"position": [1040, 120]
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"begin_frame": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "step_physics", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"step_physics": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "update_scene", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"update_scene": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "render_frame", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"render_frame": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "update_audio", "type": "main", "index": 0 },
|
||||
{ "node": "dispatch_gui", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
config/workflows/templates/n8n_skeleton.json
Normal file
34
config/workflows/templates/n8n_skeleton.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"template": "n8n.skeleton",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_config",
|
||||
"plugin": "config.load",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"path": "config.path"
|
||||
},
|
||||
"outputs": {
|
||||
"document": "config.document"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "validate_schema",
|
||||
"plugin": "config.schema.validate",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"load_config": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_schema", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
31
docs/N8N_WORKFLOWS.md
Normal file
31
docs/N8N_WORKFLOWS.md
Normal file
@@ -0,0 +1,31 @@
|
||||
N8N-Style Workflows
|
||||
|
||||
Overview
|
||||
- Workflows can be declared with `nodes` and `connections`, modeled after n8n.
|
||||
- Each node maps to a C++ workflow plugin (via `plugin` or `type`).
|
||||
- Connections define execution order. When connections are omitted, the node array order is used.
|
||||
|
||||
Node Shape
|
||||
- `id` or `name`: required. Used to identify the node in `connections`.
|
||||
- `plugin` or `type`: required. The plugin string must match a registered step.
|
||||
- `inputs` / `outputs`: optional string maps for workflow context keys.
|
||||
- `position`: optional `[x, y]` array for layout parity with n8n.
|
||||
|
||||
Connections Shape
|
||||
```json
|
||||
{
|
||||
"connections": {
|
||||
"load_config": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_schema", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Templates
|
||||
- `config/workflows/templates/boot_default.json` and `config/workflows/templates/frame_default.json` are n8n-style now.
|
||||
- `config/workflows/templates/n8n_skeleton.json` is a minimal starting point.
|
||||
@@ -4,8 +4,7 @@
|
||||
"description": "Validation tour package with teleport checkpoints, captures, and diagnostics.",
|
||||
"defaultWorkflow": "workflows/validation_tour.json",
|
||||
"workflows": [
|
||||
"workflows/validation_tour.json",
|
||||
"workflows/stubs/validation_probe_placeholder.json"
|
||||
"workflows/validation_tour.json"
|
||||
],
|
||||
"assets": [
|
||||
"assets/validation_checks.json"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "validation.tour.checkpoint",
|
||||
"summary": "Validation probe step stub used to reserve workflow hooks before full diagnostics land.",
|
||||
"stage": "planned",
|
||||
"notes": [
|
||||
"Should sample engine framebuffers, analyze capture stats, and report mismatches.",
|
||||
"Currently registered as part of packages so workflows referencing checkpoints keep parsing."
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "boot.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_config",
|
||||
"plugin": "config.load",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"path": "config.path"
|
||||
},
|
||||
@@ -14,6 +15,7 @@
|
||||
{
|
||||
"id": "validate_schema",
|
||||
"plugin": "config.schema.validate",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path"
|
||||
@@ -22,6 +24,7 @@
|
||||
{
|
||||
"id": "build_runtime",
|
||||
"plugin": "runtime.config.build",
|
||||
"position": [520, 0],
|
||||
"inputs": {
|
||||
"document": "config.document",
|
||||
"path": "config.path"
|
||||
@@ -33,9 +36,33 @@
|
||||
{
|
||||
"id": "validation_probe",
|
||||
"plugin": "validation.tour.checkpoint",
|
||||
"position": [780, 0],
|
||||
"inputs": {
|
||||
"checkpoint": "packages.engine_tester"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"load_config": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_schema", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"validate_schema": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "build_runtime", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"build_runtime": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validation_probe", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"description": "Workflow package describing the GUI demo, focused on UI updates + frame capture validation.",
|
||||
"defaultWorkflow": "workflows/gui_frame.json",
|
||||
"workflows": [
|
||||
"workflows/gui_frame.json",
|
||||
"workflows/stubs/gui_layout_placeholder.json"
|
||||
"workflows/gui_frame.json"
|
||||
],
|
||||
"assets": [
|
||||
"assets/gui_widgets.json",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "frame.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gui_begin",
|
||||
"plugin": "frame.begin",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta",
|
||||
"elapsed": "frame.elapsed"
|
||||
@@ -12,6 +13,7 @@
|
||||
{
|
||||
"id": "gui_layout",
|
||||
"plugin": "frame.gui",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
}
|
||||
@@ -19,6 +21,7 @@
|
||||
{
|
||||
"id": "render_ui",
|
||||
"plugin": "frame.render",
|
||||
"position": [520, 0],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
}
|
||||
@@ -26,9 +29,33 @@
|
||||
{
|
||||
"id": "capture_ui",
|
||||
"plugin": "validation.tour.checkpoint",
|
||||
"position": [780, 0],
|
||||
"inputs": {
|
||||
"checkpoint": "packages.gui_demo"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"gui_begin": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "gui_layout", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"gui_layout": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "render_ui", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"render_ui": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "capture_ui", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "frame.gui",
|
||||
"summary": "Placeholder for GUI layout + input processing steps that are still being refactored.",
|
||||
"stage": "planned",
|
||||
"notes": [
|
||||
"Should resolve UI command bundles and update ImGui state.",
|
||||
"Currently registered so workflows can declare GUI dependencies without crashing."
|
||||
]
|
||||
}
|
||||
@@ -4,8 +4,7 @@
|
||||
"description": "Quake3-style example package bundling physics, scene, and map metadata.",
|
||||
"defaultWorkflow": "workflows/quake3_frame.json",
|
||||
"workflows": [
|
||||
"workflows/quake3_frame.json",
|
||||
"workflows/stubs/quake3_physics_placeholder.json"
|
||||
"workflows/quake3_frame.json"
|
||||
],
|
||||
"assets": [
|
||||
"assets/quake3_materials.json"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "frame.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "quake_begin",
|
||||
"plugin": "frame.begin",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -11,6 +12,7 @@
|
||||
{
|
||||
"id": "quake_physics",
|
||||
"plugin": "frame.bullet_physics",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -18,6 +20,7 @@
|
||||
{
|
||||
"id": "quake_scene",
|
||||
"plugin": "frame.scene",
|
||||
"position": [520, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -25,6 +28,7 @@
|
||||
{
|
||||
"id": "quake_render",
|
||||
"plugin": "frame.render",
|
||||
"position": [780, 0],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
}
|
||||
@@ -32,9 +36,40 @@
|
||||
{
|
||||
"id": "quake_validation",
|
||||
"plugin": "validation.tour.checkpoint",
|
||||
"position": [1040, 0],
|
||||
"inputs": {
|
||||
"checkpoint": "packages.quake3_map"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"quake_begin": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "quake_physics", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"quake_physics": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "quake_scene", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"quake_scene": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "quake_render", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"quake_render": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "quake_validation", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "frame.bullet_physics",
|
||||
"summary": "Quake3 physics stub to ensure workflows can declare bullet-dependent steps.",
|
||||
"stage": "planned",
|
||||
"notes": [
|
||||
"Should load BSP geometry, apply gravity, and resolve collisions.",
|
||||
"Widgets will be active once the physics service is refactored accordingly."
|
||||
]
|
||||
}
|
||||
@@ -4,8 +4,7 @@
|
||||
"description": "Template package describing a boot-to-frame workflow, assets, and validation presets for the demo cube.",
|
||||
"defaultWorkflow": "workflows/demo_gameplay.json",
|
||||
"workflows": [
|
||||
"workflows/demo_gameplay.json",
|
||||
"workflows/stubs/bullet_physics_placeholder.json"
|
||||
"workflows/demo_gameplay.json"
|
||||
],
|
||||
"assets": [
|
||||
"assets/cube_materials.json"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "frame.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "begin_frame",
|
||||
"plugin": "frame.begin",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta",
|
||||
"elapsed": "frame.elapsed"
|
||||
@@ -12,6 +13,7 @@
|
||||
{
|
||||
"id": "camera_control",
|
||||
"plugin": "frame.camera",
|
||||
"position": [260, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -19,6 +21,7 @@
|
||||
{
|
||||
"id": "bullet_physics",
|
||||
"plugin": "frame.bullet_physics",
|
||||
"position": [520, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -26,6 +29,7 @@
|
||||
{
|
||||
"id": "scene",
|
||||
"plugin": "frame.scene",
|
||||
"position": [780, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta"
|
||||
}
|
||||
@@ -33,6 +37,7 @@
|
||||
{
|
||||
"id": "render",
|
||||
"plugin": "frame.render",
|
||||
"position": [1040, 0],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
}
|
||||
@@ -40,9 +45,47 @@
|
||||
{
|
||||
"id": "validate_capture",
|
||||
"plugin": "validation.tour.checkpoint",
|
||||
"position": [1300, 0],
|
||||
"inputs": {
|
||||
"checkpoint": "gameplay.startup_camera"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"begin_frame": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "camera_control", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"camera_control": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "bullet_physics", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"bullet_physics": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "scene", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"scene": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "render", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"render": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_capture", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "frame.bullet_physics",
|
||||
"summary": "Stub for bullet physics integration; wiring exists but needs a concrete implementation before it becomes active.",
|
||||
"stage": "planned",
|
||||
"notes": [
|
||||
"Dependencies: physics service + rigid body registry",
|
||||
"Placeholder step ensures the workflow registry can parse missing plugins without crashing.",
|
||||
"Once implemented, this step should apply gravity + constraints before the scene update step."
|
||||
]
|
||||
}
|
||||
@@ -4,11 +4,7 @@
|
||||
"description": "Workflow template for the soundboard experience (audio cues + GUI controls).",
|
||||
"defaultWorkflow": "workflows/soundboard_flow.json",
|
||||
"workflows": [
|
||||
"workflows/soundboard_flow.json",
|
||||
"workflows/stubs/soundboard_catalog_scan_stub.json",
|
||||
"workflows/stubs/soundboard_gui_stub.json",
|
||||
"workflows/stubs/soundboard_audio_stub.json",
|
||||
"workflows/stubs/audio_visualizer_placeholder.json"
|
||||
"workflows/soundboard_flow.json"
|
||||
],
|
||||
"assets": [
|
||||
"assets/sound/sound_samples.json",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"template": "frame.default",
|
||||
"steps": [
|
||||
"nodes": [
|
||||
{
|
||||
"id": "begin_frame",
|
||||
"plugin": "frame.begin",
|
||||
"position": [0, 0],
|
||||
"inputs": {
|
||||
"delta": "frame.delta",
|
||||
"elapsed": "frame.elapsed"
|
||||
@@ -12,6 +13,7 @@
|
||||
{
|
||||
"id": "catalog_scan",
|
||||
"plugin": "soundboard.catalog.scan",
|
||||
"position": [260, -120],
|
||||
"outputs": {
|
||||
"catalog": "soundboard.catalog"
|
||||
}
|
||||
@@ -19,6 +21,7 @@
|
||||
{
|
||||
"id": "gui_render",
|
||||
"plugin": "soundboard.gui",
|
||||
"position": [520, -120],
|
||||
"inputs": {
|
||||
"catalog": "soundboard.catalog",
|
||||
"layout": "soundboard.layout"
|
||||
@@ -30,6 +33,7 @@
|
||||
{
|
||||
"id": "audio_dispatch",
|
||||
"plugin": "soundboard.audio",
|
||||
"position": [780, -120],
|
||||
"inputs": {
|
||||
"catalog": "soundboard.catalog",
|
||||
"selection": "soundboard.selection"
|
||||
@@ -41,6 +45,7 @@
|
||||
{
|
||||
"id": "render_frame",
|
||||
"plugin": "frame.render",
|
||||
"position": [520, 120],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
}
|
||||
@@ -48,9 +53,48 @@
|
||||
{
|
||||
"id": "validation_capture",
|
||||
"plugin": "validation.tour.checkpoint",
|
||||
"position": [780, 120],
|
||||
"inputs": {
|
||||
"checkpoint": "packages.soundboard"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"connections": {
|
||||
"begin_frame": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "catalog_scan", "type": "main", "index": 0 },
|
||||
{ "node": "render_frame", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"catalog_scan": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "gui_render", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"gui_render": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "audio_dispatch", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"audio_dispatch": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validation_capture", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"render_frame": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validation_capture", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "frame.audio",
|
||||
"summary": "Audio dispatch stub for the soundboard; actual cue scheduling + mixing service is pending.",
|
||||
"stage": "planned",
|
||||
"notes": [
|
||||
"Should queue audio commands and report current sample index to UI.",
|
||||
"Registered so workflows can mention the audio step without requiring implementation."
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"id": "soundboard.audio",
|
||||
"summary": "Plays clips requested by the GUI and reports playback status.",
|
||||
"stage": "planned",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"type": "soundboard.catalog",
|
||||
"description": "Catalog data to resolve clip paths."
|
||||
},
|
||||
{
|
||||
"name": "selection",
|
||||
"type": "soundboard.selection",
|
||||
"description": "Selected clip identifier emitted by the GUI step."
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "status",
|
||||
"type": "soundboard.status",
|
||||
"description": "Playback status message (playing/failed/missing)."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"Should interact with the runtime audio command pipeline (`audio_play_sound` equivalent) to queue clips.",
|
||||
"Status outputs let the GUI display feedback (success, failure, no clip available)."
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"id": "soundboard.catalog.scan",
|
||||
"summary": "Scans `assets/audio_catalog.json` plus audio folders to publish clip metadata.",
|
||||
"stage": "planned",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "catalog_descriptor",
|
||||
"type": "json_path",
|
||||
"description": "Path to `assets/audio_catalog.json` describing categories + relative folders."
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"type": "soundboard.catalog",
|
||||
"description": "Structured catalog with categories, clip names, absolute/audio-relative paths."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"This step should read the JSON descriptor, scan the referenced folders (`assets/audio/sfx`, `assets/audio/tts`), and sort clip names alphabetically per category.",
|
||||
"The output catalog should include the category ID, display name, and clip list (filename + label + path)."
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"id": "soundboard.gui",
|
||||
"summary": "Renders buttons for every clip in the catalog and emits selection events.",
|
||||
"stage": "planned",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"type": "soundboard.catalog",
|
||||
"description": "Catalog produced by `soundboard.catalog.scan` (categories + clip names/paths)."
|
||||
},
|
||||
{
|
||||
"name": "layout",
|
||||
"type": "json_path",
|
||||
"description": "GUI layout definition from `assets/soundboard_gui.json`."
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "selection",
|
||||
"type": "soundboard.selection",
|
||||
"description": "Clip selection event including category, clip id, and display label."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"Should render a panel with column tabs per category, draw buttons for each clip, and feed button presses into the audio dispatcher.",
|
||||
"Should also publish status messages (e.g., playback started/failed) for preview."
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "workflow_frame_audio_step.hpp"
|
||||
#include "workflow_frame_begin_step.hpp"
|
||||
#include "workflow_frame_bullet_physics_step.hpp"
|
||||
#include "workflow_frame_gui_step.hpp"
|
||||
#include "workflow_frame_physics_step.hpp"
|
||||
#include "workflow_frame_render_step.hpp"
|
||||
@@ -43,6 +44,9 @@ void FrameWorkflowStepRegistrar::RegisterUsedSteps(
|
||||
if (plugins.contains("frame.physics")) {
|
||||
registry->RegisterStep(std::make_shared<WorkflowFramePhysicsStep>(physicsService_, logger_));
|
||||
}
|
||||
if (plugins.contains("frame.bullet_physics")) {
|
||||
registry->RegisterStep(std::make_shared<WorkflowFrameBulletPhysicsStep>(physicsService_, logger_));
|
||||
}
|
||||
if (plugins.contains("frame.scene")) {
|
||||
registry->RegisterStep(std::make_shared<WorkflowFrameSceneStep>(sceneService_, logger_));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <rapidjson/document.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
@@ -36,34 +37,197 @@ std::unordered_map<std::string, std::string> ReadStringMap(const rapidjson::Valu
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ReadNodeId(const rapidjson::Value& node, size_t index) {
|
||||
if (node.HasMember("id") && node["id"].IsString()) {
|
||||
return node["id"].GetString();
|
||||
}
|
||||
if (node.HasMember("name") && node["name"].IsString()) {
|
||||
return node["name"].GetString();
|
||||
}
|
||||
throw std::runtime_error("Workflow node[" + std::to_string(index) + "] requires string id or name");
|
||||
}
|
||||
|
||||
std::string ReadNodePlugin(const rapidjson::Value& node, const std::string& nodeId) {
|
||||
if (node.HasMember("plugin") && node["plugin"].IsString()) {
|
||||
return node["plugin"].GetString();
|
||||
}
|
||||
if (node.HasMember("type") && node["type"].IsString()) {
|
||||
return node["type"].GetString();
|
||||
}
|
||||
throw std::runtime_error("Workflow node '" + nodeId + "' requires string plugin or type");
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> ReadConnections(const rapidjson::Value& document) {
|
||||
if (!document.HasMember("connections")) {
|
||||
return {};
|
||||
}
|
||||
const auto& connectionsValue = document["connections"];
|
||||
if (!connectionsValue.IsObject()) {
|
||||
throw std::runtime_error("Workflow member 'connections' must be an object");
|
||||
}
|
||||
std::vector<std::pair<std::string, std::string>> edges;
|
||||
for (auto it = connectionsValue.MemberBegin(); it != connectionsValue.MemberEnd(); ++it) {
|
||||
const std::string fromNode = it->name.GetString();
|
||||
if (!it->value.IsObject()) {
|
||||
throw std::runtime_error("Workflow connections for '" + fromNode + "' must be an object");
|
||||
}
|
||||
if (!it->value.HasMember("main")) {
|
||||
continue;
|
||||
}
|
||||
const auto& mainValue = it->value["main"];
|
||||
if (!mainValue.IsArray()) {
|
||||
throw std::runtime_error("Workflow connections.main for '" + fromNode + "' must be an array");
|
||||
}
|
||||
for (const auto& branch : mainValue.GetArray()) {
|
||||
if (!branch.IsArray()) {
|
||||
throw std::runtime_error("Workflow connections.main entries for '" + fromNode + "' must be arrays");
|
||||
}
|
||||
for (const auto& connection : branch.GetArray()) {
|
||||
if (!connection.IsObject() || !connection.HasMember("node") || !connection["node"].IsString()) {
|
||||
throw std::runtime_error("Workflow connection entries for '" + fromNode + "' require a node string");
|
||||
}
|
||||
edges.emplace_back(fromNode, connection["node"].GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
std::vector<std::string> SortNodesByConnections(
|
||||
const std::vector<std::string>& nodeOrder,
|
||||
const std::vector<std::pair<std::string, std::string>>& edges) {
|
||||
std::unordered_map<std::string, size_t> indexById;
|
||||
std::unordered_map<std::string, size_t> indegree;
|
||||
std::unordered_map<std::string, std::vector<std::string>> adjacency;
|
||||
indexById.reserve(nodeOrder.size());
|
||||
indegree.reserve(nodeOrder.size());
|
||||
adjacency.reserve(nodeOrder.size());
|
||||
for (size_t i = 0; i < nodeOrder.size(); ++i) {
|
||||
indexById.emplace(nodeOrder[i], i);
|
||||
indegree.emplace(nodeOrder[i], 0);
|
||||
adjacency.emplace(nodeOrder[i], std::vector<std::string>{});
|
||||
}
|
||||
|
||||
for (const auto& edge : edges) {
|
||||
const auto fromIt = indexById.find(edge.first);
|
||||
if (fromIt == indexById.end()) {
|
||||
throw std::runtime_error("Workflow connection references unknown node '" + edge.first + "'");
|
||||
}
|
||||
const auto toIt = indexById.find(edge.second);
|
||||
if (toIt == indexById.end()) {
|
||||
throw std::runtime_error("Workflow connection references unknown node '" + edge.second + "'");
|
||||
}
|
||||
adjacency[edge.first].push_back(edge.second);
|
||||
++indegree[edge.second];
|
||||
}
|
||||
|
||||
std::set<std::pair<size_t, std::string>> ready;
|
||||
for (const auto& nodeId : nodeOrder) {
|
||||
if (indegree[nodeId] == 0u) {
|
||||
ready.emplace(indexById[nodeId], nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ordered;
|
||||
ordered.reserve(nodeOrder.size());
|
||||
while (!ready.empty()) {
|
||||
auto it = ready.begin();
|
||||
const std::string nodeId = it->second;
|
||||
ready.erase(it);
|
||||
ordered.push_back(nodeId);
|
||||
for (const auto& next : adjacency[nodeId]) {
|
||||
auto indegreeIt = indegree.find(next);
|
||||
if (indegreeIt == indegree.end()) {
|
||||
continue;
|
||||
}
|
||||
if (--indegreeIt->second == 0u) {
|
||||
ready.emplace(indexById[next], next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ordered.size() != nodeOrder.size()) {
|
||||
throw std::runtime_error("Workflow connections contain a cycle");
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WorkflowDefinition WorkflowDefinitionParser::ParseFile(const std::filesystem::path& path) const {
|
||||
json_config::JsonConfigDocumentParser parser;
|
||||
rapidjson::Document document = parser.Parse(path, "workflow file");
|
||||
|
||||
if (!document.HasMember("steps") || !document["steps"].IsArray()) {
|
||||
throw std::runtime_error("Workflow must contain a 'steps' array");
|
||||
const bool hasSteps = document.HasMember("steps");
|
||||
const bool hasNodes = document.HasMember("nodes");
|
||||
if (hasSteps && hasNodes) {
|
||||
throw std::runtime_error("Workflow cannot define both 'steps' and 'nodes'");
|
||||
}
|
||||
if (!hasSteps && !hasNodes) {
|
||||
throw std::runtime_error("Workflow must contain a 'steps' array or 'nodes' array");
|
||||
}
|
||||
|
||||
WorkflowDefinition workflow;
|
||||
if (document.HasMember("template")) {
|
||||
if (!document["template"].IsString()) {
|
||||
throw std::runtime_error("Workflow member 'template' must be a string");
|
||||
}
|
||||
workflow.templateName = document["template"].GetString();
|
||||
workflow.templateName = ReadRequiredString(document, "template");
|
||||
}
|
||||
|
||||
for (const auto& entry : document["steps"].GetArray()) {
|
||||
if (hasSteps) {
|
||||
if (!document["steps"].IsArray()) {
|
||||
throw std::runtime_error("Workflow must contain a 'steps' array");
|
||||
}
|
||||
for (const auto& entry : document["steps"].GetArray()) {
|
||||
if (!entry.IsObject()) {
|
||||
throw std::runtime_error("Workflow steps must be objects");
|
||||
}
|
||||
WorkflowStepDefinition step;
|
||||
step.id = ReadRequiredString(entry, "id");
|
||||
step.plugin = ReadRequiredString(entry, "plugin");
|
||||
step.inputs = ReadStringMap(entry, "inputs");
|
||||
step.outputs = ReadStringMap(entry, "outputs");
|
||||
workflow.steps.push_back(std::move(step));
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
|
||||
if (!document["nodes"].IsArray()) {
|
||||
throw std::runtime_error("Workflow must contain a 'nodes' array");
|
||||
}
|
||||
|
||||
std::vector<WorkflowStepDefinition> nodes;
|
||||
std::vector<std::string> nodeOrder;
|
||||
for (rapidjson::SizeType i = 0; i < document["nodes"].Size(); ++i) {
|
||||
const auto& entry = document["nodes"][i];
|
||||
if (!entry.IsObject()) {
|
||||
throw std::runtime_error("Workflow steps must be objects");
|
||||
throw std::runtime_error("Workflow nodes must be objects");
|
||||
}
|
||||
WorkflowStepDefinition step;
|
||||
step.id = ReadRequiredString(entry, "id");
|
||||
step.plugin = ReadRequiredString(entry, "plugin");
|
||||
step.id = ReadNodeId(entry, i);
|
||||
step.plugin = ReadNodePlugin(entry, step.id);
|
||||
step.inputs = ReadStringMap(entry, "inputs");
|
||||
step.outputs = ReadStringMap(entry, "outputs");
|
||||
workflow.steps.push_back(std::move(step));
|
||||
nodes.push_back(step);
|
||||
nodeOrder.push_back(step.id);
|
||||
}
|
||||
|
||||
const auto edges = ReadConnections(document);
|
||||
std::vector<std::string> orderedIds = edges.empty()
|
||||
? nodeOrder
|
||||
: SortNodesByConnections(nodeOrder, edges);
|
||||
|
||||
std::unordered_map<std::string, WorkflowStepDefinition> nodeMap;
|
||||
nodeMap.reserve(nodes.size());
|
||||
for (const auto& node : nodes) {
|
||||
nodeMap.emplace(node.id, node);
|
||||
}
|
||||
workflow.steps.reserve(nodes.size());
|
||||
for (const auto& nodeId : orderedIds) {
|
||||
auto it = nodeMap.find(nodeId);
|
||||
if (it == nodeMap.end()) {
|
||||
throw std::runtime_error("Workflow nodes missing entry for '" + nodeId + "'");
|
||||
}
|
||||
workflow.steps.push_back(it->second);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
|
||||
35
src/services/impl/workflow_frame_bullet_physics_step.cpp
Normal file
35
src/services/impl/workflow_frame_bullet_physics_step.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "workflow_frame_bullet_physics_step.hpp"
|
||||
#include "workflow_step_io_resolver.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
WorkflowFrameBulletPhysicsStep::WorkflowFrameBulletPhysicsStep(std::shared_ptr<IPhysicsService> physicsService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: physicsService_(std::move(physicsService)),
|
||||
logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowFrameBulletPhysicsStep::GetPluginId() const {
|
||||
return "frame.bullet_physics";
|
||||
}
|
||||
|
||||
void WorkflowFrameBulletPhysicsStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (!physicsService_) {
|
||||
throw std::runtime_error("frame.bullet_physics requires an IPhysicsService");
|
||||
}
|
||||
WorkflowStepIoResolver resolver;
|
||||
const std::string deltaKey = resolver.GetRequiredInputKey(step, "delta");
|
||||
const auto* delta = context.TryGet<double>(deltaKey);
|
||||
if (!delta) {
|
||||
throw std::runtime_error("frame.bullet_physics missing delta input");
|
||||
}
|
||||
physicsService_->StepSimulation(static_cast<float>(*delta));
|
||||
if (logger_) {
|
||||
logger_->Trace("WorkflowFrameBulletPhysicsStep", "Execute",
|
||||
"delta=" + std::to_string(*delta),
|
||||
"Bullet physics step completed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
25
src/services/impl/workflow_frame_bullet_physics_step.hpp
Normal file
25
src/services/impl/workflow_frame_bullet_physics_step.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_physics_service.hpp"
|
||||
#include "../interfaces/i_workflow_step.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class WorkflowFrameBulletPhysicsStep final : public IWorkflowStep {
|
||||
public:
|
||||
WorkflowFrameBulletPhysicsStep(std::shared_ptr<IPhysicsService> physicsService,
|
||||
std::shared_ptr<ILogger> logger);
|
||||
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IPhysicsService> physicsService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -80,3 +80,71 @@ TEST(WorkflowDefinitionParserTest, FailsWhenPluginIsMissing) {
|
||||
EXPECT_NE(message.find("plugin"), std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WorkflowDefinitionParserTest, ParsesN8nNodesWithConnections) {
|
||||
const std::string json = R"json({
|
||||
"template": "boot.default",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_config",
|
||||
"plugin": "config.load",
|
||||
"inputs": { "path": "config.path" },
|
||||
"outputs": { "document": "config.document" }
|
||||
},
|
||||
{
|
||||
"name": "validate_schema",
|
||||
"type": "config.schema.validate",
|
||||
"inputs": { "document": "config.document", "path": "config.path" }
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"load_config": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "validate_schema", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
})json";
|
||||
|
||||
TempFile file(json);
|
||||
sdl3cpp::services::impl::WorkflowDefinitionParser parser;
|
||||
const auto workflow = parser.ParseFile(file.path);
|
||||
|
||||
ASSERT_EQ(workflow.steps.size(), 2u);
|
||||
EXPECT_EQ(workflow.steps[0].id, "load_config");
|
||||
EXPECT_EQ(workflow.steps[0].plugin, "config.load");
|
||||
EXPECT_EQ(workflow.steps[1].id, "validate_schema");
|
||||
EXPECT_EQ(workflow.steps[1].plugin, "config.schema.validate");
|
||||
}
|
||||
|
||||
TEST(WorkflowDefinitionParserTest, RejectsUnknownNodeInConnections) {
|
||||
const std::string json = R"json({
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"plugin": "config.load"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"start": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "missing", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
})json";
|
||||
|
||||
TempFile file(json);
|
||||
sdl3cpp::services::impl::WorkflowDefinitionParser parser;
|
||||
try {
|
||||
parser.ParseFile(file.path);
|
||||
FAIL() << "Expected workflow parser to reject unknown node connection";
|
||||
} catch (const std::runtime_error& error) {
|
||||
const std::string message = error.what();
|
||||
EXPECT_NE(message.find("unknown node"), std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user