feat: Enhance JSON configuration writer and add heartbeat recording to crash recovery service

- Updated JsonConfigWriterService to structure the JSON output with new sections for scripts, window settings, input bindings, paths, rendering, and GUI configurations.
- Introduced a new method in ICrashRecoveryService to record frame heartbeats, allowing for better tracking of long-running operations.
- Refactored existing code to improve readability and maintainability, including the addition of helper functions for adding string members to JSON objects.
This commit is contained in:
2026-01-08 16:57:24 +00:00
parent 4fdbcdc4bc
commit df19ae9264
18 changed files with 1079 additions and 471 deletions
+85
View File
@@ -0,0 +1,85 @@
local resolver = {}
local function as_table(value)
if type(value) == "table" then
return value
end
return nil
end
function resolver.resolve_materialx(config)
local root = as_table(config)
if not root then
return nil
end
local rendering = as_table(root.rendering)
if rendering then
local materialx = as_table(rendering.materialx)
if materialx then
return materialx
end
end
return as_table(root.materialx)
end
function resolver.resolve_materialx_materials(config)
local materialx = resolver.resolve_materialx(config)
if materialx then
local materials = as_table(materialx.materials)
if materials then
return materials
end
end
local root = as_table(config)
if root then
return as_table(root.materialx_materials)
end
return nil
end
function resolver.resolve_input_bindings(config)
local root = as_table(config)
if not root then
return nil
end
local input = as_table(root.input)
if input then
local bindings = as_table(input.bindings)
if bindings then
return bindings
end
end
return as_table(root.input_bindings)
end
function resolver.resolve_gui_font(config)
local root = as_table(config)
if not root then
return nil
end
local gui = as_table(root.gui)
if gui then
local font = as_table(gui.font)
if font then
return font
end
end
return as_table(root.gui_font)
end
function resolver.resolve_gui_opacity(config)
local root = as_table(config)
if not root then
return nil
end
local gui = as_table(root.gui)
if gui and type(gui.opacity) == "number" then
return gui.opacity
end
if type(root.gui_opacity) == "number" then
return root.gui_opacity
end
return nil
end
return resolver
+5 -4
View File
@@ -1,5 +1,6 @@
local scene_framework = require("scene_framework")
local math3d = require("math3d")
local config_resolver = require("config_resolver")
local cube_mesh_info = {
path = "models/cube.stl",
@@ -709,19 +710,19 @@ local function resolve_material_shader()
if type(config) ~= "table" then
error("Missing config table for MaterialX shader selection")
end
local materialx = config.materialx
local materialx = config_resolver.resolve_materialx(config)
if type(materialx) ~= "table" or not materialx.enabled then
error("MaterialX config missing or disabled; shader selection cannot proceed")
end
local materials = config.materialx_materials
local materials = config_resolver.resolve_materialx_materials(config)
if type(materials) == "table" and type(materials[1]) == "table" then
local first_key = materials[1].shader_key
if type(first_key) == "string" and first_key ~= "" then
log_debug("Using first materialx_materials shader_key=%s", first_key)
log_debug("Using first materialx materials shader_key=%s", first_key)
return first_key
end
end
error("MaterialX enabled but no materialx_materials shader_key found")
error("MaterialX enabled but no materials shader_key found")
end
-- Delegate to framework
+42 -23
View File
@@ -1250,30 +1250,45 @@ def gui(args: argparse.Namespace) -> None:
"description": f"3D {name} project based on cube demo template",
"enabled": True
},
"window_width": 1024,
"window_height": 768,
"lua_script": f"scripts/{project_id}_logic.lua",
"scripts_directory": "scripts",
"mouse_grab": {
"enabled": True,
"grab_on_click": True,
"release_on_escape": True,
"start_grabbed": False,
"hide_cursor": True,
"relative_mode": True,
"grab_mouse_button": "left",
"release_key": "escape"
"schema_version": 2,
"window": {
"title": name,
"size": {
"width": 1024,
"height": 768
},
"mouse_grab": {
"enabled": True,
"grab_on_click": True,
"release_on_escape": True,
"start_grabbed": False,
"hide_cursor": True,
"relative_mode": True,
"grab_mouse_button": "left",
"release_key": "escape"
}
},
"input_bindings": {
"move_forward": "W",
"move_back": "S",
"move_left": "A",
"move_right": "D",
"fly_up": "Q",
"fly_down": "Z",
"jump": "Space",
"noclip_toggle": "N",
"music_toggle": "M"
"scripts": {
"entry": f"scripts/{project_id}_logic.lua",
"lua_debug": False
},
"paths": {
"project_root": "../",
"scripts": "scripts",
"shaders": "shaders"
},
"input": {
"bindings": {
"move_forward": "W",
"move_back": "S",
"move_left": "A",
"move_right": "D",
"fly_up": "Q",
"fly_down": "Z",
"jump": "Space",
"noclip_toggle": "N",
"music_toggle": "M"
}
}
},
"lua_script": f"""-- {name} Logic Script
@@ -1512,6 +1527,10 @@ return {{
config_data = json.load(f)
lua_script_path = config_data.get("lua_script", "")
if not lua_script_path:
scripts_config = config_data.get("scripts", {})
if isinstance(scripts_config, dict):
lua_script_path = scripts_config.get("entry", "")
if not lua_script_path:
self.lua_editor.setPlainText("# No Lua script specified in config")
self.lua_file_label.setText("No Lua script found")
+5 -3
View File
@@ -1,5 +1,6 @@
-- Lightweight Lua-based 2D GUI framework that emits draw commands
-- and handles interaction for buttons, textboxes, and list views.
local config_resolver = require("config_resolver")
local Gui = {}
-- {r,g,b,a} colors
@@ -126,14 +127,15 @@ function Context:new(options)
options = options or {}
local style = options.style or DEFAULT_STYLE
if options.style == nil and type(config) == "table" then
local guiFont = config.gui_font
local guiFont = config_resolver.resolve_gui_font(config)
if type(guiFont) == "table" and type(guiFont.font_size) == "number" then
style.fontSize = guiFont.font_size
end
end
local opacity = 1.0
if type(config) == "table" and type(config.gui_opacity) == "number" then
opacity = config.gui_opacity
local resolvedOpacity = config_resolver.resolve_gui_opacity(config)
if type(resolvedOpacity) == "number" then
opacity = resolvedOpacity
end
local instance = {
commands = {},
+2 -1
View File
@@ -1,5 +1,6 @@
local Gui = require('gui')
local math3d = require('math3d')
local config_resolver = require('config_resolver')
local ctx = Gui.newContext()
local input = Gui.newInputState()
@@ -24,7 +25,7 @@ local fpsMode = false
local fpsToggleWasDown = false
local fpsToggleKey = "F1"
if type(config) == "table" then
local bindings = config.input_bindings
local bindings = config_resolver.resolve_input_bindings(config)
if type(bindings) == "table" then
local key = bindings.fps_toggle or bindings.fps_toggle_key
if type(key) == "string" or type(key) == "number" then
+15 -9
View File
@@ -1,4 +1,5 @@
local math3d = require("math3d")
local config_resolver = require("config_resolver")
local function log_debug(fmt, ...)
if not lua_debug or not fmt then
@@ -59,15 +60,20 @@ local map_offset = resolve_vec3(quake3_config.offset, {0.0, 0.0, 0.0})
local map_shader_key = nil
if type(quake3_config.shader_key) == "string" and quake3_config.shader_key ~= "" then
map_shader_key = quake3_config.shader_key
elseif type(config) == "table"
and type(config.materialx_materials) == "table"
and type(config.materialx_materials[1]) == "table"
and type(config.materialx_materials[1].shader_key) == "string"
and config.materialx_materials[1].shader_key ~= "" then
map_shader_key = config.materialx_materials[1].shader_key
log_debug("Using MaterialX shader_key for Quake3 map=%s", map_shader_key)
else
error("Quake3 config requires a shader_key or materialx_materials[1].shader_key")
local materials = config_resolver.resolve_materialx_materials(config)
if type(materials) == "table"
and type(materials[1]) == "table"
and type(materials[1].shader_key) == "string"
and materials[1].shader_key ~= "" then
map_shader_key = materials[1].shader_key
end
end
if map_shader_key then
log_debug("Using shader_key for Quake3 map=%s", map_shader_key)
else
error("Quake3 config requires a shader_key or a MaterialX materials shader_key")
end
local function scale_matrix(x, y, z)
@@ -218,7 +224,7 @@ local fallback_bindings = {
noclip_toggle = "N",
}
local input_bindings = resolve_table(type(config) == "table" and config.input_bindings)
local input_bindings = resolve_table(config_resolver.resolve_input_bindings(config))
local function get_binding(action_name)
if type(input_bindings[action_name]) == "string" then
return input_bindings[action_name]
+4 -2
View File
@@ -2,6 +2,7 @@
-- Provides mesh generation, object builders, and utility functions
local math3d = require("math3d")
local config_resolver = require("config_resolver")
local framework = {}
@@ -328,8 +329,9 @@ function framework.MaterialRegistry.new(config)
self.materials = {}
self.default_key = nil
if config and config.materialx_materials then
for i, mat in ipairs(config.materialx_materials) do
local materials = config_resolver.resolve_materialx_materials(config)
if type(materials) == "table" then
for i, mat in ipairs(materials) do
if mat.shader_key then
self.materials[mat.shader_key] = mat
if i == 1 then
+18 -7
View File
@@ -1,10 +1,18 @@
local Gui = require("gui")
local math3d = require("math3d")
local config_resolver = require("config_resolver")
local ctx = Gui.newContext()
local input = Gui.newInputState()
local statusMessage = "Select a clip to play"
local function log_debug(fmt, ...)
if not lua_debug or not fmt then
return
end
print(string.format(fmt, ...))
end
local function findScriptDirectory()
local info = debug.getinfo(1, "S")
local source = info.source or ""
@@ -247,17 +255,20 @@ local function createCube(position)
local offset = math3d.translation(position[1], position[2], position[3])
return math3d.multiply(offset, base)
end
local materials = config_resolver.resolve_materialx_materials(config)
if type(materials) ~= "table"
or type(materials[1]) ~= "table"
or type(materials[1].shader_key) ~= "string"
or materials[1].shader_key == "" then
error("Soundboard requires rendering.materialx.materials[1].shader_key or materialx_materials[1].shader_key")
end
local shader_key = materials[1].shader_key
log_debug("Soundboard using material shader_key=%s", shader_key)
return {
vertices = cubeVertices,
indices = cubeIndices,
compute_model_matrix = computeModel,
if type(config) ~= "table" or type(config.materialx_materials) ~= "table" or
type(config.materialx_materials[1]) ~= "table" or
type(config.materialx_materials[1].shader_key) ~= "string" or
config.materialx_materials[1].shader_key == "" then
error("Soundboard requires materialx_materials[1].shader_key")
end
shader_keys = {config.materialx_materials[1].shader_key},
shader_keys = {shader_key},
}
end
+8 -4
View File
@@ -214,10 +214,14 @@ assert_equal(time_value, 42, "dynamic compute function receives time")
print("Testing material registry...")
local test_config = {
materialx_materials = {
{shader_key = "floor", document = "floor.mtlx", material = "Floor"},
{shader_key = "wall", document = "wall.mtlx", material = "Wall"},
{shader_key = "ceiling", document = "ceiling.mtlx", material = "Ceiling"},
rendering = {
materialx = {
materials = {
{shader_key = "floor", document = "floor.mtlx", material = "Floor"},
{shader_key = "wall", document = "wall.mtlx", material = "Wall"},
{shader_key = "ceiling", document = "ceiling.mtlx", material = "Ceiling"},
}
}
}
}