Skip to main content

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                && let Some(declared_scope) = Scopes::from_snake_case(token.as_str())
129                && !scope.intersects(declared_scope)
130            {
131                warn(ErrorKey::Scopes)
132                    .msg("SetRoot scope does not match scripted gui scope")
133                    .loc(&chain.codes[1].name)
134                    .loc_msg(token, "scripted gui scope here")
135                    .push();
136            }
137            let mut sc = ScopeContext::new(scope, &code.name);
138            if ghw {
139                #[cfg(feature = "ck3")]
140                sc.define_name("great_holy_war", Scopes::GreatHolyWar, &chain.codes[0].name);
141            }
142
143            // Get the additional scopes
144            for code in chain.codes.iter().skip(2) {
145                if code.name.is("AddScope") {
146                    if code.arguments.len() != 2 {
147                        // The caller already warns about this
148                        return;
149                    }
150                    let scope = if let CodeArg::Chain(chain) = &code.arguments[1] {
151                        deduce_scope(chain, data, context_sc, dc)
152                    } else {
153                        Scopes::all()
154                    };
155                    match &code.arguments[0] {
156                        CodeArg::Literal(name) => sc.define_name(name.as_str(), scope, name),
157                        CodeArg::Chain(_) => sc.set_strict_scopes(false),
158                    }
159                } else if !code.name.is("End") {
160                    warn(ErrorKey::Gui).msg("expected AddScope or End").loc(&code.name).push();
161                    return;
162                }
163            }
164            match code.name.as_str() {
165                "BuildTooltip" => {
166                    if let Some(block) = block.get_field_block("is_valid") {
167                        validate_trigger(block, data, &mut sc.clone(), Tooltipped::Yes);
168                    }
169                    if let Some(block) = block.get_field_block("effect") {
170                        validate_effect(block, data, &mut sc, Tooltipped::Yes);
171                    }
172                }
173                "Execute" => {
174                    if let Some(block) = block.get_field_block("effect") {
175                        validate_effect(block, data, &mut sc, Tooltipped::No);
176                    } else {
177                        err(ErrorKey::Gui)
178                            .msg(format!("scripted gui `{key}` has no effect block"))
179                            .loc(&code.name)
180                            .loc_msg(key, "scripted gui here")
181                            .push();
182                    }
183                }
184                "ExecuteTooltip" => {
185                    if let Some(block) = block.get_field_block("effect") {
186                        validate_effect(block, data, &mut sc, Tooltipped::Yes);
187                    } else {
188                        warn(ErrorKey::Gui)
189                            .msg(format!("scripted gui `{key}` has no effect block"))
190                            .loc(&code.name)
191                            .loc_msg(key, "scripted gui here")
192                            .push();
193                    }
194                }
195                "IsShown" => {
196                    if let Some(block) = block.get_field_block("is_shown") {
197                        validate_trigger(block, data, &mut sc, Tooltipped::No);
198                    }
199                }
200                "IsShownTooltip" => {
201                    if let Some(block) = block.get_field_block("is_shown") {
202                        validate_trigger(block, data, &mut sc, Tooltipped::Yes);
203                    }
204                }
205                "IsValid" => {
206                    if let Some(block) = block.get_field_block("is_valid") {
207                        validate_trigger(block, data, &mut sc, Tooltipped::No);
208                    }
209                }
210                "IsValidTooltip" => {
211                    if let Some(block) = block.get_field_block("is_valid") {
212                        validate_trigger(block, data, &mut sc, Tooltipped::Yes);
213                    }
214                }
215                // Checked at the top of the function
216                _ => unreachable!(),
217            }
218        }
219    }
220}
221
222// TODO: handle MakeScopeValue calls
223fn deduce_scope(
224    chain: &CodeChain,
225    data: &Everything,
226    context_sc: &mut ScopeContext,
227    dc: &DataContext,
228) -> Scopes {
229    // Deduce the scope type from the argument chain. It's made a bit
230    // tricky by the MakeScope at the end, which transforms the actual
231    // scope type into just a Datatype::Scope, so leave that off the
232    // chain.
233    if chain.codes.last().is_some_and(|code| code.name.is("MakeScope")) {
234        let chain = chain.without_last();
235        let rtype =
236            validate_datatypes(&chain, data, context_sc, dc, Datatype::Unknown, None, None, true);
237        scope_from_datatype(rtype).unwrap_or(Scopes::all())
238    } else {
239        Scopes::all()
240    }
241}