tiger_lib/data/
on_actions.rs

1use std::path::PathBuf;
2
3use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
4
5use crate::block::Block;
6use crate::context::ScopeContext;
7use crate::effect::validate_effect;
8use crate::everything::Everything;
9use crate::fileset::{FileEntry, FileHandler};
10use crate::game::Game;
11use crate::helpers::TigerHashMap;
12use crate::item::Item;
13use crate::on_action::on_action_scopecontext;
14use crate::parse::ParserMemory;
15use crate::pdxfile::PdxFile;
16#[cfg(feature = "ck3")]
17use crate::report::warn;
18#[cfg(any(feature = "ck3", feature = "hoi4"))]
19use crate::report::{ErrorKey, err};
20use crate::scopes::Scopes;
21use crate::token::Token;
22use crate::tooltipped::Tooltipped;
23use crate::trigger::validate_trigger;
24#[cfg(feature = "jomini")]
25use crate::validate::validate_duration;
26use crate::validate::validate_modifiers_with_base;
27use crate::validator::Validator;
28use crate::variables::Variables;
29
30#[derive(Clone, Debug, Default)]
31pub struct OnActions {
32    on_actions: TigerHashMap<&'static str, OnAction>,
33}
34
35impl OnActions {
36    fn load_item(&mut self, key: Token, block: Block) {
37        if let Some(other) = self.on_actions.get_mut(key.as_str()) {
38            other.add(key, block);
39        } else {
40            self.on_actions.insert(key.as_str(), OnAction::new(key, block));
41        }
42    }
43
44    pub fn scan_variables(&self, registry: &mut Variables) {
45        for item in self.on_actions.values() {
46            for (_, block) in &item.actions {
47                registry.scan(block);
48            }
49        }
50    }
51
52    pub fn exists(&self, key: &str) -> bool {
53        self.on_actions.contains_key(key)
54    }
55
56    pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
57        // SAFETY: The item's constructor guarantees at least one element in `actions`.
58        self.on_actions.values().map(|item| &item.actions[0].0)
59    }
60
61    pub fn validate(&self, data: &Everything) {
62        self.on_actions.par_iter().for_each(|(_, v)| {
63            v.validate(data);
64        });
65    }
66
67    pub fn validate_call(&self, key: &Token, data: &Everything, sc: &mut ScopeContext) {
68        if let Some(action) = self.on_actions.get(key.as_str()) {
69            action.validate_call(data, sc);
70        }
71    }
72}
73
74impl FileHandler<Block> for OnActions {
75    fn subpath(&self) -> PathBuf {
76        match Game::game() {
77            #[cfg(feature = "ck3")]
78            Game::Ck3 => PathBuf::from("common/on_action"),
79            #[cfg(feature = "vic3")]
80            Game::Vic3 => PathBuf::from("common/on_actions"),
81            #[cfg(feature = "imperator")]
82            Game::Imperator => PathBuf::from("common/on_action"),
83            #[cfg(feature = "eu5")]
84            Game::Eu5 => PathBuf::from("common/on_action"),
85            #[cfg(feature = "hoi4")]
86            Game::Hoi4 => PathBuf::from("common/on_actions"),
87        }
88    }
89
90    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Block> {
91        if !entry.filename().to_string_lossy().ends_with(".txt") {
92            return None;
93        }
94
95        #[cfg(feature = "hoi4")]
96        if Game::is_hoi4() {
97            return PdxFile::read_no_bom(entry, parser);
98        }
99        PdxFile::read(entry, parser)
100    }
101
102    fn handle_file(&mut self, _entry: &FileEntry, mut block: Block) {
103        for (key, block) in block.drain_definitions_warn() {
104            if Game::is_hoi4() {
105                #[cfg(feature = "hoi4")]
106                let mut block = block;
107                #[cfg(feature = "hoi4")]
108                if key.is("on_actions") {
109                    for (key, block) in block.drain_definitions_warn() {
110                        self.load_item(key, block);
111                    }
112                } else {
113                    let msg = "unexpected key";
114                    let info = "expected only `on_actions` here";
115                    err(ErrorKey::UnknownField).msg(msg).info(info).loc(key).push();
116                }
117            } else {
118                self.load_item(key, block);
119            }
120        }
121    }
122}
123
124#[derive(Clone, Debug)]
125/// Actions override in a special way, which is why this struct contains all actions defined under
126/// the same name, rather than each new one replacing the prior one.
127pub struct OnAction {
128    actions: Vec<(Token, Block)>,
129}
130
131impl OnAction {
132    pub fn new(key: Token, block: Block) -> Self {
133        Self { actions: vec![(key, block)] }
134    }
135
136    pub fn add(&mut self, key: Token, block: Block) {
137        self.actions.push((key, block));
138    }
139
140    pub fn validate(&self, data: &Everything) {
141        let mut seen_trigger = false;
142        let mut seen_effect = false;
143        for (key, block) in self.actions.iter().rev() {
144            // Make an sc for each array entry, to make sure it uses the local `key`.
145            // This is important to distinguish between vanilla errors and mod errors.
146            let mut sc = if let Some(builtin_sc) = on_action_scopecontext(key, data) {
147                builtin_sc
148            } else {
149                let mut generated_sc = ScopeContext::new(Scopes::non_primitive(), key);
150                generated_sc.set_strict_scopes(false);
151                generated_sc
152            };
153            validate_on_action_internal(block, data, &mut sc, &mut seen_trigger, &mut seen_effect);
154        }
155    }
156
157    /// Revalidate an `on_action` under the given scope context.
158    /// It is not necessary to validate anything non scope related,
159    /// but in this case it is easier to just call the full function because
160    /// everything in an action is scope related.
161    pub fn validate_call(&self, data: &Everything, sc: &mut ScopeContext) {
162        let mut seen_trigger = false;
163        let mut seen_effect = false;
164        for (_, block) in self.actions.iter().rev() {
165            validate_on_action_internal(block, data, sc, &mut seen_trigger, &mut seen_effect);
166        }
167    }
168}
169
170fn validate_on_action_internal(
171    block: &Block,
172    data: &Everything,
173    sc: &mut ScopeContext,
174    seen_trigger: &mut bool,
175    seen_effect: &mut bool,
176) {
177    let mut vd = Validator::new(block, data);
178    if Game::is_jomini() {
179        vd.field_validated_block("trigger", |block, data| {
180            if !*seen_trigger {
181                *seen_trigger = true;
182                validate_trigger(block, data, sc, Tooltipped::No);
183            }
184        });
185        vd.field_validated_block_sc("weight_multiplier", sc, validate_modifiers_with_base);
186    }
187
188    if Game::is_hoi4() {
189        // Hoi4 allows defining an effect in each entry.
190        vd.multi_field_validated_block("effect", |block, data| {
191            *seen_effect = true;
192            validate_effect(block, data, sc, Tooltipped::No);
193        });
194    } else {
195        vd.field_validated_block("effect", |block, data| {
196            if !*seen_effect {
197                *seen_effect = true;
198                validate_effect(block, data, sc, Tooltipped::No);
199            }
200        });
201    }
202
203    // TODO: multiple random_events blocks in one on_action aren't outright bugged on Vic3,
204    // but they might still get merged together into one big event pool. Verify.
205
206    let mut count = 0;
207    if Game::is_jomini() {
208        #[allow(unused_variables)] // vic3 doesn't use `key`
209        vd.multi_field_validated_key_block("events", |key, b, data| {
210            let mut vd = Validator::new(b, data);
211            #[cfg(feature = "jomini")]
212            if Game::is_jomini() {
213                vd.multi_field_validated_block_sc("delay", sc, validate_duration);
214            }
215            for token in vd.values() {
216                data.verify_exists(Item::Event, token);
217                data.event_check_scope(token, sc);
218                if let Some(mut event_sc) = sc.root_for_event(token, data) {
219                    data.event_validate_call(token, &mut event_sc);
220                }
221            }
222            count += 1;
223            #[cfg(feature = "ck3")] // Verified: this is only a problem in CK3
224            if Game::is_ck3() && count == 2 {
225                // TODO: verify
226                let msg = format!("not sure if multiple `{key}` blocks in one on_action work");
227                let info = "try combining them into one block";
228                warn(ErrorKey::Validation).msg(msg).info(info).loc(key).push();
229            }
230        });
231    }
232    count = 0;
233    #[allow(unused_variables)] // vic3 doesn't use `key`
234    vd.multi_field_validated_key_block("random_events", |key, b, data| {
235        let mut vd = Validator::new(b, data);
236        #[cfg(feature = "jomini")]
237        if Game::is_jomini() {
238            vd.field_numeric("chance_to_happen"); // TODO: 0 - 100
239            vd.field_script_value("chance_of_no_event", sc);
240            vd.multi_field_validated_block_sc("delay", sc, validate_duration); // undocumented
241        }
242        for (_key, token) in vd.integer_values() {
243            if token.is("0") {
244                continue;
245            }
246            data.verify_exists(Item::Event, token);
247            data.event_check_scope(token, sc);
248            // Hoi4 uses the scope context directly.
249            if Game::is_hoi4() {
250                data.event_validate_call(token, sc);
251            } else if let Some(mut event_sc) = sc.root_for_event(token, data) {
252                data.event_validate_call(token, &mut event_sc);
253            }
254        }
255        count += 1;
256        #[cfg(feature = "ck3")] // Verified: this is only a problem in CK3
257        if Game::is_ck3() && count == 2 {
258            let msg = format!("multiple `{key}` blocks in one on_action do not work");
259            let info = "try putting each into its own on_action and firing those separately";
260            err(ErrorKey::Validation).msg(msg).info(info).loc(key).push();
261        }
262    });
263    if Game::is_jomini() {
264        count = 0;
265        #[allow(unused_variables)] // vic3 doesn't use `key`
266        vd.multi_field_validated_key_block("first_valid", |key, b, data| {
267            let mut vd = Validator::new(b, data);
268            for token in vd.values() {
269                data.verify_exists(Item::Event, token);
270                data.event_check_scope(token, sc);
271                if let Some(mut event_sc) = sc.root_for_event(token, data) {
272                    data.event_validate_call(token, &mut event_sc);
273                }
274            }
275            count += 1;
276            #[cfg(feature = "ck3")] // Verified: this is only a problem in CK3
277            if Game::is_ck3() && count == 2 {
278                // TODO: verify
279                let msg = format!("not sure if multiple `{key}` blocks in one on_action work");
280                let info = "try putting each into its own on_action and firing those separately";
281                warn(ErrorKey::Validation).msg(msg).info(info).loc(key).push();
282            }
283        });
284        count = 0;
285        #[allow(unused_variables)] // vic3 doesn't use `key`
286        vd.multi_field_validated_key_block("on_actions", |key, b, data| {
287            let mut vd = Validator::new(b, data);
288            #[cfg(feature = "jomini")]
289            if Game::is_jomini() {
290                vd.multi_field_validated_block_sc("delay", sc, validate_duration);
291            }
292            for token in vd.values() {
293                data.verify_exists(Item::OnAction, token);
294                if let Some(mut action_sc) = sc.root_for_action(token, data) {
295                    data.on_actions.validate_call(token, data, &mut action_sc);
296                }
297            }
298            count += 1;
299            #[cfg(feature = "ck3")] // Verified: this is only a problem in CK3
300            if Game::is_ck3() && count == 2 {
301                // TODO: verify
302                let msg = format!("not sure if multiple `{key}` blocks in one on_action work");
303                let info = "try combining them into one block";
304                warn(ErrorKey::Validation).msg(msg).info(info).loc(key).push();
305            }
306        });
307        count = 0;
308        #[allow(unused_variables)] // vic3 doesn't use `key`
309        vd.multi_field_validated_key_block("random_on_action", |key, b, data| {
310            let mut vd = Validator::new(b, data);
311            #[cfg(feature = "jomini")]
312            if Game::is_jomini() {
313                vd.field_numeric("chance_to_happen"); // TODO: 0 - 100
314                vd.field_script_value("chance_of_no_event", sc);
315            }
316            for (_key, token) in vd.integer_values() {
317                if token.is("0") {
318                    continue;
319                }
320                data.verify_exists(Item::OnAction, token);
321                if let Some(mut action_sc) = sc.root_for_action(token, data) {
322                    data.on_actions.validate_call(token, data, &mut action_sc);
323                }
324            }
325            count += 1;
326            #[cfg(feature = "ck3")] // Verified: this is only a problem in CK3
327            if Game::is_ck3() && count == 2 {
328                // TODO: verify
329                let msg = format!("not sure if multiple `{key}` blocks in one on_action work");
330                let info = "try putting each into its own on_action and firing those separately";
331                warn(ErrorKey::Validation).msg(msg).info(info).loc(key).push();
332            }
333        });
334        count = 0;
335        #[allow(unused_variables)] // vic3 doesn't use `key`
336        vd.multi_field_validated_key_block("first_valid_on_action", |key, b, data| {
337            let mut vd = Validator::new(b, data);
338            for token in vd.values() {
339                data.verify_exists(Item::OnAction, token);
340                if let Some(mut action_sc) = sc.root_for_action(token, data) {
341                    data.on_actions.validate_call(token, data, &mut action_sc);
342                }
343            }
344            count += 1;
345            #[cfg(feature = "ck3")] // Verified: this is only a problem in CK3
346            if Game::is_ck3() && count == 2 {
347                // TODO: verify
348                let msg = format!("not sure if multiple `{key}` blocks in one on_action work");
349                let info = "try putting each into its own on_action and firing those separately";
350                warn(ErrorKey::Validation).msg(msg).info(info).loc(key).push();
351            }
352        });
353        // TODO: check for infinite fallback loops?
354        vd.field_action("fallback", sc);
355    }
356}
357
358#[cfg(feature = "vic3")]
359pub fn validate_on_action(block: &Block, data: &Everything, sc: &mut ScopeContext) {
360    let mut seen_trigger = false;
361    let mut seen_effect = false;
362    validate_on_action_internal(block, data, sc, &mut seen_trigger, &mut seen_effect);
363}