1use crate::block::Block;
2use crate::context::ScopeContext;
3use crate::datacontext::DataContext;
4use crate::datatype::{
5 Code, CodeArg, CodeChain, Datatype, scope_from_datatype, validate_datatypes,
6};
7use crate::db::{Db, DbKind};
8use crate::desc::validate_desc;
9use crate::effect::validate_effect;
10use crate::everything::Everything;
11use crate::game::{Game, GameFlags};
12use crate::item::{Item, ItemLoader};
13use crate::report::{ErrorKey, err, warn};
14use crate::scopes::Scopes;
15use crate::script_value::validate_non_dynamic_script_value;
16use crate::token::Token;
17use crate::tooltipped::Tooltipped;
18use crate::trigger::validate_trigger;
19use crate::validate::validate_modifiers_with_base;
20use crate::validator::Validator;
21
22#[derive(Clone, Debug)]
23pub struct ScriptedGui {}
24
25inventory::submit! {
26 ItemLoader::Normal(GameFlags::jomini(), Item::ScriptedGui, ScriptedGui::add)
27}
28
29impl ScriptedGui {
30 pub fn add(db: &mut Db, key: Token, block: Block) {
31 db.add(Item::ScriptedGui, key, block, Box::new(Self {}));
32 }
33}
34
35impl DbKind for ScriptedGui {
36 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
37 let mut vd = Validator::new(block, data);
38 let mut sc = ScopeContext::new(Scopes::None, key);
39 if let Some(token) = vd.field_value("scope") {
40 if let Some(scope) = Scopes::from_snake_case(token.as_str()) {
41 sc = ScopeContext::new(scope, token);
42 } else {
43 warn(ErrorKey::Scopes).msg("unknown scope type").loc(token).push();
44 }
45 }
46
47 vd.field_value("notification_key");
49 vd.field_validated_sc("confirm_title", &mut sc.clone(), validate_desc);
50 vd.field_validated_sc("confirm_text", &mut sc.clone(), validate_desc);
51 vd.field_trigger("ai_is_valid", Tooltipped::No, &mut sc.clone());
52 vd.field_validated_block_sc("ai_chance", &mut sc.clone(), validate_modifiers_with_base);
53 vd.field_validated("ai_frequency", validate_non_dynamic_script_value);
54
55 vd.field_validated_list("saved_scopes", |token, _| {
56 sc.define_name(token.as_str(), Scopes::all_but_none(), token);
57 });
58 sc.set_strict_scopes(false);
60 vd.field_trigger("is_shown", Tooltipped::No, &mut sc.clone());
61 vd.field_trigger("is_valid", Tooltipped::No, &mut sc.clone());
62 vd.field_effect("effect", Tooltipped::No, &mut sc.clone());
63 }
64}
65
66const KNOWN_SGUICALLS: &[&str] = &[
67 "BuildTooltip",
68 "Execute",
69 "ExecuteTooltip",
70 "IsValid",
71 "IsValidTooltip",
72 "IsShown",
73 "IsShownTooltip",
74];
75
76impl ScriptedGui {
77 #[allow(clippy::unused_self)] pub fn validate_guicall(
79 &self,
80 key: &Token,
81 block: &Block,
82 data: &Everything,
83 context_sc: &mut ScopeContext,
84 dc: &DataContext,
85 code: &Code,
86 ) {
87 if !KNOWN_SGUICALLS.contains(&code.name.as_str()) || code.arguments.len() != 1 {
88 return;
89 }
90 if let CodeArg::Chain(chain) = &code.arguments[0] {
91 if chain.codes.len() < 2 {
92 warn(ErrorKey::Gui)
93 .msg("expected GuiScope.SetRoot in argument")
94 .loc(&code.name)
95 .push();
96 return;
97 }
98
99 let ghw = Game::is_ck3()
100 && chain.codes[0].name.is("GreatHolyWarWindow")
101 && chain.codes[1].name.is("GetScope");
102
103 if !ghw {
104 if !chain.codes[0].name.is("GuiScope") {
105 warn(ErrorKey::Gui).msg("expected GuiScope").loc(&chain.codes[0].name).push();
106 return;
107 }
108 if !chain.codes[1].name.is("SetRoot") {
109 warn(ErrorKey::Gui).msg("expected SetRoot").loc(&chain.codes[1].name).push();
110 return;
111 }
112 if chain.codes[1].arguments.len() != 1 {
113 return;
115 }
116 }
117 let scope = if ghw {
119 Scopes::Character
120 } else if let CodeArg::Chain(chain) = &chain.codes[1].arguments[0] {
121 deduce_scope(chain, data, context_sc, dc)
122 } else {
123 return;
125 };
126 if let Some(token) = block.get_field_value("scope") {
128 if let Some(declared_scope) = Scopes::from_snake_case(token.as_str()) {
129 if !scope.intersects(declared_scope) {
130 warn(ErrorKey::Scopes)
131 .msg("SetRoot scope does not match scripted gui scope")
132 .loc(&chain.codes[1].name)
133 .loc_msg(token, "scripted gui scope here")
134 .push();
135 }
136 }
137 }
138 let mut sc = ScopeContext::new(scope, &code.name);
139 if ghw {
140 #[cfg(feature = "ck3")]
141 sc.define_name("great_holy_war", Scopes::GreatHolyWar, &chain.codes[0].name);
142 }
143
144 for code in chain.codes.iter().skip(2) {
146 if code.name.is("AddScope") {
147 if code.arguments.len() != 2 {
148 return;
150 }
151 let scope = if let CodeArg::Chain(chain) = &code.arguments[1] {
152 deduce_scope(chain, data, context_sc, dc)
153 } else {
154 Scopes::all()
155 };
156 match &code.arguments[0] {
157 CodeArg::Literal(name) => sc.define_name(name.as_str(), scope, name),
158 CodeArg::Chain(_) => sc.set_strict_scopes(false),
159 }
160 } else if !code.name.is("End") {
161 warn(ErrorKey::Gui).msg("expected AddScope or End").loc(&code.name).push();
162 return;
163 }
164 }
165 match code.name.as_str() {
166 "BuildTooltip" => {
167 if let Some(block) = block.get_field_block("is_valid") {
168 validate_trigger(block, data, &mut sc.clone(), Tooltipped::Yes);
169 }
170 if let Some(block) = block.get_field_block("effect") {
171 validate_effect(block, data, &mut sc, Tooltipped::Yes);
172 }
173 }
174 "Execute" => {
175 if let Some(block) = block.get_field_block("effect") {
176 validate_effect(block, data, &mut sc, Tooltipped::No);
177 } else {
178 err(ErrorKey::Gui)
179 .msg(format!("scripted gui `{key}` has no effect block"))
180 .loc(&code.name)
181 .loc_msg(key, "scripted gui here")
182 .push();
183 }
184 }
185 "ExecuteTooltip" => {
186 if let Some(block) = block.get_field_block("effect") {
187 validate_effect(block, data, &mut sc, Tooltipped::Yes);
188 } else {
189 warn(ErrorKey::Gui)
190 .msg(format!("scripted gui `{key}` has no effect block"))
191 .loc(&code.name)
192 .loc_msg(key, "scripted gui here")
193 .push();
194 }
195 }
196 "IsShown" => {
197 if let Some(block) = block.get_field_block("is_shown") {
198 validate_trigger(block, data, &mut sc, Tooltipped::No);
199 }
200 }
201 "IsShownTooltip" => {
202 if let Some(block) = block.get_field_block("is_shown") {
203 validate_trigger(block, data, &mut sc, Tooltipped::Yes);
204 }
205 }
206 "IsValid" => {
207 if let Some(block) = block.get_field_block("is_valid") {
208 validate_trigger(block, data, &mut sc, Tooltipped::No);
209 }
210 }
211 "IsValidTooltip" => {
212 if let Some(block) = block.get_field_block("is_valid") {
213 validate_trigger(block, data, &mut sc, Tooltipped::Yes);
214 }
215 }
216 _ => unreachable!(),
218 }
219 }
220 }
221}
222
223fn deduce_scope(
225 chain: &CodeChain,
226 data: &Everything,
227 context_sc: &mut ScopeContext,
228 dc: &DataContext,
229) -> Scopes {
230 if chain.codes.last().is_some_and(|code| code.name.is("MakeScope")) {
235 let chain = chain.without_last();
236 let rtype =
237 validate_datatypes(&chain, data, context_sc, dc, Datatype::Unknown, None, None, true);
238 scope_from_datatype(rtype).unwrap_or(Scopes::all())
239 } else {
240 Scopes::all()
241 }
242}