1use std::collections::HashMap;
20
21use crate::types::*;
22
23pub fn s2_to_osmap(s2: S2Map, source_filename: &str) -> OpenStrandedMap {
39 let terrain_size = s2.terrain_size;
40 let half = terrain_size as f32 / 2.0;
41
42 let heights = flip_heightmap_z(&s2.heights, terrain_size);
45
46 let terrain = TerrainData {
47 size: terrain_size,
48 heights,
49 seaground_level: -2.0,
50 };
51
52 let colormap = if s2.colormap_dim > 0 {
53 Some(ColormapData {
54 dim: s2.colormap_dim,
55 pixels: s2.colormap.clone(),
56 })
57 } else {
58 None
59 };
60
61 let grass = if !s2.grass.is_empty() {
62 Some(GrassData {
63 dim: s2.colormap_dim + 1,
64 values: s2.grass.clone(),
65 })
66 } else {
67 None
68 };
69
70 let mut entities: Vec<MapEntity> = Vec::new();
89
90 for obj in &s2.objects {
91 let y = sample_height(&terrain.heights, terrain_size, obj.x, obj.z);
93 entities.push(MapEntity {
94 class: "object".into(),
95 type_id: obj.typ,
96 id: obj.id,
97 position: (obj.x - half, y, half - obj.z),
98 rotation_yaw: deg_to_rad(obj.yaw),
99 health: obj.health,
100 health_max: obj.health_max,
101 amount: 0,
102 parent_id: 0,
103 parent_class: 0,
104 links: vec![],
105 build_progress: 0,
106 states: vec![],
107 script: None,
108 unit_data: None,
109 extensions: extract_extensions_for_parent(&s2.extensions, 1, obj.id),
110 });
111 }
112
113 for unit in &s2.units {
114 entities.push(MapEntity {
115 class: "unit".into(),
116 type_id: unit.typ,
117 id: unit.id,
118 position: (unit.x - half, unit.y, half - unit.z),
119 rotation_yaw: deg_to_rad(unit.yaw),
120 health: unit.health,
121 health_max: unit.health_max,
122 amount: 0,
123 parent_id: 0,
124 parent_class: 0,
125 links: vec![],
126 build_progress: 0,
127 states: vec![],
128 script: None,
129 unit_data: Some(UnitData {
130 hunger: unit.hunger,
131 thirst: unit.thirst,
132 exhaustion: unit.exhaustion,
133 ai_center_x: unit.ai_center_x - half,
134 ai_center_z: half - unit.ai_center_z,
135 day_timer: 0.0,
136 }),
137 extensions: extract_extensions_for_parent(&s2.extensions, 2, unit.id),
138 });
139 }
140
141 for item in &s2.items {
142 entities.push(MapEntity {
143 class: "item".into(),
144 type_id: item.typ,
145 id: item.id,
146 position: (item.x - half, item.y, half - item.z),
147 rotation_yaw: deg_to_rad(item.yaw),
148 health: item.health,
149 health_max: 0.0,
150 amount: item.count,
151 parent_id: item.parent_id,
152 parent_class: item.parent_class,
153 links: vec![],
154 build_progress: 0,
155 states: vec![],
156 script: None,
157 unit_data: None,
158 extensions: extract_extensions_for_parent(&s2.extensions, 3, item.id),
159 });
160 }
161
162 for info in &s2.infos {
163 entities.push(MapEntity {
164 class: "info".into(),
165 type_id: info.typ as u32,
166 id: info.id,
167 position: (info.x - half, info.y, half - info.z),
168 rotation_yaw: deg_to_rad(info.yaw),
169 health: 0.0,
170 health_max: 0.0,
171 amount: 0,
172 parent_id: 0,
173 parent_class: 0,
174 links: vec![],
175 build_progress: 0,
176 states: vec![],
177 script: Some(info.vars.clone()),
178 unit_data: None,
179 extensions: extract_extensions_for_parent(&s2.extensions, 4, info.id),
180 });
181 }
182
183 let mut environment = HashMap::new();
185 environment.insert("day".into(), s2.env_vars.day.to_string());
186 environment.insert("hour".into(), s2.env_vars.hour.to_string());
187 environment.insert("minute".into(), s2.env_vars.minute.to_string());
188 environment.insert("freezetime".into(), s2.env_vars.freezetime.to_string());
189 environment.insert("skybox".into(), s2.env_vars.skybox);
190 environment.insert("multiplayer".into(), s2.env_vars.multiplayer.to_string());
191 environment.insert("climate".into(), s2.env_vars.climate.to_string());
192 environment.insert("music".into(), s2.env_vars.music);
193 let briefing = s2.env_vars.briefing.clone();
194 let decoded_pw = s2.password.decoded.clone();
195 environment.insert("briefing".into(), s2.env_vars.briefing);
196 environment.insert("fog_r".into(), s2.env_vars.fog[0].to_string());
197 environment.insert("fog_g".into(), s2.env_vars.fog[1].to_string());
198 environment.insert("fog_b".into(), s2.env_vars.fog[2].to_string());
199 environment.insert("fog_mode".into(), s2.env_vars.fog[3].to_string());
200 if !decoded_pw.is_empty() {
201 environment.insert("password".into(), decoded_pw);
202 }
203
204 let mut scripts: Vec<ScriptData> = Vec::new();
206 for info in &s2.infos {
207 if !info.vars.is_empty() {
208 scripts.push(ScriptData {
209 text: info.vars.clone(),
210 entity_id: info.id,
211 });
212 }
213 }
214 if !briefing.is_empty() {
215 scripts.push(ScriptData {
216 text: briefing,
217 entity_id: 0,
218 });
219 }
220
221 OpenStrandedMap {
222 meta: MapMeta {
223 name: source_filename.trim_end_matches(".s2").to_string(),
224 engine_version: env!("CARGO_PKG_VERSION").to_string(),
225 map_version: "0.2.0".to_string(),
226 source_file: source_filename.to_string(),
227 s2_header: Some(s2.header),
228 created_at: String::new(),
229 },
230 terrain,
231 entities,
232 player_spawn: Some(PlayerSpawn {
236 position: (0.0, 2.0, 0.0),
237 rotation_yaw: 0.0,
238 }),
239 environment,
240 colormap,
241 grass,
242 password: s2.password.decoded.clone(),
243 scripts,
244 }
245}
246
247fn flip_heightmap_z(heights: &[f32], terrain_size: u32) -> Vec<f32> {
253 let stride = (terrain_size + 1) as usize;
254 let mut out = vec![0.0; heights.len()];
255 for z in 0..=terrain_size as usize {
256 for x in 0..stride {
257 let src = z * stride + x;
258 let dst = (terrain_size as usize - z) * stride + x;
259 out[dst] = heights[src];
260 }
261 }
262 out
263}
264
265fn sample_height(heights: &[f32], terrain_size: u32, x: f32, z: f32) -> f32 {
267 if heights.is_empty() {
268 return 0.0;
269 }
270 let size = terrain_size as f32;
271 let ix = (x.clamp(0.0, size)).round() as usize;
272 let iz = (z.clamp(0.0, size)).round() as usize;
273 let idx = iz * (terrain_size as usize + 1) + ix;
274 if idx < heights.len() {
275 heights[idx]
276 } else {
277 0.0
278 }
279}
280
281fn deg_to_rad(deg: f32) -> f32 {
283 deg * std::f32::consts::PI / 180.0
284}
285
286fn extract_extensions_for_parent(
288 extensions: &[S2Extension],
289 parent_class: u8,
290 parent_id: u32,
291) -> HashMap<String, String> {
292 let mut map = HashMap::new();
293 for ext in extensions {
294 if ext.parent_class == parent_class && ext.parent_id == parent_id {
295 let key = if ext.key.is_empty() {
296 format!("mode_{}", ext.mode)
297 } else {
298 ext.key.clone()
299 };
300 map.insert(key.clone(), ext.value.clone());
301 if !ext.stuff.is_empty() {
302 map.insert(format!("{}_stuff", key), ext.stuff.clone());
303 }
304 }
305 }
306 map
307}