tiger_lib/data/
scripted_effects.rs

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