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