Blacksite/crates/scene/src/migrate.rs
Rbanh 9f95ba3082
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Add brush authoring schema
2026-06-06 05:42:19 -04:00

260 lines
7.6 KiB
Rust

//! Versioned scene text migrations.
use shared::ActorKind;
pub const DEFAULT_SUN_ILLUMINANCE_LUX: f32 = 100_000.0;
/// Migrates v1 scene text to schema v2 (ActorKind backfill + directional lux fix).
pub fn migrate_v1_to_v2(body: &str) -> String {
let mut out = body.to_string();
out = migrate_directional_light_intensity(&out);
out = backfill_actor_kinds(&out);
out
}
fn backfill_actor_kinds(text: &str) -> String {
let mut result = String::with_capacity(text.len() + 1024);
let mut rest = text;
while let Some(idx) = rest.find("components: {") {
result.push_str(&rest[..idx]);
let after = &rest[idx..];
let Some(close) = find_components_close(after) else {
result.push_str(after);
break;
};
let block = &after[..close];
let tail = &after[close..];
if block.contains("\"shared::components::ActorKind\"") {
result.push_str(block);
} else if block.contains("\"shared::components::LevelObject\"") {
let kind = infer_kind_from_block(block);
result.push_str(&inject_actor_kind(block, kind));
} else {
result.push_str(block);
}
rest = tail;
}
result.push_str(rest);
result
}
fn find_components_close(after: &str) -> Option<usize> {
let mut depth = 0;
let bytes = after.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
match b {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
_ => {}
}
}
None
}
fn inject_actor_kind(block: &str, kind: &str) -> String {
let Some(close) = block.rfind('}') else {
return block.to_string();
};
format!(
"{block_prefix}\"shared::components::ActorKind\": {kind},\n {close_brace}",
block_prefix = &block[..close],
close_brace = &block[close..]
)
}
fn infer_kind_from_block(block: &str) -> &'static str {
if block.contains("\"shared::components::PlayerSpawn\"") {
return actor_kind_ron(ActorKind::PlayerSpawn);
}
if block.contains("\"shared::components::WeaponSpawn\"") {
return actor_kind_ron(ActorKind::WeaponSpawn);
}
if block.contains("\"shared::components::TriggerVolume\"") {
return actor_kind_ron(ActorKind::TriggerVolume);
}
if block.contains("\"shared::components::PostProcessVolumeDesc\"") {
return actor_kind_ron(ActorKind::PostProcessVolume);
}
if block.contains("\"shared::components::TeamSpawn\"") {
return actor_kind_ron(ActorKind::TeamSpawn);
}
if block.contains("\"shared::components::ObjectiveMarker\"") {
return actor_kind_ron(ActorKind::Objective);
}
if block.contains("\"shared::components::LightDesc\"") {
return actor_kind_ron(ActorKind::Light);
}
if block.contains("\"shared::components::ModelRef\"") {
return actor_kind_ron(ActorKind::ImportedModel);
}
if block.contains("\"shared::components::BrushDesc\"") {
return actor_kind_ron(ActorKind::Brush);
}
if block.contains("\"shared::components::Primitive\"")
|| block.contains("\"shared::components::StaticMeshRenderer\"")
{
return actor_kind_ron(ActorKind::StaticMesh);
}
if block.contains("\"shared::components::PrefabRef\"")
|| block.contains("\"shared::components::PrefabInstance\"")
{
return actor_kind_ron(ActorKind::PrefabAnchor);
}
actor_kind_ron(ActorKind::Empty)
}
fn actor_kind_ron(kind: ActorKind) -> &'static str {
match kind {
ActorKind::Empty => "Empty",
ActorKind::Brush => "Brush",
ActorKind::StaticMesh => "StaticMesh",
ActorKind::ImportedModel => "ImportedModel",
ActorKind::Light => "Light",
ActorKind::PrefabAnchor => "PrefabAnchor",
ActorKind::PlayerSpawn => "PlayerSpawn",
ActorKind::WeaponSpawn => "WeaponSpawn",
ActorKind::TriggerVolume => "TriggerVolume",
ActorKind::PostProcessVolume => "PostProcessVolume",
ActorKind::TeamSpawn => "TeamSpawn",
ActorKind::Objective => "Objective",
}
}
fn migrate_directional_light_intensity(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(start) = rest.find("\"shared::components::LightDesc\"") {
out.push_str(&rest[..start]);
let block_start = start;
let Some(rel_end) = find_light_desc_block_end(&rest[block_start..]) else {
out.push_str(&rest[block_start..]);
return out;
};
let end = block_start + rel_end;
let block = &rest[block_start..end];
let migrated = if block.contains("kind: Directional") {
fix_directional_intensity(block)
} else {
block.to_string()
};
out.push_str(&migrated);
rest = &rest[end..];
}
out.push_str(rest);
out
}
fn find_light_desc_block_end(text: &str) -> Option<usize> {
let open = text.find('(')?;
let mut depth = 0;
for (i, ch) in text[open..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(open + i + 1);
}
}
_ => {}
}
}
None
}
fn fix_directional_intensity(block: &str) -> String {
let Some(intensity_idx) = block.find("intensity:") else {
return block.to_string();
};
let after = &block[intensity_idx + "intensity:".len()..];
let trimmed = after.trim_start();
let end = trimmed
.find(|c: char| c == ',' || c == ')')
.unwrap_or(trimmed.len());
let value_str = trimmed[..end].trim();
let Ok(value) = value_str.parse::<f32>() else {
return block.to_string();
};
if value >= 1000.0 {
return block.to_string();
}
let replacement = format!("intensity: {}", DEFAULT_SUN_ILLUMINANCE_LUX);
block.replace(&format!("intensity: {value_str}"), &replacement)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backfills_actor_kind_for_primitive_entity() {
let body = r#"(
resources: {},
entities: {
1: (
components: {
"shared::components::LevelObject": (),
"shared::components::Primitive": (
shape: Box,
size: (1.0, 1.0, 1.0),
),
},
),
},
)"#;
let migrated = migrate_v1_to_v2(body);
assert!(migrated.contains("ActorKind"));
assert!(migrated.contains("StaticMesh"));
}
#[test]
fn backfills_actor_kind_for_brush_entity() {
let body = r#"(
resources: {},
entities: {
1: (
components: {
"shared::components::LevelObject": (),
"shared::components::BrushDesc": (
kind: Additive,
faces: [],
cast_shadows: true,
receive_shadows: true,
),
},
),
},
)"#;
let migrated = migrate_v1_to_v2(body);
assert!(migrated.contains("ActorKind"));
assert!(migrated.contains("Brush"));
}
#[test]
fn fixes_low_directional_lux() {
let body = r#"(
resources: {},
entities: {
1: (
components: {
"shared::components::LightDesc": (
kind: Directional,
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
intensity: 10.0,
range: 0.0,
shadows: true,
),
},
),
},
)"#;
let migrated = migrate_v1_to_v2(body);
assert!(migrated.contains("intensity: 100000"));
}
}