Skip to main content

openstranded_map_tool/
types.rs

1// openstranded-map-tool — types for the .s2/.osmap map formats
2// Copyright (C) 2026  OpenStranded contributors
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Data types for the OpenStranded Map (`.osmap`) format.
18
19use std::collections::HashMap;
20
21use serde::{Deserialize, Serialize};
22
23// ===========================================================================
24// .osmap types
25// ===========================================================================
26
27/// Root type for an `.osmap` file.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct OpenStrandedMap {
30    /// Map metadata.
31    pub meta: MapMeta,
32    /// Terrain heightmap data.
33    pub terrain: TerrainData,
34    /// Spawned entities (objects, buildings, items, units, infos).
35    #[serde(default)]
36    pub entities: Vec<MapEntity>,
37    /// The player spawn point.
38    #[serde(default)]
39    pub player_spawn: Option<PlayerSpawn>,
40    /// Environment/global variables set on this map.
41    #[serde(default)]
42    pub environment: HashMap<String, String>,
43    /// Colormap (terrain colour texture) — pixel data in row-major RGB.
44    #[serde(default)]
45    pub colormap: Option<ColormapData>,
46    /// Grass layer — (colormap_dim+1)² bytes, column-major, 0/1 values.
47    #[serde(default)]
48    pub grass: Option<GrassData>,
49    /// Map password (decoded).
50    #[serde(default)]
51    pub password: String,
52    /// Embedded scripts from .s2 infos.
53    #[serde(default)]
54    pub scripts: Vec<ScriptData>,
55}
56
57/// Map metadata.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct MapMeta {
60    /// Human-readable map name.
61    pub name: String,
62    /// Engine version that created this map.
63    pub engine_version: String,
64    /// Map format semver (bumped on non-breaking additions).
65    pub map_version: String,
66    /// Original .s2 source file (if converted).
67    #[serde(default)]
68    pub source_file: String,
69    /// Original .s2 header fields.
70    #[serde(default)]
71    pub s2_header: Option<S2Header>,
72    /// When the map was converted or last saved (ISO 8601).
73    #[serde(default)]
74    pub created_at: String,
75}
76
77/// Original .s2 header lines.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct S2Header {
80    pub version: String,
81    pub date: String,
82    pub time: String,
83    pub author: String,
84    pub map_type: String,
85}
86
87/// Terrain heightmap data.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct TerrainData {
90    /// Grid size (number of cells — terrain_size in .s2).
91    pub size: u32,
92    /// Elevation values; row-major, [z * (size+1) + x].
93    /// Range: 0.0–1.0 (normalised).
94    pub heights: Vec<f32>,
95    /// Sea/water level (Y below which = underwater).
96    #[serde(default = "default_seaground")]
97    pub seaground_level: f32,
98}
99
100fn default_seaground() -> f32 {
101    -2.0
102}
103
104/// Colormap (terrain colour texture) data.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ColormapData {
107    /// Dimension (width = height = dim).
108    pub dim: u32,
109    /// RGB pixel data, row-major, length = dim * dim * 3.
110    pub pixels: Vec<u8>,
111}
112
113/// Grass layer data.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct GrassData {
116    /// Dimension (width = height = dim).
117    pub dim: u32,
118    /// Binary grass data, column-major, length = dim * dim.
119    /// 0 = no grass, 1 = grass.
120    pub values: Vec<u8>,
121}
122
123/// An entity placed on the map.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct MapEntity {
126    /// Entity class: "object", "building", "item", "unit", "info".
127    pub class: String,
128    /// Type ID as defined in the content pack.
129    pub type_id: u32,
130    /// Unique numeric ID within the map.
131    pub id: u32,
132    /// World position. Y is explicit.
133    pub position: (f32, f32, f32),
134    /// Yaw rotation in radians.
135    #[serde(default)]
136    pub rotation_yaw: f32,
137    /// Current health.
138    #[serde(default)]
139    pub health: f32,
140    /// Maximum health.
141    #[serde(default)]
142    pub health_max: f32,
143    /// Initial state flags.
144    #[serde(default)]
145    pub states: Vec<String>,
146    /// Embedded script override.
147    #[serde(default)]
148    pub script: Option<String>,
149    /// Item-specific: stack size.
150    #[serde(default)]
151    pub amount: u32,
152    /// Parent entity ID (for items inside containers).
153    #[serde(default)]
154    pub parent_id: u32,
155    /// Parent class (0=none, 1=obj, 2=unit, 3=item, 4=info).
156    #[serde(default)]
157    pub parent_class: u8,
158    /// Linked entity IDs.
159    #[serde(default)]
160    pub links: Vec<u32>,
161    /// Building-specific: construction progress 0–100.
162    #[serde(default)]
163    pub build_progress: u32,
164    /// Unit-specific AI data.
165    #[serde(default)]
166    pub unit_data: Option<UnitData>,
167    /// Additional custom data (extensions).
168    #[serde(default)]
169    pub extensions: HashMap<String, String>,
170}
171
172/// Unit-specific AI data.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct UnitData {
175    #[serde(default)]
176    pub hunger: f32,
177    #[serde(default)]
178    pub thirst: f32,
179    #[serde(default)]
180    pub exhaustion: f32,
181    #[serde(default)]
182    pub ai_center_x: f32,
183    #[serde(default)]
184    pub ai_center_z: f32,
185    #[serde(default)]
186    pub day_timer: f32,
187}
188
189/// Player spawn point.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct PlayerSpawn {
192    pub position: (f32, f32, f32),
193    pub rotation_yaw: f32,
194}
195
196/// Embedded script data (from .s2 infos or global briefing).
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ScriptData {
199    pub text: String,
200    pub entity_id: u32,
201}
202
203// ===========================================================================
204// .s2 raw types (intermediate — not part of .osmap output)
205// ===========================================================================
206
207/// Fully parsed `.s2` file data.
208#[derive(Debug, Clone)]
209pub struct S2Map {
210    pub header: S2Header,
211    pub minimap: Vec<u8>,
212    pub password: S2Password,
213    pub env_vars: S2EnvVars,
214    pub quickslots: Vec<String>,
215    pub colormap_dim: u32,
216    pub colormap: Vec<u8>,
217    pub terrain_size: u32,
218    pub heights: Vec<f32>,
219    pub grass: Vec<u8>,
220    pub objects: Vec<S2Object>,
221    pub units: Vec<S2Unit>,
222    pub items: Vec<S2Item>,
223    pub infos: Vec<S2Info>,
224    pub states: Vec<S2State>,
225    pub extensions: Vec<S2Extension>,
226    pub trailer: S2Trailer,
227}
228
229/// Password header between minimap and env vars.
230/// From `e_save_map.bb`: WriteByte(pwkey) + WriteLine(encodedpw$)
231#[derive(Debug, Clone)]
232pub struct S2Password {
233    /// XOR key byte (random 5–250).
234    pub key: u8,
235    /// XOR-encoded password string.
236    pub encoded: String,
237    /// Decoded password (key XOR each byte).
238    pub decoded: String,
239}
240
241#[derive(Debug, Clone)]
242pub struct S2EnvVars {
243    pub day: u32,
244    pub hour: u8,
245    pub minute: u8,
246    pub freezetime: u8,
247    pub skybox: String,
248    pub multiplayer: u8,
249    pub climate: u8,
250    pub music: String,
251    pub briefing: String,
252    pub fog: [u8; 4],
253    pub extra: u8,
254}
255
256/// Object record (source e_save_map.bb lines 137–148).
257#[derive(Debug, Clone)]
258pub struct S2Object {
259    pub id: u32,
260    pub typ: u32,
261    pub x: f32,
262    pub z: f32,
263    pub yaw: f32,
264    pub health: f32,
265    pub health_max: f32,
266    pub day_timer: u32,
267}
268
269/// Unit record (source e_save_map.bb lines 166–182).
270/// NOTE: unit sub-data (states, items, scripts) is NOT stored inline in the
271/// unit section; it's stored as Extensions.
272#[derive(Debug, Clone)]
273pub struct S2Unit {
274    pub id: u32,
275    pub typ: u32,
276    pub x: f32,
277    pub y: f32,
278    pub z: f32,
279    pub yaw: f32,
280    pub health: f32,
281    pub health_max: f32,
282    pub hunger: f32,
283    pub thirst: f32,
284    pub exhaustion: f32,
285    pub ai_center_x: f32,
286    pub ai_center_z: f32,
287}
288
289/// Item record (source e_save_map.bb lines 192–207).
290#[derive(Debug, Clone)]
291pub struct S2Item {
292    pub id: u32,
293    pub typ: u32,
294    pub x: f32,
295    pub y: f32,
296    pub z: f32,
297    pub yaw: f32,
298    pub health: f32,
299    pub count: u32,
300    pub parent_class: u8,
301    pub parent_mode: u8,
302    pub parent_id: u32,
303}
304
305/// Info record (source e_save_map.bb lines 243–251).
306#[derive(Debug, Clone)]
307pub struct S2Info {
308    pub id: u32,
309    pub typ: u8,
310    pub x: f32,
311    pub y: f32,
312    pub z: f32,
313    pub pitch: f32,
314    pub yaw: f32,
315    pub vars: String,
316}
317
318/// State record (source e_save_map.bb lines 260–272).
319#[derive(Debug, Clone)]
320pub struct S2State {
321    pub typ: u32,
322    pub parent_class: u32,
323    pub parent_id: u32,
324    pub x: f32,
325    pub y: f32,
326    pub z: f32,
327    pub fx: f32,
328    pub fy: f32,
329    pub fz: f32,
330    pub value: u32,
331    pub value_f: f32,
332    pub value_s: String,
333}
334
335/// Extension record (source e_save_map.bb lines 314–327).
336#[derive(Debug, Clone)]
337pub struct S2Extension {
338    pub typ: u8,
339    pub parent_class: u8,
340    pub parent_id: u32,
341    pub mode: u32,
342    pub key: String,
343    pub value: String,
344    pub stuff: String,
345}
346
347/// Trailer lines: WriteLine("") + WriteLine("### EOF Map File") + WriteLine("www.unrealsoftware.de")
348#[derive(Debug, Clone)]
349pub struct S2Trailer {
350    pub blank: String,
351    pub eof_marker: String,
352    pub url: String,
353}