1use std::collections::HashMap;
20
21use crate::types::*;
22
23pub fn s2_to_osmap(s2: S2Map, source_filename: &str) -> OpenStrandedMap {
25 let terrain_size = s2.terrain_size;
26
27 let terrain = TerrainData {
28 size: terrain_size,
29 heights: s2.heights.clone(),
30 seaground_level: -2.0,
31 };
32
33 let colormap = if s2.colormap_dim > 0 {
34 Some(ColormapData {
35 dim: s2.colormap_dim,
36 pixels: s2.colormap.clone(),
37 })
38 } else {
39 None
40 };
41
42 let grass = if !s2.grass.is_empty() {
43 Some(GrassData {
44 dim: s2.colormap_dim + 1,
45 values: s2.grass.clone(),
46 })
47 } else {
48 None
49 };
50
51 let mut entities: Vec<MapEntity> = Vec::new();
53
54 for obj in &s2.objects {
55 let y = sample_height(&s2.heights, terrain_size, obj.x, obj.z);
56 entities.push(MapEntity {
57 class: "object".into(),
58 type_id: obj.typ,
59 id: obj.id,
60 position: (obj.x, y, obj.z),
61 rotation_yaw: deg_to_rad(obj.yaw),
62 health: obj.health,
63 health_max: obj.health_max,
64 amount: 0,
65 parent_id: 0,
66 parent_class: 0,
67 links: vec![],
68 build_progress: 0,
69 states: vec![],
70 script: None,
71 unit_data: None,
72 extensions: extract_extensions_for_parent(&s2.extensions, 1, obj.id),
73 });
74 }
75
76 for unit in &s2.units {
77 entities.push(MapEntity {
78 class: "unit".into(),
79 type_id: unit.typ,
80 id: unit.id,
81 position: (unit.x, unit.y, unit.z),
82 rotation_yaw: deg_to_rad(unit.yaw),
83 health: unit.health,
84 health_max: unit.health_max,
85 amount: 0,
86 parent_id: 0,
87 parent_class: 0,
88 links: vec![],
89 build_progress: 0,
90 states: vec![],
91 script: None,
92 unit_data: Some(UnitData {
93 hunger: unit.hunger,
94 thirst: unit.thirst,
95 exhaustion: unit.exhaustion,
96 ai_center_x: unit.ai_center_x,
97 ai_center_z: unit.ai_center_z,
98 day_timer: 0.0,
99 }),
100 extensions: extract_extensions_for_parent(&s2.extensions, 2, unit.id),
101 });
102 }
103
104 for item in &s2.items {
105 entities.push(MapEntity {
106 class: "item".into(),
107 type_id: item.typ,
108 id: item.id,
109 position: (item.x, item.y, item.z),
110 rotation_yaw: deg_to_rad(item.yaw),
111 health: item.health,
112 health_max: 0.0,
113 amount: item.count,
114 parent_id: item.parent_id,
115 parent_class: item.parent_class,
116 links: vec![],
117 build_progress: 0,
118 states: vec![],
119 script: None,
120 unit_data: None,
121 extensions: extract_extensions_for_parent(&s2.extensions, 3, item.id),
122 });
123 }
124
125 for info in &s2.infos {
126 entities.push(MapEntity {
127 class: "info".into(),
128 type_id: info.typ as u32,
129 id: info.id,
130 position: (info.x, info.y, info.z),
131 rotation_yaw: deg_to_rad(info.yaw),
132 health: 0.0,
133 health_max: 0.0,
134 amount: 0,
135 parent_id: 0,
136 parent_class: 0,
137 links: vec![],
138 build_progress: 0,
139 states: vec![],
140 script: Some(info.vars.clone()),
141 unit_data: None,
142 extensions: extract_extensions_for_parent(&s2.extensions, 4, info.id),
143 });
144 }
145
146 let mut environment = HashMap::new();
148 environment.insert("day".into(), s2.env_vars.day.to_string());
149 environment.insert("hour".into(), s2.env_vars.hour.to_string());
150 environment.insert("minute".into(), s2.env_vars.minute.to_string());
151 environment.insert("freezetime".into(), s2.env_vars.freezetime.to_string());
152 environment.insert("skybox".into(), s2.env_vars.skybox);
153 environment.insert("multiplayer".into(), s2.env_vars.multiplayer.to_string());
154 environment.insert("climate".into(), s2.env_vars.climate.to_string());
155 environment.insert("music".into(), s2.env_vars.music);
156 let briefing = s2.env_vars.briefing.clone();
157 let decoded_pw = s2.password.decoded.clone();
158 environment.insert("briefing".into(), s2.env_vars.briefing);
159 environment.insert("fog_r".into(), s2.env_vars.fog[0].to_string());
160 environment.insert("fog_g".into(), s2.env_vars.fog[1].to_string());
161 environment.insert("fog_b".into(), s2.env_vars.fog[2].to_string());
162 environment.insert("fog_mode".into(), s2.env_vars.fog[3].to_string());
163 if !decoded_pw.is_empty() {
164 environment.insert("password".into(), decoded_pw);
165 }
166
167 let mut scripts: Vec<ScriptData> = Vec::new();
169 for info in &s2.infos {
170 if !info.vars.is_empty() {
171 scripts.push(ScriptData {
172 text: info.vars.clone(),
173 entity_id: info.id,
174 });
175 }
176 }
177 if !briefing.is_empty() {
178 scripts.push(ScriptData {
179 text: briefing,
180 entity_id: 0,
181 });
182 }
183
184 OpenStrandedMap {
185 meta: MapMeta {
186 name: source_filename.trim_end_matches(".s2").to_string(),
187 engine_version: env!("CARGO_PKG_VERSION").to_string(),
188 map_version: "0.1.0".to_string(),
189 source_file: source_filename.to_string(),
190 s2_header: Some(s2.header),
191 created_at: String::new(),
192 },
193 terrain,
194 entities,
195 player_spawn: Some(PlayerSpawn {
196 position: (0.0, 2.0, 0.0),
197 rotation_yaw: 0.0,
198 }),
199 environment,
200 colormap,
201 grass,
202 password: s2.password.decoded.clone(),
203 scripts,
204 }
205}
206
207fn sample_height(heights: &[f32], terrain_size: u32, x: f32, z: f32) -> f32 {
209 if heights.is_empty() {
210 return 0.0;
211 }
212 let size = terrain_size as f32;
213 let ix = (x.clamp(0.0, size)).round() as usize;
214 let iz = (z.clamp(0.0, size)).round() as usize;
215 let idx = iz * (terrain_size as usize + 1) + ix;
216 if idx < heights.len() {
217 heights[idx]
218 } else {
219 0.0
220 }
221}
222
223fn deg_to_rad(deg: f32) -> f32 {
225 deg * std::f32::consts::PI / 180.0
226}
227
228fn extract_extensions_for_parent(
230 extensions: &[S2Extension],
231 parent_class: u8,
232 parent_id: u32,
233) -> HashMap<String, String> {
234 let mut map = HashMap::new();
235 for ext in extensions {
236 if ext.parent_class == parent_class && ext.parent_id == parent_id {
237 let key = if ext.key.is_empty() {
238 format!("mode_{}", ext.mode)
239 } else {
240 ext.key.clone()
241 };
242 map.insert(key.clone(), ext.value.clone());
243 if !ext.stuff.is_empty() {
244 map.insert(format!("{}_stuff", key), ext.stuff.clone());
245 }
246 }
247 }
248 map
249}