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 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 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 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
255fn 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}