tiger_lib/data/
tutorials.rs

1use crate::block::Block;
2#[cfg(feature = "vic3")]
3use crate::context::ScopeContext;
4use crate::datacontext::DataContext;
5use crate::datatype::Datatype;
6use crate::db::{Db, DbKind};
7use crate::everything::Everything;
8use crate::game::{Game, GameFlags};
9use crate::gui::validate_datatype_field;
10use crate::item::{Item, ItemLoader};
11use crate::scopes::Scopes;
12use crate::token::Token;
13use crate::tooltipped::Tooltipped;
14use crate::validator::{Validator, ValueValidator};
15
16#[derive(Clone, Debug)]
17pub struct TutorialLesson {}
18
19inventory::submit! {
20    ItemLoader::Normal(GameFlags::Ck3.union(GameFlags::Vic3), Item::TutorialLesson, TutorialLesson::add)
21}
22
23impl TutorialLesson {
24    pub fn add(db: &mut Db, key: Token, block: Block) {
25        db.add(Item::TutorialLesson, key, block, Box::new(Self {}));
26    }
27}
28
29impl DbKind for TutorialLesson {
30    fn add_subitems(&self, _key: &Token, block: &Block, db: &mut Db) {
31        let chain = block.get_field_value("chain");
32        for (key, block) in block.iter_definitions() {
33            if !key.is("trigger") && !key.is("trigger_transition") {
34                db.add(
35                    Item::TutorialLessonStep,
36                    key.clone(),
37                    block.clone(),
38                    Box::new(TutorialLessonStep { chain: chain.cloned() }),
39                );
40            }
41        }
42    }
43
44    #[allow(unused_variables)] // for `key` when not vic3
45    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
46        let mut vd = Validator::new(block, data);
47
48        vd.field_item("chain", Item::TutorialLessonChain);
49        vd.field_bool("start_automatically");
50
51        vd.field_trigger_rooted("trigger", Tooltipped::No, game_tutorial_scope());
52
53        // TODO: register these as item Flags and check them for
54        // the IsTutorialTagOpen gui function?
55        // Downside: some mods may check other mods' tags for compatibility.
56        vd.multi_field_value("gui_tag");
57        // TODO: check that this is a widget name
58        vd.field_value("highlight_widget");
59        // TODO: verify this works in CK3 too
60        vd.field_validated_key("highlight_widget_dynamic_loc", |key, bv, data| {
61            validate_datatype_field(
62                Datatype::Unknown,
63                key,
64                bv,
65                data,
66                &mut DataContext::new(),
67                false,
68            );
69        });
70        #[cfg(feature = "vic3")]
71        {
72            let mut sc = ScopeContext::new(Scopes::JournalEntry, key);
73            vd.multi_field_target("highlight_target", &mut sc, Scopes::all());
74        }
75
76        vd.multi_field_validated_block("trigger_transition", validate_trigger_transition);
77
78        vd.field_integer("delay");
79        vd.field_integer("default_lesson_step_delay");
80
81        vd.field_bool("finish_gamestate_tutorial");
82        vd.field_bool("shown_in_encyclopedia");
83        vd.field_item("encyclopedia_text", Item::Localization);
84
85        // The tutorial lesson steps are validated in `TutorialLessonStep`
86        vd.unknown_block_fields(|_, _| ());
87    }
88}
89
90#[derive(Clone, Debug)]
91pub struct TutorialLessonChain {
92    gamestate_tutorial: bool,
93}
94
95inventory::submit! {
96    ItemLoader::Normal(GameFlags::Ck3.union(GameFlags::Vic3), Item::TutorialLessonChain, TutorialLessonChain::add)
97}
98
99impl TutorialLessonChain {
100    pub fn add(db: &mut Db, key: Token, block: Block) {
101        let gamestate_tutorial =
102            block.get_field_bool("save_progress_in_gamestate").unwrap_or(false);
103        db.add(Item::TutorialLessonChain, key, block, Box::new(Self { gamestate_tutorial }));
104    }
105}
106
107impl DbKind for TutorialLessonChain {
108    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
109        let mut vd = Validator::new(block, data);
110
111        // TODO: verify root scope
112        vd.field_trigger_rooted("trigger", Tooltipped::No, game_tutorial_scope());
113        vd.field_integer("delay");
114        vd.field_bool("save_progress_in_gamestate");
115    }
116
117    fn has_property(
118        &self,
119        _key: &Token,
120        _block: &Block,
121        property: &str,
122        _data: &Everything,
123    ) -> bool {
124        property == "gamestate_tutorial" && self.gamestate_tutorial
125    }
126
127    fn merge_in(&mut self, other: Box<dyn DbKind>) {
128        if let Some(other) = other.as_any().downcast_ref::<Self>() {
129            self.gamestate_tutorial |= other.gamestate_tutorial;
130        }
131    }
132}
133
134/// These are added by the [`TutorialLesson`] item loader
135#[derive(Clone, Debug)]
136pub struct TutorialLessonStep {
137    chain: Option<Token>,
138}
139
140impl DbKind for TutorialLessonStep {
141    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
142        let mut vd = Validator::new(block, data);
143
144        data.verify_exists(Item::Localization, key);
145
146        vd.field_item("text", Item::Localization);
147        vd.field_item("header_info", Item::Localization); // undocumented
148
149        // see gui_tag in TutorialLesson
150        vd.multi_field_value("gui_tag");
151        vd.field_value("window_name");
152        // TODO: check that this is a widget name
153        vd.multi_field_value("highlight_widget");
154        // TODO: verify this works in CK3 too
155        vd.field_validated_key("highlight_widget_dynamic_loc", |key, bv, data| {
156            validate_datatype_field(
157                Datatype::Unknown,
158                key,
159                bv,
160                data,
161                &mut DataContext::new(),
162                false,
163            );
164        });
165        #[cfg(feature = "vic3")]
166        {
167            let mut sc = ScopeContext::new(Scopes::JournalEntry, key);
168            vd.multi_field_target("highlight_target", &mut sc, Scopes::all());
169        }
170
171        // TODO: These two are not used in vanilla and the docs are a bit unclear
172        vd.field_item("soundeffect", Item::Sound);
173        vd.field_item("voice", Item::Sound);
174
175        vd.field_bool("repeat_sound_effect");
176        vd.field_integer("delay");
177        vd.field_value("animation");
178        vd.field_bool("shown_in_encyclopedia");
179        vd.field_item("encyclopedia_text", Item::Localization);
180
181        vd.multi_field_validated_block("gui_transition", |block, data| {
182            let mut vd = Validator::new(block, data);
183            vd.field_value("button_id");
184            vd.field_item("button_text", Item::Localization);
185            vd.field_validated_value("target", validate_lesson_target);
186            vd.field_trigger_rooted("enabled", Tooltipped::No, game_tutorial_scope());
187        });
188        vd.multi_field_validated_block("trigger_transition", validate_trigger_transition);
189
190        // TODO: verify this works in Vic3 too
191        // TODO: need a general way to restrict effects to interface effects only
192        vd.field_effect_rooted("interface_effect", Tooltipped::No, Scopes::None);
193
194        if self.chain.as_ref().is_some_and(|t| {
195            data.item_has_property(Item::TutorialLessonChain, t.as_str(), "gamestate_tutorial")
196        }) {
197            vd.field_bool("pause_game");
198            vd.field_bool("force_pause_game");
199            vd.field_effect_rooted("effect", Tooltipped::No, game_tutorial_scope());
200        } else {
201            vd.ban_field("pause_game", || "gamestate tutorial chains");
202            vd.ban_field("force_pause_game", || "gamestate tutorial chains");
203            vd.ban_field("effect", || "gamestate tutorial chains");
204        }
205
206        #[cfg(feature = "ck3")]
207        vd.field_validated_block("highlight_widget_with_index", |block, data| {
208            let mut vd = Validator::new(block, data);
209            vd.unknown_value_fields(|_, value| {
210                // TODO: validate key against widget names
211                let mut vvd = ValueValidator::new(value, data);
212                vvd.integer();
213            });
214        });
215        #[cfg(feature = "ck3")]
216        vd.field_validated_block("highlight_child_widget_of", |block, data| {
217            let mut vd = Validator::new(block, data);
218            vd.unknown_value_fields(|_, _| {
219                // TODO: validate key and value against widget names
220            });
221        });
222    }
223}
224
225fn validate_trigger_transition(block: &Block, data: &Everything) {
226    let mut vd = Validator::new(block, data);
227
228    vd.field_trigger_rooted("trigger", Tooltipped::No, game_tutorial_scope());
229    vd.field_validated_value("target", validate_lesson_target);
230    vd.field_value("button_id");
231    vd.field_item("button_text", Item::Localization);
232}
233
234fn validate_lesson_target(_key: &Token, mut vd: ValueValidator) {
235    vd.maybe_is("lesson_finish");
236    vd.maybe_is("lesson_abort");
237    vd.item(Item::TutorialLessonStep);
238}
239
240fn game_tutorial_scope() -> Scopes {
241    match Game::game() {
242        #[cfg(feature = "ck3")]
243        Game::Ck3 => Scopes::Character,
244        #[cfg(feature = "vic3")]
245        Game::Vic3 => Scopes::Country,
246        #[cfg(feature = "imperator")]
247        Game::Imperator => unimplemented!(),
248        #[cfg(feature = "hoi4")]
249        Game::Hoi4 => unimplemented!(),
250    }
251}