openstranded-map-tool 0.2.1

Convert Stranded II .s2 map files to .osmap (OpenStranded MAP) format
Documentation
// openstranded-map-tool — types for the .s2/.osmap map formats
// Copyright (C) 2026  OpenStranded contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! Data types for the OpenStranded Map (`.osmap`) format.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

// ===========================================================================
// .osmap types
// ===========================================================================

/// Root type for an `.osmap` file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenStrandedMap {
    /// Map metadata.
    pub meta: MapMeta,
    /// Terrain heightmap data.
    pub terrain: TerrainData,
    /// Spawned entities (objects, buildings, items, units, infos).
    #[serde(default)]
    pub entities: Vec<MapEntity>,
    /// The player spawn point.
    #[serde(default)]
    pub player_spawn: Option<PlayerSpawn>,
    /// Environment/global variables set on this map.
    #[serde(default)]
    pub environment: HashMap<String, String>,
    /// Colormap (terrain colour texture) — pixel data in row-major RGB.
    #[serde(default)]
    pub colormap: Option<ColormapData>,
    /// Grass layer — (colormap_dim+1)² bytes, column-major, 0/1 values.
    #[serde(default)]
    pub grass: Option<GrassData>,
    /// Map password (decoded).
    #[serde(default)]
    pub password: String,
    /// Embedded scripts from .s2 infos.
    #[serde(default)]
    pub scripts: Vec<ScriptData>,
}

/// Map metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MapMeta {
    /// Human-readable map name.
    pub name: String,
    /// Engine version that created this map.
    pub engine_version: String,
    /// Map format semver (bumped on non-breaking additions).
    pub map_version: String,
    /// Original .s2 source file (if converted).
    #[serde(default)]
    pub source_file: String,
    /// Original .s2 header fields.
    #[serde(default)]
    pub s2_header: Option<S2Header>,
    /// When the map was converted or last saved (ISO 8601).
    #[serde(default)]
    pub created_at: String,
}

/// Original .s2 header lines.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct S2Header {
    pub version: String,
    pub date: String,
    pub time: String,
    pub author: String,
    pub map_type: String,
    /// Type-format string: "tfobject tfunit tfitem" concatenated (e.g. "001", "100").
    /// Empty string means all 0 (all types are u8).
    /// Each char is '0' (u8) or '1' (u16).
    #[serde(default)]
    pub type_format: String,
}

/// Terrain heightmap data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerrainData {
    /// Grid size (number of cells — terrain_size in .s2).
    pub size: u32,
    /// Elevation values; row-major, [z * (size+1) + x].
    /// Range: 0.0–1.0 (normalised).
    pub heights: Vec<f32>,
    /// Sea/water level (Y below which = underwater).
    #[serde(default = "default_seaground")]
    pub seaground_level: f32,
}

fn default_seaground() -> f32 {
    -2.0
}

/// Colormap (terrain colour texture) data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColormapData {
    /// Dimension (width = height = dim).
    pub dim: u32,
    /// RGB pixel data, row-major, length = dim * dim * 3.
    pub pixels: Vec<u8>,
}

/// Grass layer data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrassData {
    /// Dimension (width = height = dim).
    pub dim: u32,
    /// Binary grass data, column-major, length = dim * dim.
    /// 0 = no grass, 1 = grass.
    pub values: Vec<u8>,
}

/// An entity placed on the map.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MapEntity {
    /// Entity class: "object", "building", "item", "unit", "info".
    pub class: String,
    /// Type ID as defined in the content pack.
    pub type_id: u32,
    /// Unique numeric ID within the map.
    pub id: u32,
    /// World position. Y is explicit.
    pub position: (f32, f32, f32),
    /// Yaw rotation in radians.
    #[serde(default)]
    pub rotation_yaw: f32,
    /// Pitch rotation in radians (from FRAP extension).
    #[serde(default)]
    pub rotation_pitch: f32,
    /// Roll rotation in radians (from FRAP extension).
    #[serde(default)]
    pub rotation_roll: f32,
    /// Current health.
    #[serde(default)]
    pub health: f32,
    /// Maximum health.
    #[serde(default)]
    pub health_max: f32,
    /// Initial state flags.
    #[serde(default)]
    pub states: Vec<String>,
    /// Embedded script override.
    #[serde(default)]
    pub script: Option<String>,
    /// Item-specific: stack size.
    #[serde(default)]
    pub amount: u32,
    /// Parent entity ID (for items inside containers).
    #[serde(default)]
    pub parent_id: u32,
    /// Parent class (0=none, 1=obj, 2=unit, 3=item, 4=info).
    #[serde(default)]
    pub parent_class: u8,
    /// Linked entity IDs.
    #[serde(default)]
    pub links: Vec<u32>,
    /// Building-specific: construction progress 0–100.
    #[serde(default)]
    pub build_progress: u32,
    /// Unit-specific AI data.
    #[serde(default)]
    pub unit_data: Option<UnitData>,
    /// Additional custom data (extensions).
    #[serde(default)]
    pub extensions: HashMap<String, String>,
}

