diff --git a/ROADMAP.md b/ROADMAP.md index 227364b..f4aa2e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -466,3 +466,421 @@ Option B: per-shader only - Maintain the full testing pyramid: unit (schema, merge, IR), service (render graph, shader pipeline, budgets), integration (MaterialX shader generation, shader linking with both Vulkan/OpenGL), and end-to-end verification (boot the package, run the configured workflow, capture the frame). - In addition to the existing automation bullets, add multi-method screen validation (pixel diffs, non-black coverage, histogram/mean heuristics) because we occasionally do not know what the image should contain when new JSON/lua combos hit the runtime. This logic lives inside `engine_tester` and should be pluggable so we can add new heuristics later. - Static analysis and linters are part of the default sprint (`scripts/lint.sh` covering clang-tidy, cppcheck, compiler warning builds, and sanitizer builds). Build verification must also re-run on `build-ninja-asan` or similar sanitized configurations so we catch ordering/segfault issues early. + +n8n style schema: + +```json +{ +"$schema": "https://json-schema.org/draft/2020-12/schema", +"$id": "https://example.com/schemas/n8n-workflow.schema.json", +"title": "N8N-Style Workflow", +"type": "object", +"additionalProperties": false, +"required": ["name", "nodes", "connections"], +"properties": { +"id": { +"description": "Optional external identifier (DB id, UUID, etc.).", +"type": ["string", "integer"] +}, +"name": { +"type": "string", +"minLength": 1 +}, +"active": { +"type": "boolean", +"default": false +}, +"versionId": { +"description": "Optional version identifier for optimistic concurrency.", +"type": "string" +}, +"createdAt": { +"type": "string", +"format": "date-time" +}, +"updatedAt": { +"type": "string", +"format": "date-time" +}, +"tags": { +"type": "array", +"items": { "$ref": "#/$defs/tag" }, +"default": [] +}, +"meta": { +"description": "Arbitrary metadata. Keep stable keys for tooling.", +"type": "object", +"additionalProperties": true, +"default": {} +}, +"settings": { +"$ref": "#/$defs/workflowSettings" +}, +"pinData": { +"description": "Optional pinned execution data (useful for dev).", +"type": "object", +"additionalProperties": { +"type": "array", +"items": { +"type": "object", +"additionalProperties": true +} +} +}, +"nodes": { +"type": "array", +"minItems": 1, +"items": { "$ref": "#/$defs/node" } +}, +"connections": { +"$ref": "#/$defs/connections" +}, +"staticData": { +"description": "Reserved for engine-managed workflow state.", +"type": "object", +"additionalProperties": true, +"default": {} +}, +"credentials": { +"description": "Optional top-level credential bindings (engine-specific).", +"type": "array", +"items": { "$ref": "#/$defs/credentialBinding" }, +"default": [] +} +}, +"$defs": { +"tag": { +"type": "object", +"additionalProperties": false, +"required": ["name"], +"properties": { +"id": { "type": ["string", "integer"] }, +"name": { "type": "string", "minLength": 1 } +} +}, +"workflowSettings": { +"type": "object", +"additionalProperties": false, +"properties": { +"timezone": { +"description": "IANA timezone name, e.g. Europe/London.", +"type": "string" +}, +"executionTimeout": { +"description": "Hard timeout in seconds for a workflow execution.", +"type": "integer", +"minimum": 0 +}, +"saveExecutionProgress": { +"type": "boolean", +"default": true +}, +"saveManualExecutions": { +"type": "boolean", +"default": true +}, +"saveDataErrorExecution": { +"description": "Persist execution data on error.", +"type": "string", +"enum": ["all", "none"], +"default": "all" +}, +"saveDataSuccessExecution": { +"description": "Persist execution data on success.", +"type": "string", +"enum": ["all", "none"], +"default": "all" +}, +"saveDataManualExecution": { +"description": "Persist execution data for manual runs.", +"type": "string", +"enum": ["all", "none"], +"default": "all" +}, +"errorWorkflowId": { +"description": "Optional workflow id to call on error.", +"type": ["string", "integer"] +}, +"callerPolicy": { +"description": "Optional policy controlling which workflows can call this workflow.", +"type": "string" +} +}, +"default": {} +}, +"node": { +"type": "object", +"additionalProperties": false, +"required": ["id", "name", "type", "typeVersion", "position"], +"properties": { +"id": { +"description": "Stable unique id within the workflow. Prefer UUID.", +"type": "string", +"minLength": 1 +}, +"name": { +"description": "Human-friendly name; should be unique in workflow.", +"type": "string", +"minLength": 1 +}, +"type": { +"description": "Node type identifier, e.g. n8n-nodes-base.httpRequest.", +"type": "string", +"minLength": 1 +}, +"typeVersion": { +"description": "Node implementation version.", +"type": ["integer", "number"], +"minimum": 1 +}, +"disabled": { +"type": "boolean", +"default": false +}, +"notes": { +"type": "string", +"default": "" +}, +"notesInFlow": { +"description": "When true, notes are displayed on canvas.", +"type": "boolean", +"default": false +}, +"retryOnFail": { +"type": "boolean", +"default": false +}, +"maxTries": { +"type": "integer", +"minimum": 1 +}, +"waitBetweenTries": { +"description": "Milliseconds.", +"type": "integer", +"minimum": 0 +}, +"continueOnFail": { +"type": "boolean", +"default": false +}, +"alwaysOutputData": { +"type": "boolean", +"default": false +}, +"executeOnce": { +"description": "If true, node executes only once per execution (engine-dependent).", +"type": "boolean", +"default": false +}, +"position": { +"$ref": "#/$defs/position" +}, +"parameters": { +"description": "Node-specific parameters. Typically JSON-serializable.", +"type": "object", +"additionalProperties": true, +"default": {} +}, +"credentials": { +"description": "Node-level credential references.", +"type": "object", +"additionalProperties": { +"$ref": "#/$defs/credentialRef" +}, +"default": {} +}, +"webhookId": { +"description": "Optional webhook id (for webhook-based trigger nodes).", +"type": "string" +}, +"onError": { +"description": "Node-level error routing policy (engine-dependent).", +"type": "string", +"enum": ["stopWorkflow", "continueRegularOutput", "continueErrorOutput"] +} +} +}, +"position": { +"type": "array", +"minItems": 2, +"maxItems": 2, +"items": { +"type": "number" +} +}, +"credentialRef": { +"type": "object", +"additionalProperties": false, +"required": ["id"], +"properties": { +"id": { +"description": "Credential id or stable key.", +"type": ["string", "integer"] +}, +"name": { +"description": "Optional human label.", +"type": "string" +} +} +}, +"credentialBinding": { +"type": "object", +"additionalProperties": false, +"required": ["nodeId", "credentialType", "credentialId"], +"properties": { +"nodeId": { "type": "string", "minLength": 1 }, +"credentialType": { "type": "string", "minLength": 1 }, +"credentialId": { "type": ["string", "integer"] } +} +}, +"connections": { +"description": "Adjacency map: fromNodeName -> outputType -> outputIndex -> array of targets.", +"type": "object", +"additionalProperties": { +"$ref": "#/$defs/nodeConnectionsByType" +}, +"default": {} +}, +"nodeConnectionsByType": { +"type": "object", +"additionalProperties": false, +"properties": { +"main": { +"$ref": "#/$defs/outputIndexMap" +}, +"error": { +"$ref": "#/$defs/outputIndexMap" +} +}, +"anyOf": [ +{ "required": ["main"] }, +{ "required": ["error"] } +] +}, +"outputIndexMap": { +"description": "Output index -> array of connection targets.", +"type": "object", +"additionalProperties": { +"type": "array", +"items": { "$ref": "#/$defs/connectionTarget" } +}, +"default": {} +}, +"connectionTarget": { +"type": "object", +"additionalProperties": false, +"required": ["node", "type", "index"], +"properties": { +"node": { +"description": "Target node name (n8n uses node 'name' in connections).", +"type": "string", +"minLength": 1 +}, +"type": { +"description": "Input type on target node (typically 'main' or 'error').", +"type": "string", +"minLength": 1 +}, +"index": { +"description": "Input index on target node.", +"type": "integer", +"minimum": 0 +} +} +} +} +} +``` + + +```json +example data: +{ +"id": "wf-001", +"name": "Example HTTP → Transform → Log", +"active": false, +"createdAt": "2026-01-10T10:15:00Z", +"updatedAt": "2026-01-10T10:20:00Z", +"tags": [ +{ "id": 1, "name": "example" }, +{ "id": 2, "name": "demo" } +], +"settings": { +"timezone": "Europe/London", +"executionTimeout": 300, +"saveExecutionProgress": true, +"saveDataErrorExecution": "all", +"saveDataSuccessExecution": "all" +}, +"nodes": [ +{ +"id": "node-1", +"name": "HTTP Request", +"type": "n8n-nodes-base.httpRequest", +"typeVersion": 4, +"position": [0, 0], +"parameters": { +"method": "GET", +"url": "https://api.example.com/users", +"responseFormat": "json" +}, +"credentials": { +"httpBasicAuth": { +"id": "cred-123", +"name": "Example API Auth" +} +} +}, +{ +"id": "node-2", +"name": "Transform Data", +"type": "n8n-nodes-base.function", +"typeVersion": 1, +"position": [300, 0], +"parameters": { +"code": "return items.map(i => ({ json: { id: i.json.id, email: i.json.email } }));" +} +}, +{ +"id": "node-3", +"name": "Log Output", +"type": "n8n-nodes-base.noOp", +"typeVersion": 1, +"position": [600, 0], +"parameters": {} +} +], +"connections": { +"HTTP Request": { +"main": { +"0": [ +{ +"node": "Transform Data", +"type": "main", +"index": 0 +} +] +} +}, +"Transform Data": { +"main": { +"0": [ +{ +"node": "Log Output", +"type": "main", +"index": 0 +} +] +} +} +}, +"staticData": {}, +"credentials": [ +{ +"nodeId": "node-1", +"credentialType": "httpBasicAuth", +"credentialId": "cred-123" +} +] +} +``` diff --git a/python/package_lint.py b/python/package_lint.py index 7c9ff9d..79c74c9 100644 --- a/python/package_lint.py +++ b/python/package_lint.py @@ -22,6 +22,69 @@ FIELD_TO_FOLDER = { "shaders": "shaders", "workflows": "workflows", } +WORKFLOW_TOP_LEVEL_KEYS = {"template", "nodes", "steps", "connections"} +WORKFLOW_NODE_KEYS = {"id", "name", "plugin", "type", "position", "inputs", "outputs", "parameters"} +PACKAGE_ALLOWED_KEYS = { + "name", + "version", + "description", + "defaultWorkflow", + "workflows", + "assets", + "scene", + "shaders", + "dependencies", + "bundled", + "notes", +} + + +class WorkflowReferenceProfile: + def __init__(self, + required_top_keys: set[str], + allowed_top_keys: set[str], + require_nodes: bool, + require_template: bool, + require_connections: bool, + require_id: bool, + require_plugin: bool, + require_position: bool): + self.required_top_keys = required_top_keys + self.allowed_top_keys = allowed_top_keys + self.require_nodes = require_nodes + self.require_template = require_template + self.require_connections = require_connections + self.require_id = require_id + self.require_plugin = require_plugin + self.require_position = require_position + + +def build_workflow_profile(reference: dict) -> WorkflowReferenceProfile: + required_top_keys = set(reference.keys()) + allowed_top_keys = set(reference.keys()) + require_nodes = "nodes" in reference + require_template = "template" in reference + require_connections = "connections" in reference + require_id = True + require_plugin = True + require_position = False + if require_nodes: + nodes = reference.get("nodes") + if isinstance(nodes, list) and nodes: + require_position = all( + isinstance(node, dict) and "position" in node + for node in nodes + ) + return WorkflowReferenceProfile( + required_top_keys, + allowed_top_keys, + require_nodes, + require_template, + require_connections, + require_id, + require_plugin, + require_position, + ) logger = logging.getLogger("package_lint") @@ -76,26 +139,275 @@ def validate_workflow_schema(workflow_path: Path, validator) -> list[str]: return issues +def _is_non_empty_string(value: object) -> bool: + return isinstance(value, str) and value.strip() != "" + + +def _validate_string_map(value: object, context: str) -> list[str]: + if not isinstance(value, dict): + return [f"{context} must be an object"] + issues: list[str] = [] + for key, item in value.items(): + if not _is_non_empty_string(key): + issues.append(f"{context} keys must be non-empty strings") + continue + if not _is_non_empty_string(item): + issues.append(f"{context}.{key} must map to a non-empty string") + return issues + + +def _validate_parameter_value(value: object, context: str) -> list[str]: + if isinstance(value, (str, bool, int, float)): + if isinstance(value, str) and value.strip() == "": + return [f"{context} must be a non-empty string"] + return [] + if isinstance(value, list): + if not value: + return [] + has_strings = any(isinstance(item, str) for item in value) + has_numbers = any(isinstance(item, (int, float)) for item in value) + has_other = any(not isinstance(item, (str, int, float)) for item in value) + if has_other: + return [f"{context} must contain only strings or numbers"] + if has_strings and has_numbers: + return [f"{context} cannot mix strings and numbers"] + if has_strings and any(item.strip() == "" for item in value if isinstance(item, str)): + return [f"{context} cannot contain empty strings"] + return [] + return [f"{context} must be a string, number, bool, or array"] + + +def _validate_parameters(value: object) -> list[str]: + if not isinstance(value, dict): + return ["parameters must be an object"] + issues: list[str] = [] + for key, item in value.items(): + if not _is_non_empty_string(key): + issues.append("parameters keys must be non-empty strings") + continue + issues.extend(_validate_parameter_value(item, f"parameters.{key}")) + return issues + + +def _validate_node_entry(node: dict, + index: int, + reference_profile: Optional[WorkflowReferenceProfile]) -> tuple[str, list[str]]: + issues: list[str] = [] + if not isinstance(node, dict): + return "", [f"nodes[{index}] must be an object"] + extra_keys = set(node.keys()) - WORKFLOW_NODE_KEYS + if extra_keys: + issues.append( + f"nodes[{index}] has unsupported keys: {sorted(extra_keys)}" + ) + node_id = node.get("id") if reference_profile and reference_profile.require_id else node.get("id") or node.get("name") + if not _is_non_empty_string(node_id): + issues.append(f"nodes[{index}] requires non-empty id") + plugin = node.get("plugin") if reference_profile and reference_profile.require_plugin else node.get("plugin") or node.get("type") + if not _is_non_empty_string(plugin): + issues.append(f"nodes[{index}] requires non-empty plugin") + if "inputs" in node: + issues.extend(_validate_string_map(node["inputs"], f"nodes[{index}].inputs")) + if "outputs" in node: + issues.extend(_validate_string_map(node["outputs"], f"nodes[{index}].outputs")) + if "parameters" in node: + issues.extend(_validate_parameters(node["parameters"])) + if reference_profile and reference_profile.require_position and "position" not in node: + issues.append(f"nodes[{index}] requires position") + if "position" in node: + position = node["position"] + if (not isinstance(position, list) or len(position) != 2 or + not all(isinstance(item, (int, float)) for item in position)): + issues.append(f"nodes[{index}].position must be [x, y] numbers") + return (node_id if isinstance(node_id, str) else ""), issues + + +def _validate_connections(connections: object, node_ids: set[str]) -> list[str]: + if not isinstance(connections, dict): + return ["connections must be an object"] + issues: list[str] = [] + for from_node, link in connections.items(): + if not _is_non_empty_string(from_node): + issues.append("connections keys must be non-empty strings") + continue + if from_node not in node_ids: + issues.append(f"connections references unknown node '{from_node}'") + if not isinstance(link, dict): + issues.append(f"connections.{from_node} must be an object") + continue + extra_keys = set(link.keys()) - {"main"} + if extra_keys: + issues.append(f"connections.{from_node} has unsupported keys: {sorted(extra_keys)}") + if "main" not in link: + continue + main_value = link["main"] + if not isinstance(main_value, list): + issues.append(f"connections.{from_node}.main must be an array") + continue + for branch_index, branch in enumerate(main_value): + if not isinstance(branch, list): + issues.append(f"connections.{from_node}.main[{branch_index}] must be an array") + continue + for entry_index, entry in enumerate(branch): + if not isinstance(entry, dict): + issues.append( + f"connections.{from_node}.main[{branch_index}][{entry_index}] must be an object" + ) + continue + node_name = entry.get("node") + if not _is_non_empty_string(node_name): + issues.append( + f"connections.{from_node}.main[{branch_index}][{entry_index}] missing node" + ) + continue + if node_name not in node_ids: + issues.append( + f"connections.{from_node}.main[{branch_index}][{entry_index}] " + f"references unknown node '{node_name}'" + ) + if "type" in entry and not _is_non_empty_string(entry["type"]): + issues.append( + f"connections.{from_node}.main[{branch_index}][{entry_index}].type " + "must be a non-empty string" + ) + if "index" in entry and not isinstance(entry["index"], int): + issues.append( + f"connections.{from_node}.main[{branch_index}][{entry_index}].index " + "must be an integer" + ) + return issues + + +def validate_workflow_structure(workflow_path: Path, + content: dict, + reference_profile: Optional[WorkflowReferenceProfile]) -> list[str]: + issues: list[str] = [] + logger.debug("Validating workflow structure: %s", workflow_path) + allowed_top_keys = WORKFLOW_TOP_LEVEL_KEYS + required_top_keys = set() + if reference_profile: + allowed_top_keys = reference_profile.allowed_top_keys + required_top_keys = reference_profile.required_top_keys + extra_keys = set(content.keys()) - allowed_top_keys + if extra_keys: + issues.append(f"unsupported workflow keys: {sorted(extra_keys)}") + missing_keys = required_top_keys - set(content.keys()) + if missing_keys: + issues.append(f"workflow missing required keys: {sorted(missing_keys)}") + has_nodes = "nodes" in content + has_steps = "steps" in content + if has_nodes and has_steps: + issues.append("workflow cannot define both 'nodes' and 'steps'") + if reference_profile and reference_profile.require_nodes and has_steps: + issues.append("workflow must not define 'steps' when using reference schema") + if not has_nodes and not has_steps: + issues.append("workflow must define 'nodes' or 'steps'") + return issues + if reference_profile and reference_profile.require_template and "template" not in content: + issues.append("workflow missing required template") + if "template" in content and not _is_non_empty_string(content["template"]): + issues.append("workflow template must be a non-empty string") + if reference_profile and reference_profile.require_connections and "connections" not in content: + issues.append("workflow missing required connections") + node_ids: list[str] = [] + if has_nodes: + nodes = content.get("nodes") + if not isinstance(nodes, list) or not nodes: + issues.append("workflow nodes must be a non-empty array") + else: + seen = set() + for index, node in enumerate(nodes): + node_id, node_issues = _validate_node_entry(node, index, reference_profile) + issues.extend(node_issues) + if node_id: + if node_id in seen: + issues.append(f"duplicate node id '{node_id}'") + else: + seen.add(node_id) + node_ids.append(node_id) + if has_steps: + steps = content.get("steps") + if not isinstance(steps, list) or not steps: + issues.append("workflow steps must be a non-empty array") + else: + seen = set() + for index, step in enumerate(steps): + node_id, node_issues = _validate_node_entry(step, index, reference_profile) + issues.extend(node_issues) + if node_id: + if node_id in seen: + issues.append(f"duplicate step id '{node_id}'") + else: + seen.add(node_id) + node_ids.append(node_id) + if "connections" in content: + issues.extend(_validate_connections(content["connections"], set(node_ids))) + return issues + + +def validate_workflow(workflow_path: Path, + validator: Optional["Draft7Validator"], + reference_profile: Optional[WorkflowReferenceProfile]) -> list[str]: + try: + content = load_json(workflow_path) + except json.JSONDecodeError as exc: + return [f"invalid JSON: {exc}"] + issues: list[str] = [] + if validator: + for err in sorted( + validator.iter_errors(content), + key=lambda x: tuple(x.absolute_path), + ): + pointer = "/".join(str(part) for part in err.absolute_path) or "" + issues.append(f"schema violation at {pointer}: {err.message}") + issues.extend(validate_workflow_structure(workflow_path, content, reference_profile)) + return issues + + def validate_package( pkg_root: Path, pkg_data: dict, registry_names: Sequence[str], available_dirs: Sequence[str], workflow_schema_validator: Optional["Draft7Validator"] = None, + workflow_reference_profile: Optional[WorkflowReferenceProfile] = None, ) -> tuple[list[str], list[str]]: errors: list[str] = [] warnings: list[str] = [] logger.debug("Validating %s", pkg_root) + extra_package_keys = set(pkg_data.keys()) - PACKAGE_ALLOWED_KEYS + if extra_package_keys: + warnings.append(f"unknown package keys: {sorted(extra_package_keys)}") for field in REQUIRED_FIELDS: if field not in pkg_data: errors.append(f"missing required field `{field}`") workflows = pkg_data.get("workflows") default_workflow = pkg_data.get("defaultWorkflow") - if workflows and isinstance(workflows, list): - if default_workflow and default_workflow not in workflows: + if workflows is not None: + if not isinstance(workflows, list): + errors.append("`workflows` must be an array") + elif not workflows: + errors.append("`workflows` must include at least one entry") + elif default_workflow and default_workflow not in workflows: errors.append("`defaultWorkflow` is not present in `workflows` array") + if "name" in pkg_data and not _is_non_empty_string(pkg_data["name"]): + errors.append("`name` must be a non-empty string") + if "version" in pkg_data and not _is_non_empty_string(pkg_data["version"]): + errors.append("`version` must be a non-empty string") + if "description" in pkg_data and not _is_non_empty_string(pkg_data["description"]): + errors.append("`description` must be a non-empty string") + if default_workflow is not None and not _is_non_empty_string(default_workflow): + errors.append("`defaultWorkflow` must be a non-empty string") + if _is_non_empty_string(default_workflow): + candidate = pkg_root / default_workflow + if not candidate.exists(): + errors.append(f"`defaultWorkflow` does not exist: {default_workflow}") + if "bundled" in pkg_data and not isinstance(pkg_data["bundled"], bool): + errors.append("`bundled` must be a boolean") + if "notes" in pkg_data and not _is_non_empty_string(pkg_data["notes"]): + warnings.append("`notes` should be a non-empty string when present") # schema-like validations for key in ("workflows", "assets", "scene", "shaders"): value = pkg_data.get(key) @@ -104,26 +416,61 @@ def validate_package( if not isinstance(value, list): errors.append(f"`{key}` must be an array if present") continue + if not value and key == "workflows": + errors.append("`workflows` must include at least one entry") + for entry in value: + if not isinstance(entry, str): + errors.append(f"`{key}` entries must be strings") on_exist: Optional[Callable[[Path, str], None]] = None - if key == "workflows" and workflow_schema_validator: + if key == "workflows": def on_exist(candidate: Path, rel: str) -> None: - schema_issues = validate_workflow_schema(candidate, workflow_schema_validator) + schema_issues = validate_workflow(candidate, + workflow_schema_validator, + workflow_reference_profile) for issue in schema_issues: errors.append(f"workflow `{rel}`: {issue}") + def validate_entry(entry: str) -> None: + if ".." in Path(entry).parts: + errors.append(f"`{key}` entry '{entry}' must not contain '..'") + if entry.strip() == "": + errors.append(f"`{key}` entries must be non-empty strings") + if key == "workflows" and not entry.endswith(".json"): + errors.append(f"`workflows` entry '{entry}' must be a .json file") + if entry.endswith(".json"): + try: + load_json(pkg_root / entry) + except json.JSONDecodeError as exc: + errors.append(f"`{key}` entry '{entry}' invalid JSON: {exc}") + for entry in value: + if isinstance(entry, str): + validate_entry(entry) missing = check_paths(pkg_root, value, key, on_exist=on_exist) if missing: warnings.append(f"{key} entries not found: {missing}") + string_entries = [entry for entry in value if isinstance(entry, str)] + if len(set(string_entries)) != len(string_entries): + warnings.append(f"`{key}` entries contain duplicates") # dependencies validation deps = pkg_data.get("dependencies", []) - if deps and not isinstance(deps, list): + if deps is None: + deps = [] + if not isinstance(deps, list): errors.append("`dependencies` must be an array") else: known_names = set(registry_names) known_names.update(available_dirs) for dep in deps: + if not _is_non_empty_string(dep): + errors.append("`dependencies` entries must be non-empty strings") + continue + if dep == pkg_data.get("name"): + errors.append("`dependencies` cannot include the package itself") if dep not in known_names: warnings.append(f"dependency `{dep}` is not known in registry") + dep_strings = [dep for dep in deps if isinstance(dep, str)] + if len(set(dep_strings)) != len(dep_strings): + warnings.append("`dependencies` contains duplicates") # common folder existence for field, folder in FIELD_TO_FOLDER.items(): entries = pkg_data.get(field) or [] @@ -145,6 +492,12 @@ def main() -> int: type=Path, help="Optional workflow JSON schema (default: config/schema/workflow_v1.schema.json when available)", ) + parser.add_argument( + "--workflow-reference", + type=Path, + help="Reference n8n-style workflow JSON used to validate workflow structure " + "(default: packages/seed/workflows/demo_gameplay.json when available)", + ) parser.add_argument( "--verbose", action="store_true", @@ -185,6 +538,24 @@ def main() -> int: logger.error("failed to compile workflow schema %s: %s", schema_candidate, exc) return 7 + reference_path = args.workflow_reference + default_reference = Path("packages/seed/workflows/demo_gameplay.json") + if reference_path is None and default_reference.exists(): + reference_path = default_reference + + workflow_reference_profile: Optional[WorkflowReferenceProfile] = None + if reference_path: + if not reference_path.exists(): + logger.error("specified workflow reference %s not found", reference_path) + return 8 + try: + reference_workflow = load_json(reference_path) + except json.JSONDecodeError as exc: + logger.error("invalid workflow reference %s: %s", reference_path, exc) + return 9 + workflow_reference_profile = build_workflow_profile(reference_workflow) + logger.info("workflow reference loaded: %s", reference_path) + package_dirs = [ child for child in sorted(args.packages_root.iterdir()) @@ -223,6 +594,7 @@ def main() -> int: registry_names, available_dirs, workflow_validator, + workflow_reference_profile, ) for err in errors: logger.error("%s: %s", pkg_json_file, err) diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 933d656..2a477c3 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -18,12 +18,7 @@ #include "services/impl/scene/ecs_service.hpp" #include "services/impl/graphics/graphics_service.hpp" #include "services/impl/graphics/bgfx_graphics_backend.hpp" -#include "services/impl/script/script_engine_service.hpp" -#include "services/impl/script/scene_script_service.hpp" -#include "services/impl/script/shader_script_service.hpp" #include "services/impl/shader/shader_system_registry.hpp" -#include "services/impl/script/gui_script_service.hpp" -#include "services/impl/audio/audio_command_service.hpp" #include "services/impl/scene/physics_bridge_service.hpp" #include "services/impl/scene/mesh_service.hpp" #include "services/impl/scene/scene_service.hpp" @@ -67,7 +62,9 @@ ServiceBasedApp::ServiceBasedApp(services::RuntimeConfig runtimeConfig, services logger_->SetLevel(logLevel); } - logger_->Trace("ServiceBasedApp", "ServiceBasedApp", "scriptPath=" + runtimeConfig_.scriptPath.string(), "constructor starting"); + logger_->Trace("ServiceBasedApp", "ServiceBasedApp", + "projectRoot=" + runtimeConfig_.projectRoot.string(), + "constructor starting"); try { logger_->Info("ServiceBasedApp::ServiceBasedApp: Setting up SDL"); @@ -302,7 +299,7 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService(), registry_.GetService()); - // Audio service (needed before script bindings execute) + // Audio service registry_.RegisterService( registry_.GetService()); @@ -325,52 +322,16 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService(), registry_.GetService()); - // Script bridge services + // Physics bridge services registry_.RegisterService( registry_.GetService()); - registry_.RegisterService( - registry_.GetService(), - registry_.GetService(), - registry_.GetService()); - - // Script engine service (shared Lua runtime) - registry_.RegisterService( - runtimeConfig_.scriptPath, - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - runtimeConfig_.luaDebug); // Shader system registry (pluggable shader system selection) registry_.RegisterService( registry_.GetService(), registry_.GetService(), - registry_.GetService(), registry_.GetService()); - // Script-facing services - registry_.RegisterService( - registry_.GetService(), - registry_.GetService()); - registry_.RegisterService( - registry_.GetService(), - registry_.GetService(), - registry_.GetService()); - registry_.RegisterService( - registry_.GetService(), - registry_.GetService()); - - // Connect input service to GUI script service for GUI input processing - auto inputService = registry_.GetService(); - auto guiScriptService = registry_.GetService(); - if (inputService && guiScriptService) { - inputService->SetGuiScriptService(guiScriptService.get()); - } - auto graphicsBackend = std::make_shared( registry_.GetService(), registry_.GetService(), @@ -386,7 +347,6 @@ void ServiceBasedApp::RegisterServices() { // Scene service registry_.RegisterService( - registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService()); @@ -409,12 +369,9 @@ void ServiceBasedApp::RegisterServices() { // Render coordinator service registry_.RegisterService( registry_.GetService(), - registry_.GetService(), registry_.GetService(), registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), + registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService()); diff --git a/src/events/event_types.hpp b/src/events/event_types.hpp index cf739d2..e0e303f 100644 --- a/src/events/event_types.hpp +++ b/src/events/event_types.hpp @@ -196,14 +196,6 @@ struct AudioPlayEvent { bool background; // true for music, false for sound effects }; -/** - * @brief Script loaded event data. - */ -struct ScriptLoadedEvent { - std::string scriptPath; - bool debugMode; -}; - /** * @brief Collision detection event data. */ diff --git a/src/main.cpp b/src/main.cpp index 150b2f4..136999a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,8 +57,7 @@ int main(int argc, char** argv) { logger->Info("Application starting"); logger->TraceVariable("config.width", static_cast(options.runtimeConfig.width)); logger->TraceVariable("config.height", static_cast(options.runtimeConfig.height)); - logger->TraceVariable("config.scriptPath", options.runtimeConfig.scriptPath.string()); - logger->TraceVariable("config.luaDebug", options.runtimeConfig.luaDebug); + logger->TraceVariable("config.projectRoot", options.runtimeConfig.projectRoot.string()); logger->TraceVariable("config.windowTitle", options.runtimeConfig.windowTitle); } diff --git a/src/services/impl/audio/audio_command_service.cpp b/src/services/impl/audio/audio_command_service.cpp deleted file mode 100644 index 7e34dd8..0000000 --- a/src/services/impl/audio/audio_command_service.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "audio_command_service.hpp" -#include -#include -#include - -namespace sdl3cpp::services::impl { - -AudioCommandService::AudioCommandService(std::shared_ptr configService, - std::shared_ptr audioService, - std::shared_ptr logger) - : configService_(std::move(configService)), - audioService_(std::move(audioService)), - logger_(std::move(logger)) { - if (logger_) { - logger_->Trace("AudioCommandService", "AudioCommandService", - "configService=" + std::string(configService_ ? "set" : "null") + - ", audioService=" + std::string(audioService_ ? "set" : "null")); - } -} - -bool AudioCommandService::QueueAudioCommand(AudioCommandType type, - const std::string& path, - bool loop, - std::string& error) { - if (logger_) { - logger_->Trace("AudioCommandService", "QueueAudioCommand", - "type=" + std::to_string(static_cast(type)) + - ", path=" + path + - ", loop=" + std::string(loop ? "true" : "false")); - } - if (!audioService_) { - error = "Audio service not available"; - return false; - } - if (!configService_) { - error = "Config service not available"; - return false; - } - - std::filesystem::path resolved(path); - if (!resolved.is_absolute()) { - std::filesystem::path scriptDir = configService_->GetScriptPath().parent_path(); - resolved = scriptDir / resolved; - } - - std::error_code ec; - resolved = std::filesystem::weakly_canonical(resolved, ec); - if (ec) { - error = "Failed to resolve audio path: " + ec.message(); - return false; - } - if (!std::filesystem::exists(resolved)) { - error = "Audio file not found: " + resolved.string(); - return false; - } - - try { - if (type == AudioCommandType::Background) { - audioService_->PlayBackground(resolved, loop); - } else { - audioService_->PlayEffect(resolved, loop); - } - } catch (const std::exception& ex) { - error = ex.what(); - return false; - } - - if (logger_) { - logger_->Debug("Queued audio command: " + resolved.string()); - } - - return true; -} - -bool AudioCommandService::StopBackground(std::string& error) { - if (logger_) { - logger_->Trace("AudioCommandService", "StopBackground"); - } - if (!audioService_) { - error = "Audio service not available"; - return false; - } - try { - audioService_->StopBackground(); - } catch (const std::exception& ex) { - error = ex.what(); - return false; - } - return true; -} - -} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/audio/audio_command_service.hpp b/src/services/impl/audio/audio_command_service.hpp deleted file mode 100644 index 32b5d20..0000000 --- a/src/services/impl/audio/audio_command_service.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "services/interfaces/i_audio_command_service.hpp" -#include "services/interfaces/i_audio_service.hpp" -#include "services/interfaces/i_config_service.hpp" -#include "services/interfaces/i_logger.hpp" -#include - -namespace sdl3cpp::services::impl { - -/** - * @brief Script-facing audio command service implementation. - */ -class AudioCommandService : public IAudioCommandService { -public: - AudioCommandService(std::shared_ptr configService, - std::shared_ptr audioService, - std::shared_ptr logger); - - bool QueueAudioCommand(AudioCommandType type, - const std::string& path, - bool loop, - std::string& error) override; - bool StopBackground(std::string& error) override; - -private: - std::shared_ptr configService_; - std::shared_ptr audioService_; - std::shared_ptr logger_; -}; - -} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/config/json_config_service.cpp b/src/services/impl/config/json_config_service.cpp index 32080e1..af47bb3 100644 --- a/src/services/impl/config/json_config_service.cpp +++ b/src/services/impl/config/json_config_service.cpp @@ -12,19 +12,6 @@ namespace sdl3cpp::services::impl { -namespace { -const char* SceneSourceName(SceneSource source) { - switch (source) { - case SceneSource::Config: - return "config"; - case SceneSource::Lua: - return "lua"; - default: - return "config"; - } -} -} // namespace - JsonConfigService::JsonConfigService(std::shared_ptr logger, const char* argv0, std::shared_ptr probeService) @@ -36,7 +23,7 @@ JsonConfigService::JsonConfigService(std::shared_ptr logger, logger_->Trace("JsonConfigService", "JsonConfigService", "argv0=" + std::string(argv0 ? argv0 : "")); } - config_.scriptPath = FindScriptPath(argv0); + config_.projectRoot = ResolveProjectRoot(argv0); configJson_ = BuildConfigJson(config_, {}); logger_->Info("JsonConfigService initialized with default configuration"); } @@ -68,33 +55,31 @@ JsonConfigService::JsonConfigService(std::shared_ptr logger, logger_->Trace("JsonConfigService", "JsonConfigService", "config.width=" + std::to_string(config.width) + ", config.height=" + std::to_string(config.height) + - ", config.scriptPath=" + config.scriptPath.string() + - ", config.luaDebug=" + std::string(config.luaDebug ? "true" : "false") + + ", config.projectRoot=" + config.projectRoot.string() + ", config.windowTitle=" + config.windowTitle); } logger_->Info("JsonConfigService initialized with explicit configuration"); } -std::filesystem::path JsonConfigService::FindScriptPath(const char* argv0) { +std::filesystem::path JsonConfigService::ResolveProjectRoot(const char* argv0) { if (logger_) { - logger_->Trace("JsonConfigService", "FindScriptPath", + logger_->Trace("JsonConfigService", "ResolveProjectRoot", "argv0=" + std::string(argv0 ? argv0 : "")); } - std::filesystem::path executable; + std::filesystem::path basePath; if (argv0 && *argv0 != '\0') { - executable = std::filesystem::path(argv0); - if (executable.is_relative()) { - executable = std::filesystem::current_path() / executable; + basePath = std::filesystem::path(argv0); + if (basePath.is_relative()) { + basePath = std::filesystem::current_path() / basePath; } } else { - executable = std::filesystem::current_path(); + basePath = std::filesystem::current_path(); } - executable = std::filesystem::weakly_canonical(executable); - std::filesystem::path scriptPath = executable.parent_path() / "scripts" / "cube_logic.lua"; - if (!std::filesystem::exists(scriptPath)) { - throw std::runtime_error("Could not find Lua script at " + scriptPath.string()); + std::filesystem::path resolved = std::filesystem::weakly_canonical(basePath); + if (std::filesystem::is_regular_file(resolved)) { + resolved = resolved.parent_path(); } - return scriptPath; + return resolved; } RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, @@ -150,15 +135,6 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, document.AddMember("schema_version", json_config::kRuntimeConfigSchemaVersion, allocator); document.AddMember("configVersion", json_config::kRuntimeConfigSchemaVersion, allocator); - rapidjson::Value scriptsObject(rapidjson::kObjectType); - addStringMember(scriptsObject, "entry", config.scriptPath.string()); - scriptsObject.AddMember("lua_debug", config.luaDebug, allocator); - document.AddMember("scripts", scriptsObject, allocator); - - rapidjson::Value runtimeObject(rapidjson::kObjectType); - addStringMember(runtimeObject, "scene_source", SceneSourceName(config.sceneSource)); - document.AddMember("runtime", runtimeObject, allocator); - rapidjson::Value windowObject(rapidjson::kObjectType); addStringMember(windowObject, "title", config.windowTitle); rapidjson::Value sizeObject(rapidjson::kObjectType); @@ -166,7 +142,10 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, sizeObject.AddMember("height", config.height, allocator); windowObject.AddMember("size", sizeObject, allocator); - std::filesystem::path scriptsDir = config.scriptPath.parent_path(); + std::filesystem::path projectRoot = config.projectRoot; + if (projectRoot.empty()) { + projectRoot = configPath.empty() ? std::filesystem::current_path() : configPath.parent_path(); + } rapidjson::Value mouseGrabObject(rapidjson::kObjectType); mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator); @@ -287,8 +266,6 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, config.crashRecovery.gpuHangFrameTimeMultiplier, allocator); crashObject.AddMember("max_consecutive_gpu_timeouts", static_cast(config.crashRecovery.maxConsecutiveGpuTimeouts), allocator); - crashObject.AddMember("max_lua_failures", - static_cast(config.crashRecovery.maxLuaFailures), allocator); crashObject.AddMember("max_file_format_errors", static_cast(config.crashRecovery.maxFileFormatErrors), allocator); crashObject.AddMember("max_memory_warnings", @@ -444,11 +421,7 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, inputObject.AddMember("bindings", bindingsObject, allocator); document.AddMember("input", inputObject, allocator); - std::filesystem::path projectRoot = scriptsDir.parent_path(); rapidjson::Value pathsObject(rapidjson::kObjectType); - if (!scriptsDir.empty()) { - addStringMember(pathsObject, "scripts", scriptsDir.string()); - } if (!projectRoot.empty()) { addStringMember(pathsObject, "project_root", projectRoot.string()); addStringMember(pathsObject, "shaders", (projectRoot / "shaders").string()); diff --git a/src/services/impl/config/json_config_service.hpp b/src/services/impl/config/json_config_service.hpp index b16eeb3..5ddcc4d 100644 --- a/src/services/impl/config/json_config_service.hpp +++ b/src/services/impl/config/json_config_service.hpp @@ -23,7 +23,7 @@ public: * @brief Construct with default configuration. * * @param logger Logger service for logging - * @param argv0 First command-line argument (for finding default script path) + * @param argv0 First command-line argument (for resolving default project root) * @param probeService Probe service for diagnostics (optional) */ JsonConfigService(std::shared_ptr logger, @@ -68,17 +68,11 @@ public: } return config_.height; } - std::filesystem::path GetScriptPath() const override { + std::filesystem::path GetProjectRoot() const override { if (logger_) { - logger_->Trace("JsonConfigService", "GetScriptPath"); + logger_->Trace("JsonConfigService", "GetProjectRoot"); } - return config_.scriptPath; - } - bool IsLuaDebugEnabled() const override { - if (logger_) { - logger_->Trace("JsonConfigService", "IsLuaDebugEnabled"); - } - return config_.luaDebug; + return config_.projectRoot; } std::string GetWindowTitle() const override { if (logger_) { @@ -86,12 +80,6 @@ public: } return config_.windowTitle; } - SceneSource GetSceneSource() const override { - if (logger_) { - logger_->Trace("JsonConfigService", "GetSceneSource"); - } - return config_.sceneSource; - } const InputBindings& GetInputBindings() const override { if (logger_) { logger_->Trace("JsonConfigService", "GetInputBindings"); @@ -172,7 +160,7 @@ private: RuntimeConfig config_; // Helper methods moved from main.cpp - std::filesystem::path FindScriptPath(const char* argv0); + std::filesystem::path ResolveProjectRoot(const char* argv0); static RuntimeConfig LoadFromJson(std::shared_ptr logger, std::shared_ptr probeService, const std::filesystem::path& configPath, diff --git a/src/services/impl/config/json_config_writer_service.cpp b/src/services/impl/config/json_config_writer_service.cpp index 7f15534..34c6f53 100644 --- a/src/services/impl/config/json_config_writer_service.cpp +++ b/src/services/impl/config/json_config_writer_service.cpp @@ -25,8 +25,7 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std logger_->Trace("JsonConfigWriterService", "WriteConfig", "config.width=" + std::to_string(config.width) + ", config.height=" + std::to_string(config.height) + - ", config.scriptPath=" + config.scriptPath.string() + - ", config.luaDebug=" + std::string(config.luaDebug ? "true" : "false") + + ", config.projectRoot=" + config.projectRoot.string() + ", config.windowTitle=" + config.windowTitle + ", configPath=" + configPath.string(), "Entering"); @@ -45,11 +44,6 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std document.AddMember("schema_version", json_config::kRuntimeConfigSchemaVersion, allocator); document.AddMember("configVersion", json_config::kRuntimeConfigSchemaVersion, allocator); - rapidjson::Value scriptsObject(rapidjson::kObjectType); - addStringMember(scriptsObject, "entry", config.scriptPath.string()); - scriptsObject.AddMember("lua_debug", config.luaDebug, allocator); - document.AddMember("scripts", scriptsObject, allocator); - rapidjson::Value windowObject(rapidjson::kObjectType); addStringMember(windowObject, "title", config.windowTitle); rapidjson::Value sizeObject(rapidjson::kObjectType); @@ -57,7 +51,10 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std sizeObject.AddMember("height", config.height, allocator); windowObject.AddMember("size", sizeObject, allocator); - std::filesystem::path scriptsDir = config.scriptPath.parent_path(); + std::filesystem::path projectRoot = config.projectRoot; + if (projectRoot.empty()) { + projectRoot = configPath.empty() ? std::filesystem::current_path() : configPath.parent_path(); + } rapidjson::Value mouseGrabObject(rapidjson::kObjectType); mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator); @@ -129,11 +126,7 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std inputObject.AddMember("bindings", bindingsObject, allocator); document.AddMember("input", inputObject, allocator); - std::filesystem::path projectRoot = scriptsDir.parent_path(); rapidjson::Value pathsObject(rapidjson::kObjectType); - if (!scriptsDir.empty()) { - addStringMember(pathsObject, "scripts", scriptsDir.string()); - } if (!projectRoot.empty()) { addStringMember(pathsObject, "project_root", projectRoot.string()); addStringMember(pathsObject, "shaders", (projectRoot / "shaders").string()); @@ -244,8 +237,6 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std config.crashRecovery.gpuHangFrameTimeMultiplier, allocator); crashObject.AddMember("max_consecutive_gpu_timeouts", static_cast(config.crashRecovery.maxConsecutiveGpuTimeouts), allocator); - crashObject.AddMember("max_lua_failures", - static_cast(config.crashRecovery.maxLuaFailures), allocator); crashObject.AddMember("max_file_format_errors", static_cast(config.crashRecovery.maxFileFormatErrors), allocator); crashObject.AddMember("max_memory_warnings", diff --git a/src/services/impl/config/runtime_config_builder.cpp b/src/services/impl/config/runtime_config_builder.cpp index b489f67..5847cee 100644 --- a/src/services/impl/config/runtime_config_builder.cpp +++ b/src/services/impl/config/runtime_config_builder.cpp @@ -4,7 +4,6 @@ using sdl3cpp::services::InputBindings; using sdl3cpp::services::RuntimeConfig; -using sdl3cpp::services::SceneSource; #include #include @@ -12,20 +11,11 @@ using sdl3cpp::services::SceneSource; #include #include #include +#include #include #include namespace { -SceneSource ParseSceneSource(const std::string& value, const std::string& jsonPath) { - if (value == "config") { - return SceneSource::Config; - } - if (value == "lua") { - return SceneSource::Lua; - } - throw std::runtime_error("JSON member '" + jsonPath + "' must be 'config' or 'lua'"); -} - const rapidjson::Value* GetObjectMember(const rapidjson::Value& parent, const char* name, const char* fullName) { @@ -87,34 +77,14 @@ RuntimeConfigBuilder::RuntimeConfigBuilder(std::shared_ptr logger) RuntimeConfig RuntimeConfigBuilder::Build(const rapidjson::Document& document, const std::filesystem::path& configPath) const { RuntimeConfig config; - auto scriptsValue = GetObjectMember(document, "scripts", "scripts"); auto pathsValue = GetObjectMember(document, "paths", "paths"); auto windowValue = GetObjectMember(document, "window", "window"); const auto* windowSizeValue = windowValue ? GetObjectMember(*windowValue, "size", "window.size") : nullptr; - auto runtimeValue = GetObjectMember(document, "runtime", "runtime"); auto inputValue = GetObjectMember(document, "input", "input"); const auto* inputBindingsValue = inputValue ? GetObjectMember(*inputValue, "bindings", "input.bindings") : nullptr; auto renderingValue = GetObjectMember(document, "rendering", "rendering"); auto guiValue = GetObjectMember(document, "gui", "gui"); - std::optional scriptPathValue; - if (scriptsValue && scriptsValue->HasMember("entry")) { - const auto& value = (*scriptsValue)["entry"]; - if (!value.IsString()) { - throw std::runtime_error("JSON member 'scripts.entry' must be a string"); - } - scriptPathValue = value.GetString(); - } else if (document.HasMember("lua_script")) { - const auto& value = document["lua_script"]; - if (!value.IsString()) { - throw std::runtime_error("JSON member 'lua_script' must be a string"); - } - scriptPathValue = value.GetString(); - } - if (!scriptPathValue) { - throw std::runtime_error("JSON config requires a string member 'scripts.entry' or 'lua_script'"); - } - std::optional projectRoot; if (pathsValue && pathsValue->HasMember("project_root")) { const auto& value = (*pathsValue)["project_root"]; @@ -135,24 +105,16 @@ RuntimeConfig RuntimeConfigBuilder::Build(const rapidjson::Document& document, ? std::filesystem::weakly_canonical(candidate) : std::filesystem::weakly_canonical(configPath.parent_path() / candidate); } - - std::filesystem::path scriptPath(*scriptPathValue); - if (!scriptPath.is_absolute()) { - scriptPath = projectRoot ? *projectRoot / scriptPath : configPath.parent_path() / scriptPath; - } - scriptPath = std::filesystem::weakly_canonical(scriptPath); - if (!std::filesystem::exists(scriptPath)) { - throw std::runtime_error("Lua script not found at " + scriptPath.string()); - } - config.scriptPath = scriptPath; - - if (runtimeValue && runtimeValue->HasMember("scene_source")) { - const auto& value = (*runtimeValue)["scene_source"]; - if (!value.IsString()) { - throw std::runtime_error("JSON member 'runtime.scene_source' must be a string"); - } - config.sceneSource = ParseSceneSource(value.GetString(), "runtime.scene_source"); + std::filesystem::path baseRoot = configPath.empty() + ? std::filesystem::current_path() + : configPath.parent_path(); + std::filesystem::path resolvedRoot = projectRoot ? *projectRoot : baseRoot; + std::error_code rootError; + resolvedRoot = std::filesystem::weakly_canonical(resolvedRoot, rootError); + if (rootError) { + throw std::runtime_error("Failed to resolve project root: " + rootError.message()); } + config.projectRoot = resolvedRoot; if (windowSizeValue) { if (windowSizeValue->HasMember("width")) { @@ -170,20 +132,6 @@ RuntimeConfig RuntimeConfigBuilder::Build(const rapidjson::Document& document, } } - if (scriptsValue && scriptsValue->HasMember("lua_debug")) { - const auto& value = (*scriptsValue)["lua_debug"]; - if (!value.IsBool()) { - throw std::runtime_error("JSON member 'scripts.lua_debug' must be a boolean"); - } - config.luaDebug = value.GetBool(); - } else if (document.HasMember("lua_debug")) { - const auto& value = document["lua_debug"]; - if (!value.IsBool()) { - throw std::runtime_error("JSON member 'lua_debug' must be a boolean"); - } - config.luaDebug = value.GetBool(); - } - if (windowValue && windowValue->HasMember("title")) { const auto& value = (*windowValue)["title"]; if (!value.IsString()) { @@ -722,8 +670,6 @@ RuntimeConfig RuntimeConfigBuilder::Build(const rapidjson::Document& document, "crash_recovery", config.crashRecovery.memoryLimitMB); readSizeT(*crashRecoveryValue, "max_consecutive_gpu_timeouts", "crash_recovery", config.crashRecovery.maxConsecutiveGpuTimeouts); - readSizeT(*crashRecoveryValue, "max_lua_failures", - "crash_recovery", config.crashRecovery.maxLuaFailures); readSizeT(*crashRecoveryValue, "max_file_format_errors", "crash_recovery", config.crashRecovery.maxFileFormatErrors); readSizeT(*crashRecoveryValue, "max_memory_warnings", diff --git a/src/services/impl/diagnostics/crash_recovery_service.cpp b/src/services/impl/diagnostics/crash_recovery_service.cpp index 3366f01..5766ce9 100644 --- a/src/services/impl/diagnostics/crash_recovery_service.cpp +++ b/src/services/impl/diagnostics/crash_recovery_service.cpp @@ -46,7 +46,6 @@ CrashRecoveryService::CrashRecoveryService(std::shared_ptr logger, Cras , memoryLimitBytes_(0) , lastSuccessfulFrameTime_(0.0) , consecutiveFrameTimeouts_(0) - , luaExecutionFailures_(0) , fileFormatErrors_(0) , memoryWarnings_(0) , lastHealthCheck_(std::chrono::steady_clock::now()) @@ -418,30 +417,6 @@ bool CrashRecoveryService::CheckGpuHealth(double lastFrameTime, double expectedF return true; } -bool CrashRecoveryService::ValidateLuaExecution(bool scriptResult, const std::string& scriptName) { - logger_->Trace("CrashRecoveryService", "ValidateLuaExecution", - "scriptResult=" + std::string(scriptResult ? "true" : "false") + - ", scriptName=" + scriptName); - if (!scriptResult) { - luaExecutionFailures_++; - std::lock_guard lock(crashMutex_); - crashReport_ += "\nLUA EXECUTION FAILURE: " + scriptName + "\n"; - crashReport_ += "Total Lua failures: " + std::to_string(luaExecutionFailures_) + "\n"; - - logger_->Error("CrashRecoveryService::ValidateLuaExecution: Lua script '" + - scriptName + "' failed to execute"); - - if (config_.maxLuaFailures > 0 && luaExecutionFailures_ > config_.maxLuaFailures) { - crashDetected_ = true; - crashReport_ += "CRITICAL: Multiple Lua execution failures detected\n"; - } - - return false; - } - - return true; -} - bool CrashRecoveryService::ValidateFileFormat(const std::string& filePath, const std::string& expectedFormat) { logger_->Trace("CrashRecoveryService", "ValidateFileFormat", "filePath=" + filePath + @@ -535,7 +510,6 @@ std::string CrashRecoveryService::GetSystemHealthStatus() const { ss << "Crash Detected: " << (crashDetected_ ? "YES" : "NO") << "\n"; ss << "Last Signal: " << lastSignal_ << "\n"; ss << "Consecutive Frame Timeouts: " << consecutiveFrameTimeouts_ << "\n"; - ss << "Lua Execution Failures: " << luaExecutionFailures_ << "\n"; ss << "File Format Errors: " << fileFormatErrors_ << "\n"; ss << "Memory Warnings: " << memoryWarnings_ << "\n"; ss << "Last Successful Frame Time: " << lastSuccessfulFrameTime_ << " seconds\n"; diff --git a/src/services/impl/diagnostics/crash_recovery_service.hpp b/src/services/impl/diagnostics/crash_recovery_service.hpp index 4f83fd6..e922177 100644 --- a/src/services/impl/diagnostics/crash_recovery_service.hpp +++ b/src/services/impl/diagnostics/crash_recovery_service.hpp @@ -17,7 +17,7 @@ namespace sdl3cpp::services::impl { /** * @brief Crash recovery service implementation. * - * Detects crashes, GPU hangs, Lua failures, file format issues, and provides recovery mechanisms. + * Detects crashes, GPU hangs, file format issues, and provides recovery mechanisms. * Monitors system health and provides comprehensive error detection and recovery. */ class CrashRecoveryService : public ICrashRecoveryService { @@ -34,7 +34,6 @@ public: bool AttemptRecovery() override; std::string GetCrashReport() const override; bool CheckGpuHealth(double lastFrameTime, double expectedFrameTime) override; - bool ValidateLuaExecution(bool scriptResult, const std::string& scriptName) override; bool ValidateFileFormat(const std::string& filePath, const std::string& expectedFormat) override; bool CheckMemoryHealth() override; std::string GetSystemHealthStatus() const override; @@ -72,7 +71,6 @@ private: // Health monitoring state std::atomic lastSuccessfulFrameTime_; std::atomic consecutiveFrameTimeouts_; - std::atomic luaExecutionFailures_; std::atomic fileFormatErrors_; std::atomic memoryWarnings_; std::chrono::steady_clock::time_point lastHealthCheck_; diff --git a/src/services/impl/gui/bgfx_gui_service.cpp b/src/services/impl/gui/bgfx_gui_service.cpp index fe2acf5..4ed00d1 100644 --- a/src/services/impl/gui/bgfx_gui_service.cpp +++ b/src/services/impl/gui/bgfx_gui_service.cpp @@ -488,16 +488,10 @@ std::filesystem::path BgfxGuiService::ResolvePath(const std::filesystem::path& p std::vector roots; if (configService_) { - auto scriptPath = configService_->GetScriptPath(); - if (!scriptPath.empty()) { - auto scriptDir = scriptPath.parent_path(); - if (!scriptDir.empty()) { - roots.push_back(scriptDir); - auto projectRoot = scriptDir.parent_path(); - if (!projectRoot.empty()) { - roots.push_back(projectRoot); - } - } + auto projectRoot = configService_->GetProjectRoot(); + if (!projectRoot.empty()) { + roots.push_back(projectRoot / "scripts"); + roots.push_back(projectRoot); } } roots.push_back(std::filesystem::current_path()); @@ -515,11 +509,10 @@ std::filesystem::path BgfxGuiService::ResolvePath(const std::filesystem::path& p std::filesystem::path BgfxGuiService::ResolveDefaultFontPath() const { std::vector candidates; if (configService_) { - auto scriptPath = configService_->GetScriptPath(); - if (!scriptPath.empty()) { - auto scriptDir = scriptPath.parent_path(); - candidates.push_back(scriptDir / "assets" / "fonts" / "Roboto-Regular.ttf"); - candidates.push_back(scriptDir.parent_path() / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf"); + auto projectRoot = configService_->GetProjectRoot(); + if (!projectRoot.empty()) { + candidates.push_back(projectRoot / "assets" / "fonts" / "Roboto-Regular.ttf"); + candidates.push_back(projectRoot / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf"); } } candidates.push_back(std::filesystem::current_path() / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf"); diff --git a/src/services/impl/input/sdl_input_service.cpp b/src/services/impl/input/sdl_input_service.cpp index 05ac623..f7dbc67 100644 --- a/src/services/impl/input/sdl_input_service.cpp +++ b/src/services/impl/input/sdl_input_service.cpp @@ -730,23 +730,11 @@ void SdlInputService::UpdateGamepadSnapshot() { } } -void SdlInputService::SetGuiScriptService(IGuiScriptService* guiScriptService) { - if (logger_) { - logger_->Trace("SdlInputService", "SetGuiScriptService", - "guiScriptServiceIsNull=" + std::string(guiScriptService ? "false" : "true")); - } - guiScriptService_ = guiScriptService; -} - void SdlInputService::UpdateGuiInput() { if (logger_) { - logger_->Trace("SdlInputService", "UpdateGuiInput", - "guiScriptServiceIsNull=" + std::string(guiScriptService_ ? "false" : "true")); - } - if (guiScriptService_) { - UpdateGamepadSnapshot(); - guiScriptService_->UpdateGuiInput(guiInputSnapshot_); + logger_->Trace("SdlInputService", "UpdateGuiInput"); } + UpdateGamepadSnapshot(); } } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/input/sdl_input_service.hpp b/src/services/impl/input/sdl_input_service.hpp index 58a945b..742f969 100644 --- a/src/services/impl/input/sdl_input_service.hpp +++ b/src/services/impl/input/sdl_input_service.hpp @@ -1,7 +1,6 @@ #pragma once #include "services/interfaces/i_input_service.hpp" -#include "services/interfaces/i_gui_script_service.hpp" #include "services/interfaces/i_logger.hpp" #include "services/interfaces/i_config_service.hpp" #include "../../../events/i_event_bus.hpp" @@ -45,7 +44,6 @@ public: std::pair GetMousePosition() const override; void SetRelativeMouseMode(bool enabled) override; bool IsRelativeMouseMode() const override; - void SetGuiScriptService(IGuiScriptService* guiScriptService) override; void UpdateGuiInput() override; private: @@ -54,7 +52,6 @@ private: std::shared_ptr logger_; InputState state_; GuiInputSnapshot guiInputSnapshot_; - IGuiScriptService* guiScriptService_ = nullptr; SDL_Gamepad* gamepad_ = nullptr; bool windowFocused_ = true; bool mouseGrabbed_ = false; diff --git a/src/services/impl/render/render_coordinator_service.cpp b/src/services/impl/render/render_coordinator_service.cpp index 6a1d43c..c4555f4 100644 --- a/src/services/impl/render/render_coordinator_service.cpp +++ b/src/services/impl/render/render_coordinator_service.cpp @@ -9,33 +9,24 @@ namespace sdl3cpp::services::impl { RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr logger, - std::shared_ptr configService, std::shared_ptr configCompilerService, std::shared_ptr graphicsService, - std::shared_ptr sceneScriptService, - std::shared_ptr shaderScriptService, - std::shared_ptr guiScriptService, + std::shared_ptr shaderSystemRegistry, std::shared_ptr guiService, std::shared_ptr sceneService, std::shared_ptr validationTourService) : logger_(std::move(logger)), - configService_(std::move(configService)), configCompilerService_(std::move(configCompilerService)), graphicsService_(std::move(graphicsService)), - sceneScriptService_(std::move(sceneScriptService)), - shaderScriptService_(std::move(shaderScriptService)), - guiScriptService_(std::move(guiScriptService)), + shaderSystemRegistry_(std::move(shaderSystemRegistry)), guiService_(std::move(guiService)), sceneService_(std::move(sceneService)), validationTourService_(std::move(validationTourService)) { if (logger_) { logger_->Trace("RenderCoordinatorService", "RenderCoordinatorService", - "configService=" + std::string(configService_ ? "set" : "null") + - ", configCompilerService=" + std::string(configCompilerService_ ? "set" : "null") + + "configCompilerService=" + std::string(configCompilerService_ ? "set" : "null") + ", graphicsService=" + std::string(graphicsService_ ? "set" : "null") + - ", sceneScriptService=" + std::string(sceneScriptService_ ? "set" : "null") + - ", shaderScriptService=" + std::string(shaderScriptService_ ? "set" : "null") + - ", guiScriptService=" + std::string(guiScriptService_ ? "set" : "null") + + ", shaderSystemRegistry=" + std::string(shaderSystemRegistry_ ? "set" : "null") + ", guiService=" + std::string(guiService_ ? "set" : "null") + ", sceneService=" + std::string(sceneService_ ? "set" : "null") + ", validationTourService=" + std::string(validationTourService_ ? "set" : "null"), @@ -162,49 +153,37 @@ void RenderCoordinatorService::RenderFrameInternal(float time, return; } - const bool useLuaScene = !configService_ || configService_->GetSceneSource() == SceneSource::Lua; - if (!useLuaScene && !configFirstLogged_) { - if (logger_) { - logger_->Warn("RenderCoordinatorService::RenderFrame: config-first scene source selected; Lua scene path disabled"); - } - configFirstLogged_ = true; - } - if (!shadersLoaded_) { - if (!useLuaScene) { - shadersLoaded_ = true; - } else if (!shaderScriptService_) { + if (!shaderSystemRegistry_) { if (logger_) { - logger_->Error("RenderCoordinatorService::RenderFrame: Shader script service not available"); + logger_->Error("RenderCoordinatorService::RenderFrame: Shader system registry not available"); } return; } - if (!shadersLoaded_) { - if (logger_) { - logger_->Trace("RenderCoordinatorService", "RenderFrame", - "Priming bgfx with a dummy frame before shader load"); - } - if (!graphicsService_->BeginFrame()) { - if (logger_) { - logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during shader pre-frame"); - } - graphicsService_->RecreateSwapchain(); - return; - } - if (!graphicsService_->EndFrame()) { - if (logger_) { - logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during shader pre-frame"); - } - graphicsService_->RecreateSwapchain(); - return; - } - if (logger_) { - logger_->Trace("RenderCoordinatorService", "RenderFrame", "Loading shaders from Lua"); - } - auto shaders = shaderScriptService_->LoadShaderPathsMap(); - graphicsService_->LoadShaders(shaders); - shadersLoaded_ = true; + if (logger_) { + logger_->Trace("RenderCoordinatorService", "RenderFrame", + "Priming bgfx with a dummy frame before shader load"); } + if (!graphicsService_->BeginFrame()) { + if (logger_) { + logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during shader pre-frame"); + } + graphicsService_->RecreateSwapchain(); + return; + } + if (!graphicsService_->EndFrame()) { + if (logger_) { + logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during shader pre-frame"); + } + graphicsService_->RecreateSwapchain(); + return; + } + if (logger_) { + logger_->Trace("RenderCoordinatorService", "RenderFrame", "Loading shaders from registry"); + } + auto shaders = shaderSystemRegistry_->BuildShaderMap(); + graphicsService_->LoadShaders(shaders); + shadersLoaded_ = true; } if (!graphicsService_->BeginFrame()) { @@ -215,9 +194,7 @@ void RenderCoordinatorService::RenderFrameInternal(float time, return; } - if (!useLuaScene) { - ConfigureRenderGraphPasses(); - } + ConfigureRenderGraphPasses(); if (guiService_) { auto extent = graphicsService_->GetSwapchainExtent(); @@ -228,21 +205,14 @@ void RenderCoordinatorService::RenderFrameInternal(float time, "Using workflow GUI commands"); } guiService_->PrepareFrame(*guiCommands, extent.first, extent.second); - } else if (guiScriptService_ && guiScriptService_->HasGuiCommands()) { - auto scriptCommands = guiScriptService_->LoadGuiCommands(); - if (logger_) { - logger_->Trace("RenderCoordinatorService", "RenderFrame", - "guiCommands=" + std::to_string(scriptCommands.size()), - "Using script GUI commands"); - } - guiService_->PrepareFrame(scriptCommands, extent.first, extent.second); } } - if (useLuaScene && sceneScriptService_ && sceneService_) { - auto sceneObjects = sceneScriptService_->LoadSceneObjects(); - sceneService_->LoadScene(sceneObjects); - + if (!sceneService_) { + if (logger_) { + logger_->Error("RenderCoordinatorService::RenderFrame: Scene service not available"); + } + } else { const auto& vertices = sceneService_->GetCombinedVertices(); const auto& indices = sceneService_->GetCombinedIndices(); bool geometryChanged = vertices.size() != lastVertexCount_ || indices.size() != lastIndexCount_; @@ -274,7 +244,20 @@ void RenderCoordinatorService::RenderFrameInternal(float time, validationPlan = validationTourService_->BeginFrame(aspect); } - ViewState viewState = overrideView ? *overrideView : sceneScriptService_->GetViewState(aspect); + ViewState viewState{}; + if (overrideView) { + viewState = *overrideView; + } else { + viewState.view = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + viewState.proj = viewState.view; + viewState.viewProj = viewState.view; + viewState.cameraPosition = {0.0f, 0.0f, 0.0f}; + } if (validationPlan.active && validationPlan.overrideView) { viewState = validationPlan.viewState; } diff --git a/src/services/impl/render/render_coordinator_service.hpp b/src/services/impl/render/render_coordinator_service.hpp index 0bcf083..d08f896 100644 --- a/src/services/impl/render/render_coordinator_service.hpp +++ b/src/services/impl/render/render_coordinator_service.hpp @@ -1,15 +1,12 @@ #pragma once #include "services/interfaces/i_render_coordinator_service.hpp" -#include "services/interfaces/i_config_service.hpp" #include "services/interfaces/i_config_compiler_service.hpp" #include "services/interfaces/i_graphics_service.hpp" -#include "services/interfaces/i_gui_script_service.hpp" #include "services/interfaces/i_gui_service.hpp" #include "services/interfaces/i_logger.hpp" -#include "services/interfaces/i_scene_script_service.hpp" #include "services/interfaces/i_scene_service.hpp" -#include "services/interfaces/i_shader_script_service.hpp" +#include "services/interfaces/i_shader_system_registry.hpp" #include "services/interfaces/i_validation_tour_service.hpp" #include @@ -18,12 +15,9 @@ namespace sdl3cpp::services::impl { class RenderCoordinatorService : public IRenderCoordinatorService { public: RenderCoordinatorService(std::shared_ptr logger, - std::shared_ptr configService, std::shared_ptr configCompilerService, std::shared_ptr graphicsService, - std::shared_ptr sceneScriptService, - std::shared_ptr shaderScriptService, - std::shared_ptr guiScriptService, + std::shared_ptr shaderSystemRegistry, std::shared_ptr guiService, std::shared_ptr sceneService, std::shared_ptr validationTourService); @@ -38,12 +32,9 @@ public: private: std::shared_ptr logger_; - std::shared_ptr configService_; std::shared_ptr configCompilerService_; std::shared_ptr graphicsService_; - std::shared_ptr sceneScriptService_; - std::shared_ptr shaderScriptService_; - std::shared_ptr guiScriptService_; + std::shared_ptr shaderSystemRegistry_; std::shared_ptr guiService_; std::shared_ptr sceneService_; std::shared_ptr validationTourService_; @@ -51,7 +42,6 @@ private: size_t lastIndexCount_ = 0; bool shadersLoaded_ = false; bool geometryUploaded_ = false; - bool configFirstLogged_ = false; void ConfigureRenderGraphPasses(); void RenderFrameInternal(float time, diff --git a/src/services/impl/scene/mesh_service.cpp b/src/services/impl/scene/mesh_service.cpp index 0a67ca0..cffb430 100644 --- a/src/services/impl/scene/mesh_service.cpp +++ b/src/services/impl/scene/mesh_service.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -866,7 +865,7 @@ bool MeshService::ResolvePath(const std::string& requestedPath, std::filesystem::path resolved(requestedPath); if (!resolved.is_absolute()) { - resolved = configService_->GetScriptPath().parent_path() / resolved; + resolved = configService_->GetProjectRoot() / resolved; } std::error_code ec; @@ -880,79 +879,4 @@ bool MeshService::ResolvePath(const std::string& requestedPath, return true; } -void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) { - if (logger_) { - logger_->Trace("MeshService", "PushMeshToLua", - "positions.size=" + std::to_string(payload.positions.size()) + - ", normals.size=" + std::to_string(payload.normals.size()) + - ", colors.size=" + std::to_string(payload.colors.size()) + - ", texcoords.size=" + std::to_string(payload.texcoords.size()) + - ", indices.size=" + std::to_string(payload.indices.size()) + - ", luaStateIsNull=" + std::string(L ? "false" : "true")); - } - lua_newtable(L); - - lua_newtable(L); - for (size_t vertexIndex = 0; vertexIndex < payload.positions.size(); ++vertexIndex) { - lua_newtable(L); - - lua_newtable(L); - for (size_t component = 0; component < 3; ++component) { - lua_pushnumber(L, payload.positions[vertexIndex][component]); - lua_rawseti(L, -2, static_cast(component + 1)); - } - lua_setfield(L, -2, "position"); - - lua_newtable(L); - std::array normal = {0.0f, 0.0f, 1.0f}; - if (vertexIndex < payload.normals.size()) { - normal = payload.normals[vertexIndex]; - } - for (size_t component = 0; component < 3; ++component) { - lua_pushnumber(L, normal[component]); - lua_rawseti(L, -2, static_cast(component + 1)); - } - lua_setfield(L, -2, "normal"); - - lua_newtable(L); - std::array tangent = {1.0f, 0.0f, 0.0f}; - if (vertexIndex < payload.tangents.size()) { - tangent = payload.tangents[vertexIndex]; - } - for (size_t component = 0; component < 3; ++component) { - lua_pushnumber(L, tangent[component]); - lua_rawseti(L, -2, static_cast(component + 1)); - } - lua_setfield(L, -2, "tangent"); - - lua_newtable(L); - for (size_t component = 0; component < 3; ++component) { - lua_pushnumber(L, payload.colors[vertexIndex][component]); - lua_rawseti(L, -2, static_cast(component + 1)); - } - lua_setfield(L, -2, "color"); - - lua_newtable(L); - std::array texcoord = {0.0f, 0.0f}; - if (vertexIndex < payload.texcoords.size()) { - texcoord = payload.texcoords[vertexIndex]; - } - for (size_t component = 0; component < 2; ++component) { - lua_pushnumber(L, texcoord[component]); - lua_rawseti(L, -2, static_cast(component + 1)); - } - lua_setfield(L, -2, "texcoord"); - - lua_rawseti(L, -2, static_cast(vertexIndex + 1)); - } - lua_setfield(L, -2, "vertices"); - - lua_newtable(L); - for (size_t index = 0; index < payload.indices.size(); ++index) { - lua_pushinteger(L, static_cast(payload.indices[index]) + 1); - lua_rawseti(L, -2, static_cast(index + 1)); - } - lua_setfield(L, -2, "indices"); -} - } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/scene/mesh_service.hpp b/src/services/impl/scene/mesh_service.hpp index 12a1fde..6504756 100644 --- a/src/services/impl/scene/mesh_service.hpp +++ b/src/services/impl/scene/mesh_service.hpp @@ -9,7 +9,7 @@ namespace sdl3cpp::services::impl { /** - * @brief Script-facing mesh loading service implementation. + * @brief Mesh loading service implementation. */ class MeshService : public IMeshService { public: @@ -23,7 +23,6 @@ public: const std::string& entryPath, MeshPayload& outPayload, std::string& outError) override; - void PushMeshToLua(lua_State* L, const MeshPayload& payload) override; private: bool ResolvePath(const std::string& requestedPath, diff --git a/src/services/impl/scene/scene_service.cpp b/src/services/impl/scene/scene_service.cpp index af5ef03..16124b8 100644 --- a/src/services/impl/scene/scene_service.cpp +++ b/src/services/impl/scene/scene_service.cpp @@ -5,21 +5,18 @@ namespace sdl3cpp::services::impl { -SceneService::SceneService(std::shared_ptr scriptService, - std::shared_ptr ecsService, +SceneService::SceneService(std::shared_ptr ecsService, std::shared_ptr logger, std::shared_ptr probeService) - : scriptService_(std::move(scriptService)), - ecsService_(std::move(ecsService)), + : ecsService_(std::move(ecsService)), logger_(std::move(logger)), probeService_(std::move(probeService)) { logger_->Trace("SceneService", "SceneService", - "scriptService=" + std::string(scriptService_ ? "set" : "null") + - ", ecsService=" + std::string(ecsService_ ? "set" : "null") + + "ecsService=" + std::string(ecsService_ ? "set" : "null") + ", probeService=" + std::string(probeService_ ? "set" : "null")); - if (!scriptService_ || !ecsService_) { - throw std::invalid_argument("Scene script service and ECS service cannot be null"); + if (!ecsService_) { + throw std::invalid_argument("ECS service cannot be null"); } registry_ = &ecsService_->GetRegistry(); } @@ -191,11 +188,14 @@ std::vector SceneService::GetRenderCommands(float time) const { cmd.indexCount = drawInfo.indexCount; cmd.vertexOffset = drawInfo.vertexOffset; cmd.shaderKeys = drawInfo.shaderKeys; - if (drawInfo.hasCustomModelMatrix) { - cmd.modelMatrix = drawInfo.modelMatrix; - } else { - cmd.modelMatrix = scriptService_->ComputeModelMatrix(drawInfo.computeModelMatrixRef, time); - } + cmd.modelMatrix = drawInfo.hasCustomModelMatrix + ? drawInfo.modelMatrix + : std::array{ + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; commands.push_back(cmd); } diff --git a/src/services/impl/scene/scene_service.hpp b/src/services/impl/scene/scene_service.hpp index 0d77575..5b25c06 100644 --- a/src/services/impl/scene/scene_service.hpp +++ b/src/services/impl/scene/scene_service.hpp @@ -1,7 +1,6 @@ #pragma once #include "services/interfaces/i_scene_service.hpp" -#include "services/interfaces/i_scene_script_service.hpp" #include "services/interfaces/i_ecs_service.hpp" #include "services/interfaces/i_logger.hpp" #include "services/interfaces/i_probe_service.hpp" @@ -18,13 +17,12 @@ namespace sdl3cpp::services::impl { * @brief Scene service implementation. * * Maintains scene graph state and generates render commands. - * Separated from script service to decouple scene state from Lua execution. + * Separated from script execution to keep scene state deterministic. */ class SceneService : public ISceneService, public di::IShutdownable { public: - SceneService(std::shared_ptr scriptService, - std::shared_ptr ecsService, + SceneService(std::shared_ptr ecsService, std::shared_ptr logger, std::shared_ptr probeService = nullptr); ~SceneService() override; @@ -72,7 +70,6 @@ private: const std::string& details) const; void ClearSceneEntities(); - std::shared_ptr scriptService_; std::shared_ptr ecsService_; std::shared_ptr logger_; std::shared_ptr probeService_; diff --git a/src/services/impl/shader/materialx_shader_system.cpp b/src/services/impl/shader/materialx_shader_system.cpp index e4c0092..da2d748 100644 --- a/src/services/impl/shader/materialx_shader_system.cpp +++ b/src/services/impl/shader/materialx_shader_system.cpp @@ -7,16 +7,13 @@ namespace sdl3cpp::services::impl { MaterialXShaderSystem::MaterialXShaderSystem(std::shared_ptr configService, - std::shared_ptr scriptEngineService, std::shared_ptr logger) : configService_(std::move(configService)), - scriptEngineService_(std::move(scriptEngineService)), logger_(std::move(logger)), materialxGenerator_(logger_) { if (logger_) { logger_->Trace("MaterialXShaderSystem", "MaterialXShaderSystem", - "configService=" + std::string(configService_ ? "set" : "null") + - ", scriptEngineService=" + std::string(scriptEngineService_ ? "set" : "null")); + "configService=" + std::string(configService_ ? "set" : "null")); } } @@ -40,8 +37,8 @@ std::unordered_map MaterialXShaderSystem::BuildShaderM ", baseEnabled=" + std::string(materialConfig.enabled ? "true" : "false")); } - const auto scriptDirectory = scriptEngineService_ - ? scriptEngineService_->GetScriptDirectory() + const auto scriptDirectory = configService_ + ? configService_->GetProjectRoot() : std::filesystem::path{}; const auto addShader = [&](const MaterialXConfig& config, const std::string& sourceLabel) { diff --git a/src/services/impl/shader/materialx_shader_system.hpp b/src/services/impl/shader/materialx_shader_system.hpp index 0f1b3b0..df47879 100644 --- a/src/services/impl/shader/materialx_shader_system.hpp +++ b/src/services/impl/shader/materialx_shader_system.hpp @@ -2,7 +2,6 @@ #include "services/interfaces/i_config_service.hpp" #include "services/interfaces/i_logger.hpp" -#include "services/interfaces/i_script_engine_service.hpp" #include "services/interfaces/i_shader_system.hpp" #include "services/impl/materialx/materialx_shader_generator.hpp" @@ -17,7 +16,6 @@ namespace sdl3cpp::services::impl { class MaterialXShaderSystem final : public IShaderSystem { public: MaterialXShaderSystem(std::shared_ptr configService, - std::shared_ptr scriptEngineService, std::shared_ptr logger); std::string GetId() const override { return "materialx"; } @@ -30,7 +28,6 @@ public: private: std::shared_ptr configService_; - std::shared_ptr scriptEngineService_; std::shared_ptr logger_; MaterialXShaderGenerator materialxGenerator_; std::unordered_map lastShaderMap_; diff --git a/src/services/impl/shader/shader_system_registry.cpp b/src/services/impl/shader/shader_system_registry.cpp index 98796bd..dc7435e 100644 --- a/src/services/impl/shader/shader_system_registry.cpp +++ b/src/services/impl/shader/shader_system_registry.cpp @@ -9,15 +9,13 @@ namespace sdl3cpp::services::impl { ShaderSystemRegistry::ShaderSystemRegistry(std::shared_ptr configService, std::shared_ptr configCompilerService, - std::shared_ptr scriptEngineService, std::shared_ptr logger) : configService_(std::move(configService)), logger_(std::move(logger)) { if (logger_) { logger_->Trace("ShaderSystemRegistry", "ShaderSystemRegistry", "configService=" + std::string(configService_ ? "set" : "null") + - ", configCompilerService=" + std::string(configCompilerService ? "set" : "null") + - ", scriptEngineService=" + std::string(scriptEngineService ? "set" : "null")); + ", configCompilerService=" + std::string(configCompilerService ? "set" : "null")); } if (configCompilerService) { @@ -27,7 +25,7 @@ ShaderSystemRegistry::ShaderSystemRegistry(std::shared_ptr confi logger_->Warn("ShaderSystemRegistry: Config compiler missing; GLSL shader system disabled"); } - auto materialxSystem = std::make_shared(configService_, scriptEngineService, logger_); + auto materialxSystem = std::make_shared(configService_, logger_); systems_.emplace(materialxSystem->GetId(), std::move(materialxSystem)); } diff --git a/src/services/impl/shader/shader_system_registry.hpp b/src/services/impl/shader/shader_system_registry.hpp index 54642ef..ccf5387 100644 --- a/src/services/impl/shader/shader_system_registry.hpp +++ b/src/services/impl/shader/shader_system_registry.hpp @@ -3,7 +3,6 @@ #include "services/interfaces/i_config_compiler_service.hpp" #include "services/interfaces/i_config_service.hpp" #include "services/interfaces/i_logger.hpp" -#include "services/interfaces/i_script_engine_service.hpp" #include "services/interfaces/i_shader_system_registry.hpp" #include "glsl_shader_system.hpp" #include "materialx_shader_system.hpp" @@ -21,7 +20,6 @@ class ShaderSystemRegistry final : public IShaderSystemRegistry { public: ShaderSystemRegistry(std::shared_ptr configService, std::shared_ptr configCompilerService, - std::shared_ptr scriptEngineService, std::shared_ptr logger); std::unordered_map BuildShaderMap() override; diff --git a/src/services/impl/soundboard/soundboard_path_resolver.cpp b/src/services/impl/soundboard/soundboard_path_resolver.cpp index 425da80..0b1bdbe 100644 --- a/src/services/impl/soundboard/soundboard_path_resolver.cpp +++ b/src/services/impl/soundboard/soundboard_path_resolver.cpp @@ -23,9 +23,9 @@ std::filesystem::path FindPackageRoot(const std::filesystem::path& seed) { std::filesystem::path ResolveSoundboardPackageRoot(const std::shared_ptr& configService) { std::filesystem::path seed = std::filesystem::current_path(); if (configService) { - const auto scriptPath = configService->GetScriptPath(); - if (!scriptPath.empty()) { - seed = scriptPath.parent_path(); + const auto projectRoot = configService->GetProjectRoot(); + if (!projectRoot.empty()) { + seed = projectRoot; } } return FindPackageRoot(seed) / "packages" / "soundboard"; diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index bd53bce..7f02950 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -86,11 +86,6 @@ struct BgfxConfig { std::string renderer = "vulkan"; }; -enum class SceneSource { - Config, - Lua -}; - struct MaterialXConfig { bool enabled = false; std::filesystem::path documentPath; @@ -144,7 +139,6 @@ struct CrashRecoveryConfig { size_t memoryLimitMB = 1024; double gpuHangFrameTimeMultiplier = 10.0; size_t maxConsecutiveGpuTimeouts = 5; - size_t maxLuaFailures = 3; size_t maxFileFormatErrors = 2; size_t maxMemoryWarnings = 3; }; @@ -222,9 +216,7 @@ struct ValidationTourConfig { struct RuntimeConfig { uint32_t width = 1024; uint32_t height = 768; - std::filesystem::path scriptPath; - bool luaDebug = false; - SceneSource sceneSource = SceneSource::Config; + std::filesystem::path projectRoot; std::string windowTitle = "SDL3 Bgfx Demo"; MouseGrabConfig mouseGrab{}; InputBindings inputBindings{}; diff --git a/src/services/interfaces/i_audio_command_service.hpp b/src/services/interfaces/i_audio_command_service.hpp deleted file mode 100644 index d18c9b2..0000000 --- a/src/services/interfaces/i_audio_command_service.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include - -namespace sdl3cpp::services { - -/** - * @brief Audio command type for script bindings. - */ -enum class AudioCommandType { - Background, - Effect -}; - -/** - * @brief Script-facing audio command service interface. - */ -class IAudioCommandService { -public: - virtual ~IAudioCommandService() = default; - - virtual bool QueueAudioCommand(AudioCommandType type, - const std::string& path, - bool loop, - std::string& error) = 0; - virtual bool StopBackground(std::string& error) = 0; -}; - -} // namespace sdl3cpp::services diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index a63af43..3f01629 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -31,16 +31,10 @@ public: virtual uint32_t GetWindowHeight() const = 0; /** - * @brief Get the path to the Lua script to execute. - * @return Path to the script file + * @brief Get the resolved project root path. + * @return Project root path */ - virtual std::filesystem::path GetScriptPath() const = 0; - - /** - * @brief Check if Lua debug mode is enabled. - * @return true if debug mode is enabled, false otherwise - */ - virtual bool IsLuaDebugEnabled() const = 0; + virtual std::filesystem::path GetProjectRoot() const = 0; /** * @brief Get the window title. @@ -48,12 +42,6 @@ public: */ virtual std::string GetWindowTitle() const = 0; - /** - * @brief Get the configured scene source. - * @return Scene source enum - */ - virtual SceneSource GetSceneSource() const = 0; - /** * @brief Get configured input bindings. * @return Input bindings structure diff --git a/src/services/interfaces/i_crash_recovery_service.hpp b/src/services/interfaces/i_crash_recovery_service.hpp index b0fda41..c3b37fe 100644 --- a/src/services/interfaces/i_crash_recovery_service.hpp +++ b/src/services/interfaces/i_crash_recovery_service.hpp @@ -9,7 +9,7 @@ namespace sdl3cpp::services { * @brief Crash recovery service interface. * * Provides mechanisms for detecting and recovering from crashes and infinite loops. - * Monitors GPU state, Lua execution, file operations, and other critical subsystems. + * Monitors GPU state, file operations, and other critical subsystems. */ class ICrashRecoveryService { public: @@ -72,15 +72,6 @@ public: */ virtual bool CheckGpuHealth(double lastFrameTime, double expectedFrameTime = 1.0/60.0) = 0; - /** - * @brief Validate Lua script execution. - * - * @param scriptResult Result from Lua operation - * @param scriptName Name of the script for error reporting - * @return true if script executed successfully - */ - virtual bool ValidateLuaExecution(bool scriptResult, const std::string& scriptName) = 0; - /** * @brief Check file format validity. * diff --git a/src/services/interfaces/i_input_service.hpp b/src/services/interfaces/i_input_service.hpp index a2caef5..751734a 100644 --- a/src/services/interfaces/i_input_service.hpp +++ b/src/services/interfaces/i_input_service.hpp @@ -6,9 +6,6 @@ namespace sdl3cpp::services { -// Forward declaration -class IGuiScriptService; - /** * @brief Input state snapshot for a single frame. */ @@ -29,7 +26,7 @@ struct InputState { * * Subscribes to input events from the event bus and maintains * the current input state for queries by other services. - * Also handles GUI input processing for script integration. + * Also handles GUI input processing for GUI workflows. */ class IInputService { public: @@ -94,8 +91,6 @@ public: /** * @brief Set whether mouse input should be treated as relative motion. * - * This updates internal cursor tracking for script usage. - * * @param enabled true for relative mode, false for absolute */ virtual void SetRelativeMouseMode(bool enabled) = 0; @@ -108,20 +103,7 @@ public: virtual bool IsRelativeMouseMode() const = 0; /** - * @brief Set the GUI script service for GUI input processing. - * - * The input service will update GUI input state to the script service - * when events are processed. - * - * @param guiScriptService Pointer to the GUI script service, or nullptr to disable - */ - virtual void SetGuiScriptService(IGuiScriptService* guiScriptService) = 0; - - /** - * @brief Update GUI input state to the script service. - * - * Called at the end of each frame to send accumulated GUI input - * to the script engine for processing. + * @brief Update GUI input state for downstream consumers. */ virtual void UpdateGuiInput() = 0; }; diff --git a/src/services/interfaces/i_mesh_service.hpp b/src/services/interfaces/i_mesh_service.hpp index 97a35cb..540e8d6 100644 --- a/src/services/interfaces/i_mesh_service.hpp +++ b/src/services/interfaces/i_mesh_service.hpp @@ -3,8 +3,6 @@ #include "mesh_types.hpp" #include -struct lua_State; - namespace sdl3cpp::services { /** @@ -21,7 +19,6 @@ public: const std::string& entryPath, MeshPayload& outPayload, std::string& outError) = 0; - virtual void PushMeshToLua(lua_State* L, const MeshPayload& payload) = 0; }; } // namespace sdl3cpp::services