screeps_utils/
offline_map.rs

1use std::{collections::HashMap, fs, mem::MaybeUninit};
2
3use screeps::{
4    constants::{Density, ResourceType, ROOM_SIZE},
5    game::map::RoomStatus,
6    local::{LocalRoomTerrain, RawObjectId, RoomCoordinate, RoomName},
7};
8use serde::{
9    de::{Error as _, Unexpected},
10    Deserialize, Deserializer,
11};
12
13const ROOM_AREA: usize = (ROOM_SIZE as usize) * (ROOM_SIZE as usize);
14
15#[derive(Clone, Deserialize, Debug)]
16pub struct OfflineShardData {
17    /// A text description of the map dump
18    pub description: String,
19    /// Each room's entry in the map dump
20    #[serde(deserialize_with = "deserialize_offline_rooms")]
21    pub rooms: HashMap<RoomName, OfflineRoomData>,
22}
23
24#[derive(Clone, Deserialize, Debug)]
25pub struct OfflineRoomData {
26    #[serde(rename = "room")]
27    pub room_name: RoomName,
28    #[serde(deserialize_with = "deserialize_room_status")]
29    pub status: RoomStatus,
30    /// Whether the room is a highway room
31    #[serde(default)]
32    pub bus: bool,
33    #[serde(deserialize_with = "deserialize_room_terrain")]
34    pub terrain: LocalRoomTerrain,
35    pub objects: Vec<OfflineObject>,
36}
37
38#[derive(Clone, Deserialize, Debug)]
39#[serde(rename_all = "camelCase", tag = "type")]
40pub enum OfflineObject {
41    #[serde(rename_all = "camelCase")]
42    ConstructedWall {
43        #[serde(rename = "_id")]
44        id: RawObjectId,
45        room: RoomName,
46        x: RoomCoordinate,
47        y: RoomCoordinate,
48    },
49    #[serde(rename_all = "camelCase")]
50    Controller {
51        #[serde(rename = "_id")]
52        id: RawObjectId,
53        room: RoomName,
54        x: RoomCoordinate,
55        y: RoomCoordinate,
56
57        level: u8,
58    },
59    #[serde(rename_all = "camelCase")]
60    Extractor {
61        #[serde(rename = "_id")]
62        id: RawObjectId,
63        room: RoomName,
64        x: RoomCoordinate,
65        y: RoomCoordinate,
66    },
67    #[serde(rename_all = "camelCase")]
68    KeeperLair {
69        #[serde(rename = "_id")]
70        id: RawObjectId,
71        room: RoomName,
72        x: RoomCoordinate,
73        y: RoomCoordinate,
74    },
75    #[serde(rename_all = "camelCase")]
76    Mineral {
77        #[serde(rename = "_id")]
78        id: RawObjectId,
79        room: RoomName,
80        x: RoomCoordinate,
81        y: RoomCoordinate,
82
83        density: Density,
84        mineral_type: ResourceType,
85        mineral_amount: u32,
86    },
87    #[serde(rename_all = "camelCase")]
88    Portal {
89        #[serde(rename = "_id")]
90        id: RawObjectId,
91        room: RoomName,
92        x: RoomCoordinate,
93        y: RoomCoordinate,
94
95        destination: OfflinePortalDestination,
96    },
97    #[serde(rename_all = "camelCase")]
98    Source {
99        #[serde(rename = "_id")]
100        id: RawObjectId,
101        room: RoomName,
102        x: RoomCoordinate,
103        y: RoomCoordinate,
104
105        energy: u16,
106        energy_capacity: u16,
107        ticks_to_regeneration: u16,
108    },
109    #[serde(rename_all = "camelCase")]
110    Terminal {
111        #[serde(rename = "_id")]
112        id: RawObjectId,
113        room: RoomName,
114        x: RoomCoordinate,
115        y: RoomCoordinate,
116    },
117    #[serde(other)]
118    Unknown,
119}
120
121#[derive(Clone, Deserialize, Debug)]
122#[serde(untagged)]
123pub enum OfflinePortalDestination {
124    InterRoom {
125        room: RoomName,
126        x: RoomCoordinate,
127        y: RoomCoordinate,
128    },
129    InterShard {
130        room: RoomName,
131        shard: String,
132    },
133}
134
135fn deserialize_offline_rooms<'de, D>(
136    deserializer: D,
137) -> Result<HashMap<RoomName, OfflineRoomData>, D::Error>
138where
139    D: Deserializer<'de>,
140{
141    let mut rooms = HashMap::new();
142    for room in Vec::<OfflineRoomData>::deserialize(deserializer)? {
143        rooms.insert(room.room_name, room);
144    }
145    Ok(rooms)
146}
147
148fn deserialize_room_status<'de, D>(deserializer: D) -> Result<RoomStatus, D::Error>
149where
150    D: Deserializer<'de>,
151{
152    let s = <&'de str>::deserialize(deserializer)?;
153    match s {
154        "normal" => Ok(RoomStatus::Normal),
155        "closed" => Ok(RoomStatus::Closed),
156        "novice" => Ok(RoomStatus::Novice),
157        "respawn" => Ok(RoomStatus::Respawn),
158        // "out of borders" value appears in API returns,
159        // map to closed since that's effectively identical
160        "out of borders" => Ok(RoomStatus::Closed),
161        _ => Err(D::Error::invalid_value(
162            Unexpected::Str(s),
163            &"valid room status",
164        )),
165    }
166}
167
168fn deserialize_room_terrain<'de, D>(deserializer: D) -> Result<LocalRoomTerrain, D::Error>
169where
170    D: Deserializer<'de>,
171{
172    let s = <&'de str>::deserialize(deserializer)?;
173    if s.len() == ROOM_AREA {
174        let mut data: Box<[MaybeUninit<u8>; ROOM_AREA]> =
175            Box::new([MaybeUninit::uninit(); ROOM_AREA]);
176        for (i, c) in s.chars().enumerate() {
177            let value = match c {
178                '0' => 0,
179                '1' => 1,
180                '2' => 2,
181                // leave the plain-swamps alone, against my better judgement?
182                '3' => 3,
183                _ => {
184                    return Err(D::Error::invalid_value(
185                        Unexpected::Char(c),
186                        &"valid terrain integer value",
187                    ))
188                }
189            };
190            data[i].write(value);
191        }
192        // SAFETY: we've initialized all the bytes, because we know we had 2500 to start
193        // with
194        Ok(LocalRoomTerrain::new_from_bits(unsafe {
195            std::mem::transmute::<Box<[MaybeUninit<u8>; ROOM_AREA]>, Box<[u8; ROOM_AREA]>>(data)
196        }))
197    } else {
198        Err(D::Error::invalid_value(
199            Unexpected::Str(s),
200            &"terrain string of correct length",
201        ))
202    }
203}
204
205pub fn load_shard_map_json<P: AsRef<std::path::Path>>(path: P) -> OfflineShardData {
206    let shard_data_json = fs::read_to_string(path).expect("readable file at specified path");
207    serde_json::from_str(&shard_data_json).expect("valid shard map json")
208}