/// Unit-specific AI data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnitData {
    #[serde(default)]
    pub hunger: f32,
    #[serde(default)]
    pub thirst: f32,
    #[serde(default)]
    pub exhaustion: f32,
    #[serde(default)]
    pub ai_center_x: f32,
    #[serde(default)]
    pub ai_center_z: f32,
    #[serde(default)]
    pub day_timer: f32,
}

/// Player spawn point.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerSpawn {
    pub position: (f32, f32, f32),
    pub rotation_yaw: f32,
}

/// Embedded script data (from .s2 infos or global briefing).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptData {
    pub text: String,
    pub entity_id: u32,
}

// ===========================================================================
// .s2 raw types (intermediate — not part of .osmap output)
// ===========================================================================

/// Fully parsed `.s2` file data.
#[derive(Debug, Clone)]
pub struct S2Map {
    pub header: S2Header,
    pub minimap: Vec<u8>,
    pub password: S2Password,
    pub env_vars: S2EnvVars,
    pub quickslots: Vec<String>,
    pub colormap_dim: u32,
    pub colormap: Vec<u8>,
    pub terrain_size: u32,
    pub heights: Vec<f32>,
    pub grass: Vec<u8>,
    pub objects: Vec<S2Object>,
    pub units: Vec<S2Unit>,
    pub items: Vec<S2Item>,
    pub infos: Vec<S2Info>,
    pub states: Vec<S2State>,
    pub extensions: Vec<S2Extension>,
    pub trailer: S2Trailer,
}

/// Password header between minimap and env vars.
/// From `e_save_map.bb`: WriteByte(pwkey) + WriteLine(encodedpw$)
#[derive(Debug, Clone)]
pub struct S2Password {
    /// XOR key byte (random 5–250).
    pub key: u8,
    /// XOR-encoded password string.
    pub encoded: String,
    /// Decoded password (key XOR each byte).
    pub decoded: String,
}

#[derive(Debug, Clone)]
pub struct S2EnvVars {
    pub day: u32,
    pub hour: u8,
    pub minute: u8,
    pub freezetime: u8,
    pub skybox: String,
    pub multiplayer: u8,
    pub climate: u8,
    pub music: String,
    pub briefing: String,
    pub fog: [u8; 4],
    pub extra: u8,
}

/// Object record (source e_save_map.bb lines 137–148).
#[derive(Debug, Clone)]
pub struct S2Object {
    pub id: u32,
    pub typ: u32,
    pub x: f32,
    pub z: f32,
    pub yaw: f32,
    pub health: f32,
    pub health_max: f32,
    pub day_timer: u32,
}

/// Unit record (source e_save_map.bb lines 166–182).
/// NOTE: unit sub-data (states, items, scripts) is NOT stored inline in the
/// unit section; it's stored as Extensions.
#[derive(Debug, Clone)]
pub struct S2Unit {
    pub id: u32,
    pub typ: u32,
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub yaw: f32,
    pub health: f32,
    pub health_max: f32,
    pub hunger: f32,
    pub thirst: f32,
    pub exhaustion: f32,
    pub ai_center_x: f32,
    pub ai_center_z: f32,
}

/// Item record (source e_save_map.bb lines 192–207).
#[derive(Debug, Clone)]
pub struct S2Item {
    pub id: u32,
    pub typ: u32,
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub yaw: f32,
    pub health: f32,
    pub count: u32,
    pub parent_class: u8,
    pub parent_mode: u8,
    pub parent_id: u32,
}

/// Info record (source e_save_map.bb lines 243–251).
#[derive(Debug, Clone)]
pub struct S2Info {
    pub id: u32,
    pub typ: u8,
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub pitch: f32,
    pub yaw: f32,
    pub vars: String,
}

/// State record (source e_save_map.bb lines 260–272).
#[derive(Debug, Clone)]
pub struct S2State {
    pub typ: u32,
    pub parent_class: u32,
    pub parent_id: u32,
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub fx: f32,
    pub fy: f32,
    pub fz: f32,
    pub value: u32,
    pub value_f: f32,
    pub value_s: String,
}

/// Extension record (source e_save_map.bb lines 314–327).
#[derive(Debug, Clone)]
pub struct S2Extension {
    pub typ: u8,
    pub parent_class: u8,
    pub parent_id: u32,
    pub mode: u32,
    pub key: String,
    pub value: String,
    pub stuff: String,
}

/// Trailer lines: WriteLine("") + WriteLine("### EOF Map File") + WriteLine("www.unrealsoftware.de")
#[derive(Debug, Clone)]
pub struct S2Trailer {
    pub blank: String,
    pub eof_marker: String,
    pub url: String,
}