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 pub description: String,
19 #[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 #[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" => 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 '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 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}