1use std::{
2 collections::HashMap,
3 fmt::{Debug, Formatter, Result as FmtResult},
4};
5
6use crate::{Acronym, GameModIntermode};
7
8#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12#[cfg_attr(
13 feature = "rkyv",
14 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
15)]
16#[derive(Clone, Debug, PartialEq)]
17pub struct GameModSimple {
18 pub acronym: Acronym,
19 #[cfg_attr(feature = "serde", serde(default))]
20 pub settings: HashMap<Box<str>, SettingSimple>,
21}
22
23impl GameModSimple {
24 pub fn as_intermode(&self) -> GameModIntermode {
26 GameModIntermode::from_acronym(self.acronym)
27 }
28
29 #[cfg(feature = "serde")]
51 #[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "serde")))]
52 pub fn try_as_mod(
53 self,
54 seed: crate::serde::GameModSeed,
55 ) -> Result<crate::GameMod, GameModSimpleConversionError> {
56 use serde::de::DeserializeSeed;
57
58 use crate::serde::GameModSettings;
59
60 let settings = GameModSettings::from_simple_settings(&self.settings);
61
62 let d = simple_deserializer::SimpleMapDeserializer::new(self.acronym.as_str(), &settings);
65
66 seed.deserialize(d)
67 .map_err(|e| GameModSimpleConversionError {
68 msg: e.to_string().into_boxed_str(),
69 })
70 }
71}
72
73#[cfg(feature = "serde")]
85#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "serde")))]
86#[derive(Debug)]
87pub struct GameModSimpleConversionError {
88 msg: Box<str>,
89}
90
91#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
93#[cfg_attr(
94 feature = "rkyv",
95 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
96)]
97#[derive(Clone, PartialEq)]
98pub enum SettingSimple {
99 Bool(bool),
100 Number(f64),
101 String(String),
102}
103
104impl Debug for SettingSimple {
105 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
106 match self {
107 SettingSimple::Bool(value) => Debug::fmt(value, f),
108 SettingSimple::Number(value) => Debug::fmt(value, f),
109 SettingSimple::String(value) => Debug::fmt(value, f),
110 }
111 }
112}
113
114#[cfg(feature = "serde")]
115#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "serde")))]
116const _: () = {
117 use std::{error::Error, fmt::Display};
118
119 use serde::de::{Deserialize, Deserializer};
120
121 use crate::serde::Value;
122
123 impl<'de> Deserialize<'de> for SettingSimple {
124 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
125 match Value::deserialize(d)? {
126 Value::Bool(value) => Ok(Self::Bool(value)),
127 Value::Str(value) => Ok(Self::String(value.into_owned())),
128 Value::Number(value) => Ok(Self::Number(value)),
129 }
130 }
131 }
132
133 impl Display for GameModSimpleConversionError {
134 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
135 f.write_str(&self.msg)
136 }
137 }
138
139 impl Error for GameModSimpleConversionError {}
140};
141
142#[cfg(feature = "serde")]
143mod simple_deserializer {
144 use serde::{
145 de::{value::BorrowedStrDeserializer, DeserializeSeed, Error, MapAccess, Visitor},
146 Deserializer,
147 };
148
149 use crate::serde::{GameModDeserializeError, GameModSettings};
150
151 pub(super) struct SimpleMapDeserializer<'a> {
161 acronym: &'a str,
162 settings: &'a GameModSettings<'a>,
163 }
164
165 impl<'a> SimpleMapDeserializer<'a> {
166 pub(super) const fn new(acronym: &'a str, settings: &'a GameModSettings<'a>) -> Self {
167 Self { acronym, settings }
168 }
169 }
170
171 impl<'de, 'a: 'de> Deserializer<'de> for SimpleMapDeserializer<'a> {
172 type Error = GameModDeserializeError;
173
174 fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
175 self.deserialize_map(visitor)
176 }
177
178 fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
179 visitor.visit_map(SimpleMapAccess::new(self.acronym, self.settings))
180 }
181
182 serde::forward_to_deserialize_any! {
183 bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes
184 byte_buf option unit unit_struct newtype_struct seq tuple tuple_struct
185 struct enum identifier ignored_any
186 }
187 }
188
189 enum MapState {
192 AcronymKey,
193 AcronymValue,
194 SettingsKey,
195 SettingsValue,
196 Done,
197 }
198
199 struct SimpleMapAccess<'a> {
200 acronym: &'a str,
201 settings: &'a GameModSettings<'a>,
202 state: MapState,
203 }
204
205 impl<'a> SimpleMapAccess<'a> {
206 const fn new(acronym: &'a str, settings: &'a GameModSettings<'a>) -> Self {
207 Self {
208 acronym,
209 settings,
210 state: MapState::AcronymKey,
211 }
212 }
213 }
214
215 impl<'de, 'a: 'de> MapAccess<'de> for SimpleMapAccess<'a> {
216 type Error = GameModDeserializeError;
217
218 fn next_key_seed<K: DeserializeSeed<'de>>(
219 &mut self,
220 seed: K,
221 ) -> Result<Option<K::Value>, Self::Error> {
222 match self.state {
223 MapState::AcronymKey => {
224 self.state = MapState::AcronymValue;
225 let d = BorrowedStrDeserializer::new("acronym");
226
227 seed.deserialize(d).map(Some)
228 }
229 MapState::SettingsKey => {
230 self.state = MapState::SettingsValue;
231 let d = BorrowedStrDeserializer::new("settings");
232
233 seed.deserialize(d).map(Some)
234 }
235 _ => Ok(None),
236 }
237 }
238
239 fn next_value_seed<V: DeserializeSeed<'de>>(
240 &mut self,
241 seed: V,
242 ) -> Result<V::Value, Self::Error> {
243 match self.state {
244 MapState::AcronymValue => {
245 self.state = MapState::SettingsKey;
246 let d = BorrowedStrDeserializer::<GameModDeserializeError>::new(self.acronym);
247
248 seed.deserialize(d)
249 }
250 MapState::SettingsValue => {
251 self.state = MapState::Done;
252
253 seed.deserialize(self.settings)
256 }
257 _ => Err(GameModDeserializeError::custom(
258 "next_value called out of sequence",
259 )),
260 }
261 }
262
263 fn size_hint(&self) -> Option<usize> {
264 Some(2)
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 mod common {
272 #![allow(unused, reason = "depends on enabled features")]
273
274 pub(super) use crate::{GameMod, GameMode};
275
276 pub(super) use super::super::*;
277
278 pub(super) const JSON: &str = r#"[
279 {
280 "acronym":"DA",
281 "settings":{
282 "scroll_speed":2
283 }
284 },
285 {
286 "acronym":"CS"
287 }
288 ]"#;
289 }
290
291 #[allow(unused, reason = "depends on enabled features")]
292 use common::*;
293
294 #[test]
295 #[cfg(feature = "serde")]
296 fn roundtrip_serde() {
297 let mods: Vec<GameModSimple> = serde_json::from_str(JSON).unwrap();
298
299 let expected = vec![
300 GameModSimple {
301 acronym: "DA".parse().unwrap(),
302 settings: vec![("scroll_speed".into(), SettingSimple::Number(2.0))]
303 .into_iter()
304 .collect(),
305 },
306 GameModSimple {
307 acronym: "CS".parse().unwrap(),
308 settings: HashMap::new(),
309 },
310 ];
311
312 assert_eq!(mods, expected);
313
314 let serialized = serde_json::to_string(&mods).unwrap();
315 let deserialized: Vec<GameModSimple> = serde_json::from_str(&serialized).unwrap();
316
317 assert_eq!(mods, deserialized);
318 }
319
320 #[test]
323 #[cfg(feature = "serde")]
324 fn try_as_mod_known_with_setting() {
325 use crate::{generated_mods::DifficultyAdjustTaiko, serde::GameModSeed};
326
327 let simple = GameModSimple {
328 acronym: "DA".parse().unwrap(),
329 settings: [("scroll_speed".into(), SettingSimple::Number(2.0))]
330 .into_iter()
331 .collect(),
332 };
333
334 assert_eq!(
335 simple
336 .try_as_mod(GameModSeed::Mode {
337 mode: GameMode::Taiko,
338 deny_unknown_fields: true
339 })
340 .unwrap(),
341 GameMod::DifficultyAdjustTaiko(DifficultyAdjustTaiko {
342 scroll_speed: Some(2.0),
343 ..Default::default()
344 })
345 );
346 }
347
348 #[test]
350 #[cfg(feature = "serde")]
351 fn try_as_mod_multiple_settings() {
352 use crate::serde::GameModSeed;
353
354 let simple = GameModSimple {
355 acronym: "DA".parse().unwrap(),
356 settings: [
357 ("approach_rate".into(), SettingSimple::Number(9.5)),
358 ("circle_size".into(), SettingSimple::Number(4.0)),
359 ]
360 .into_iter()
361 .collect(),
362 };
363
364 let GameMod::DifficultyAdjustOsu(da) = simple
365 .try_as_mod(GameModSeed::Mode {
366 mode: GameMode::Osu,
367 deny_unknown_fields: true,
368 })
369 .unwrap()
370 else {
371 panic!("expected DifficultyAdjustOsu");
372 };
373
374 assert_eq!(da.approach_rate, Some(9.5));
375 assert_eq!(da.circle_size, Some(4.0));
376 }
377
378 #[test]
380 #[cfg(feature = "serde")]
381 fn try_as_mod_no_settings() {
382 use crate::serde::GameModSeed;
383
384 let simple = GameModSimple {
385 acronym: "CS".parse().unwrap(),
386 settings: HashMap::new(),
387 };
388
389 assert_eq!(
390 simple
391 .try_as_mod(GameModSeed::Mode {
392 mode: GameMode::Taiko,
393 deny_unknown_fields: true
394 })
395 .unwrap(),
396 GameMod::ConstantSpeedTaiko(Default::default())
397 );
398 }
399
400 #[test]
403 #[cfg(feature = "serde")]
404 fn try_as_mod_unknown_acronym_is_ok() {
405 use crate::{generated_mods::UnknownMod, serde::GameModSeed};
406
407 let simple = GameModSimple {
408 acronym: "XX".parse().unwrap(),
409 settings: HashMap::new(),
410 };
411
412 assert_eq!(
413 simple
414 .try_as_mod(GameModSeed::Mode {
415 mode: GameMode::Osu,
416 deny_unknown_fields: true
417 })
418 .unwrap(),
419 GameMod::UnknownOsu(UnknownMod {
420 acronym: "XX".parse().unwrap()
421 })
422 );
423 }
424
425 #[test]
428 #[cfg(feature = "serde")]
429 fn try_as_mod_wrong_mode_is_ok_unknown() {
430 use crate::{generated_mods::UnknownMod, serde::GameModSeed};
431
432 let simple = GameModSimple {
434 acronym: "FI".parse().unwrap(),
435 settings: HashMap::new(),
436 };
437
438 assert_eq!(
439 simple
440 .try_as_mod(GameModSeed::Mode {
441 mode: GameMode::Osu,
442 deny_unknown_fields: true
443 })
444 .unwrap(),
445 GameMod::UnknownOsu(UnknownMod {
446 acronym: "FI".parse().unwrap()
447 })
448 );
449 }
450
451 #[test]
453 #[cfg(feature = "serde")]
454 fn try_as_mod_guess_mode_picks_correct_variant() {
455 use crate::{generated_mods::FadeInMania, serde::GameModSeed};
456
457 let simple = GameModSimple {
459 acronym: "FI".parse().unwrap(),
460 settings: HashMap::new(),
461 };
462
463 assert_eq!(
464 simple
465 .try_as_mod(GameModSeed::GuessMode {
466 deny_unknown_fields: true
467 })
468 .unwrap(),
469 GameMod::FadeInMania(FadeInMania::default())
470 );
471 }
472
473 #[test]
476 #[cfg(feature = "serde")]
477 fn try_as_mod_guess_mode_uses_settings_to_disambiguate() {
478 use crate::{generated_mods::DifficultyAdjustTaiko, serde::GameModSeed};
479
480 let simple = GameModSimple {
483 acronym: "DA".parse().unwrap(),
484 settings: [("scroll_speed".into(), SettingSimple::Number(1.5))]
485 .into_iter()
486 .collect(),
487 };
488
489 assert_eq!(
490 simple
491 .try_as_mod(GameModSeed::GuessMode {
492 deny_unknown_fields: true
493 })
494 .unwrap(),
495 GameMod::DifficultyAdjustTaiko(DifficultyAdjustTaiko {
496 scroll_speed: Some(1.5),
497 ..Default::default()
498 })
499 );
500 }
501
502 #[test]
504 #[cfg(feature = "serde")]
505 fn try_as_mod_unknown_field_denied_is_err() {
506 use crate::serde::GameModSeed;
507
508 let simple = GameModSimple {
509 acronym: "CS".parse().unwrap(),
510 settings: [("not_a_real_field".into(), SettingSimple::Bool(true))]
511 .into_iter()
512 .collect(),
513 };
514
515 assert!(simple
516 .try_as_mod(GameModSeed::Mode {
517 mode: GameMode::Taiko,
518 deny_unknown_fields: true
519 })
520 .is_err());
521 }
522
523 #[test]
526 #[cfg(feature = "serde")]
527 fn try_as_mod_unknown_field_allowed_is_ok() {
528 use crate::serde::GameModSeed;
529
530 let simple = GameModSimple {
531 acronym: "CS".parse().unwrap(),
532 settings: [("not_a_real_field".into(), SettingSimple::Bool(true))]
533 .into_iter()
534 .collect(),
535 };
536
537 assert_eq!(
538 simple
539 .try_as_mod(GameModSeed::Mode {
540 mode: GameMode::Taiko,
541 deny_unknown_fields: false
542 })
543 .unwrap(),
544 GameMod::ConstantSpeedTaiko(Default::default())
545 );
546 }
547
548 #[test]
551 #[cfg(feature = "serde")]
552 fn try_as_mod_wrong_value_type_is_err() {
553 use crate::serde::GameModSeed;
554
555 let simple = GameModSimple {
557 acronym: "DA".parse().unwrap(),
558 settings: [("scroll_speed".into(), SettingSimple::Bool(true))]
559 .into_iter()
560 .collect(),
561 };
562
563 assert!(simple
564 .clone()
565 .try_as_mod(GameModSeed::Mode {
566 mode: GameMode::Taiko,
567 deny_unknown_fields: false
568 })
569 .is_err());
570 assert!(simple
571 .try_as_mod(GameModSeed::Mode {
572 mode: GameMode::Taiko,
573 deny_unknown_fields: true
574 })
575 .is_err());
576 }
577
578 #[test]
580 #[cfg(feature = "serde")]
581 fn try_as_mod_string_setting() {
582 use crate::serde::GameModSeed;
583
584 let simple = GameModSimple {
585 acronym: "AC".parse().unwrap(),
586 settings: [(
587 "accuracy_judge_mode".into(),
588 SettingSimple::String("standard_all".into()),
589 )]
590 .into_iter()
591 .collect(),
592 };
593
594 let GameMod::AccuracyChallengeOsu(ac) = simple
595 .try_as_mod(GameModSeed::Mode {
596 mode: GameMode::Osu,
597 deny_unknown_fields: true,
598 })
599 .unwrap()
600 else {
601 panic!("expected AccuracyChallengeOsu");
602 };
603
604 assert_eq!(ac.accuracy_judge_mode.as_deref(), Some("standard_all"));
605 }
606
607 #[test]
609 #[cfg(feature = "serde")]
610 fn try_as_mod_bool_setting() {
611 use crate::{generated_mods::SuddenDeathOsu, serde::GameModSeed};
612
613 let simple = GameModSimple {
614 acronym: "SD".parse().unwrap(),
615 settings: [("restart".into(), SettingSimple::Bool(true))]
616 .into_iter()
617 .collect(),
618 };
619
620 assert_eq!(
621 simple
622 .try_as_mod(GameModSeed::Mode {
623 mode: GameMode::Osu,
624 deny_unknown_fields: true
625 })
626 .unwrap(),
627 GameMod::SuddenDeathOsu(SuddenDeathOsu {
628 restart: Some(true),
629 ..Default::default()
630 })
631 );
632 }
633
634 #[test]
635 #[cfg(feature = "rkyv")]
636 fn roundtrip_rkyv() {
637 use rkyv::{
638 rancor::{BoxedError as Err, Strategy},
639 Archived, Deserialize,
640 };
641
642 let mods: Vec<GameModSimple> = serde_json::from_str(JSON).unwrap();
643
644 let bytes = rkyv::to_bytes::<Err>(&mods).unwrap();
645 let archived = rkyv::access::<Archived<Vec<GameModSimple>>, Err>(&bytes).unwrap();
646 let deserialized: Vec<GameModSimple> = archived
647 .deserialize(Strategy::<_, Err>::wrap(&mut ()))
648 .unwrap();
649
650 assert_eq!(mods, deserialized);
651 }
652}