1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use crate::*;
use ayaka_bindings_types::VarMap;
use fallback::Fallback;
use serde::Deserialize;
use std::collections::HashMap;

/// The paragraph in a paragraph config.
#[derive(Debug, Deserialize)]
pub struct Paragraph {
    /// The tag and key of a paragraph.
    /// They are referenced in `next`.
    pub tag: String,
    /// The title of a paragraph.
    /// It can be [`None`], but better with a human-readable one.
    pub title: Option<String>,
    /// The texts.
    /// They will be parsed into [`ayaka_primitive::Text`] later.
    pub texts: Vec<Line>,
    /// The next paragraph.
    /// If [`None`], the game meets the end.
    pub next: Option<Text>,
}

/// The Ayaka config.
/// It should be deserialized from a YAML file.
#[derive(Debug, Default, Deserialize)]
pub struct GameConfig {
    /// The title of the game.
    pub title: String,
    /// The author of the game.
    #[serde(default)]
    pub author: String,
    /// The paragraphs path.
    pub paras: String,
    /// The start paragraph tag.
    pub start: String,
    /// The plugin config.
    #[serde(default)]
    pub plugins: PluginConfig,
    /// The global game properties.
    #[serde(default)]
    pub props: HashMap<String, String>,
    /// The resources path.
    pub res: Option<String>,
    /// The base language.
    /// If the runtime fails to choose a best match,
    /// it fallbacks to this one.
    pub base_lang: Locale,
}

/// The plugin config.
#[derive(Debug, Default, Deserialize)]
pub struct PluginConfig {
    /// The directory of the plugins.
    pub dir: String,
    /// The names of the plugins, without extension.
    #[serde(default)]
    pub modules: Vec<String>,
}

/// The full Ayaka game.
/// It consists of global config and all paragraphs.
pub struct Game {
    /// The game config.
    pub config: GameConfig,
    /// The paragraphs, indexed by locale.
    /// The inner is the paragraphs indexed by file names.
    pub paras: HashMap<Locale, HashMap<String, Vec<Paragraph>>>,
    /// The resources, indexed by locale.
    pub res: HashMap<Locale, VarMap>,
}

impl Game {
    /// Create a [`RawContext`] at the start of the game.
    pub fn start_context(&self) -> RawContext {
        RawContext {
            cur_base_para: self.config.start.clone(),
            cur_para: self.config.start.clone(),
            ..Default::default()
        }
    }

    fn choose_from_keys<'a, V>(&'a self, loc: &Locale, map: &'a HashMap<Locale, V>) -> &'a Locale {
        loc.choose_from(map.keys())
            .unwrap_or(&self.config.base_lang)
    }

    /// Find a paragraph by tag, with specified locale.
    pub fn find_para(&self, loc: &Locale, base_tag: &str, tag: &str) -> Option<&Paragraph> {
        if let Some(paras) = self.paras.get(loc) {
            if let Some(paras) = paras.get(base_tag) {
                for p in paras.iter() {
                    if p.tag == tag {
                        return Some(p);
                    }
                }
            }
        }
        None
    }

    /// Find a paragraph by tag, with specified locale.
    pub fn find_para_fallback(
        &self,
        loc: &Locale,
        base_tag: &str,
        tag: &str,
    ) -> Fallback<&Paragraph> {
        let key = self.choose_from_keys(loc, &self.paras);
        let base_key = self.choose_from_keys(&self.config.base_lang, &self.paras);
        Fallback::new(
            if key == base_key {
                None
            } else {
                self.find_para(key, base_tag, tag)
            },
            self.find_para(base_key, base_tag, tag),
        )
    }

    fn find_res(&self, loc: &Locale) -> Option<&VarMap> {
        self.res.get(loc)
    }

    /// Find the resource map with specified locale.
    pub fn find_res_fallback(&self, loc: &Locale) -> Fallback<&VarMap> {
        let key = self.choose_from_keys(loc, &self.res);
        let base_key = self.choose_from_keys(&self.config.base_lang, &self.res);
        Fallback::new(
            if key == base_key {
                None
            } else {
                self.find_res(key)
            },
            self.find_res(base_key),
        )
    }
}