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 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)]
125pub 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 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 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 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 let mut count = 0;
207 if Game::is_jomini() {
208 #[allow(unused_variables)] 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")] if Game::is_ck3() && count == 2 {
225 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)] 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"); vd.field_script_value("chance_of_no_event", sc);
240 vd.multi_field_validated_block_sc("delay", sc, validate_duration); }
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 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")] 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)] 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")] if Game::is_ck3() && count == 2 {
278 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)] 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")] if Game::is_ck3() && count == 2 {
301 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)] 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"); 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")] if Game::is_ck3() && count == 2 {
328 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)] 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")] if Game::is_ck3() && count == 2 {
347 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 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}