1use std::borrow::Cow;
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7use std::sync::LazyLock;
8
9use phf::phf_map;
10use strum_macros::{Display, EnumString};
11
12#[cfg(feature = "ck3")]
13use crate::ck3::data::religions::CUSTOM_RELIGION_LOCAS;
14use crate::context::ScopeContext;
15#[cfg(feature = "jomini")]
16use crate::data::customloca::CustomLocalization;
17use crate::data::localization::Language;
18#[cfg(feature = "jomini")]
19use crate::data::scripted_guis::ScriptedGui;
20use crate::datacontext::DataContext;
21use crate::everything::Everything;
22use crate::game::Game;
23use crate::helpers::BiTigerHashMap;
24#[cfg(feature = "hoi4")]
25use crate::helpers::is_country_tag;
26#[cfg(feature = "hoi4")]
27use crate::hoi4::data::scripted_localisation::ScriptedLocalisation;
28use crate::item::Item;
29#[cfg(feature = "hoi4")]
30use crate::report::Severity;
31#[cfg(feature = "jomini")]
32use crate::report::err;
33use crate::report::{ErrorKey, warn};
34use crate::scopes::Scopes;
35use crate::token::Token;
36
37#[cfg(feature = "ck3")]
39include!("ck3/tables/include/datatypes.rs");
40#[cfg(feature = "vic3")]
41include!("vic3/tables/include/datatypes.rs");
42#[cfg(feature = "imperator")]
43include!("imperator/tables/include/datatypes.rs");
44#[cfg(feature = "eu5")]
45include!("eu5/tables/include/datatypes.rs");
46#[cfg(feature = "hoi4")]
47include!("hoi4/tables/include/datatypes.rs");
48
49#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
65#[allow(non_camel_case_types)]
66pub enum Datatype {
67 Unknown,
69 AnyScope,
70
71 CFixedPoint,
73 CString,
74 CUTF8String,
75 CVector2f,
76 CVector2i,
77 CVector3f,
78 CVector3i,
79 CVector4f,
80 CVector4i,
81 Date,
82 Scope,
83 TopScope,
84 bool,
85 double,
86 float,
87 int16,
88 int32,
89 int64,
90 int8,
91 uint16,
92 uint32,
93 uint64,
94 uint8,
95 void,
96
97 #[cfg(feature = "ck3")]
99 Ck3(Ck3Datatype),
100 #[cfg(feature = "vic3")]
101 Vic3(Vic3Datatype),
102 #[cfg(feature = "imperator")]
103 Imperator(ImperatorDatatype),
104 #[cfg(feature = "eu5")]
105 Eu5(Eu5Datatype),
106 #[cfg(feature = "hoi4")]
107 Hoi4(Hoi4Datatype),
108}
109
110static STR_DATATYPE_MAP: phf::Map<&'static str, Datatype> = phf_map! {
111 "Unknown" => Datatype::Unknown,
112 "AnyScope" => Datatype::AnyScope,
113 "CFixedPoint" => Datatype::CFixedPoint,
114 "CString" => Datatype::CString,
115 "CUTF8String" => Datatype::CUTF8String,
116 "CVector2f" => Datatype::CVector2f,
117 "CVector2i" => Datatype::CVector2i,
118 "CVector3f" => Datatype::CVector3f,
119 "CVector3i" => Datatype::CVector3i,
120 "CVector4f" => Datatype::CVector4f,
121 "CVector4i" => Datatype::CVector4i,
122 "Date" => Datatype::Date,
123 "Scope" => Datatype::Scope,
124 "TopScope" => Datatype::TopScope,
125 "bool" => Datatype::bool,
126 "double" => Datatype::double,
127 "float" => Datatype::float,
128 "int16" => Datatype::int16,
129 "int32" => Datatype::int32,
130 "int64" => Datatype::int64,
131 "int8" => Datatype::int8,
132 "uint16" => Datatype::uint16,
133 "uint32" => Datatype::uint32,
134 "uint64" => Datatype::uint64,
135 "uint8" => Datatype::uint8,
136 "void" => Datatype::void,
137};
138
139impl FromStr for Datatype {
140 type Err = strum::ParseError;
141 fn from_str(s: &str) -> Result<Self, strum::ParseError> {
143 STR_DATATYPE_MAP.get(s).copied().ok_or(strum::ParseError::VariantNotFound).or_else(|_| {
144 match Game::game() {
145 #[cfg(feature = "ck3")]
146 Game::Ck3 => Ck3Datatype::from_str(s).map(Datatype::Ck3),
147 #[cfg(feature = "vic3")]
148 Game::Vic3 => Vic3Datatype::from_str(s).map(Datatype::Vic3),
149 #[cfg(feature = "imperator")]
150 Game::Imperator => ImperatorDatatype::from_str(s).map(Datatype::Imperator),
151 #[cfg(feature = "eu5")]
152 Game::Eu5 => Eu5Datatype::from_str(s).map(Datatype::Eu5),
153 #[cfg(feature = "hoi4")]
154 Game::Hoi4 => Hoi4Datatype::from_str(s).map(Datatype::Hoi4),
155 }
156 })
157 }
158}
159
160impl Display for Datatype {
161 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
163 match *self {
165 Datatype::Unknown => write!(f, "Unknown"),
166 Datatype::AnyScope => write!(f, "AnyScope"),
167 Datatype::CFixedPoint => write!(f, "CFixedPoint"),
168 Datatype::CString => write!(f, "CString"),
169 Datatype::CUTF8String => write!(f, "CUTF8String"),
170 Datatype::CVector2f => write!(f, "CVector2f"),
171 Datatype::CVector2i => write!(f, "CVector2i"),
172 Datatype::CVector3f => write!(f, "CVector3f"),
173 Datatype::CVector3i => write!(f, "CVector3i"),
174 Datatype::CVector4f => write!(f, "CVector4f"),
175 Datatype::CVector4i => write!(f, "CVector4i"),
176 Datatype::Date => write!(f, "Date"),
177 Datatype::Scope => write!(f, "Scope"),
178 Datatype::TopScope => write!(f, "TopScope"),
179 Datatype::bool => write!(f, "bool"),
180 Datatype::double => write!(f, "double"),
181 Datatype::float => write!(f, "float"),
182 Datatype::int16 => write!(f, "int16"),
183 Datatype::int32 => write!(f, "int32"),
184 Datatype::int64 => write!(f, "int64"),
185 Datatype::int8 => write!(f, "int8"),
186 Datatype::uint16 => write!(f, "uint16"),
187 Datatype::uint32 => write!(f, "uint32"),
188 Datatype::uint64 => write!(f, "uint64"),
189 Datatype::uint8 => write!(f, "uint8"),
190 Datatype::void => write!(f, "void"),
191 #[cfg(feature = "ck3")]
192 Datatype::Ck3(dt) => dt.fmt(f),
193 #[cfg(feature = "vic3")]
194 Datatype::Vic3(dt) => dt.fmt(f),
195 #[cfg(feature = "imperator")]
196 Datatype::Imperator(dt) => dt.fmt(f),
197 #[cfg(feature = "eu5")]
198 Datatype::Eu5(dt) => dt.fmt(f),
199 #[cfg(feature = "hoi4")]
200 Datatype::Hoi4(dt) => dt.fmt(f),
201 }
202 }
203}
204
205#[derive(Clone, Debug, Default)]
219pub struct CodeChain {
220 pub codes: Box<[Code]>,
221}
222
223#[derive(Clone, Debug)]
227pub struct Code {
228 pub name: Token,
229 pub arguments: Vec<CodeArg>,
230}
231
232#[derive(Clone, Debug)]
234#[allow(dead_code)] pub enum CodeArg {
236 Chain(CodeChain),
238 Literal(Token),
242}
243
244impl CodeChain {
245 #[cfg(feature = "ck3")]
246 pub fn as_gameconcept(&self) -> Option<&Token> {
247 if self.codes.len() == 1 && self.codes[0].arguments.is_empty() {
248 Some(&self.codes[0].name)
249 } else if self.codes.len() == 1
250 && self.codes[0].name.is("Concept")
251 && self.codes[0].arguments.len() == 2
252 {
253 if let CodeArg::Literal(token) = &self.codes[0].arguments[0] {
254 Some(token)
255 } else {
256 None
257 }
258 } else {
259 None
260 }
261 }
262
263 #[cfg(feature = "jomini")]
264 pub fn without_last(&self) -> Self {
265 if self.codes.is_empty() {
266 CodeChain { codes: Box::new([]) }
267 } else {
268 CodeChain { codes: Box::from(&self.codes[..self.codes.len() - 1]) }
269 }
270 }
271}
272
273#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
277pub enum Arg {
278 #[cfg(feature = "jomini")]
281 DType(Datatype),
282 #[cfg(feature = "jomini")]
285 IType(Item),
286}
287
288#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
292pub enum Args {
293 Unknown,
294 Args(&'static [Arg]),
295}
296
297#[derive(Copy, Clone, Debug)]
299enum LookupResult {
300 NotFound,
302 WrongType,
304 Found(Args, Datatype),
307}
308
309#[cfg(feature = "jomini")]
317fn validate_custom(token: &Token, data: &Everything, scopes: Scopes, lang: Option<Language>) {
318 data.verify_exists(Item::CustomLocalization, token);
319 if let Some((key, block)) = data.get_key_block(Item::CustomLocalization, token.as_str()) {
320 CustomLocalization::validate_custom_call(key, block, data, token, scopes, lang, "", None);
321 }
322}
323
324#[cfg(feature = "jomini")]
333fn validate_argument(
334 arg: &CodeArg,
335 data: &Everything,
336 sc: &mut ScopeContext,
337 dc: &DataContext,
338 expect_arg: Arg,
339 lang: Option<Language>,
340 format: Option<&Token>,
341) {
342 match expect_arg {
343 Arg::DType(expect_type) => {
344 match arg {
345 CodeArg::Chain(chain) => {
346 validate_datatypes(chain, data, sc, dc, expect_type, lang, format, false);
347 }
348 CodeArg::Literal(token) => {
349 if token.as_str().starts_with('(') && token.as_str().contains(')') {
350 let dtype =
352 token.as_str().split(')').next().unwrap().strip_prefix('(').unwrap();
353 if dtype == "hex" {
354 if expect_type != Datatype::Unknown && expect_type != Datatype::int32 {
355 let msg = format!("expected {expect_type}, got {dtype}");
356 warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
357 }
358 } else if let Ok(dtype) = Datatype::from_str(dtype) {
359 if expect_type != Datatype::Unknown && expect_type != dtype {
360 let msg = format!("expected {expect_type}, got {dtype}");
361 warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
362 }
363 } else {
364 let msg = format!("unrecognized datatype {dtype}");
365 warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
366 }
367 } else if expect_type != Datatype::Unknown && expect_type != Datatype::CString {
368 let msg = format!("expected {expect_type}, got CString");
369 warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
370 }
371 }
372 }
373 }
374 Arg::IType(itype) => match arg {
375 CodeArg::Chain(chain) => {
376 validate_datatypes(chain, data, sc, dc, Datatype::CString, lang, format, false);
377 }
378 CodeArg::Literal(token) => {
379 data.verify_exists(itype, token);
380 }
381 },
382 }
383}
384
385#[allow(unused_variables)] #[allow(clippy::too_many_arguments)] pub fn validate_datatypes(
400 chain: &CodeChain,
401 data: &Everything,
402 sc: &mut ScopeContext,
403 dc: &DataContext,
404 expect_type: Datatype,
405 lang: Option<Language>,
406 format: Option<&Token>,
407 expect_promote: bool,
408) -> Datatype {
409 let mut curtype = Datatype::Unknown;
410 #[allow(unused_mut)] let mut codes = Cow::from(&chain.codes[..]);
412 #[cfg(any(feature = "ck3", feature = "vic3"))]
413 let mut macro_count = 0;
414 let mut i = 0;
416 let mut in_variable = false;
417 while i < codes.len() {
418 #[cfg(any(feature = "ck3", feature = "vic3"))]
419 if Game::is_ck3() || Game::is_vic3() {
420 while let Some(binding) = data.data_bindings.get(codes[i].name.as_str()) {
421 if let Some(replacement) = binding.replace(&codes[i]) {
422 macro_count += 1;
423 if macro_count > 255 {
424 let msg =
425 format!("substituted data bindings {macro_count} times, giving up");
426 err(ErrorKey::Macro).msg(msg).loc(&codes[i].name).push();
427 return Datatype::Unknown;
428 }
429 codes.to_mut().splice(i..=i, replacement.codes);
430 } else {
431 return Datatype::Unknown;
432 }
433 }
434 }
435
436 let code = &codes[i];
437 let is_first = i == 0;
438 let is_last = i == codes.len() - 1;
439 let mut args = Args::Args(&[]);
440 let mut rtype = Datatype::Unknown;
441
442 if code.name.is("") {
443 warn(ErrorKey::Datafunctions).msg("empty fragment").loc(&code.name).push();
445 return Datatype::Unknown;
446 }
447
448 let lookup_gf = lookup_global_function(code.name.as_str());
449 let lookup_gp = lookup_global_promote(code.name.as_str());
450 let lookup_f = lookup_function(code.name.as_str(), curtype);
451 let lookup_p = lookup_promote(code.name.as_str(), curtype);
452
453 let gf_found = lookup_gf.is_some();
454 let gp_found = lookup_gp.is_some();
455 let f_found = !matches!(lookup_f, LookupResult::NotFound);
456 let p_found = !matches!(lookup_p, LookupResult::NotFound);
457
458 let mut found = false;
459
460 if is_first && is_last && !expect_promote {
461 if let Some((xargs, xrtype)) = lookup_gf {
462 found = true;
463 args = xargs;
464 rtype = xrtype;
465 }
466 } else if is_first && (!is_last || expect_promote) {
467 if let Some((xargs, xrtype)) = lookup_gp {
468 found = true;
469 args = xargs;
470 rtype = xrtype;
471 }
472 } else if !is_first && (!is_last || expect_promote) {
473 match lookup_p {
474 LookupResult::Found(xargs, xrtype) => {
475 found = true;
476 args = xargs;
477 rtype = xrtype;
478 }
479 LookupResult::WrongType => {
480 let msg = format!("{} cannot follow a {curtype} promote", code.name);
481 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
482 return Datatype::Unknown;
483 }
484 LookupResult::NotFound => (),
485 }
486 } else if !is_first && is_last && !expect_promote {
487 match lookup_f {
488 LookupResult::Found(xargs, xrtype) => {
489 found = true;
490 args = xargs;
491 rtype = xrtype;
492 }
493 LookupResult::WrongType => {
494 let msg = format!("{} cannot follow a {curtype} promote", code.name);
495 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
496 return Datatype::Unknown;
497 }
498 LookupResult::NotFound => (),
499 }
500 }
501
502 if Game::is_hoi4() && !found && !is_first && code.name.is("FROM") {
503 found = true;
505 rtype = Datatype::Unknown;
507 } else if Game::is_hoi4() && !found && !is_first && code.name.is("OWNER") {
508 found = true;
510 rtype = Datatype::Unknown;
512 }
513
514 if !found {
515 if is_first && (p_found || f_found) && !gp_found && !gf_found {
518 let msg = format!("{} cannot be the first in a chain", code.name);
519 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
520 return Datatype::Unknown;
521 }
522 if is_last && (gp_found || p_found) && !gf_found && !f_found && !expect_promote {
523 let msg = format!("{} cannot be last in a chain", code.name);
524 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
525 return Datatype::Unknown;
526 }
527 if expect_promote && (gf_found || f_found) {
528 let msg = format!("{} cannot be used in this field", code.name);
529 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
530 return Datatype::Unknown;
531 }
532 if !is_first && (gp_found || gf_found) && !p_found && !f_found {
533 let msg = format!("{} must be the first in a chain", code.name);
534 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
535 return Datatype::Unknown;
536 }
537 if !is_last && (gf_found || f_found) && !gp_found && !p_found {
538 let msg = format!("{} must be last in the chain", code.name);
539 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
540 return Datatype::Unknown;
541 }
542 if gp_found || gf_found || p_found || f_found {
544 let msg = format!("{} is improperly used here", code.name);
545 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
546 return Datatype::Unknown;
547 }
548 }
549
550 #[cfg(feature = "vic3")]
551 if Game::is_vic3()
553 && !found
554 && is_first
555 && data.item_exists(Item::Country, code.name.as_str())
556 {
557 found = true;
558 args = Args::Args(&[]);
559 rtype = Datatype::Vic3(Vic3Datatype::Country);
560 }
561
562 #[cfg(feature = "imperator")]
563 if Game::is_imperator()
564 && !found
565 && is_first
566 && data.item_exists(Item::Country, code.name.as_str())
567 {
568 found = true;
569 args = Args::Args(&[]);
570 rtype = Datatype::Imperator(ImperatorDatatype::Country);
571 }
572
573 #[cfg(feature = "vic3")]
576 if Game::is_vic3()
577 && !found
578 && is_first
579 && is_last
580 && code.name.as_str().starts_with("concept_")
581 {
582 found = true;
583 if let Some(concept) = code.name.as_str().strip_suffix("_desc") {
584 data.verify_exists_implied(Item::GameConcept, concept, &code.name);
585 } else {
586 data.verify_exists(Item::GameConcept, &code.name);
587 }
588 args = Args::Args(&[]);
589 rtype = Datatype::CString;
590 }
591
592 #[cfg(feature = "eu5")]
595 if Game::is_eu5() && !found && is_first && is_last {
596 found = true;
597 if let Some(concept) = code.name.as_str().strip_suffix("_with_icon") {
598 data.verify_exists_implied(Item::GameConcept, concept, &code.name);
599 } else if let Some(concept) = code.name.as_str().strip_suffix("_icon") {
600 data.verify_exists_implied(Item::GameConcept, concept, &code.name);
601 } else {
602 data.verify_exists(Item::GameConcept, &code.name);
603 }
604 args = Args::Args(&[]);
605 rtype = Datatype::CString;
606 }
607
608 #[cfg(feature = "ck3")]
609 if Game::is_ck3()
610 && !found
611 && is_first
612 && is_last
613 && data.item_exists(Item::GameConcept, code.name.as_str())
614 {
615 let game_concept_formatting =
616 format.is_some_and(|fmt| fmt.as_str().contains('E') || fmt.as_str().contains('e'));
617 if sc.is_name_defined(code.name.as_str(), data).is_some() && !game_concept_formatting {
630 let msg = format!("`{}` is both a named scope and a game concept here", &code.name);
631 let info = format!(
632 "The game concept will take precedence. Do `{}.Self` if you want the named scope.",
633 &code.name
634 );
635 warn(ErrorKey::Datafunctions).msg(msg).info(info).loc(&code.name).push();
636 }
637
638 found = true;
639 args = Args::Args(&[]);
640 rtype = Datatype::CString;
641 }
642
643 if Game::is_hoi4() && !found && in_variable {
644 in_variable = false;
646 found = true;
647 rtype = Datatype::Unknown;
649 }
650
651 if !found && is_first {
657 if let Some(scopes) = sc.is_name_defined(code.name.as_str(), data) {
658 found = true;
659 args = Args::Args(&[]);
660 rtype = datatype_from_scopes(scopes);
661 }
662 }
663
664 let first_char = code.name.as_str().chars().next().unwrap();
669 if !found
670 && is_first
671 && !sc.is_strict()
672 && (first_char.is_lowercase() || first_char.is_ascii_digit())
673 {
674 found = true;
675 args = Args::Args(&[]);
676 rtype = Datatype::Unknown;
680 }
681
682 #[cfg(feature = "hoi4")]
683 if Game::is_hoi4() && !found && is_country_tag(code.name.as_str()) {
684 found = true;
685 data.verify_exists_max_sev(Item::CountryTag, &code.name, Severity::Warning);
686 rtype = Datatype::Hoi4(Hoi4Datatype::Country);
687 }
688
689 #[cfg(feature = "hoi4")]
690 if Game::is_hoi4()
691 && !found
692 && data.item_exists(Item::ScriptedLocalisation, code.name.as_str())
693 {
694 found = true;
695 rtype = Datatype::CString;
696 if let Some((_, block)) =
697 data.get_key_block(Item::ScriptedLocalisation, code.name.as_str())
698 {
699 ScriptedLocalisation::validate_loca_call(block, data, lang);
700 }
701 }
702
703 #[cfg(feature = "hoi4")]
704 if Game::is_hoi4() && !found && code.name.starts_with("?") {
705 found = true;
708 rtype = Datatype::Unknown;
710 let reference = code.name.strip_prefix("?").unwrap();
711 if reference.lowercase_is("global") || reference.is("FROM") || reference.is("PREV") {
713 in_variable = true;
714 } else if is_country_tag(reference.as_str()) {
715 in_variable = true;
716 data.verify_exists_max_sev(Item::CountryTag, &reference, Severity::Warning);
717 } else if reference.is_integer() {
718 in_variable = true;
721 }
722 }
723
724 if !found {
726 let msg = format!("unknown datafunction {}", &code.name);
728 if let Some(alternative) = lookup_alternative(code.name.as_str()) {
729 let info = format!("did you mean {alternative}?");
730 warn(ErrorKey::Datafunctions).msg(msg).info(info).loc(&code.name).push();
731 } else {
732 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
733 }
734 return Datatype::Unknown;
735 }
736
737 if let Args::Args(a) = args {
739 if a.len() != code.arguments.len() {
740 let msg = format!(
741 "{} takes {} arguments but was given {} here",
742 code.name,
743 a.len(),
744 code.arguments.len()
745 );
746 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
747 return Datatype::Unknown;
748 }
749 }
750
751 #[cfg(feature = "jomini")]
752 if Game::is_jomini() && is_first {
754 let name = if code.name.is("GetScriptedGui") {
755 if let Some(CodeArg::Literal(name)) = code.arguments.first() {
757 Some(name)
758 } else {
759 None
760 }
761 } else if code.name.is("ScriptedGui") {
762 dc.sgui_name()
764 } else {
765 None
766 };
767 if let Some(name) = name {
768 if let Some(code) = codes.get(1) {
770 if let Some((key, block, kind)) =
771 data.get_item::<ScriptedGui>(Item::ScriptedGui, name.as_str())
772 {
773 kind.validate_guicall(key, block, data, sc, dc, code);
774 }
775 }
776 }
777 }
778
779 #[cfg(feature = "ck3")]
781 if Game::is_ck3()
782 && curtype != Datatype::Ck3(Ck3Datatype::Faith)
783 && (code.name.is("Custom") && code.arguments.len() == 1)
784 || (code.name.is("Custom2") && code.arguments.len() == 2)
785 {
786 if let CodeArg::Literal(ref token) = code.arguments[0] {
788 if let Some(scopes) = scope_from_datatype(curtype) {
789 validate_custom(token, data, scopes, lang);
790 } else if (curtype == Datatype::Unknown
791 || curtype == Datatype::AnyScope
792 || curtype == Datatype::TopScope)
793 && !CUSTOM_RELIGION_LOCAS.contains(&token.as_str())
794 {
795 validate_custom(token, data, Scopes::all(), lang);
797 }
798 }
799 }
800
801 #[cfg(feature = "vic3")]
802 if Game::is_vic3() && code.name.is("GetCustom") && code.arguments.len() == 1 {
803 if let CodeArg::Literal(ref token) = code.arguments[0] {
804 if let Some(scopes) = scope_from_datatype(curtype) {
805 validate_custom(token, data, scopes, lang);
806 } else if curtype == Datatype::Unknown
807 || curtype == Datatype::AnyScope
808 || curtype == Datatype::TopScope
809 {
810 validate_custom(token, data, Scopes::all(), lang);
812 }
813 }
814 }
815
816 #[cfg(feature = "imperator")]
817 if Game::is_imperator() && code.name.is("Custom") && code.arguments.len() == 1 {
818 if let CodeArg::Literal(ref token) = code.arguments[0] {
819 if let Some(scopes) = scope_from_datatype(curtype) {
820 validate_custom(token, data, scopes, lang);
821 } else if curtype == Datatype::Unknown
822 || curtype == Datatype::AnyScope
823 || curtype == Datatype::TopScope
824 {
825 validate_custom(token, data, Scopes::all(), lang);
827 }
828 }
829 }
830
831 #[cfg(feature = "jomini")]
833 if code.name.is("GetDefine") && code.arguments.len() == 2 {
834 if let CodeArg::Literal(ref token1) = code.arguments[0] {
835 if let CodeArg::Literal(ref token2) = code.arguments[1] {
836 let key = format!("{token1}|{token2}");
837 if data.defines.get_bv(&key).is_none() {
838 let msg = format!("{key} not defined in common/defines/");
839 err(ErrorKey::MissingItem).msg(msg).loc(token2).push();
840 }
841 }
842 }
843 }
844
845 if code.name.is("Localize") && code.arguments.len() == 1 {
847 if let CodeArg::Literal(ref token) = code.arguments[0] {
848 if token.as_str().is_ascii() {
852 data.localization.verify_exists_lang(token, lang);
853 }
854 }
855 }
856
857 #[cfg(feature = "jomini")]
858 if let Args::Args(a) = args {
859 for (i, arg) in a.iter().enumerate() {
860 if Game::is_jomini() && code.name.is("SelectLocalization") && i > 0 {
862 if let CodeArg::Chain(chain) = &code.arguments[i] {
863 if chain.codes.len() == 1
864 && chain.codes[0].arguments.is_empty()
865 && data.item_exists(Item::GameConcept, chain.codes[0].name.as_str())
866 {
867 continue;
868 }
869 }
870 }
871 validate_argument(&code.arguments[i], data, sc, dc, *arg, lang, format);
872 }
873 }
874
875 curtype = rtype;
876
877 if is_last
878 && curtype != Datatype::Unknown
879 && expect_type != Datatype::Unknown
880 && curtype != expect_type
881 {
882 if expect_type == Datatype::AnyScope {
883 if scope_from_datatype(curtype).is_none() {
884 let msg =
885 format!("{} returns {curtype} but a scope type is needed here", code.name);
886 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
887 return Datatype::Unknown;
888 }
889 } else {
890 let msg =
891 format!("{} returns {curtype} but a {expect_type} is needed here", code.name);
892 warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
893 return Datatype::Unknown;
894 }
895 }
896
897 i += 1;
898 }
899 curtype
900}
901
902fn lookup_global_promote(lookup_name: &str) -> Option<(Args, Datatype)> {
903 let global_promotes_map = match Game::game() {
904 #[cfg(feature = "ck3")]
905 Game::Ck3 => &crate::ck3::tables::datafunctions::GLOBAL_PROMOTES_MAP,
906 #[cfg(feature = "vic3")]
907 Game::Vic3 => &crate::vic3::tables::datafunctions::GLOBAL_PROMOTES_MAP,
908 #[cfg(feature = "imperator")]
909 Game::Imperator => &crate::imperator::tables::datafunctions::GLOBAL_PROMOTES_MAP,
910 #[cfg(feature = "eu5")]
911 Game::Eu5 => &crate::eu5::tables::datafunctions::GLOBAL_PROMOTES_MAP,
912 #[cfg(feature = "hoi4")]
913 Game::Hoi4 => &crate::hoi4::tables::datafunctions::GLOBAL_PROMOTES_MAP,
914 };
915
916 if let result @ Some(_) = global_promotes_map.get(lookup_name).copied() {
917 return result;
918 }
919
920 if let Ok(dtype) = Datatype::from_str(lookup_name) {
922 return Some((Args::Args(&[]), dtype));
923 }
924
925 None
926}
927
928fn lookup_global_function(lookup_name: &str) -> Option<(Args, Datatype)> {
929 let global_functions_map = match Game::game() {
930 #[cfg(feature = "ck3")]
931 Game::Ck3 => &crate::ck3::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
932 #[cfg(feature = "vic3")]
933 Game::Vic3 => &crate::vic3::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
934 #[cfg(feature = "imperator")]
935 Game::Imperator => &crate::imperator::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
936 #[cfg(feature = "eu5")]
937 Game::Eu5 => &crate::eu5::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
938 #[cfg(feature = "hoi4")]
939 Game::Hoi4 => &crate::hoi4::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
940 };
941 global_functions_map.get(lookup_name).copied()
942}
943
944fn lookup_promote_or_function(ltype: Datatype, vec: &[(Datatype, Args, Datatype)]) -> LookupResult {
945 let mut possible_args = None;
946 let mut possible_rtype = None;
947
948 for (intype, args, rtype) in vec.iter().copied() {
949 if ltype == Datatype::Unknown {
950 if possible_rtype.is_none() {
951 possible_args = Some(args);
952 possible_rtype = Some(rtype);
953 } else {
954 if possible_rtype != Some(rtype) {
955 possible_rtype = Some(Datatype::Unknown);
956 }
957 if possible_args != Some(args) {
958 possible_args = Some(Args::Unknown);
959 }
960 }
961 } else if ltype == intype {
962 return LookupResult::Found(args, rtype);
963 }
964 }
965
966 if ltype == Datatype::Unknown {
967 LookupResult::Found(possible_args.unwrap(), possible_rtype.unwrap())
968 } else {
969 LookupResult::WrongType
971 }
972}
973
974fn lookup_promote(lookup_name: &str, ltype: Datatype) -> LookupResult {
975 let promotes_map = match Game::game() {
976 #[cfg(feature = "ck3")]
977 Game::Ck3 => &crate::ck3::tables::datafunctions::PROMOTES_MAP,
978 #[cfg(feature = "vic3")]
979 Game::Vic3 => &crate::vic3::tables::datafunctions::PROMOTES_MAP,
980 #[cfg(feature = "imperator")]
981 Game::Imperator => &crate::imperator::tables::datafunctions::PROMOTES_MAP,
982 #[cfg(feature = "eu5")]
983 Game::Eu5 => &crate::eu5::tables::datafunctions::PROMOTES_MAP,
984 #[cfg(feature = "hoi4")]
985 Game::Hoi4 => &crate::hoi4::tables::datafunctions::PROMOTES_MAP,
986 };
987
988 promotes_map
989 .get(lookup_name)
990 .map_or(LookupResult::NotFound, |x| lookup_promote_or_function(ltype, x))
991}
992
993fn lookup_function(lookup_name: &str, ltype: Datatype) -> LookupResult {
994 let functions_map = match Game::game() {
995 #[cfg(feature = "ck3")]
996 Game::Ck3 => &crate::ck3::tables::datafunctions::FUNCTIONS_MAP,
997 #[cfg(feature = "vic3")]
998 Game::Vic3 => &crate::vic3::tables::datafunctions::FUNCTIONS_MAP,
999 #[cfg(feature = "imperator")]
1000 Game::Imperator => &crate::imperator::tables::datafunctions::FUNCTIONS_MAP,
1001 #[cfg(feature = "eu5")]
1002 Game::Eu5 => &crate::eu5::tables::datafunctions::FUNCTIONS_MAP,
1003 #[cfg(feature = "hoi4")]
1004 Game::Hoi4 => &crate::hoi4::tables::datafunctions::FUNCTIONS_MAP,
1005 };
1006
1007 functions_map
1008 .get(lookup_name)
1009 .map_or(LookupResult::NotFound, |x| lookup_promote_or_function(ltype, x))
1010}
1011
1012pub struct CaseInsensitiveStr(pub(crate) &'static str);
1013
1014impl PartialEq for CaseInsensitiveStr {
1015 fn eq(&self, other: &Self) -> bool {
1016 self.0.eq_ignore_ascii_case(other.0)
1017 }
1018}
1019
1020impl Eq for CaseInsensitiveStr {}
1021
1022impl std::hash::Hash for CaseInsensitiveStr {
1023 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1024 self.0.to_ascii_lowercase().hash(state);
1025 }
1026}
1027
1028fn lookup_alternative(lookup_name: &'static str) -> Option<&'static str> {
1033 let lowercase_datatype_set = match Game::game() {
1034 #[cfg(feature = "ck3")]
1035 Game::Ck3 => &crate::ck3::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1036 #[cfg(feature = "vic3")]
1037 Game::Vic3 => &crate::vic3::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1038 #[cfg(feature = "imperator")]
1039 Game::Imperator => &crate::imperator::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1040 #[cfg(feature = "eu5")]
1041 Game::Eu5 => &crate::eu5::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1042 #[cfg(feature = "hoi4")]
1043 Game::Hoi4 => &crate::hoi4::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1044 };
1045
1046 lowercase_datatype_set.get(&CaseInsensitiveStr(lookup_name)).map(|x| x.0)
1047}
1048
1049fn datatype_and_scope_map() -> &'static LazyLock<BiTigerHashMap<Datatype, Scopes>> {
1050 match Game::game() {
1051 #[cfg(feature = "ck3")]
1052 Game::Ck3 => &crate::ck3::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1053 #[cfg(feature = "vic3")]
1054 Game::Vic3 => &crate::vic3::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1055 #[cfg(feature = "imperator")]
1056 Game::Imperator => &crate::imperator::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1057 #[cfg(feature = "eu5")]
1058 Game::Eu5 => &crate::eu5::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1059 #[cfg(feature = "hoi4")]
1060 Game::Hoi4 => &crate::hoi4::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1061 }
1062}
1063
1064pub fn scope_from_datatype(dtype: Datatype) -> Option<Scopes> {
1067 datatype_and_scope_map().get_by_left(&dtype).copied()
1068}
1069
1070fn datatype_from_scopes(scopes: Scopes) -> Datatype {
1074 datatype_and_scope_map().get_by_right(&scopes).copied().unwrap_or(Datatype::Unknown)
1075}