use std::collections::HashMap;
use crate::types::*;
pub fn s2_to_osmap(s2: S2Map, source_filename: &str) -> OpenStrandedMap {
let terrain_size = s2.terrain_size;
let half = terrain_size as f32 / 2.0;
let heights = flip_heightmap_z(&s2.heights, terrain_size);
let terrain = TerrainData {
size: terrain_size,
heights,
seaground_level: -2.0,
};
let colormap = if s2.colormap_dim > 0 {
Some(ColormapData {
dim: s2.colormap_dim,
pixels: s2.colormap.clone(),
})
} else {
None
};
let grass = if !s2.grass.is_empty() {
Some(GrassData {
dim: s2.colormap_dim + 1,
values: s2.grass.clone(),
})
} else {
None
};
let mut entities: Vec<MapEntity> = Vec::new();
for obj in &s2.objects {
let y = sample_height(&terrain.heights, terrain_size, obj.x, obj.z);
entities.push(MapEntity {
class: "object".into(),
type_id: obj.typ,
id: obj.id,
position: (obj.x - half, y, half - obj.z),
rotation_yaw: deg_to_rad(obj.yaw),
health: obj.health,
health_max: obj.health_max,
amount: 0,
parent_id: 0,
parent_class: 0,
links: vec![],
build_progress: 0,
states: vec![],
script: None,
unit_data: None,
extensions: extract_extensions_for_parent(&s2.extensions, 1, obj.id),
});
}
for unit in &s2.units {
entities.push(MapEntity {
class: "unit".into(),
type_id: unit.typ,
id: unit.id,
position: (unit.x - half, unit.y, half - unit.z),
rotation_yaw: deg_to_rad(unit.yaw),
health: unit.health,
health_max: unit.health_max,
amount: 0,
parent_id: 0,
parent_class: 0,
links: vec![],
build_progress: 0,
states: vec![],
script: None,
unit_data: Some(UnitData {
hunger: unit.hunger,
thirst: unit.thirst,
exhaustion: unit.exhaustion,
ai_center_x: unit.ai_center_x - half,
ai_center_z: half - unit.ai_center_z,
day_timer: 0.0,
}),
extensions: extract_extensions_for_parent(&s2.extensions, 2, unit.id),
});
}
for item in &s2.items {
entities.push(MapEntity {
class: "item".into(),
type_id: item.typ,
id: item.id,
position: (item.x - half, item.y, half - item.z),
rotation_yaw: deg_to_rad(item.yaw),
health: item.health,
health_max: 0.0,
amount: item.count,
parent_id: item.parent_id,
parent_class: item.parent_class,
links: vec![],
build_progress: 0,
states: vec![],
script: None,
unit_data: None,
extensions: extract_extensions_for_parent(&s2.extensions, 3, item.id),
});
}
for info in &s2.infos {
entities.push(MapEntity {
class: "info".into(),
type_id: info.typ as u32,
id: info.id,
position: (info.x - half, info.y, half - info.z),
rotation_yaw: deg_to_rad(info.yaw),
health: 0.0,
health_max: 0.0,
amount: 0,
parent_id: 0,
parent_class: 0,
links: vec![],
build_progress: 0,
states: vec![],
script: Some(info.vars.clone()),
unit_data: None,
extensions: extract_extensions_for_parent(&s2.extensions, 4, info.id),
});
}
let mut environment = HashMap::new();
environment.insert("day".into(), s2.env_vars.day.to_string());
environment.insert("hour".into(), s2.env_vars.hour.to_string());
environment.insert("minute".into(), s2.env_vars.minute.to_string());
environment.insert("freezetime".into(), s2.env_vars.freezetime.to_string());
environment.insert("skybox".into(), s2.env_vars.skybox);
environment.insert("multiplayer".into(), s2.env_vars.multiplayer.to_string());
environment.insert("climate".into(), s2.env_vars.climate.to_string());
environment.insert("music".into(), s2.env_vars.music);
let briefing = s2.env_vars.briefing.clone();
let decoded_pw = s2.password.decoded.clone();
environment.insert("briefing".into(), s2.env_vars.briefing);
environment.insert("fog_r".into(), s2.env_vars.fog[0].to_string());
environment.insert("fog_g".into(), s2.env_vars.fog[1].to_string());
environment.insert("fog_b".into(), s2.env_vars.fog[2].to_string());
environment.insert("fog_mode".into(), s2.env_vars.fog[3].to_string());
if !decoded_pw.is_empty() {
environment.insert("password".into(), decoded_pw);
}
let mut scripts: Vec<ScriptData> = Vec::new();
for info in &s2.infos {
if !info.vars.is_empty() {
scripts.push(ScriptData {
text: info.vars.clone(),
entity_id: info.id,
});
}
}
if !briefing.is_empty() {
scripts.push(ScriptData {
text: briefing,
entity_id: 0,
});
}
OpenStrandedMap {
meta: MapMeta {
name: source_filename.trim_end_matches(".s2").to_string(),
engine_version: env!("CARGO_PKG_VERSION").to_string(),
map_version: "0.2.0".to_string(),
source_file: source_filename.to_string(),
s2_header: Some(s2.header),
created_at: String::new(),
},
terrain,
entities,
player_spawn: Some(PlayerSpawn {
position: (0.0, 2.0, 0.0),
rotation_yaw: 0.0,
}),
environment,
colormap,
grass,
password: s2.password.decoded.clone(),
scripts,
}
}
fn flip_heightmap_z(heights: &[f32], terrain_size: u32) -> Vec<f32> {
let stride = (terrain_size + 1) as usize;
let mut out = vec![0.0; heights.len()];
for z in 0..=terrain_size as usize {
for x in 0..stride {
let src = z * stride + x;
let dst = (terrain_size as usize - z) * stride + x;
out[dst] = heights[src];
}
}
out
}
fn sample_height(heights: &[f32], terrain_size: u32, x: f32, z: f32) -> f32 {
if heights.is_empty() {
return 0.0;
}
let size = terrain_size as f32;
let ix = (x.clamp(0.0, size)).round() as usize;
let iz = (z.clamp(0.0, size)).round() as usize;
let idx = iz * (terrain_size as usize + 1) + ix;
if idx < heights.len() {
heights[idx]
} else {
0.0
}
}
fn deg_to_rad(deg: f32) -> f32 {
deg * std::f32::consts::PI / 180.0
}
fn extract_extensions_for_parent(
extensions: &[S2Extension],
parent_class: u8,
parent_id: u32,
) -> HashMap<String, String> {
let mut map = HashMap::new();
for ext in extensions {
if ext.parent_class == parent_class && ext.parent_id == parent_id {
let key = if ext.key.is_empty() {
format!("mode_{}", ext.mode)
} else {
ext.key.clone()
};
map.insert(key.clone(), ext.value.clone());
if !ext.stuff.is_empty() {
map.insert(format!("{}_stuff", key), ext.stuff.clone());
}
}
}
map
}