260 lines
7.6 KiB
Rust
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"));
|
|
}
|
|
}
|