mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
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:
@@ -1,120 +1,137 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"launcher": {
|
||||
"name": "Cube Demo",
|
||||
"description": "3D cube room with first-person controls, lanterns, and physics interactions",
|
||||
"enabled": true
|
||||
},
|
||||
"window_width": 1024,
|
||||
"window_height": 768,
|
||||
"lua_script": "scripts/cube_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"
|
||||
},
|
||||
"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",
|
||||
"music_toggle_gamepad": "start",
|
||||
"gamepad_move_x_axis": "leftx",
|
||||
"gamepad_move_y_axis": "lefty",
|
||||
"gamepad_look_x_axis": "rightx",
|
||||
"gamepad_look_y_axis": "righty",
|
||||
"gamepad_dpad_up": "dpup",
|
||||
"gamepad_dpad_down": "dpdown",
|
||||
"gamepad_dpad_left": "dpleft",
|
||||
"gamepad_dpad_right": "dpright",
|
||||
"gamepad_button_actions": {
|
||||
"a": "gamepad_a",
|
||||
"b": "gamepad_b",
|
||||
"x": "gamepad_x",
|
||||
"y": "gamepad_y",
|
||||
"leftshoulder": "gamepad_lb",
|
||||
"rightshoulder": "gamepad_rb",
|
||||
"leftstick": "gamepad_ls",
|
||||
"rightstick": "gamepad_rs",
|
||||
"back": "gamepad_back",
|
||||
"start": "gamepad_start"
|
||||
"window": {
|
||||
"title": "SDL3 Bgfx Demo",
|
||||
"size": {
|
||||
"width": 1024,
|
||||
"height": 768
|
||||
},
|
||||
"gamepad_axis_actions": {
|
||||
"lefttrigger": "gamepad_lt",
|
||||
"righttrigger": "gamepad_rt"
|
||||
},
|
||||
"gamepad_axis_action_threshold": 0.5
|
||||
},
|
||||
"project_root": "../",
|
||||
"shaders_directory": "shaders",
|
||||
"bgfx": {
|
||||
"renderer": "vulkan"
|
||||
},
|
||||
"materialx": {
|
||||
"enabled": true,
|
||||
"parameters_enabled": true,
|
||||
"library_path": "MaterialX/libraries",
|
||||
"library_folders": [
|
||||
"stdlib",
|
||||
"pbrlib",
|
||||
"lights",
|
||||
"bxdf",
|
||||
"cmlib",
|
||||
"nprlib",
|
||||
"targets"
|
||||
]
|
||||
},
|
||||
"materialx_materials": [
|
||||
{
|
||||
"shader_key": "floor",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx",
|
||||
"material": "Tiled_Wood"
|
||||
},
|
||||
{
|
||||
"shader_key": "wall",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brick_procedural.mtlx",
|
||||
"material": "M_BrickPattern"
|
||||
},
|
||||
{
|
||||
"shader_key": "ceiling",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx",
|
||||
"material": "Marble_3D"
|
||||
},
|
||||
{
|
||||
"shader_key": "solid",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx",
|
||||
"material": "Tiled_Brass"
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"atmospherics": {
|
||||
"ambient_strength": 0.006,
|
||||
"fog_density": 0.006,
|
||||
"fog_color": [0.03, 0.04, 0.06],
|
||||
"sky_color": [0.02, 0.03, 0.05],
|
||||
"gamma": 2.2,
|
||||
"exposure": 1.15,
|
||||
"enable_tone_mapping": true,
|
||||
"enable_shadows": true,
|
||||
"enable_ssgi": true,
|
||||
"enable_volumetric_lighting": true,
|
||||
"pbr_roughness": 0.28,
|
||||
"pbr_metallic": 0.08
|
||||
},
|
||||
"gui_font": {
|
||||
"use_freetype": true,
|
||||
"font_path": "scripts/assets/fonts/Roboto-Regular.ttf",
|
||||
"font_size": 18.0
|
||||
"scripts": {
|
||||
"entry": "scripts/cube_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",
|
||||
"music_toggle_gamepad": "start",
|
||||
"gamepad_move_x_axis": "leftx",
|
||||
"gamepad_move_y_axis": "lefty",
|
||||
"gamepad_look_x_axis": "rightx",
|
||||
"gamepad_look_y_axis": "righty",
|
||||
"gamepad_dpad_up": "dpup",
|
||||
"gamepad_dpad_down": "dpdown",
|
||||
"gamepad_dpad_left": "dpleft",
|
||||
"gamepad_dpad_right": "dpright",
|
||||
"gamepad_button_actions": {
|
||||
"a": "gamepad_a",
|
||||
"b": "gamepad_b",
|
||||
"x": "gamepad_x",
|
||||
"y": "gamepad_y",
|
||||
"leftshoulder": "gamepad_lb",
|
||||
"rightshoulder": "gamepad_rb",
|
||||
"leftstick": "gamepad_ls",
|
||||
"rightstick": "gamepad_rs",
|
||||
"back": "gamepad_back",
|
||||
"start": "gamepad_start"
|
||||
},
|
||||
"gamepad_axis_actions": {
|
||||
"lefttrigger": "gamepad_lt",
|
||||
"righttrigger": "gamepad_rt"
|
||||
},
|
||||
"gamepad_axis_action_threshold": 0.5
|
||||
}
|
||||
},
|
||||
"rendering": {
|
||||
"bgfx": {
|
||||
"renderer": "vulkan"
|
||||
},
|
||||
"materialx": {
|
||||
"enabled": true,
|
||||
"parameters_enabled": true,
|
||||
"library_path": "MaterialX/libraries",
|
||||
"library_folders": [
|
||||
"stdlib",
|
||||
"pbrlib",
|
||||
"lights",
|
||||
"bxdf",
|
||||
"cmlib",
|
||||
"nprlib",
|
||||
"targets"
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"shader_key": "floor",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx",
|
||||
"material": "Tiled_Wood"
|
||||
},
|
||||
{
|
||||
"shader_key": "wall",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brick_procedural.mtlx",
|
||||
"material": "M_BrickPattern"
|
||||
},
|
||||
{
|
||||
"shader_key": "ceiling",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx",
|
||||
"material": "Marble_3D"
|
||||
},
|
||||
{
|
||||
"shader_key": "solid",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx",
|
||||
"material": "Tiled_Brass"
|
||||
}
|
||||
]
|
||||
},
|
||||
"atmospherics": {
|
||||
"ambient_strength": 0.006,
|
||||
"fog_density": 0.006,
|
||||
"fog_color": [0.03, 0.04, 0.06],
|
||||
"sky_color": [0.02, 0.03, 0.05],
|
||||
"gamma": 2.2,
|
||||
"exposure": 1.15,
|
||||
"enable_tone_mapping": true,
|
||||
"enable_shadows": true,
|
||||
"enable_ssgi": true,
|
||||
"enable_volumetric_lighting": true,
|
||||
"pbr_roughness": 0.28,
|
||||
"pbr_metallic": 0.08
|
||||
}
|
||||
},
|
||||
"gui": {
|
||||
"font": {
|
||||
"use_freetype": true,
|
||||
"font_path": "scripts/assets/fonts/Roboto-Regular.ttf",
|
||||
"font_size": 18.0
|
||||
},
|
||||
"opacity": 1.0
|
||||
},
|
||||
"gui_opacity": 1.0,
|
||||
"config_file": "config/seed_runtime.json"
|
||||
}
|
||||
|
||||
@@ -1,120 +1,137 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"launcher": {
|
||||
"name": "Cube Demo",
|
||||
"description": "3D cube room with first-person controls, lanterns, and physics interactions",
|
||||
"enabled": true
|
||||
},
|
||||
"window_width": 1024,
|
||||
"window_height": 768,
|
||||
"lua_script": "scripts/cube_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"
|
||||
},
|
||||
"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",
|
||||
"music_toggle_gamepad": "start",
|
||||
"gamepad_move_x_axis": "leftx",
|
||||
"gamepad_move_y_axis": "lefty",
|
||||
"gamepad_look_x_axis": "rightx",
|
||||
"gamepad_look_y_axis": "righty",
|
||||
"gamepad_dpad_up": "dpup",
|
||||
"gamepad_dpad_down": "dpdown",
|
||||
"gamepad_dpad_left": "dpleft",
|
||||
"gamepad_dpad_right": "dpright",
|
||||
"gamepad_button_actions": {
|
||||
"a": "gamepad_a",
|
||||
"b": "gamepad_b",
|
||||
"x": "gamepad_x",
|
||||
"y": "gamepad_y",
|
||||
"leftshoulder": "gamepad_lb",
|
||||
"rightshoulder": "gamepad_rb",
|
||||
"leftstick": "gamepad_ls",
|
||||
"rightstick": "gamepad_rs",
|
||||
"back": "gamepad_back",
|
||||
"start": "gamepad_start"
|
||||
"window": {
|
||||
"title": "SDL3 Bgfx Demo",
|
||||
"size": {
|
||||
"width": 1024,
|
||||
"height": 768
|
||||
},
|
||||
"gamepad_axis_actions": {
|
||||
"lefttrigger": "gamepad_lt",
|
||||
"righttrigger": "gamepad_rt"
|
||||
},
|
||||
"gamepad_axis_action_threshold": 0.5
|
||||
},
|
||||
"project_root": "../",
|
||||
"shaders_directory": "shaders",
|
||||
"bgfx": {
|
||||
"renderer": "opengl"
|
||||
},
|
||||
"materialx": {
|
||||
"enabled": true,
|
||||
"parameters_enabled": true,
|
||||
"library_path": "MaterialX/libraries",
|
||||
"library_folders": [
|
||||
"stdlib",
|
||||
"pbrlib",
|
||||
"lights",
|
||||
"bxdf",
|
||||
"cmlib",
|
||||
"nprlib",
|
||||
"targets"
|
||||
]
|
||||
},
|
||||
"materialx_materials": [
|
||||
{
|
||||
"shader_key": "floor",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx",
|
||||
"material": "Tiled_Wood"
|
||||
},
|
||||
{
|
||||
"shader_key": "wall",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brick_procedural.mtlx",
|
||||
"material": "M_BrickPattern"
|
||||
},
|
||||
{
|
||||
"shader_key": "ceiling",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx",
|
||||
"material": "Marble_3D"
|
||||
},
|
||||
{
|
||||
"shader_key": "solid",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx",
|
||||
"material": "Tiled_Brass"
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"atmospherics": {
|
||||
"ambient_strength": 0.006,
|
||||
"fog_density": 0.006,
|
||||
"fog_color": [0.03, 0.04, 0.06],
|
||||
"sky_color": [0.02, 0.03, 0.05],
|
||||
"gamma": 2.2,
|
||||
"exposure": 1.15,
|
||||
"enable_tone_mapping": true,
|
||||
"enable_shadows": true,
|
||||
"enable_ssgi": true,
|
||||
"enable_volumetric_lighting": true,
|
||||
"pbr_roughness": 0.28,
|
||||
"pbr_metallic": 0.08
|
||||
},
|
||||
"gui_font": {
|
||||
"use_freetype": true,
|
||||
"font_path": "scripts/assets/fonts/Roboto-Regular.ttf",
|
||||
"font_size": 18.0
|
||||
"scripts": {
|
||||
"entry": "scripts/cube_logic.lua",
|
||||
"lua_debug": false
|
||||
},
|
||||
"gui_opacity": 1.0,
|
||||
"config_file": "config/seed_runtime.json"
|
||||
"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",
|
||||
"music_toggle_gamepad": "start",
|
||||
"gamepad_move_x_axis": "leftx",
|
||||
"gamepad_move_y_axis": "lefty",
|
||||
"gamepad_look_x_axis": "rightx",
|
||||
"gamepad_look_y_axis": "righty",
|
||||
"gamepad_dpad_up": "dpup",
|
||||
"gamepad_dpad_down": "dpdown",
|
||||
"gamepad_dpad_left": "dpleft",
|
||||
"gamepad_dpad_right": "dpright",
|
||||
"gamepad_button_actions": {
|
||||
"a": "gamepad_a",
|
||||
"b": "gamepad_b",
|
||||
"x": "gamepad_x",
|
||||
"y": "gamepad_y",
|
||||
"leftshoulder": "gamepad_lb",
|
||||
"rightshoulder": "gamepad_rb",
|
||||
"leftstick": "gamepad_ls",
|
||||
"rightstick": "gamepad_rs",
|
||||
"back": "gamepad_back",
|
||||
"start": "gamepad_start"
|
||||
},
|
||||
"gamepad_axis_actions": {
|
||||
"lefttrigger": "gamepad_lt",
|
||||
"righttrigger": "gamepad_rt"
|
||||
},
|
||||
"gamepad_axis_action_threshold": 0.5
|
||||
}
|
||||
},
|
||||
"rendering": {
|
||||
"bgfx": {
|
||||
"renderer": "opengl"
|
||||
},
|
||||
"materialx": {
|
||||
"enabled": true,
|
||||
"parameters_enabled": true,
|
||||
"library_path": "MaterialX/libraries",
|
||||
"library_folders": [
|
||||
"stdlib",
|
||||
"pbrlib",
|
||||
"lights",
|
||||
"bxdf",
|
||||
"cmlib",
|
||||
"nprlib",
|
||||
"targets"
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"shader_key": "floor",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx",
|
||||
"material": "Tiled_Wood"
|
||||
},
|
||||
{
|
||||
"shader_key": "wall",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brick_procedural.mtlx",
|
||||
"material": "M_BrickPattern"
|
||||
},
|
||||
{
|
||||
"shader_key": "ceiling",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx",
|
||||
"material": "Marble_3D"
|
||||
},
|
||||
{
|
||||
"shader_key": "solid",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx",
|
||||
"material": "Tiled_Brass"
|
||||
}
|
||||
]
|
||||
},
|
||||
"atmospherics": {
|
||||
"ambient_strength": 0.006,
|
||||
"fog_density": 0.006,
|
||||
"fog_color": [0.03, 0.04, 0.06],
|
||||
"sky_color": [0.02, 0.03, 0.05],
|
||||
"gamma": 2.2,
|
||||
"exposure": 1.15,
|
||||
"enable_tone_mapping": true,
|
||||
"enable_shadows": true,
|
||||
"enable_ssgi": true,
|
||||
"enable_volumetric_lighting": true,
|
||||
"pbr_roughness": 0.28,
|
||||
"pbr_metallic": 0.08
|
||||
}
|
||||
},
|
||||
"gui": {
|
||||
"font": {
|
||||
"use_freetype": true,
|
||||
"font_path": "scripts/assets/fonts/Roboto-Regular.ttf",
|
||||
"font_size": 18.0
|
||||
},
|
||||
"opacity": 1.0
|
||||
},
|
||||
"config_file": "config/seed_runtime_opengl.json"
|
||||
}
|
||||
|
||||
85
scripts/config_resolver.lua
Normal file
85
scripts/config_resolver.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,14 +132,15 @@ void ServiceBasedApp::Run() {
|
||||
|
||||
// Run the main application loop with crash recovery
|
||||
if (crashRecoveryService_) {
|
||||
constexpr int kMainLoopTimeoutMs = 24 * 60 * 60 * 1000; // Safety net; heartbeat monitor handles hangs.
|
||||
bool success = crashRecoveryService_->ExecuteWithTimeout(
|
||||
[this]() { applicationLoopService_->Run(); },
|
||||
30000, // 30 second timeout for the main loop
|
||||
kMainLoopTimeoutMs,
|
||||
"Main Application Loop"
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
logger_->Warn("ServiceBasedApp::Run: Main loop timed out, attempting recovery");
|
||||
logger_->Warn("ServiceBasedApp::Run: Main loop stopped by crash recovery, attempting recovery");
|
||||
if (crashRecoveryService_->AttemptRecovery()) {
|
||||
logger_->Info("ServiceBasedApp::Run: Recovery successful, restarting main loop");
|
||||
applicationLoopService_->Run(); // Try again
|
||||
@@ -329,7 +330,8 @@ void ServiceBasedApp::RegisterServices() {
|
||||
registry_.GetService<services::IPhysicsService>(),
|
||||
registry_.GetService<services::ISceneService>(),
|
||||
registry_.GetService<services::IRenderCoordinatorService>(),
|
||||
registry_.GetService<services::IAudioService>());
|
||||
registry_.GetService<services::IAudioService>(),
|
||||
registry_.GetService<services::ICrashRecoveryService>());
|
||||
|
||||
logger_->Trace("ServiceBasedApp", "RegisterServices", "", "Exiting");
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ ApplicationLoopService::ApplicationLoopService(std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IPhysicsService> physicsService,
|
||||
std::shared_ptr<ISceneService> sceneService,
|
||||
std::shared_ptr<IRenderCoordinatorService> renderCoordinatorService,
|
||||
std::shared_ptr<IAudioService> audioService)
|
||||
std::shared_ptr<IAudioService> audioService,
|
||||
std::shared_ptr<ICrashRecoveryService> crashRecoveryService)
|
||||
: logger_(std::move(logger)),
|
||||
windowService_(std::move(windowService)),
|
||||
eventBus_(std::move(eventBus)),
|
||||
@@ -19,7 +20,8 @@ ApplicationLoopService::ApplicationLoopService(std::shared_ptr<ILogger> logger,
|
||||
physicsService_(std::move(physicsService)),
|
||||
sceneService_(std::move(sceneService)),
|
||||
renderCoordinatorService_(std::move(renderCoordinatorService)),
|
||||
audioService_(std::move(audioService)) {
|
||||
audioService_(std::move(audioService)),
|
||||
crashRecoveryService_(std::move(crashRecoveryService)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("ApplicationLoopService", "ApplicationLoopService",
|
||||
"windowService=" + std::string(windowService_ ? "set" : "null") +
|
||||
@@ -28,7 +30,8 @@ ApplicationLoopService::ApplicationLoopService(std::shared_ptr<ILogger> logger,
|
||||
", physicsService=" + std::string(physicsService_ ? "set" : "null") +
|
||||
", sceneService=" + std::string(sceneService_ ? "set" : "null") +
|
||||
", renderCoordinatorService=" + std::string(renderCoordinatorService_ ? "set" : "null") +
|
||||
", audioService=" + std::string(audioService_ ? "set" : "null"),
|
||||
", audioService=" + std::string(audioService_ ? "set" : "null") +
|
||||
", crashRecoveryService=" + std::string(crashRecoveryService_ ? "set" : "null"),
|
||||
"Created");
|
||||
}
|
||||
}
|
||||
@@ -49,6 +52,18 @@ void ApplicationLoopService::Run() {
|
||||
float deltaTime = std::chrono::duration<float>(currentTime - lastTime).count();
|
||||
float elapsedTime = std::chrono::duration<float>(currentTime - startTime).count();
|
||||
lastTime = currentTime;
|
||||
const double elapsedSeconds = static_cast<double>(elapsedTime);
|
||||
|
||||
if (crashRecoveryService_) {
|
||||
crashRecoveryService_->RecordFrameHeartbeat(static_cast<double>(deltaTime));
|
||||
if (elapsedSeconds - lastMemoryCheckSeconds_ >= memoryCheckIntervalSeconds_) {
|
||||
lastMemoryCheckSeconds_ = elapsedSeconds;
|
||||
if (!crashRecoveryService_->CheckMemoryHealth()) {
|
||||
logger_->Warn("ApplicationLoopService::Run: Memory health check failed, stopping loop");
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HandleEvents();
|
||||
ProcessFrame(deltaTime, elapsedTime);
|
||||
|
||||
@@ -24,6 +24,12 @@ std::string BuildStackTrace() {
|
||||
}
|
||||
return trace.to_string();
|
||||
}
|
||||
|
||||
int64_t GetSteadyClockNs() {
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
}
|
||||
|
||||
// Static instance for signal handler
|
||||
@@ -33,13 +39,16 @@ CrashRecoveryService::CrashRecoveryService(std::shared_ptr<ILogger> logger)
|
||||
: logger_(logger)
|
||||
, crashDetected_(false)
|
||||
, lastSignal_(0)
|
||||
, signalHandlersInstalled_(false)
|
||||
, lastHeartbeatNs_(0)
|
||||
, heartbeatSeen_(false)
|
||||
, heartbeatMonitorRunning_(false)
|
||||
, lastSuccessfulFrameTime_(0.0)
|
||||
, consecutiveFrameTimeouts_(0)
|
||||
, luaExecutionFailures_(0)
|
||||
, fileFormatErrors_(0)
|
||||
, memoryWarnings_(0)
|
||||
, lastHealthCheck_(std::chrono::steady_clock::now()) {
|
||||
, lastHealthCheck_(std::chrono::steady_clock::now())
|
||||
, signalHandlersInstalled_(false) {
|
||||
logger_->Trace("CrashRecoveryService", "CrashRecoveryService",
|
||||
"logger=" + std::string(logger_ ? "set" : "null"),
|
||||
"Created");
|
||||
@@ -56,14 +65,25 @@ void CrashRecoveryService::Initialize() {
|
||||
SetupSignalHandlers();
|
||||
crashDetected_ = false;
|
||||
lastSignal_ = 0;
|
||||
lastHeartbeatNs_ = 0;
|
||||
heartbeatSeen_ = false;
|
||||
crashReport_.clear();
|
||||
|
||||
if (!heartbeatMonitorRunning_.exchange(true)) {
|
||||
heartbeatMonitorThread_ = std::thread(&CrashRecoveryService::MonitorHeartbeats, this);
|
||||
}
|
||||
|
||||
logger_->Info("CrashRecoveryService::Initialize: Crash recovery service initialized");
|
||||
}
|
||||
|
||||
void CrashRecoveryService::Shutdown() {
|
||||
logger_->Trace("CrashRecoveryService", "Shutdown", "", "Shutting down crash recovery service");
|
||||
|
||||
heartbeatMonitorRunning_ = false;
|
||||
if (heartbeatMonitorThread_.joinable()) {
|
||||
heartbeatMonitorThread_.join();
|
||||
}
|
||||
|
||||
RemoveSignalHandlers();
|
||||
|
||||
logger_->Info("CrashRecoveryService::Shutdown: Crash recovery service shutdown");
|
||||
@@ -87,47 +107,102 @@ bool CrashRecoveryService::ExecuteWithTimeout(std::function<void()> func, int ti
|
||||
}
|
||||
});
|
||||
|
||||
if (completionFuture.wait_for(std::chrono::milliseconds(timeoutMs)) == std::future_status::timeout) {
|
||||
logger_->Warn("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' timed out after " + std::to_string(timeoutMs) + "ms");
|
||||
logger_->Trace("CrashRecoveryService", "ExecuteWithTimeout",
|
||||
"timeoutMs=" + std::to_string(timeoutMs) + ", operationName=" + operationName,
|
||||
"Timeout detected; marking crash and detaching worker thread");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto timeoutDuration = std::chrono::milliseconds(timeoutMs);
|
||||
const auto pollInterval = std::chrono::milliseconds(50);
|
||||
|
||||
{
|
||||
while (true) {
|
||||
if (completionFuture.wait_for(pollInterval) == std::future_status::ready) {
|
||||
try {
|
||||
completionFuture.get(); // Re-throw any exceptions
|
||||
logger_->Trace("CrashRecoveryService", "ExecuteWithTimeout", "", "Operation completed successfully");
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
logger_->Error("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' threw exception: " + e.what());
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
throw;
|
||||
} catch (...) {
|
||||
logger_->Error("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' threw unknown exception");
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (crashDetected_.load()) {
|
||||
logger_->Warn("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' aborted after crash detection");
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.detach();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timeoutMs >= 0 && std::chrono::steady_clock::now() - start >= timeoutDuration) {
|
||||
logger_->Warn("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' timed out after " + std::to_string(timeoutMs) + "ms");
|
||||
logger_->Trace("CrashRecoveryService", "ExecuteWithTimeout",
|
||||
"timeoutMs=" + std::to_string(timeoutMs) + ", operationName=" + operationName,
|
||||
"Timeout detected; marking crash and detaching worker thread");
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(crashMutex_);
|
||||
crashDetected_ = true;
|
||||
crashReport_ += "\nTIMEOUT: Operation '" + operationName + "' exceeded " +
|
||||
std::to_string(timeoutMs) + "ms\n";
|
||||
}
|
||||
|
||||
// No safe way to cancel; detach so we can report the timeout promptly.
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.detach();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CrashRecoveryService::RecordFrameHeartbeat(double frameTimeSeconds) {
|
||||
lastHeartbeatNs_.store(GetSteadyClockNs(), std::memory_order_relaxed);
|
||||
heartbeatSeen_.store(true, std::memory_order_release);
|
||||
lastSuccessfulFrameTime_ = frameTimeSeconds;
|
||||
}
|
||||
|
||||
void CrashRecoveryService::MonitorHeartbeats() {
|
||||
logger_->Trace("CrashRecoveryService", "MonitorHeartbeats", "", "Starting heartbeat monitor");
|
||||
|
||||
const int64_t timeoutNs = static_cast<int64_t>(heartbeatTimeout_.count()) * 1000 * 1000;
|
||||
while (heartbeatMonitorRunning_.load()) {
|
||||
std::this_thread::sleep_for(heartbeatPollInterval_);
|
||||
if (!heartbeatSeen_.load(std::memory_order_acquire)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int64_t lastHeartbeat = lastHeartbeatNs_.load(std::memory_order_relaxed);
|
||||
if (lastHeartbeat == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int64_t nowNs = GetSteadyClockNs();
|
||||
const int64_t elapsedNs = nowNs - lastHeartbeat;
|
||||
if (elapsedNs >= timeoutNs) {
|
||||
const int64_t elapsedMs = elapsedNs / (1000 * 1000);
|
||||
std::lock_guard<std::mutex> lock(crashMutex_);
|
||||
crashDetected_ = true;
|
||||
crashReport_ += "\nTIMEOUT: Operation '" + operationName + "' exceeded " +
|
||||
std::to_string(timeoutMs) + "ms\n";
|
||||
if (!crashDetected_) {
|
||||
crashDetected_ = true;
|
||||
crashReport_ += "\nHEARTBEAT TIMEOUT: No frame heartbeat for " +
|
||||
std::to_string(elapsedMs) + "ms\n";
|
||||
logger_->Error("CrashRecoveryService::MonitorHeartbeats: Frame heartbeat stalled for " +
|
||||
std::to_string(elapsedMs) + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
// No safe way to cancel; detach so we can report the timeout promptly.
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.detach();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
completionFuture.get(); // Re-throw any exceptions
|
||||
logger_->Trace("CrashRecoveryService", "ExecuteWithTimeout", "", "Operation completed successfully");
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
logger_->Error("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' threw exception: " + e.what());
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
throw;
|
||||
} catch (...) {
|
||||
logger_->Error("CrashRecoveryService::ExecuteWithTimeout: Operation '" + operationName + "' threw unknown exception");
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
logger_->Trace("CrashRecoveryService", "MonitorHeartbeats", "", "Heartbeat monitor exiting");
|
||||
}
|
||||
|
||||
bool CrashRecoveryService::IsCrashDetected() const {
|
||||
@@ -150,6 +225,8 @@ bool CrashRecoveryService::AttemptRecovery() {
|
||||
crashDetected_ = false;
|
||||
lastSignal_ = 0;
|
||||
crashReport_.clear();
|
||||
lastHeartbeatNs_ = 0;
|
||||
heartbeatSeen_ = false;
|
||||
logger_->Info("CrashRecoveryService::AttemptRecovery: Recovery successful");
|
||||
} else {
|
||||
logger_->Error("CrashRecoveryService::AttemptRecovery: Recovery failed");
|
||||
@@ -435,6 +512,13 @@ std::string CrashRecoveryService::GetSystemHealthStatus() const {
|
||||
ss << "File Format Errors: " << fileFormatErrors_ << "\n";
|
||||
ss << "Memory Warnings: " << memoryWarnings_ << "\n";
|
||||
ss << "Last Successful Frame Time: " << lastSuccessfulFrameTime_ << " seconds\n";
|
||||
if (heartbeatSeen_.load(std::memory_order_acquire)) {
|
||||
const int64_t lastHeartbeat = lastHeartbeatNs_.load(std::memory_order_relaxed);
|
||||
const int64_t ageMs = (GetSteadyClockNs() - lastHeartbeat) / (1000 * 1000);
|
||||
ss << "Last Frame Heartbeat Age: " << ageMs << " ms\n";
|
||||
} else {
|
||||
ss << "Last Frame Heartbeat Age: n/a\n";
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto timeSinceLastCheck = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <csignal>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
@@ -27,6 +28,7 @@ public:
|
||||
void Initialize() override;
|
||||
void Shutdown() override;
|
||||
bool ExecuteWithTimeout(std::function<void()> func, int timeoutMs, const std::string& operationName) override;
|
||||
void RecordFrameHeartbeat(double frameTimeSeconds) override;
|
||||
bool IsCrashDetected() const override;
|
||||
bool AttemptRecovery() override;
|
||||
std::string GetCrashReport() const override;
|
||||
@@ -50,10 +52,17 @@ private:
|
||||
void UpdateHealthMetrics();
|
||||
size_t GetCurrentMemoryUsage() const;
|
||||
bool IsGpuResponsive() const;
|
||||
void MonitorHeartbeats();
|
||||
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::atomic<bool> crashDetected_;
|
||||
std::atomic<int> lastSignal_;
|
||||
std::atomic<int64_t> lastHeartbeatNs_;
|
||||
std::atomic<bool> heartbeatSeen_;
|
||||
std::atomic<bool> heartbeatMonitorRunning_;
|
||||
std::thread heartbeatMonitorThread_;
|
||||
std::chrono::milliseconds heartbeatTimeout_{5000};
|
||||
std::chrono::milliseconds heartbeatPollInterval_{200};
|
||||
std::string crashReport_;
|
||||
mutable std::mutex crashMutex_;
|
||||
|
||||
@@ -74,4 +83,4 @@ private:
|
||||
bool signalHandlersInstalled_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
} // namespace sdl3cpp::services::impl
|
||||
|
||||
@@ -110,15 +110,68 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
}
|
||||
}
|
||||
|
||||
const char* scriptField = "lua_script";
|
||||
if (!document.HasMember(scriptField) || !document[scriptField].IsString()) {
|
||||
throw std::runtime_error("JSON config requires a string member '" + std::string(scriptField) + "'");
|
||||
auto getObjectMember = [&](const rapidjson::Value& parent,
|
||||
const char* name,
|
||||
const char* fullName) -> const rapidjson::Value* {
|
||||
if (!parent.HasMember(name)) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& value = parent[name];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member '" + std::string(fullName) + "' must be an object");
|
||||
}
|
||||
return &value;
|
||||
};
|
||||
|
||||
const auto* scriptsValue = getObjectMember(document, "scripts", "scripts");
|
||||
const auto* pathsValue = getObjectMember(document, "paths", "paths");
|
||||
const auto* windowValue = getObjectMember(document, "window", "window");
|
||||
const auto* windowSizeValue = windowValue
|
||||
? getObjectMember(*windowValue, "size", "window.size")
|
||||
: nullptr;
|
||||
const auto* inputValue = getObjectMember(document, "input", "input");
|
||||
const auto* inputBindingsValue = inputValue
|
||||
? getObjectMember(*inputValue, "bindings", "input.bindings")
|
||||
: nullptr;
|
||||
const auto* renderingValue = getObjectMember(document, "rendering", "rendering");
|
||||
const auto* guiValue = getObjectMember(document, "gui", "gui");
|
||||
|
||||
std::optional<std::string> 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<std::filesystem::path> projectRoot;
|
||||
const char* projectRootField = "project_root";
|
||||
if (document.HasMember(projectRootField) && document[projectRootField].IsString()) {
|
||||
std::filesystem::path candidate(document[projectRootField].GetString());
|
||||
if (pathsValue && pathsValue->HasMember("project_root")) {
|
||||
const auto& value = (*pathsValue)["project_root"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'paths.project_root' must be a string");
|
||||
}
|
||||
std::filesystem::path candidate(value.GetString());
|
||||
if (candidate.is_absolute()) {
|
||||
projectRoot = std::filesystem::weakly_canonical(candidate);
|
||||
} else {
|
||||
projectRoot = std::filesystem::weakly_canonical(configPath.parent_path() / candidate);
|
||||
}
|
||||
} else if (document.HasMember("project_root")) {
|
||||
const auto& value = document["project_root"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'project_root' must be a string");
|
||||
}
|
||||
std::filesystem::path candidate(value.GetString());
|
||||
if (candidate.is_absolute()) {
|
||||
projectRoot = std::filesystem::weakly_canonical(candidate);
|
||||
} else {
|
||||
@@ -127,8 +180,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
}
|
||||
|
||||
RuntimeConfig config;
|
||||
const auto& scriptValue = document[scriptField];
|
||||
std::filesystem::path scriptPath(scriptValue.GetString());
|
||||
std::filesystem::path scriptPath(*scriptPathValue);
|
||||
if (!scriptPath.is_absolute()) {
|
||||
if (projectRoot) {
|
||||
scriptPath = *projectRoot / scriptPath;
|
||||
@@ -159,10 +211,38 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
throw std::runtime_error(std::string("JSON member '") + name + "' must be a non-negative integer");
|
||||
};
|
||||
|
||||
config.width = parseDimension("window_width", config.width);
|
||||
config.height = parseDimension("window_height", config.height);
|
||||
auto parseDimensionValue = [&](const rapidjson::Value& value, const char* name) -> uint32_t {
|
||||
if (value.IsUint()) {
|
||||
return value.GetUint();
|
||||
}
|
||||
if (value.IsInt()) {
|
||||
int maybeValue = value.GetInt();
|
||||
if (maybeValue >= 0) {
|
||||
return static_cast<uint32_t>(maybeValue);
|
||||
}
|
||||
}
|
||||
throw std::runtime_error(std::string("JSON member '") + name + "' must be a non-negative integer");
|
||||
};
|
||||
|
||||
if (document.HasMember("lua_debug")) {
|
||||
if (windowSizeValue) {
|
||||
if (windowSizeValue->HasMember("width")) {
|
||||
config.width = parseDimensionValue((*windowSizeValue)["width"], "window.size.width");
|
||||
}
|
||||
if (windowSizeValue->HasMember("height")) {
|
||||
config.height = parseDimensionValue((*windowSizeValue)["height"], "window.size.height");
|
||||
}
|
||||
} else {
|
||||
config.width = parseDimension("window_width", config.width);
|
||||
config.height = parseDimension("window_height", config.height);
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -170,7 +250,13 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
config.luaDebug = value.GetBool();
|
||||
}
|
||||
|
||||
if (document.HasMember("window_title")) {
|
||||
if (windowValue && windowValue->HasMember("title")) {
|
||||
const auto& value = (*windowValue)["title"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'window.title' must be a string");
|
||||
}
|
||||
config.windowTitle = value.GetString();
|
||||
} else if (document.HasMember("window_title")) {
|
||||
const auto& value = document["window_title"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'window_title' must be a string");
|
||||
@@ -178,28 +264,40 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
config.windowTitle = value.GetString();
|
||||
}
|
||||
|
||||
if (document.HasMember("mouse_grab")) {
|
||||
const auto& mouseGrabValue = document["mouse_grab"];
|
||||
if (!mouseGrabValue.IsObject()) {
|
||||
const rapidjson::Value* mouseGrabValue = nullptr;
|
||||
std::string mouseGrabPath = "mouse_grab";
|
||||
if (windowValue && windowValue->HasMember("mouse_grab")) {
|
||||
const auto& value = (*windowValue)["mouse_grab"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'window.mouse_grab' must be an object");
|
||||
}
|
||||
mouseGrabValue = &value;
|
||||
mouseGrabPath = "window.mouse_grab";
|
||||
} else if (document.HasMember("mouse_grab")) {
|
||||
const auto& value = document["mouse_grab"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'mouse_grab' must be an object");
|
||||
}
|
||||
mouseGrabValue = &value;
|
||||
}
|
||||
if (mouseGrabValue) {
|
||||
auto readBool = [&](const char* name, bool& target) {
|
||||
if (!mouseGrabValue.HasMember(name)) {
|
||||
if (!mouseGrabValue->HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = mouseGrabValue[name];
|
||||
const auto& value = (*mouseGrabValue)[name];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'mouse_grab." + std::string(name) + "' must be a boolean");
|
||||
throw std::runtime_error("JSON member '" + mouseGrabPath + "." + std::string(name) + "' must be a boolean");
|
||||
}
|
||||
target = value.GetBool();
|
||||
};
|
||||
auto readString = [&](const char* name, std::string& target) {
|
||||
if (!mouseGrabValue.HasMember(name)) {
|
||||
if (!mouseGrabValue->HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = mouseGrabValue[name];
|
||||
const auto& value = (*mouseGrabValue)[name];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'mouse_grab." + std::string(name) + "' must be a string");
|
||||
throw std::runtime_error("JSON member '" + mouseGrabPath + "." + std::string(name) + "' must be a string");
|
||||
}
|
||||
target = value.GetString();
|
||||
};
|
||||
@@ -213,11 +311,19 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
readString("release_key", config.mouseGrab.releaseKey);
|
||||
}
|
||||
|
||||
if (document.HasMember("input_bindings")) {
|
||||
const auto& bindingsValue = document["input_bindings"];
|
||||
if (!bindingsValue.IsObject()) {
|
||||
const rapidjson::Value* bindingsValue = nullptr;
|
||||
std::string bindingsPath = "input_bindings";
|
||||
if (inputBindingsValue) {
|
||||
bindingsValue = inputBindingsValue;
|
||||
bindingsPath = "input.bindings";
|
||||
} else if (document.HasMember("input_bindings")) {
|
||||
const auto& value = document["input_bindings"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'input_bindings' must be an object");
|
||||
}
|
||||
bindingsValue = &value;
|
||||
}
|
||||
if (bindingsValue) {
|
||||
struct BindingSpec {
|
||||
const char* name;
|
||||
std::string InputBindings::* member;
|
||||
@@ -244,12 +350,12 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
}};
|
||||
|
||||
auto readBinding = [&](const BindingSpec& spec) {
|
||||
if (!bindingsValue.HasMember(spec.name)) {
|
||||
if (!bindingsValue->HasMember(spec.name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = bindingsValue[spec.name];
|
||||
const auto& value = (*bindingsValue)[spec.name];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'input_bindings." + std::string(spec.name) + "' must be a string");
|
||||
throw std::runtime_error("JSON member '" + bindingsPath + "." + std::string(spec.name) + "' must be a string");
|
||||
}
|
||||
config.inputBindings.*(spec.member) = value.GetString();
|
||||
};
|
||||
@@ -260,16 +366,16 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
|
||||
auto readMapping = [&](const char* name,
|
||||
std::unordered_map<std::string, std::string>& target) {
|
||||
if (!bindingsValue.HasMember(name)) {
|
||||
if (!bindingsValue->HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& mappingValue = bindingsValue[name];
|
||||
const auto& mappingValue = (*bindingsValue)[name];
|
||||
if (!mappingValue.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must be an object");
|
||||
throw std::runtime_error("JSON member '" + bindingsPath + "." + std::string(name) + "' must be an object");
|
||||
}
|
||||
for (auto it = mappingValue.MemberBegin(); it != mappingValue.MemberEnd(); ++it) {
|
||||
if (!it->name.IsString() || !it->value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'input_bindings." + std::string(name) +
|
||||
throw std::runtime_error("JSON member '" + bindingsPath + "." + std::string(name) +
|
||||
"' must contain string pairs");
|
||||
}
|
||||
std::string key = it->name.GetString();
|
||||
@@ -285,54 +391,69 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
readMapping("gamepad_button_actions", config.inputBindings.gamepadButtonActions);
|
||||
readMapping("gamepad_axis_actions", config.inputBindings.gamepadAxisActions);
|
||||
|
||||
if (bindingsValue.HasMember("gamepad_axis_action_threshold")) {
|
||||
const auto& value = bindingsValue["gamepad_axis_action_threshold"];
|
||||
if (bindingsValue->HasMember("gamepad_axis_action_threshold")) {
|
||||
const auto& value = (*bindingsValue)["gamepad_axis_action_threshold"];
|
||||
if (!value.IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'input_bindings.gamepad_axis_action_threshold' must be a number");
|
||||
throw std::runtime_error("JSON member '" + bindingsPath + ".gamepad_axis_action_threshold' must be a number");
|
||||
}
|
||||
config.inputBindings.gamepadAxisActionThreshold = static_cast<float>(value.GetDouble());
|
||||
}
|
||||
}
|
||||
|
||||
if (document.HasMember("atmospherics")) {
|
||||
const auto& atmosphericsValue = document["atmospherics"];
|
||||
if (!atmosphericsValue.IsObject()) {
|
||||
const rapidjson::Value* atmosphericsValue = nullptr;
|
||||
std::string atmosphericsPath = "atmospherics";
|
||||
if (renderingValue) {
|
||||
atmosphericsValue = getObjectMember(*renderingValue, "atmospherics", "rendering.atmospherics");
|
||||
if (atmosphericsValue) {
|
||||
atmosphericsPath = "rendering.atmospherics";
|
||||
}
|
||||
}
|
||||
if (!atmosphericsValue && document.HasMember("atmospherics")) {
|
||||
const auto& value = document["atmospherics"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'atmospherics' must be an object");
|
||||
}
|
||||
atmosphericsValue = &value;
|
||||
}
|
||||
if (atmosphericsValue) {
|
||||
|
||||
auto readFloat = [&](const char* name, float& target) {
|
||||
if (!atmosphericsValue.HasMember(name)) {
|
||||
if (!atmosphericsValue->HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = atmosphericsValue[name];
|
||||
const auto& value = (*atmosphericsValue)[name];
|
||||
if (!value.IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "' must be a number");
|
||||
throw std::runtime_error("JSON member '" + atmosphericsPath + "." + std::string(name) +
|
||||
"' must be a number");
|
||||
}
|
||||
target = static_cast<float>(value.GetDouble());
|
||||
};
|
||||
|
||||
auto readBool = [&](const char* name, bool& target) {
|
||||
if (!atmosphericsValue.HasMember(name)) {
|
||||
if (!atmosphericsValue->HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = atmosphericsValue[name];
|
||||
const auto& value = (*atmosphericsValue)[name];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "' must be a boolean");
|
||||
throw std::runtime_error("JSON member '" + atmosphericsPath + "." + std::string(name) +
|
||||
"' must be a boolean");
|
||||
}
|
||||
target = value.GetBool();
|
||||
};
|
||||
|
||||
auto readFloatArray3 = [&](const char* name, std::array<float, 3>& target) {
|
||||
if (!atmosphericsValue.HasMember(name)) {
|
||||
if (!atmosphericsValue->HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = atmosphericsValue[name];
|
||||
const auto& value = (*atmosphericsValue)[name];
|
||||
if (!value.IsArray() || value.Size() != 3) {
|
||||
throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "' must be an array of 3 numbers");
|
||||
throw std::runtime_error("JSON member '" + atmosphericsPath + "." + std::string(name) +
|
||||
"' must be an array of 3 numbers");
|
||||
}
|
||||
for (rapidjson::SizeType i = 0; i < 3; ++i) {
|
||||
if (!value[i].IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "[" + std::to_string(i) + "]' must be a number");
|
||||
throw std::runtime_error("JSON member '" + atmosphericsPath + "." + std::string(name) +
|
||||
"[" + std::to_string(i) + "]' must be a number");
|
||||
}
|
||||
target[i] = static_cast<float>(value[i].GetDouble());
|
||||
}
|
||||
@@ -352,112 +473,151 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
readFloat("pbr_metallic", config.atmospherics.pbrMetallic);
|
||||
}
|
||||
|
||||
if (document.HasMember("bgfx")) {
|
||||
const auto& bgfxValue = document["bgfx"];
|
||||
if (!bgfxValue.IsObject()) {
|
||||
const rapidjson::Value* bgfxValue = nullptr;
|
||||
std::string bgfxPath = "bgfx";
|
||||
if (renderingValue) {
|
||||
bgfxValue = getObjectMember(*renderingValue, "bgfx", "rendering.bgfx");
|
||||
if (bgfxValue) {
|
||||
bgfxPath = "rendering.bgfx";
|
||||
}
|
||||
}
|
||||
if (!bgfxValue && document.HasMember("bgfx")) {
|
||||
const auto& value = document["bgfx"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'bgfx' must be an object");
|
||||
}
|
||||
if (bgfxValue.HasMember("renderer")) {
|
||||
const auto& value = bgfxValue["renderer"];
|
||||
bgfxValue = &value;
|
||||
}
|
||||
if (bgfxValue) {
|
||||
if (bgfxValue->HasMember("renderer")) {
|
||||
const auto& value = (*bgfxValue)["renderer"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'bgfx.renderer' must be a string");
|
||||
throw std::runtime_error("JSON member '" + bgfxPath + ".renderer' must be a string");
|
||||
}
|
||||
config.bgfx.renderer = value.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
bool materialShaderKeyProvided = false;
|
||||
if (document.HasMember("materialx")) {
|
||||
const auto& materialValue = document["materialx"];
|
||||
if (!materialValue.IsObject()) {
|
||||
const rapidjson::Value* materialValue = nullptr;
|
||||
std::string materialPath = "materialx";
|
||||
if (renderingValue) {
|
||||
materialValue = getObjectMember(*renderingValue, "materialx", "rendering.materialx");
|
||||
if (materialValue) {
|
||||
materialPath = "rendering.materialx";
|
||||
}
|
||||
}
|
||||
if (!materialValue && document.HasMember("materialx")) {
|
||||
const auto& value = document["materialx"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'materialx' must be an object");
|
||||
}
|
||||
if (materialValue.HasMember("enabled")) {
|
||||
const auto& value = materialValue["enabled"];
|
||||
materialValue = &value;
|
||||
}
|
||||
|
||||
bool materialShaderKeyProvided = false;
|
||||
if (materialValue) {
|
||||
if (materialValue->HasMember("enabled")) {
|
||||
const auto& value = (*materialValue)["enabled"];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'materialx.enabled' must be a boolean");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".enabled' must be a boolean");
|
||||
}
|
||||
config.materialX.enabled = value.GetBool();
|
||||
}
|
||||
if (materialValue.HasMember("document")) {
|
||||
const auto& value = materialValue["document"];
|
||||
if (materialValue->HasMember("document")) {
|
||||
const auto& value = (*materialValue)["document"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx.document' must be a string");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".document' must be a string");
|
||||
}
|
||||
config.materialX.documentPath = value.GetString();
|
||||
}
|
||||
if (materialValue.HasMember("shader_key")) {
|
||||
const auto& value = materialValue["shader_key"];
|
||||
if (materialValue->HasMember("shader_key")) {
|
||||
const auto& value = (*materialValue)["shader_key"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx.shader_key' must be a string");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".shader_key' must be a string");
|
||||
}
|
||||
config.materialX.shaderKey = value.GetString();
|
||||
materialShaderKeyProvided = true;
|
||||
}
|
||||
if (materialValue.HasMember("material")) {
|
||||
const auto& value = materialValue["material"];
|
||||
if (materialValue->HasMember("material")) {
|
||||
const auto& value = (*materialValue)["material"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx.material' must be a string");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".material' must be a string");
|
||||
}
|
||||
config.materialX.materialName = value.GetString();
|
||||
}
|
||||
if (materialValue.HasMember("library_path")) {
|
||||
const auto& value = materialValue["library_path"];
|
||||
if (materialValue->HasMember("library_path")) {
|
||||
const auto& value = (*materialValue)["library_path"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx.library_path' must be a string");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".library_path' must be a string");
|
||||
}
|
||||
config.materialX.libraryPath = value.GetString();
|
||||
}
|
||||
if (materialValue.HasMember("library_folders")) {
|
||||
const auto& value = materialValue["library_folders"];
|
||||
if (materialValue->HasMember("library_folders")) {
|
||||
const auto& value = (*materialValue)["library_folders"];
|
||||
if (!value.IsArray()) {
|
||||
throw std::runtime_error("JSON member 'materialx.library_folders' must be an array");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".library_folders' must be an array");
|
||||
}
|
||||
config.materialX.libraryFolders.clear();
|
||||
for (rapidjson::SizeType i = 0; i < value.Size(); ++i) {
|
||||
if (!value[i].IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx.library_folders[" + std::to_string(i) + "]' must be a string");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".library_folders[" +
|
||||
std::to_string(i) + "]' must be a string");
|
||||
}
|
||||
config.materialX.libraryFolders.emplace_back(value[i].GetString());
|
||||
}
|
||||
}
|
||||
if (materialValue.HasMember("use_constant_color")) {
|
||||
const auto& value = materialValue["use_constant_color"];
|
||||
if (materialValue->HasMember("use_constant_color")) {
|
||||
const auto& value = (*materialValue)["use_constant_color"];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'materialx.use_constant_color' must be a boolean");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".use_constant_color' must be a boolean");
|
||||
}
|
||||
config.materialX.useConstantColor = value.GetBool();
|
||||
}
|
||||
if (materialValue.HasMember("constant_color")) {
|
||||
const auto& value = materialValue["constant_color"];
|
||||
if (materialValue->HasMember("constant_color")) {
|
||||
const auto& value = (*materialValue)["constant_color"];
|
||||
if (!value.IsArray() || value.Size() != 3) {
|
||||
throw std::runtime_error("JSON member 'materialx.constant_color' must be an array of 3 numbers");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".constant_color' must be an array of 3 numbers");
|
||||
}
|
||||
for (rapidjson::SizeType i = 0; i < 3; ++i) {
|
||||
if (!value[i].IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'materialx.constant_color[" + std::to_string(i) + "]' must be a number");
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".constant_color[" +
|
||||
std::to_string(i) + "]' must be a number");
|
||||
}
|
||||
config.materialX.constantColor[i] = static_cast<float>(value[i].GetDouble());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (document.HasMember("materialx_materials")) {
|
||||
const auto& materialsValue = document["materialx_materials"];
|
||||
if (!materialsValue.IsArray()) {
|
||||
const rapidjson::Value* materialsValue = nullptr;
|
||||
std::string materialsPath = "materialx_materials";
|
||||
if (materialValue && materialValue->HasMember("materials")) {
|
||||
const auto& value = (*materialValue)["materials"];
|
||||
if (!value.IsArray()) {
|
||||
throw std::runtime_error("JSON member '" + materialPath + ".materials' must be an array");
|
||||
}
|
||||
materialsValue = &value;
|
||||
materialsPath = materialPath + ".materials";
|
||||
}
|
||||
if (!materialsValue && document.HasMember("materialx_materials")) {
|
||||
const auto& value = document["materialx_materials"];
|
||||
if (!value.IsArray()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials' must be an array");
|
||||
}
|
||||
materialsValue = &value;
|
||||
}
|
||||
if (materialsValue) {
|
||||
config.materialXMaterials.clear();
|
||||
for (rapidjson::SizeType i = 0; i < materialsValue.Size(); ++i) {
|
||||
const auto& entry = materialsValue[i];
|
||||
for (rapidjson::SizeType i = 0; i < materialsValue->Size(); ++i) {
|
||||
const auto& entry = (*materialsValue)[i];
|
||||
if (!entry.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + "]' must be an object");
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"]' must be an object");
|
||||
}
|
||||
MaterialXMaterialConfig materialConfig;
|
||||
if (entry.HasMember("enabled")) {
|
||||
const auto& value = entry["enabled"];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].enabled' must be a boolean");
|
||||
}
|
||||
materialConfig.enabled = value.GetBool();
|
||||
@@ -465,7 +625,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
if (entry.HasMember("document")) {
|
||||
const auto& value = entry["document"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].document' must be a string");
|
||||
}
|
||||
materialConfig.documentPath = value.GetString();
|
||||
@@ -473,7 +633,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
if (entry.HasMember("shader_key")) {
|
||||
const auto& value = entry["shader_key"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].shader_key' must be a string");
|
||||
}
|
||||
materialConfig.shaderKey = value.GetString();
|
||||
@@ -481,7 +641,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
if (entry.HasMember("material")) {
|
||||
const auto& value = entry["material"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].material' must be a string");
|
||||
}
|
||||
materialConfig.materialName = value.GetString();
|
||||
@@ -489,7 +649,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
if (entry.HasMember("use_constant_color")) {
|
||||
const auto& value = entry["use_constant_color"];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].use_constant_color' must be a boolean");
|
||||
}
|
||||
materialConfig.useConstantColor = value.GetBool();
|
||||
@@ -497,12 +657,12 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
if (entry.HasMember("constant_color")) {
|
||||
const auto& value = entry["constant_color"];
|
||||
if (!value.IsArray() || value.Size() != 3) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].constant_color' must be an array of 3 numbers");
|
||||
}
|
||||
for (rapidjson::SizeType channel = 0; channel < 3; ++channel) {
|
||||
if (!value[channel].IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].constant_color[" + std::to_string(channel) +
|
||||
"]' must be a number");
|
||||
}
|
||||
@@ -511,11 +671,11 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
}
|
||||
|
||||
if (materialConfig.shaderKey.empty()) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].shader_key' must be provided");
|
||||
}
|
||||
if (materialConfig.documentPath.empty() && !materialConfig.useConstantColor) {
|
||||
throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) +
|
||||
throw std::runtime_error("JSON member '" + materialsPath + "[" + std::to_string(i) +
|
||||
"].document' is required when use_constant_color is false");
|
||||
}
|
||||
|
||||
@@ -532,35 +692,53 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
}
|
||||
}
|
||||
|
||||
if (document.HasMember("gui_font")) {
|
||||
const auto& fontValue = document["gui_font"];
|
||||
if (!fontValue.IsObject()) {
|
||||
const rapidjson::Value* guiFontValue = nullptr;
|
||||
std::string guiFontPath = "gui_font";
|
||||
if (guiValue && guiValue->HasMember("font")) {
|
||||
const auto& value = (*guiValue)["font"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'gui.font' must be an object");
|
||||
}
|
||||
guiFontValue = &value;
|
||||
guiFontPath = "gui.font";
|
||||
} else if (document.HasMember("gui_font")) {
|
||||
const auto& value = document["gui_font"];
|
||||
if (!value.IsObject()) {
|
||||
throw std::runtime_error("JSON member 'gui_font' must be an object");
|
||||
}
|
||||
if (fontValue.HasMember("use_freetype")) {
|
||||
const auto& value = fontValue["use_freetype"];
|
||||
guiFontValue = &value;
|
||||
}
|
||||
if (guiFontValue) {
|
||||
if (guiFontValue->HasMember("use_freetype")) {
|
||||
const auto& value = (*guiFontValue)["use_freetype"];
|
||||
if (!value.IsBool()) {
|
||||
throw std::runtime_error("JSON member 'gui_font.use_freetype' must be a boolean");
|
||||
throw std::runtime_error("JSON member '" + guiFontPath + ".use_freetype' must be a boolean");
|
||||
}
|
||||
config.guiFont.useFreeType = value.GetBool();
|
||||
}
|
||||
if (fontValue.HasMember("font_path")) {
|
||||
const auto& value = fontValue["font_path"];
|
||||
if (guiFontValue->HasMember("font_path")) {
|
||||
const auto& value = (*guiFontValue)["font_path"];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error("JSON member 'gui_font.font_path' must be a string");
|
||||
throw std::runtime_error("JSON member '" + guiFontPath + ".font_path' must be a string");
|
||||
}
|
||||
config.guiFont.fontPath = value.GetString();
|
||||
}
|
||||
if (fontValue.HasMember("font_size")) {
|
||||
const auto& value = fontValue["font_size"];
|
||||
if (guiFontValue->HasMember("font_size")) {
|
||||
const auto& value = (*guiFontValue)["font_size"];
|
||||
if (!value.IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'gui_font.font_size' must be a number");
|
||||
throw std::runtime_error("JSON member '" + guiFontPath + ".font_size' must be a number");
|
||||
}
|
||||
config.guiFont.fontSize = static_cast<float>(value.GetDouble());
|
||||
}
|
||||
}
|
||||
|
||||
if (document.HasMember("gui_opacity")) {
|
||||
if (guiValue && guiValue->HasMember("opacity")) {
|
||||
const auto& value = (*guiValue)["opacity"];
|
||||
if (!value.IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'gui.opacity' must be a number");
|
||||
}
|
||||
config.guiOpacity = static_cast<float>(value.GetDouble());
|
||||
} else if (document.HasMember("gui_opacity")) {
|
||||
const auto& value = document["gui_opacity"];
|
||||
if (!value.IsNumber()) {
|
||||
throw std::runtime_error("JSON member 'gui_opacity' must be a number");
|
||||
@@ -577,22 +755,27 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
|
||||
document.SetObject();
|
||||
auto& allocator = document.GetAllocator();
|
||||
|
||||
auto addStringMember = [&](const char* name, const std::string& value) {
|
||||
auto addStringMember = [&](rapidjson::Value& target, const char* name, const std::string& value) {
|
||||
rapidjson::Value nameValue(name, allocator);
|
||||
rapidjson::Value stringValue(value.c_str(), allocator);
|
||||
document.AddMember(nameValue, stringValue, allocator);
|
||||
target.AddMember(nameValue, stringValue, allocator);
|
||||
};
|
||||
|
||||
document.AddMember("window_width", config.width, allocator);
|
||||
document.AddMember("window_height", config.height, allocator);
|
||||
addStringMember("lua_script", config.scriptPath.string());
|
||||
addStringMember("window_title", config.windowTitle);
|
||||
document.AddMember("lua_debug", config.luaDebug, allocator);
|
||||
document.AddMember("schema_version", 2, 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);
|
||||
sizeObject.AddMember("width", config.width, allocator);
|
||||
sizeObject.AddMember("height", config.height, allocator);
|
||||
windowObject.AddMember("size", sizeObject, allocator);
|
||||
|
||||
std::filesystem::path scriptsDir = config.scriptPath.parent_path();
|
||||
if (!scriptsDir.empty()) {
|
||||
addStringMember("scripts_directory", scriptsDir.string());
|
||||
}
|
||||
|
||||
rapidjson::Value mouseGrabObject(rapidjson::kObjectType);
|
||||
mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator);
|
||||
@@ -607,13 +790,13 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
|
||||
mouseGrabObject.AddMember("release_key",
|
||||
rapidjson::Value(config.mouseGrab.releaseKey.c_str(), allocator),
|
||||
allocator);
|
||||
document.AddMember("mouse_grab", mouseGrabObject, allocator);
|
||||
windowObject.AddMember("mouse_grab", mouseGrabObject, allocator);
|
||||
document.AddMember("window", windowObject, allocator);
|
||||
|
||||
rapidjson::Value bgfxObject(rapidjson::kObjectType);
|
||||
bgfxObject.AddMember("renderer",
|
||||
rapidjson::Value(config.bgfx.renderer.c_str(), allocator),
|
||||
allocator);
|
||||
document.AddMember("bgfx", bgfxObject, allocator);
|
||||
|
||||
rapidjson::Value materialObject(rapidjson::kObjectType);
|
||||
materialObject.AddMember("enabled", config.materialX.enabled, allocator);
|
||||
@@ -640,7 +823,6 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
|
||||
constantColor.PushBack(config.materialX.constantColor[1], allocator);
|
||||
constantColor.PushBack(config.materialX.constantColor[2], allocator);
|
||||
materialObject.AddMember("constant_color", constantColor, allocator);
|
||||
document.AddMember("materialx", materialObject, allocator);
|
||||
|
||||
if (!config.materialXMaterials.empty()) {
|
||||
rapidjson::Value materialsArray(rapidjson::kArrayType);
|
||||
@@ -664,16 +846,36 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
|
||||
entry.AddMember("constant_color", materialColor, allocator);
|
||||
materialsArray.PushBack(entry, allocator);
|
||||
}
|
||||
document.AddMember("materialx_materials", materialsArray, allocator);
|
||||
materialObject.AddMember("materials", materialsArray, allocator);
|
||||
}
|
||||
|
||||
rapidjson::Value fontObject(rapidjson::kObjectType);
|
||||
fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator);
|
||||
fontObject.AddMember("font_path",
|
||||
rapidjson::Value(config.guiFont.fontPath.string().c_str(), allocator),
|
||||
allocator);
|
||||
fontObject.AddMember("font_size", config.guiFont.fontSize, allocator);
|
||||
document.AddMember("gui_font", fontObject, allocator);
|
||||
rapidjson::Value renderingObject(rapidjson::kObjectType);
|
||||
renderingObject.AddMember("bgfx", bgfxObject, allocator);
|
||||
renderingObject.AddMember("materialx", materialObject, allocator);
|
||||
|
||||
rapidjson::Value atmosphericsObject(rapidjson::kObjectType);
|
||||
atmosphericsObject.AddMember("ambient_strength", config.atmospherics.ambientStrength, allocator);
|
||||
atmosphericsObject.AddMember("fog_density", config.atmospherics.fogDensity, allocator);
|
||||
rapidjson::Value fogColor(rapidjson::kArrayType);
|
||||
fogColor.PushBack(config.atmospherics.fogColor[0], allocator);
|
||||
fogColor.PushBack(config.atmospherics.fogColor[1], allocator);
|
||||
fogColor.PushBack(config.atmospherics.fogColor[2], allocator);
|
||||
atmosphericsObject.AddMember("fog_color", fogColor, allocator);
|
||||
rapidjson::Value skyColor(rapidjson::kArrayType);
|
||||
skyColor.PushBack(config.atmospherics.skyColor[0], allocator);
|
||||
skyColor.PushBack(config.atmospherics.skyColor[1], allocator);
|
||||
skyColor.PushBack(config.atmospherics.skyColor[2], allocator);
|
||||
atmosphericsObject.AddMember("sky_color", skyColor, allocator);
|
||||
atmosphericsObject.AddMember("gamma", config.atmospherics.gamma, allocator);
|
||||
atmosphericsObject.AddMember("exposure", config.atmospherics.exposure, allocator);
|
||||
atmosphericsObject.AddMember("enable_tone_mapping", config.atmospherics.enableToneMapping, allocator);
|
||||
atmosphericsObject.AddMember("enable_shadows", config.atmospherics.enableShadows, allocator);
|
||||
atmosphericsObject.AddMember("enable_ssgi", config.atmospherics.enableSSGI, allocator);
|
||||
atmosphericsObject.AddMember("enable_volumetric_lighting", config.atmospherics.enableVolumetricLighting, allocator);
|
||||
atmosphericsObject.AddMember("pbr_roughness", config.atmospherics.pbrRoughness, allocator);
|
||||
atmosphericsObject.AddMember("pbr_metallic", config.atmospherics.pbrMetallic, allocator);
|
||||
renderingObject.AddMember("atmospherics", atmosphericsObject, allocator);
|
||||
document.AddMember("rendering", renderingObject, allocator);
|
||||
|
||||
rapidjson::Value bindingsObject(rapidjson::kObjectType);
|
||||
auto addBindingMember = [&](const char* name, const std::string& value) {
|
||||
@@ -725,20 +927,36 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
|
||||
addMappingObject("gamepad_axis_actions", config.inputBindings.gamepadAxisActions, bindingsObject);
|
||||
bindingsObject.AddMember("gamepad_axis_action_threshold",
|
||||
config.inputBindings.gamepadAxisActionThreshold, allocator);
|
||||
document.AddMember("input_bindings", bindingsObject, allocator);
|
||||
rapidjson::Value inputObject(rapidjson::kObjectType);
|
||||
inputObject.AddMember("bindings", bindingsObject, allocator);
|
||||
document.AddMember("input", inputObject, allocator);
|
||||
|
||||
std::filesystem::path projectRoot = scriptsDir.parent_path();
|
||||
if (!projectRoot.empty()) {
|
||||
addStringMember("project_root", projectRoot.string());
|
||||
addStringMember("shaders_directory", (projectRoot / "shaders").string());
|
||||
} else {
|
||||
addStringMember("shaders_directory", "shaders");
|
||||
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());
|
||||
} else {
|
||||
addStringMember(pathsObject, "shaders", "shaders");
|
||||
}
|
||||
document.AddMember("paths", pathsObject, allocator);
|
||||
|
||||
document.AddMember("gui_opacity", config.guiOpacity, allocator);
|
||||
rapidjson::Value guiObject(rapidjson::kObjectType);
|
||||
rapidjson::Value fontObject(rapidjson::kObjectType);
|
||||
fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator);
|
||||
fontObject.AddMember("font_path",
|
||||
rapidjson::Value(config.guiFont.fontPath.string().c_str(), allocator),
|
||||
allocator);
|
||||
fontObject.AddMember("font_size", config.guiFont.fontSize, allocator);
|
||||
guiObject.AddMember("font", fontObject, allocator);
|
||||
guiObject.AddMember("opacity", config.guiOpacity, allocator);
|
||||
document.AddMember("gui", guiObject, allocator);
|
||||
|
||||
if (!configPath.empty()) {
|
||||
addStringMember("config_file", configPath.string());
|
||||
addStringMember(document, "config_file", configPath.string());
|
||||
}
|
||||
|
||||
rapidjson::StringBuffer buffer;
|
||||
|
||||
@@ -35,18 +35,27 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std
|
||||
document.SetObject();
|
||||
auto& allocator = document.GetAllocator();
|
||||
|
||||
auto addStringMember = [&](const char* name, const std::string& value) {
|
||||
auto addStringMember = [&](rapidjson::Value& target, const char* name, const std::string& value) {
|
||||
rapidjson::Value nameValue(name, allocator);
|
||||
rapidjson::Value stringValue(value.c_str(), allocator);
|
||||
document.AddMember(nameValue, stringValue, allocator);
|
||||
target.AddMember(nameValue, stringValue, allocator);
|
||||
};
|
||||
|
||||
document.AddMember("window_width", config.width, allocator);
|
||||
document.AddMember("window_height", config.height, allocator);
|
||||
addStringMember("lua_script", config.scriptPath.string());
|
||||
document.AddMember("schema_version", 2, 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);
|
||||
sizeObject.AddMember("width", config.width, allocator);
|
||||
sizeObject.AddMember("height", config.height, allocator);
|
||||
windowObject.AddMember("size", sizeObject, allocator);
|
||||
|
||||
std::filesystem::path scriptsDir = config.scriptPath.parent_path();
|
||||
addStringMember("scripts_directory", scriptsDir.string());
|
||||
|
||||
rapidjson::Value mouseGrabObject(rapidjson::kObjectType);
|
||||
mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator);
|
||||
@@ -61,7 +70,8 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std
|
||||
mouseGrabObject.AddMember("release_key",
|
||||
rapidjson::Value(config.mouseGrab.releaseKey.c_str(), allocator),
|
||||
allocator);
|
||||
document.AddMember("mouse_grab", mouseGrabObject, allocator);
|
||||
windowObject.AddMember("mouse_grab", mouseGrabObject, allocator);
|
||||
document.AddMember("window", windowObject, allocator);
|
||||
|
||||
rapidjson::Value bindingsObject(rapidjson::kObjectType);
|
||||
auto addBindingMember = [&](const char* name, const std::string& value) {
|
||||
@@ -113,22 +123,120 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std
|
||||
addMappingObject("gamepad_axis_actions", config.inputBindings.gamepadAxisActions, bindingsObject);
|
||||
bindingsObject.AddMember("gamepad_axis_action_threshold",
|
||||
config.inputBindings.gamepadAxisActionThreshold, allocator);
|
||||
document.AddMember("input_bindings", bindingsObject, allocator);
|
||||
rapidjson::Value inputObject(rapidjson::kObjectType);
|
||||
inputObject.AddMember("bindings", bindingsObject, allocator);
|
||||
document.AddMember("input", inputObject, allocator);
|
||||
|
||||
std::filesystem::path projectRoot = scriptsDir.parent_path();
|
||||
if (!projectRoot.empty()) {
|
||||
addStringMember("project_root", projectRoot.string());
|
||||
addStringMember("shaders_directory", (projectRoot / "shaders").string());
|
||||
} else {
|
||||
addStringMember("shaders_directory", "shaders");
|
||||
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());
|
||||
} else {
|
||||
addStringMember(pathsObject, "shaders", "shaders");
|
||||
}
|
||||
document.AddMember("paths", pathsObject, allocator);
|
||||
|
||||
rapidjson::Value bgfxObject(rapidjson::kObjectType);
|
||||
bgfxObject.AddMember("renderer",
|
||||
rapidjson::Value(config.bgfx.renderer.c_str(), allocator),
|
||||
allocator);
|
||||
document.AddMember("bgfx", bgfxObject, allocator);
|
||||
addStringMember("config_file", configPath.string());
|
||||
rapidjson::Value materialObject(rapidjson::kObjectType);
|
||||
materialObject.AddMember("enabled", config.materialX.enabled, allocator);
|
||||
materialObject.AddMember("document",
|
||||
rapidjson::Value(config.materialX.documentPath.string().c_str(), allocator),
|
||||
allocator);
|
||||
materialObject.AddMember("shader_key",
|
||||
rapidjson::Value(config.materialX.shaderKey.c_str(), allocator),
|
||||
allocator);
|
||||
materialObject.AddMember("material",
|
||||
rapidjson::Value(config.materialX.materialName.c_str(), allocator),
|
||||
allocator);
|
||||
materialObject.AddMember("library_path",
|
||||
rapidjson::Value(config.materialX.libraryPath.string().c_str(), allocator),
|
||||
allocator);
|
||||
rapidjson::Value libraryFolders(rapidjson::kArrayType);
|
||||
for (const auto& folder : config.materialX.libraryFolders) {
|
||||
libraryFolders.PushBack(rapidjson::Value(folder.c_str(), allocator), allocator);
|
||||
}
|
||||
materialObject.AddMember("library_folders", libraryFolders, allocator);
|
||||
materialObject.AddMember("use_constant_color", config.materialX.useConstantColor, allocator);
|
||||
rapidjson::Value constantColor(rapidjson::kArrayType);
|
||||
constantColor.PushBack(config.materialX.constantColor[0], allocator);
|
||||
constantColor.PushBack(config.materialX.constantColor[1], allocator);
|
||||
constantColor.PushBack(config.materialX.constantColor[2], allocator);
|
||||
materialObject.AddMember("constant_color", constantColor, allocator);
|
||||
|
||||
if (!config.materialXMaterials.empty()) {
|
||||
rapidjson::Value materialsArray(rapidjson::kArrayType);
|
||||
for (const auto& material : config.materialXMaterials) {
|
||||
rapidjson::Value entry(rapidjson::kObjectType);
|
||||
entry.AddMember("enabled", material.enabled, allocator);
|
||||
entry.AddMember("document",
|
||||
rapidjson::Value(material.documentPath.string().c_str(), allocator),
|
||||
allocator);
|
||||
entry.AddMember("shader_key",
|
||||
rapidjson::Value(material.shaderKey.c_str(), allocator),
|
||||
allocator);
|
||||
entry.AddMember("material",
|
||||
rapidjson::Value(material.materialName.c_str(), allocator),
|
||||
allocator);
|
||||
entry.AddMember("use_constant_color", material.useConstantColor, allocator);
|
||||
rapidjson::Value materialColor(rapidjson::kArrayType);
|
||||
materialColor.PushBack(material.constantColor[0], allocator);
|
||||
materialColor.PushBack(material.constantColor[1], allocator);
|
||||
materialColor.PushBack(material.constantColor[2], allocator);
|
||||
entry.AddMember("constant_color", materialColor, allocator);
|
||||
materialsArray.PushBack(entry, allocator);
|
||||
}
|
||||
materialObject.AddMember("materials", materialsArray, allocator);
|
||||
}
|
||||
|
||||
rapidjson::Value atmosphericsObject(rapidjson::kObjectType);
|
||||
atmosphericsObject.AddMember("ambient_strength", config.atmospherics.ambientStrength, allocator);
|
||||
atmosphericsObject.AddMember("fog_density", config.atmospherics.fogDensity, allocator);
|
||||
rapidjson::Value fogColor(rapidjson::kArrayType);
|
||||
fogColor.PushBack(config.atmospherics.fogColor[0], allocator);
|
||||
fogColor.PushBack(config.atmospherics.fogColor[1], allocator);
|
||||
fogColor.PushBack(config.atmospherics.fogColor[2], allocator);
|
||||
atmosphericsObject.AddMember("fog_color", fogColor, allocator);
|
||||
rapidjson::Value skyColor(rapidjson::kArrayType);
|
||||
skyColor.PushBack(config.atmospherics.skyColor[0], allocator);
|
||||
skyColor.PushBack(config.atmospherics.skyColor[1], allocator);
|
||||
skyColor.PushBack(config.atmospherics.skyColor[2], allocator);
|
||||
atmosphericsObject.AddMember("sky_color", skyColor, allocator);
|
||||
atmosphericsObject.AddMember("gamma", config.atmospherics.gamma, allocator);
|
||||
atmosphericsObject.AddMember("exposure", config.atmospherics.exposure, allocator);
|
||||
atmosphericsObject.AddMember("enable_tone_mapping", config.atmospherics.enableToneMapping, allocator);
|
||||
atmosphericsObject.AddMember("enable_shadows", config.atmospherics.enableShadows, allocator);
|
||||
atmosphericsObject.AddMember("enable_ssgi", config.atmospherics.enableSSGI, allocator);
|
||||
atmosphericsObject.AddMember("enable_volumetric_lighting", config.atmospherics.enableVolumetricLighting, allocator);
|
||||
atmosphericsObject.AddMember("pbr_roughness", config.atmospherics.pbrRoughness, allocator);
|
||||
atmosphericsObject.AddMember("pbr_metallic", config.atmospherics.pbrMetallic, allocator);
|
||||
|
||||
rapidjson::Value renderingObject(rapidjson::kObjectType);
|
||||
renderingObject.AddMember("bgfx", bgfxObject, allocator);
|
||||
renderingObject.AddMember("materialx", materialObject, allocator);
|
||||
renderingObject.AddMember("atmospherics", atmosphericsObject, allocator);
|
||||
document.AddMember("rendering", renderingObject, allocator);
|
||||
|
||||
rapidjson::Value guiObject(rapidjson::kObjectType);
|
||||
rapidjson::Value fontObject(rapidjson::kObjectType);
|
||||
fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator);
|
||||
fontObject.AddMember("font_path",
|
||||
rapidjson::Value(config.guiFont.fontPath.string().c_str(), allocator),
|
||||
allocator);
|
||||
fontObject.AddMember("font_size", config.guiFont.fontSize, allocator);
|
||||
guiObject.AddMember("font", fontObject, allocator);
|
||||
guiObject.AddMember("opacity", config.guiOpacity, allocator);
|
||||
document.AddMember("gui", guiObject, allocator);
|
||||
|
||||
if (!configPath.empty()) {
|
||||
addStringMember(document, "config_file", configPath.string());
|
||||
}
|
||||
|
||||
rapidjson::StringBuffer buffer;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
|
||||
|
||||
@@ -35,6 +35,13 @@ public:
|
||||
*/
|
||||
virtual bool ExecuteWithTimeout(std::function<void()> func, int timeoutMs, const std::string& operationName) = 0;
|
||||
|
||||
/**
|
||||
* @brief Record a heartbeat for the main loop or long-running operation.
|
||||
*
|
||||
* @param frameTimeSeconds Delta time for the last frame in seconds
|
||||
*/
|
||||
virtual void RecordFrameHeartbeat(double frameTimeSeconds) = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if a crash has been detected.
|
||||
*
|
||||
@@ -98,4 +105,4 @@ public:
|
||||
virtual std::string GetSystemHealthStatus() const = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
} // namespace sdl3cpp::services
|
||||
|
||||
Reference in New Issue
Block a user