use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_cook::authoring::world::take_entry_id;
use concinnity_core::components::ProceduralMesh;
use concinnity_core::components::Story;
use concinnity_core::components::VolumetricFog;
use concinnity_core::gfx::mesh_payload;
use concinnity_core::render::backend;
use concinnity_core::render::volumetric_fog;
use concinnity_engine::live_edit::hot_reload_sources::*;
use concinnity_host::thread::asset_id;
#[derive(Default, Debug)]
pub(crate) struct FogReloadResult {
pub updated: bool,
}
pub(super) fn reload_volumetric_fog(
path: &str,
last_pushed: &mut Option<volumetric_fog::FogSettings>,
backend: &mut dyn backend::RenderBackend,
) -> FogReloadResult {
let mut result = FogReloadResult::default();
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
tracing::error!(
"VolumetricFog hot-reload: failed to read '{}': {} (no update)",
path,
e,
);
return result;
}
};
let entries = match concinnity_cook::build_only::expand_world_from_str(
concinnity_cook::WorldSource::file(&content, std::path::Path::new(path)),
crate::project::assets_dir().as_deref(),
) {
Ok(v) => v,
Err(e) => {
tracing::error!(
"VolumetricFog hot-reload: failed to parse '{}': {} (no update)",
path,
e,
);
return result;
}
};
let mut resolved: Option<volumetric_fog::FogSettings> = None;
for entry in &entries {
if entry_type(entry) != Some(RegisteredType::VolumetricFog) {
continue;
}
let args = entry
.get("args")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parsed: VolumetricFog = match serde_json::from_value(args) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
"VolumetricFog hot-reload: failed to parse args: {} (kept previous state)",
e,
);
return result;
}
};
let clamped = concinnity_cook::authoring::validate::volumetric_fog(parsed);
resolved = volumetric_fog::resolve_asset(&clamped);
break;
}
if resolved != *last_pushed {
backend.update_fog_settings(resolved);
*last_pushed = resolved;
result.updated = true;
}
result
}
#[derive(Default, Debug)]
pub(crate) struct ProceduralMeshReloadResult {
pub regenerated: usize,
pub unchanged: usize,
pub failed: usize,
}
pub(super) fn reload_procedural_meshes(
path: &str,
procedural_meshes: &mut ProceduralMeshSourceMap,
backend: &mut dyn backend::RenderBackend,
) -> ProceduralMeshReloadResult {
let mut result = ProceduralMeshReloadResult::default();
if procedural_meshes.is_empty() {
return result;
}
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
tracing::error!(
"ProceduralMesh hot-reload: failed to read '{}': {} (no regen)",
path,
e
);
return result;
}
};
let entries = match concinnity_cook::build_only::expand_world_from_str(
concinnity_cook::WorldSource::file(&content, std::path::Path::new(path)),
crate::project::assets_dir().as_deref(),
) {
Ok(v) => v,
Err(e) => {
tracing::error!(
"ProceduralMesh hot-reload: failed to parse '{}': {} (no regen)",
path,
e
);
return result;
}
};
let mut new_args_by_name: std::collections::HashMap<
String,
(ProceduralMesh, serde_json::Value),
> = std::collections::HashMap::new();
for entry in &entries {
if entry_type(entry) != Some(RegisteredType::ProceduralMesh) {
continue;
}
let mut entry = entry.clone();
let Some(name) = take_entry_id(&mut entry) else {
continue;
};
let raw_args = entry
.get("args")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parsed: ProceduralMesh = match serde_json::from_value(raw_args.clone()) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
"ProceduralMesh hot-reload: failed to parse '{}' args: {} \
(kept old geometry)",
name,
e
);
continue;
}
};
new_args_by_name.insert(name.to_string(), (parsed, raw_args));
}
let mut rebuild_changes: Vec<backend::DrawGeometryUpdate> = Vec::new();
let mut rebuild_entry_indices: Vec<usize> = Vec::new();
let mut staged_args: Vec<(usize, ProceduralMesh)> = Vec::new();
for (entry_idx, entry) in procedural_meshes.entries.iter().enumerate() {
let Some((new_args, raw_args)) = new_args_by_name.get(&entry.name) else {
result.unchanged += 1;
continue;
};
if new_args == &entry.args {
result.unchanged += 1;
continue;
}
let payload = match concinnity_cook::compile::mesh::compile_mesh_payload(
raw_args,
crate::project::assets_dir().as_deref(),
) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
"ProceduralMesh hot-reload: regen failed for '{}': {} (kept old geometry)",
entry.name,
e
);
result.failed += 1;
continue;
}
};
let (vertices, indices, lod_alternates) =
match mesh_payload::deserialize_with_lods(&payload) {
Ok(t) => t,
Err(e) => {
tracing::warn!(
"ProceduralMesh hot-reload: deserialize failed for '{}': {} \
(kept old geometry)",
entry.name,
e
);
result.failed += 1;
continue;
}
};
let size_changed = entry.draw_indices.iter().any(|&draw_idx| {
let base_changed = match backend.draw_geometry_size(draw_idx) {
Some((v, i)) => v != vertices.len() || i != indices.len(),
None => false,
};
let lod_changed = match backend.draw_lod_index_counts(draw_idx) {
Some(counts) => {
counts.len() != lod_alternates.len()
|| counts
.iter()
.zip(lod_alternates.iter())
.any(|(c, (_, idx))| *c != idx.len())
}
None => false,
};
base_changed || lod_changed
});
if size_changed {
for &draw_idx in &entry.draw_indices {
rebuild_changes.push(backend::DrawGeometryUpdate {
draw_idx,
vertices: vertices.clone(),
indices: indices.clone(),
lod_alternates: lod_alternates.clone(),
});
}
rebuild_entry_indices.push(entry_idx);
staged_args.push((entry_idx, new_args.clone()));
continue;
}
let mut slot_failures = 0usize;
for &draw_idx in &entry.draw_indices {
if let Err(e) =
backend.update_mesh_geometry(draw_idx, &vertices, &indices, &lod_alternates)
{
tracing::warn!(
"ProceduralMesh hot-reload: backend rejected update for '{}' draw {}: \
{}",
entry.name,
draw_idx,
e
);
slot_failures += 1;
}
}
if slot_failures == 0 {
result.regenerated += 1;
staged_args.push((entry_idx, new_args.clone()));
} else {
result.failed += 1;
}
}
if !rebuild_changes.is_empty() {
match backend.rebuild_static_geometry(rebuild_changes) {
Ok(()) => {
result.regenerated += rebuild_entry_indices.len();
}
Err(e) => {
tracing::error!(
"ProceduralMesh hot-reload: rebuild_static_geometry failed: {} \
({} entry/entries kept old geometry)",
e,
rebuild_entry_indices.len()
);
result.failed += rebuild_entry_indices.len();
let failed_idxs: std::collections::HashSet<usize> =
rebuild_entry_indices.iter().copied().collect();
staged_args.retain(|(idx, _)| !failed_idxs.contains(idx));
}
}
}
for (entry_idx, new_args) in staged_args {
if let Some(e) = procedural_meshes.entries.get_mut(entry_idx) {
e.args = new_args;
}
}
result
}
pub(super) fn reload_stories(
path: &str,
snapshots: &mut std::collections::HashMap<String, serde_json::Value>,
) -> Vec<Story> {
asset_id::ensure_name_resolver();
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
tracing::error!(
"story hot-reload: failed to read '{}': {} (no update)",
path,
e
);
return Vec::new();
}
};
let entries = match concinnity_cook::build_only::expand_world_from_str(
concinnity_cook::WorldSource::file(&content, std::path::Path::new(path)),
crate::project::assets_dir().as_deref(),
) {
Ok(v) => v,
Err(e) => {
tracing::error!(
"story hot-reload: failed to expand '{}': {} (kept the running story)",
path,
e,
);
return Vec::new();
}
};
let mut out = Vec::new();
for entry in &entries {
if entry.get("type").and_then(|t| t.as_str()) != Some("Story") {
continue;
}
let mut entry = entry.clone();
let Some(name) = take_entry_id(&mut entry) else {
continue;
};
let args = entry
.get("args")
.cloned()
.unwrap_or(serde_json::Value::Null);
if snapshots.get(&name) == Some(&args) {
continue;
}
match serde_json::from_value::<Story>(args.clone()) {
Ok(story) => {
snapshots.insert(name.to_string(), args);
out.push(story);
}
Err(e) => {
tracing::error!(
"story hot-reload: failed to parse compiled story '{}': {} \
(kept the running story)",
name,
e,
);
}
}
}
out
}
fn entry_type(entry: &serde_json::Value) -> Option<RegisteredType> {
entry
.get("type")
.and_then(|v| v.as_str())
.and_then(RegisteredType::parse)
}