1use std::cmp::Ordering;
5use std::convert::TryFrom;
6use std::fmt::{self, Debug, Display};
7
8#[derive(Clone, Copy, PartialEq, Eq)]
10pub struct SCXVersion(pub(crate) [u8; 4]);
11
12impl SCXVersion {
13 pub fn as_bytes(&self) -> &[u8] {
15 &self.0
16 }
17
18 pub(crate) fn to_player_version(self) -> Option<f32> {
19 match self.as_bytes() {
20 b"1.07" => Some(1.07),
21 b"1.09" | b"1.10" | b"1.11" => Some(1.11),
22 b"1.12" | b"1.13" | b"1.14" | b"1.15" | b"1.16" => Some(1.12),
23 b"1.18" | b"1.19" => Some(1.13),
24 b"1.20" | b"1.21" | b"1.32" | b"1.36" | b"1.37" => Some(1.14),
25 _ => None,
26 }
27 }
28}
29
30impl Default for SCXVersion {
31 fn default() -> Self {
32 Self(*b"1.21")
33 }
34}
35
36impl Debug for SCXVersion {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "{:?}", std::str::from_utf8(&self.0).unwrap())
39 }
40}
41
42impl Display for SCXVersion {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(f, "{}", std::str::from_utf8(&self.0).unwrap())
45 }
46}
47
48impl PartialEq<[u8; 4]> for SCXVersion {
49 fn eq(&self, other: &[u8; 4]) -> bool {
50 other[0] == self.0[0] && other[1] == b'.' && other[2] == self.0[2] && other[3] == self.0[3]
51 }
52}
53
54impl PartialEq<SCXVersion> for [u8; 4] {
55 fn eq(&self, other: &SCXVersion) -> bool {
56 other == self
57 }
58}
59
60impl Ord for SCXVersion {
61 fn cmp(&self, other: &SCXVersion) -> Ordering {
62 match self.0[0].cmp(&other.0[0]) {
63 Ordering::Equal => {}
64 ord => return ord,
65 }
66 match self.0[2].cmp(&other.0[2]) {
67 Ordering::Equal => {}
68 ord => return ord,
69 }
70 self.0[3].cmp(&other.0[3])
71 }
72}
73
74impl PartialOrd for SCXVersion {
75 fn partial_cmp(&self, other: &SCXVersion) -> Option<Ordering> {
76 Some(self.cmp(other))
77 }
78}
79
80#[derive(Debug, Clone, Copy, thiserror::Error)]
82#[error("invalid diplomatic stance {} (must be 0/1/3)", .0)]
83pub struct ParseDiplomaticStanceError(i32);
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum DiplomaticStance {
88 Ally = 0,
90 Neutral = 1,
92 Enemy = 3,
94}
95
96impl TryFrom<i32> for DiplomaticStance {
97 type Error = ParseDiplomaticStanceError;
98
99 fn try_from(n: i32) -> Result<Self, Self::Error> {
100 match n {
101 0 => Ok(DiplomaticStance::Ally),
102 1 => Ok(DiplomaticStance::Neutral),
103 3 => Ok(DiplomaticStance::Enemy),
104 n => Err(ParseDiplomaticStanceError(n)),
105 }
106 }
107}
108
109impl From<DiplomaticStance> for i32 {
110 fn from(stance: DiplomaticStance) -> i32 {
111 match stance {
112 DiplomaticStance::Ally => 0,
113 DiplomaticStance::Neutral => 1,
114 DiplomaticStance::Enemy => 3,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, thiserror::Error)]
121#[error("invalid data set {} (must be 0/1)", .0)]
122pub struct ParseDataSetError(i32);
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum DataSet {
127 BaseGame,
129 Expansions,
131}
132
133impl TryFrom<i32> for DataSet {
134 type Error = ParseDataSetError;
135 fn try_from(n: i32) -> Result<Self, Self::Error> {
136 match n {
137 0 => Ok(DataSet::BaseGame),
138 1 => Ok(DataSet::Expansions),
139 n => Err(ParseDataSetError(n)),
140 }
141 }
142}
143
144impl From<DataSet> for i32 {
145 fn from(id: DataSet) -> i32 {
146 match id {
147 DataSet::BaseGame => 0,
148 DataSet::Expansions => 1,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Copy, thiserror::Error)]
155#[error("unknown dlc package {}", .0)]
156pub struct ParseDLCPackageError(i32);
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum DLCPackage {
161 AgeOfKings,
163 AgeOfConquerors,
165 TheForgotten,
167 AfricanKingdoms,
169 RiseOfTheRajas,
171 LastKhans,
173}
174
175impl TryFrom<i32> for DLCPackage {
176 type Error = ParseDLCPackageError;
177 fn try_from(n: i32) -> Result<Self, Self::Error> {
178 match n {
179 2 => Ok(DLCPackage::AgeOfKings),
180 3 => Ok(DLCPackage::AgeOfConquerors),
181 4 => Ok(DLCPackage::TheForgotten),
182 5 => Ok(DLCPackage::AfricanKingdoms),
183 6 => Ok(DLCPackage::RiseOfTheRajas),
184 7 => Ok(DLCPackage::LastKhans),
185 n => Err(ParseDLCPackageError(n)),
186 }
187 }
188}
189
190impl From<DLCPackage> for i32 {
191 fn from(dlc_id: DLCPackage) -> i32 {
192 match dlc_id {
193 DLCPackage::AgeOfKings => 2,
194 DLCPackage::AgeOfConquerors => 3,
195 DLCPackage::TheForgotten => 4,
196 DLCPackage::AfricanKingdoms => 5,
197 DLCPackage::RiseOfTheRajas => 6,
198 DLCPackage::LastKhans => 7,
199 }
200 }
201}
202
203fn expected_range(version: f32) -> &'static str {
204 if version < 1.25 {
205 "-1-4"
206 } else {
207 "-1-6"
208 }
209}
210
211#[derive(Debug, Clone, Copy, thiserror::Error)]
213#[error("invalid starting age {} (must be {})", .found, expected_range(*.version))]
214pub struct ParseStartingAgeError {
215 version: f32,
216 found: i32,
217}
218
219#[derive(Debug, PartialEq, Eq, Clone, Copy)]
221pub enum StartingAge {
222 Default = -1,
224 Nomad = -2,
226 DarkAge = 0,
228 FeudalAge = 1,
230 CastleAge = 2,
232 ImperialAge = 3,
234 PostImperialAge = 4,
236}
237
238impl StartingAge {
239 pub fn try_from(n: i32, version: f32) -> Result<Self, ParseStartingAgeError> {
242 if version < 1.25 {
243 match n {
244 -1 => Ok(StartingAge::Default),
245 0 => Ok(StartingAge::DarkAge),
246 1 => Ok(StartingAge::FeudalAge),
247 2 => Ok(StartingAge::CastleAge),
248 3 => Ok(StartingAge::ImperialAge),
249 4 => Ok(StartingAge::PostImperialAge),
250 _ => Err(ParseStartingAgeError { version, found: n }),
251 }
252 } else {
253 match n {
254 -1 | 0 => Ok(StartingAge::Default),
255 1 => Ok(StartingAge::Nomad),
256 2 => Ok(StartingAge::DarkAge),
257 3 => Ok(StartingAge::FeudalAge),
258 4 => Ok(StartingAge::CastleAge),
259 5 => Ok(StartingAge::ImperialAge),
260 6 => Ok(StartingAge::PostImperialAge),
261 _ => Err(ParseStartingAgeError { version, found: n }),
262 }
263 }
264 }
265
266 pub fn to_i32(self, version: f32) -> i32 {
268 if version < 1.25 {
269 match self {
270 StartingAge::Default => -1,
271 StartingAge::Nomad | StartingAge::DarkAge => 0,
272 StartingAge::FeudalAge => 1,
273 StartingAge::CastleAge => 2,
274 StartingAge::ImperialAge => 3,
275 StartingAge::PostImperialAge => 4,
276 }
277 } else {
278 match self {
279 StartingAge::Default => 0,
280 StartingAge::Nomad => 1,
281 StartingAge::DarkAge => 2,
282 StartingAge::FeudalAge => 3,
283 StartingAge::CastleAge => 4,
284 StartingAge::ImperialAge => 5,
285 StartingAge::PostImperialAge => 6,
286 }
287 }
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum VictoryCondition {
293 Capture,
294 Create,
295 Destroy,
296 DestroyMultiple,
297 BringToArea,
298 BringToObject,
299 Attribute,
300 Explore,
301 CreateInArea,
302 DestroyAll,
303 DestroyPlayer,
304 Points,
305 Other(u8),
306}
307
308impl From<u8> for VictoryCondition {
309 fn from(n: u8) -> Self {
310 match n {
311 0 => VictoryCondition::Capture,
312 1 => VictoryCondition::Create,
313 2 => VictoryCondition::Destroy,
314 3 => VictoryCondition::DestroyMultiple,
315 4 => VictoryCondition::BringToArea,
316 5 => VictoryCondition::BringToObject,
317 6 => VictoryCondition::Attribute,
318 7 => VictoryCondition::Explore,
319 8 => VictoryCondition::CreateInArea,
320 9 => VictoryCondition::DestroyAll,
321 10 => VictoryCondition::DestroyPlayer,
322 11 => VictoryCondition::Points,
323 n => VictoryCondition::Other(n),
324 }
325 }
326}
327
328impl From<VictoryCondition> for u8 {
329 fn from(condition: VictoryCondition) -> Self {
330 match condition {
331 VictoryCondition::Capture => 0,
332 VictoryCondition::Create => 1,
333 VictoryCondition::Destroy => 2,
334 VictoryCondition::DestroyMultiple => 3,
335 VictoryCondition::BringToArea => 4,
336 VictoryCondition::BringToObject => 5,
337 VictoryCondition::Attribute => 6,
338 VictoryCondition::Explore => 7,
339 VictoryCondition::CreateInArea => 8,
340 VictoryCondition::DestroyAll => 9,
341 VictoryCondition::DestroyPlayer => 10,
342 VictoryCondition::Points => 11,
343 VictoryCondition::Other(n) => n,
344 }
345 }
346}
347
348#[derive(Debug, Clone, PartialEq)]
350pub struct VersionBundle {
351 pub format: SCXVersion,
353 pub header: u32,
355 pub dlc_options: Option<i32>,
357 pub data: f32,
359 pub picture: u32,
361 pub victory: f32,
363 pub triggers: Option<f64>,
365 pub map: u32,
367}
368
369impl VersionBundle {
370 pub fn aoe() -> Self {
372 unimplemented!()
373 }
374
375 pub fn ror() -> Self {
377 Self {
378 format: SCXVersion(*b"1.11"),
379 header: 2,
380 dlc_options: None,
381 data: 1.15,
382 picture: 1,
383 victory: 2.0,
384 triggers: None,
385 map: 0,
386 }
387 }
388
389 pub fn aok() -> Self {
391 Self {
392 format: SCXVersion(*b"1.18"),
393 header: 2,
394 dlc_options: None,
395 data: 1.2,
396 picture: 1,
397 victory: 2.0,
398 triggers: Some(1.6),
399 map: 0,
400 }
401 }
402
403 pub fn aoc() -> Self {
405 Self {
406 format: SCXVersion(*b"1.21"),
407 header: 2,
408 dlc_options: None,
409 data: 1.22,
410 picture: 1,
411 victory: 2.0,
412 triggers: Some(1.6),
413 map: 0,
414 }
415 }
416
417 pub fn userpatch_14() -> Self {
419 Self::aoc()
420 }
421
422 pub fn userpatch_15() -> Self {
424 Self::userpatch_14()
425 }
426
427 pub fn hd_edition() -> Self {
429 Self {
430 format: SCXVersion(*b"1.21"),
431 header: 3,
432 dlc_options: Some(1000),
433 data: 1.26,
434 picture: 3,
435 victory: 2.0,
436 triggers: Some(1.6),
437 map: 0,
438 }
439 }
440
441 pub fn aoe2_de() -> Self {
445 Self {
446 format: SCXVersion(*b"1.37"),
447 header: 5,
448 dlc_options: Some(1000),
449 data: 1.37,
450 picture: 3,
451 victory: 2.0,
452 triggers: Some(2.2),
453 map: 2,
454 }
455 }
456
457 pub fn is_aok(&self) -> bool {
459 match self.format.as_bytes() {
460 b"1.18" | b"1.19" | b"1.20" => true,
461 _ => false,
462 }
463 }
464
465 pub fn is_aoc(&self) -> bool {
467 self.format == *b"1.21" && self.data <= 1.22
468 }
469
470 pub fn is_hd_edition(&self) -> bool {
472 self.format == *b"1.21" || self.format == *b"1.22" && self.data > 1.22
473 }
474
475 pub fn is_age2_de(&self) -> bool {
477 self.data >= 1.28
478 }
479}