tiger_lib/data/
scripted_guis.rs

1use crate::block::Block;
2use crate::context::ScopeContext;
3use crate::datacontext::DataContext;
4use crate::datatype::{
5    Code, CodeArg, CodeChain, Datatype, scope_from_datatype, validate_datatypes,
6};
7use crate::db::{Db, DbKind};
8use crate::desc::validate_desc;
9use crate::effect::validate_effect;
10use crate::everything::Everything;
11use crate::game::{Game, GameFlags};
12use crate::item::{Item, ItemLoader};
13use crate::report::{ErrorKey, err, warn};
14use crate::scopes::Scopes;
15use crate::script_value::validate_non_dynamic_script_value;
16use crate::token::Token;
17use crate::tooltipped::Tooltipped;
18use crate::trigger::validate_trigger;
19use crate::validate::validate_modifiers_with_base;
20use crate::validator::Validator;
21
22#[derive(Clone, Debug)]
23pub struct ScriptedGui {}
24
25inventory::submit! {
26    ItemLoader::Normal(GameFlags::jomini(), Item::ScriptedGui, ScriptedGui::add)
27}
28
29impl ScriptedGui {
30    pub fn add(db: &mut Db, key: Token, block: Block) {
31        db.add(Item::ScriptedGui, key, block, Box::new(Self {}));
32    }
33}
34
35impl DbKind for ScriptedGui {
36    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
37        let mut vd = Validator::new(block, data);
38        let mut sc = ScopeContext::new(Scopes::None, key);
39        if let Some(token) = vd.field_value("scope") {
40            if let Some(scope) = Scopes::from_snake_case(token.as_str()) {
41                sc = ScopeContext::new(scope, token);
42            } else {
43                warn(ErrorKey::Scopes).msg("unknown scope type").loc(token).push();
44            }
45        }
46
47        // TODO: JominiNotification
48        vd.field_value("notification_key");
49        vd.field_validated_sc("confirm_title", &mut sc.clone(), validate_desc);
50        vd.field_validated_sc("confirm_text", &mut sc.clone(), validate_desc);
51        vd.field_trigger("ai_is_valid", Tooltipped::No, &mut sc.clone());
52        vd.field_validated_block_sc("ai_chance", &mut sc.clone(), validate_modifiers_with_base);
53        vd.field_validated("ai_frequency", validate_non_dynamic_script_value);
54
55        vd.field_validated_list("saved_scopes", |token, _| {
56            sc.define_name(token.as_str(), Scopes::all_but_none(), token);
57        });
58        // validate_guicall() will evaluate these with strict scopes.
59        sc.set_strict_scopes(false);
60        vd.field_trigger("is_shown", Tooltipped::No, &mut sc.clone());
61        vd.field_trigger("is_valid", Tooltipped::No, &mut sc.clone());
62        vd.field_effect("effect", Tooltipped::No, &mut sc.clone());
63    }
64}
65
66const KNOWN_SGUICALLS: &[&str] = &[
67    "BuildTooltip",
68    "Execute",
69    "ExecuteTooltip",
70    "IsValid",
71    "IsValidTooltip",
72    "IsShown",
73    "IsShownTooltip",
74];
75
76impl ScriptedGui {
77    #[allow(clippy::unused_self)] // self is unused but don't want that in the API
78    pub fn validate_guicall(
79        &self,
80        key: &Token,
81        block: &Block,
82        data: &Everything,
83        context_sc: &mut ScopeContext,
84        dc: &DataContext,
85        code: &Code,
86    ) {
87        if !KNOWN_SGUICALLS.contains(&code.name.as_str()) || code.arguments.len() != 1 {
88            return;
89        }
90        if let CodeArg::Chain(chain) = &code.arguments[0] {
91            if chain.codes.len() < 2 {
92                warn(ErrorKey::Gui)
93                    .msg("expected GuiScope.SetRoot in argument")
94                    .loc(&code.name)
95                    .push();
96                return;
97            }
98
99            let ghw = Game::is_ck3()
100                && chain.codes[0].name.is("GreatHolyWarWindow")
101                && chain.codes[1].name.is("GetScope");
102
103            if !ghw {
104                if !chain.codes[0].name.is("GuiScope") {
105                    warn(ErrorKey::Gui).msg("expected GuiScope").loc(&chain.codes[0].name).push();
106                    return;
107                }
108                if !chain.codes[1].name.is("SetRoot") {
109                    warn(ErrorKey::Gui).msg("expected SetRoot").loc(&chain.codes[1].name).push();
110                    return;
111                }
112                if chain.codes[1].arguments.len() != 1 {
113                    // The caller already warns about this
114                    return;
115                }
116            }
117            // Get the root scope
118            let scope = if ghw {
119                Scopes::Character
120            } else if let CodeArg::Chain(chain) = &chain.codes[1].arguments[0] {
121                deduce_scope(chain, data, context_sc, dc)
122            } else {
123                // TODO: caller will warn about this once argument type is filled in
124                return;
125            };
126            // Compare it to the declared root scope of the scripted gui
127            if let Some(token) = block.get_field_value("scope") {
128                if let Some(declared_scope) = Scopes::from_snake_case(token.as_str()) {
129                    if !scope.intersects(declared_scope) {
130                        warn(ErrorKey::Scopes)
131                            .msg("SetRoot scope does not match scripted gui scope")
132                            .loc(&chain.codes[1].name)
133                            .loc_msg(token, "scripted gui scope here")
134                            .push();
135                    }
136                }
137            }
138            let mut sc = ScopeContext::new(scope, &code.name);
139            if ghw {
140                #[cfg(feature = "ck3")]
141                sc.define_name("great_holy_war", Scopes::GreatHolyWar, &chain.codes[0].name);
142            }
143
144            // Get the additional scopes
145            for code in chain.codes.iter().skip(2) {
146                if code.name.is("AddScope") {
147                    if code.arguments.len() != 2 {
148                        // The caller already warns about this
149                        return;
150                    }
151                    let scope = if let CodeArg::Chain(chain) = &code.arguments[1] {
152                        deduce_scope(chain, data, context_sc, dc)
153                    } else {
154                        Scopes::all()
155                    };
156                    match &code.arguments[0] {
157                        CodeArg::Literal(name) => sc.define_name(name.as_str(), scope, name),
158                        CodeArg::Chain(_) => sc.set_strict_scopes(false),
159                    }
160                } else if !code.name.is("End") {
161                    warn(ErrorKey::Gui).msg("expected AddScope or End").loc(&code.name).push();
162                    return;
163                }
164            }
165            match code.name.as_str() {
166                "BuildTooltip" => {
167                    if let Some(block) = block.get_field_block("is_valid") {
168                        validate_trigger(block, data, &mut sc.clone(), Tooltipped::Yes);
169                    }
170                    if let Some(block) = block.get_field_block("effect") {
171                        validate_effect(block, data, &mut sc, Tooltipped::Yes);
172                    }
173                }
174                "Execute" => {
175                    if let Some(block) = block.get_field_block("effect") {
176                        validate_effect(block, data, &mut sc, Tooltipped::No);
177                    } else {
178                        err(ErrorKey::Gui)
179                            .msg(format!("scripted gui `{key}` has no effect block"))
180                            .loc(&code.name)
181                            .loc_msg(key, "scripted gui here")
182                            .push();
183                    }
184                }
185                "ExecuteTooltip" => {
186                    if let Some(block) = block.get_field_block("effect") {
187                        validate_effect(block, data, &mut sc, Tooltipped::Yes);
188                    } else {
189                        warn(ErrorKey::Gui)
190                            .msg(format!("scripted gui `{key}` has no effect block"))
191                            .loc(&code.name)
192                            .loc_msg(key, "scripted gui here")
193                            .push();
194                    }
195                }
196                "IsShown" => {
197                    if let Some(block) = block.get_field_block("is_shown") {
198                        validate_trigger(block, data, &mut sc, Tooltipped::No);
199                    }
200                }
201                "IsShownTooltip" => {
202                    if let Some(block) = block.get_field_block("is_shown") {
203                        validate_trigger(block, data, &mut sc, Tooltipped::Yes);
204                    }
205                }
206                "IsValid" => {
207                    if let Some(block) = block.get_field_block("is_valid") {
208                        validate_trigger(block, data, &mut sc, Tooltipped::No);
209                    }
210                }
211                "IsValidTooltip" => {
212                    if let Some(block) = block.get_field_block("is_valid") {
213                        validate_trigger(block, data, &mut sc, Tooltipped::Yes);
214                    }
215                }
216                // Checked at the top of the function
217                _ => unreachable!(),
218            }
219        }
220    }
221}
222
223// TODO: handle MakeScopeValue calls
224fn deduce_scope(
225    chain: &CodeChain,
226    data: &Everything,
227    context_sc: &mut ScopeContext,
228    dc: &DataContext,
229) -> Scopes {
230    // Deduce the scope type from the argument chain. It's made a bit
231    // tricky by the MakeScope at the end, which transforms the actual
232    // scope type into just a Datatype::Scope, so leave that off the
233    // chain.
234    if chain.codes.last().is_some_and(|code| code.name.is("MakeScope")) {
235        let chain = chain.without_last();
236        let rtype =
237            validate_datatypes(&chain, data, context_sc, dc, Datatype::Unknown, None, None, true);
238        scope_from_datatype(rtype).unwrap_or(Scopes::all())
239    } else {
240        Scopes::all()
241    }
242}