1use std::collections::HashMap;
20
21use crate::types::*;
22
23const CWORLD_SIZE: f32 = 64.0;
26const CWORLD_HEIGHT: f32 = 3200.0;
28
29fn parse_frap_value(value: &str) -> Option<(f32, f32, f32)> {
38 let parts: Vec<&str> = value.split(',').collect();
39 if parts.len() >= 3 {
40 let y = parts[0].trim().parse().ok()?;
41 let pitch = parts[1].trim().parse().unwrap_or(0.0);
42 let roll = parts[2].trim().parse().unwrap_or(0.0);
43 Some((y, pitch, roll))
44 } else {
45 None
46 }
47}
48
49pub fn s2_to_osmap(s2: S2Map, source_filename: &str) -> OpenStrandedMap {
73 let terrain_size = s2.terrain_size;
74
75 let terrain = TerrainData {
79 size: terrain_size,
80 heights: s2.heights.clone(),
81 seaground_level: -2.0,
82 };
83
84 let colormap = if s2.colormap_dim > 0 {
85 Some(ColormapData {
86 dim: s2.colormap_dim,
87 pixels: s2.colormap.clone(),
88 })
89 } else {
90 None
91 };
92
93 let grass = if !s2.grass.is_empty() {
94 Some(GrassData {
95 dim: s2.colormap_dim + 1,
96 values: s2.grass.clone(),
97 })
98 } else {
99 None
100 };
101
102 let mut entities: Vec<MapEntity> = Vec::new();
117 let engine_y_from_s2 = |s2_y: f32| -> f32 {
118 (s2_y / CWORLD_HEIGHT + 0.5 - 0.35) * 60.0
119 };
120
121 for obj in &s2.objects {
122 let h_raw = sample_height(&terrain.heights, terrain_size, obj.x, obj.z);
124 let mut y = (h_raw - 0.35) * 60.0;
125
126 let mut extensions = extract_extensions_for_parent(&s2.extensions, 1, obj.id);
128
129 let mut rotation_pitch = 0.0;
135 let mut rotation_roll = 0.0;
136 if let Some(frap_val) = extensions.get("f") {
137 if let Some((frap_y, pitch_deg, roll_deg)) = parse_frap_value(frap_val) {
138 y = engine_y_from_s2(frap_y);
140 rotation_pitch = deg_to_rad(pitch_deg);
141 rotation_roll = deg_to_rad(roll_deg);
142 }
143 }
144 extensions.remove("f"); entities.push(MapEntity {
147 class: "object".into(),
148 type_id: obj.typ,
149 id: obj.id,
150 position: (obj.x / CWORLD_SIZE, y, obj.z / CWORLD_SIZE),
151 rotation_yaw: deg_to_rad(obj.yaw),
152 rotation_pitch,
153 rotation_roll,
154 health: obj.health,
155 health_max: obj.health_max,
156 amount: 0,
157 parent_id: 0,
158 parent_class: 0,
159 links: vec![],
160 build_progress: 0,
161 states: vec![],
162 script: None,
163 unit_data: None,
164 extensions,
165 });
166 }
167
168 for unit in &s2.units {
169 let y = engine_y_from_s2(unit.y);
170 entities.push(MapEntity {
171 class: "unit".into(),
172 type_id: unit.typ,
173 id: unit.id,
174 position: (unit.x / CWORLD_SIZE, y, unit.z / CWORLD_SIZE),
175 rotation_yaw: deg_to_rad(unit.yaw),
176 rotation_pitch: 0.0,
177 rotation_roll: 0.0,
178 health: unit.health,
179 health_max: unit.health_max,
180 amount: 0,
181 parent_id: 0,
182 parent_class: 0,
183 links: vec![],
184 build_progress: 0,
185 states: vec![],
186 script: None,
187 unit_data: Some(UnitData {
188 hunger: unit.hunger,
189 thirst: unit.thirst,
190 exhaustion: unit.exhaustion,
191 ai_center_x: unit.ai_center_x / CWORLD_SIZE,
192 ai_center_z: unit.ai_center_z / CWORLD_SIZE,
193 day_timer: 0.0,
194 }),
195 extensions: extract_extensions_for_parent(&s2.extensions, 2, unit.id),
196 });
197 }
198
199 for item in &s2.items {
200 let y = engine_y_from_s2(item.y);
201 entities.push(MapEntity {
202 class: "item".into(),
203 type_id: item.typ,
204 id: item.id,
205 position: (item.x / CWORLD_SIZE, y, item.z / CWORLD_SIZE),
206 rotation_yaw: deg_to_rad(item.yaw),
207 rotation_pitch: 0.0,
208 rotation_roll: 0.0,
209 health: item.health,
210 health_max: 0.0,
211 amount: item.count,
212 parent_id: item.parent_id,
213 parent_class: item.parent_class,
214 links: vec![],
215 build_progress: 0,
216 states: vec![],
217 script: None,
218 unit_data: None,
219 extensions: extract_extensions_for_parent(&s2.extensions, 3, item.id),
220 });
221 }
222
223 for info in &s2.infos {
224 let y = engine_y_from_s2(info.y);
225 entities.push(MapEntity {
226 class: "info".into(),
227 type_id: info.typ as u32,
228 id: info.id,
229 position: (info.x / CWORLD_SIZE, y, info.z / CWORLD_SIZE),
230 rotation_yaw: deg_to_rad(info.yaw),
231 rotation_pitch: 0.0,
232 rotation_roll: 0.0,
233 health: 0.0,
234 health_max: 0.0,
235 amount: 0,
236 parent_id: 0,
237 parent_class: 0,
238 links: vec![],
239 build_progress: 0,
240 states: vec![],
241 script: Some(info.vars.clone()),
242 unit_data: None,
243 extensions: extract_extensions_for_parent(&s2.extensions, 4, info.id),
244 });
245 }
246
247 let mut environment = HashMap::new();
249 environment.insert("day".into(), s2.env_vars.day.to_string());
250 environment.insert("hour".into(), s2.env_vars.hour.to_string());
251 environment.insert("minute".into(), s2.env_vars.minute.to_string());
252 environment.insert("freezetime".into(), s2.env_vars.freezetime.to_string());
253 environment.insert("skybox".into(), s2.env_vars.skybox);
254 environment.insert("multiplayer".into(), s2.env_vars.multiplayer.to_string());
255 environment.insert("climate".into(), s2.env_vars.climate.to_string());
256 environment.insert("music".into(), s2.env_vars.music);
257 let briefing = s2.env_vars.briefing.clone();
258 let decoded_pw = s2.password.decoded.clone();
259 environment.insert("briefing".into(), s2.env_vars.briefing);
260 environment.insert("fog_r".into(), s2.env_vars.fog[0].to_string());
261 environment.insert("fog_g".into(), s2.env_vars.fog[1].to_string());
262 environment.insert("fog_b".into(), s2.env_vars.fog[2].to_string());
263 environment.insert("fog_mode".into(), s2.env_vars.fog[3].to_string());
264 if !decoded_pw.is_empty() {
265 environment.insert("password".into(), decoded_pw);
266 }
267
268 let spawn_pos = spawn_center_engine_y(terrain_size, &terrain.heights);
270
271 let mut scripts: Vec<ScriptData> = Vec::new();
273 for info in &s2.infos {
274 if !info.vars.is_empty() {
275 scripts.push(ScriptData {
276 text: info.vars.clone(),
277 entity_id: info.id,
278 });
279 }
280 }
281 if !briefing.is_empty() {
282 scripts.push(ScriptData {
283 text: briefing,
284 entity_id: 0,
285 });
286 }
287
288 OpenStrandedMap {
289 meta: MapMeta {
290 name: source_filename.trim_end_matches(".s2").to_string(),
291 engine_version: env!("CARGO_PKG_VERSION").to_string(),
292 map_version: "0.2.0".to_string(),
293 source_file: source_filename.to_string(),
294 s2_header: Some(s2.header),
295 created_at: String::new(),
296 },
297 terrain,
298 entities,
299 player_spawn: Some(PlayerSpawn {
300 position: spawn_pos,
301 rotation_yaw: 0.0,
302 }),
303 environment,
304 colormap,
305 grass,
306 password: s2.password.decoded.clone(),
307 scripts,
308 }
309}
310
311fn spawn_center_engine_y(terrain_size: u32, heights: &[f32]) -> (f32, f32, f32) {
314 let mid = terrain_size as usize / 2;
315 let stride = (terrain_size + 1) as usize;
316 let centre_h = heights.get(mid * stride + mid).copied().unwrap_or(0.35);
317 let y = (centre_h - 0.35) * 60.0; (0.0, y, 0.0) }
320
321fn sample_height(heights: &[f32], terrain_size: u32, x: f32, z: f32) -> f32 {
327 if heights.is_empty() {
328 return 0.0;
329 }
330 let half = terrain_size as f32 / 2.0;
331 let gx = (x / CWORLD_SIZE + half).clamp(0.0, terrain_size as f32);
332 let gz = (z / CWORLD_SIZE + half).clamp(0.0, terrain_size as f32);
333 let ix = gx.round() as usize;
334 let iz = gz.round() as usize;
335 let idx = iz * (terrain_size as usize + 1) + ix;
336 if idx < heights.len() {
337 heights[idx]
338 } else {
339 0.0
340 }
341}
342
343fn deg_to_rad(deg: f32) -> f32 {
345 deg * std::f32::consts::PI / 180.0
346}
347
348fn extract_extensions_for_parent(
350 extensions: &[S2Extension],
351 parent_class: u8,
352 parent_id: u32,
353) -> HashMap<String, String> {
354 let mut map = HashMap::new();
355 for ext in extensions {
356 if ext.parent_class == parent_class && ext.parent_id == parent_id {
357 let key = if ext.key.is_empty() {
358 format!("mode_{}", ext.mode)
359 } else {
360 ext.key.clone()
361 };
362 map.insert(key.clone(), ext.value.clone());
363 if !ext.stuff.is_empty() {
364 map.insert(format!("{}_stuff", key), ext.stuff.clone());
365 }
366 }
367 }
368 map
369}