tiger_lib/data/
scripted_triggers.rs

1use std::path::PathBuf;
2
3use crate::block::Block;
4use crate::context::ScopeContext;
5use crate::everything::Everything;
6use crate::fileset::{FileEntry, FileHandler};
7#[cfg(feature = "hoi4")]
8use crate::game::Game;
9use crate::helpers::{BANNED_NAMES, TigerHashMap, limited_item_prefix_should_insert};
10use crate::item::Item;
11use crate::lowercase::Lowercase;
12use crate::macros::{MACRO_MAP, MacroCache};
13use crate::parse::ParserMemory;
14use crate::pdxfile::PdxFile;
15use crate::report::{ErrorKey, err, warn};
16use crate::scopes::Scopes;
17use crate::token::Token;
18use crate::tooltipped::Tooltipped;
19use crate::trigger::validate_trigger_internal;
20use crate::validate::ListType;
21use crate::validator::Validator;
22use crate::variables::Variables;
23
24#[derive(Debug, Default)]
25pub struct Triggers {
26    scope_overrides: TigerHashMap<&'static str, Scopes>,
27    triggers: TigerHashMap<&'static str, Trigger>,
28}
29
30impl Triggers {
31    fn load_item(&mut self, key: Token, block: Block) {
32        if BANNED_NAMES.contains(&key.as_str()) {
33            let msg = "scripted trigger has the same name as an important builtin";
34            err(ErrorKey::NameConflict).strong().msg(msg).loc(key).push();
35        } else if let Some(name) =
36            limited_item_prefix_should_insert(Item::ScriptedTrigger, key, |key| {
37                self.triggers.get(key).map(|entry| &entry.key)
38            })
39        {
40            let scope_override = self
41                .scope_overrides
42                .get(name.as_str())
43                .copied()
44                .or_else(|| builtin_scope_overrides(&name));
45            if block.source.is_some() {
46                MACRO_MAP.insert_or_get_loc(name.loc);
47            }
48            self.triggers.insert(name.as_str(), Trigger::new(name, block, scope_override));
49        }
50    }
51
52    pub fn scan_variables(&self, registry: &mut Variables) {
53        for item in self.triggers.values() {
54            registry.scan(&item.block);
55        }
56    }
57
58    pub fn exists(&self, key: &str) -> bool {
59        self.triggers.contains_key(key)
60    }
61
62    pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
63        self.triggers.values().map(|item| &item.key)
64    }
65
66    pub fn get(&self, key: &str) -> Option<&Trigger> {
67        self.triggers.get(key)
68    }
69
70    pub fn validate(&self, data: &Everything) {
71        for item in self.triggers.values() {
72            item.validate(data);
73        }
74    }
75}
76
77impl FileHandler<Block> for Triggers {
78    fn config(&mut self, config: &Block) {
79        if let Some(block) = config.get_field_block("scope_override") {
80            for (key, token) in block.iter_assignments() {
81                let mut scopes = Scopes::empty();
82                if token.lowercase_is("all") {
83                    scopes = Scopes::all();
84                } else {
85                    for part in token.split('|') {
86                        if let Some(scope) = Scopes::from_snake_case(part.as_str()) {
87                            scopes |= scope;
88                        } else {
89                            let msg = format!("unknown scope type `{part}`");
90                            warn(ErrorKey::Config).msg(msg).loc(part).push();
91                        }
92                    }
93                }
94                self.scope_overrides.insert(key.as_str(), scopes);
95            }
96        }
97    }
98
99    fn subpath(&self) -> PathBuf {
100        PathBuf::from("common/scripted_triggers")
101    }
102
103    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Block> {
104        if !entry.filename().to_string_lossy().ends_with(".txt") {
105            return None;
106        }
107
108        #[cfg(feature = "hoi4")]
109        if Game::is_hoi4() {
110            return PdxFile::read_no_bom(entry, parser);
111        }
112        PdxFile::read(entry, parser)
113    }
114
115    fn handle_file(&mut self, _entry: &FileEntry, mut block: Block) {
116        for (key, block) in block.drain_definitions_warn() {
117            self.load_item(key, block);
118        }
119    }
120}
121
122#[derive(Debug)]
123pub struct Trigger {
124    pub key: Token,
125    pub block: Block,
126    cache: MacroCache<ScopeContext>,
127    scope_override: Option<Scopes>,
128}
129
130impl Trigger {
131    pub fn new(key: Token, block: Block, scope_override: Option<Scopes>) -> Self {
132        Self { key, block, cache: MacroCache::default(), scope_override }
133    }
134
135    pub fn validate(&self, data: &Everything) {
136        // We could let triggers get "naturally" validated by being called from other places,
137        // but we want to also validate triggers that aren't called from anywhere yet.
138        if self.block.source.is_none() {
139            let mut sc = ScopeContext::new_unrooted(Scopes::all(), &self.key);
140            sc.set_strict_scopes(false);
141            if self.scope_override.is_some() {
142                sc.set_no_warn(true);
143            }
144            self.validate_call(&self.key, data, &mut sc, Tooltipped::No, false);
145        }
146    }
147
148    pub fn validate_call(
149        &self,
150        key: &Token,
151        data: &Everything,
152        sc: &mut ScopeContext,
153        tooltipped: Tooltipped,
154        negated: bool,
155    ) {
156        if !self.cached_compat(key, &[], tooltipped, negated, sc, data) {
157            let mut our_sc = ScopeContext::new_unrooted(Scopes::all(), &self.key);
158            our_sc.set_strict_scopes(false);
159            if self.scope_override.is_some() {
160                our_sc.set_no_warn(true);
161            }
162            self.cache.insert(key, &[], tooltipped, negated, our_sc.clone());
163            let vd = Validator::new(&self.block, data);
164            validate_trigger_internal(
165                Lowercase::empty(),
166                ListType::None,
167                &self.block,
168                data,
169                &mut our_sc,
170                vd,
171                tooltipped,
172                negated,
173            );
174            if let Some(scopes) = self.scope_override {
175                our_sc = ScopeContext::new_unrooted(scopes, key);
176                our_sc.set_strict_scopes(false);
177            }
178            sc.expect_compatibility(&our_sc, key, data);
179            self.cache.insert(key, &[], tooltipped, negated, our_sc);
180        }
181    }
182
183    pub fn macro_parms(&self) -> Vec<&'static str> {
184        self.block.macro_parms()
185    }
186
187    pub fn cached_compat(
188        &self,
189        key: &Token,
190        args: &[(&'static str, Token)],
191        tooltipped: Tooltipped,
192        negated: bool,
193        sc: &mut ScopeContext,
194        data: &Everything,
195    ) -> bool {
196        self.cache.perform(key, args, tooltipped, negated, |our_sc| {
197            sc.expect_compatibility(our_sc, key, data);
198        })
199    }
200
201    pub fn validate_macro_expansion(
202        &self,
203        key: &Token,
204        args: &[(&'static str, Token)],
205        data: &Everything,
206        sc: &mut ScopeContext,
207        tooltipped: Tooltipped,
208        negated: bool,
209    ) {
210        // Every invocation is treated as different even if the args are the same,
211        // because we want to point to the correct one when reporting errors.
212        if !self.cached_compat(key, args, tooltipped, negated, sc, data) {
213            if let Some(block) = self.block.expand_macro(args, key.loc, &data.parser.pdxfile) {
214                let mut our_sc = ScopeContext::new_unrooted(Scopes::all(), &self.key);
215                our_sc.set_strict_scopes(false);
216                if self.scope_override.is_some() {
217                    our_sc.set_no_warn(true);
218                }
219                // Insert the dummy sc before continuing. That way, if we recurse, we'll hit
220                // that dummy context instead of macro-expanding again.
221                self.cache.insert(key, args, tooltipped, negated, our_sc.clone());
222                let vd = Validator::new(&block, data);
223                validate_trigger_internal(
224                    Lowercase::empty(),
225                    ListType::None,
226                    &block,
227                    data,
228                    &mut our_sc,
229                    vd,
230                    tooltipped,
231                    negated,
232                );
233                if let Some(scopes) = self.scope_override {
234                    our_sc = ScopeContext::new_unrooted(scopes, key);
235                    our_sc.set_strict_scopes(false);
236                }
237                sc.expect_compatibility(&our_sc, key, data);
238                self.cache.insert(key, args, tooltipped, negated, our_sc);
239            }
240        }
241    }
242}
243
244const BUILTIN_OVERRIDE_TRIGGERS: &[&str] = &[
245    #[cfg(feature = "ck3")]
246    "artifact_low_rarity_trigger",
247    #[cfg(feature = "ck3")]
248    "artifact_medium_rarity_trigger",
249    #[cfg(feature = "ck3")]
250    "artifact_high_rarity_trigger",
251    #[cfg(feature = "ck3")]
252    "artifact_region_trigger",
253];
254
255/// There are vanilla triggers that are known to confuse tiger's scope checking.
256/// Rather than wait for the user to update their config files, just program them in as defaults,
257/// but only if the key is from vanilla. If it's from the mod, they may have implemented the
258/// trigger differently.
259fn builtin_scope_overrides(key: &Token) -> Option<Scopes> {
260    (key.loc.kind.counts_as_vanilla() && BUILTIN_OVERRIDE_TRIGGERS.contains(&key.as_str()))
261        .then_some(Scopes::all())
262}