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 && let Some(declared_scope) = Scopes::from_snake_case(token.as_str())
129 && !scope.intersects(declared_scope)
130 {
131 warn(ErrorKey::Scopes)
132 .msg("SetRoot scope does not match scripted gui scope")
133 .loc(&chain.codes[1].name)
134 .loc_msg(token, "scripted gui scope here")
135 .push();
136 }
137 let mut sc = ScopeContext::new(scope, &code.name);
138 if ghw {
139 #[cfg(feature = "ck3")]
140 sc.define_name("great_holy_war", Scopes::GreatHolyWar, &chain.codes[0].name);
141 }
142
143 for code in chain.codes.iter().skip(2) {
145 if code.name.is("AddScope") {
146 if code.arguments.len() != 2 {
147 return;
149 }
150 let scope = if let CodeArg::Chain(chain) = &code.arguments[1] {
151 deduce_scope(chain, data, context_sc, dc)
152 } else {
153 Scopes::all()
154 };
155 match &code.arguments[0] {
156 CodeArg::Literal(name) => sc.define_name(name.as_str(), scope, name),
157 CodeArg::Chain(_) => sc.set_strict_scopes(false),
158 }
159 } else if !code.name.is("End") {
160 warn(ErrorKey::Gui).msg("expected AddScope or End").loc(&code.name).push();
161 return;
162 }
163 }
164 match code.name.as_str() {
165 "BuildTooltip" => {
166 if let Some(block) = block.get_field_block("is_valid") {
167 validate_trigger(block, data, &mut sc.clone(), Tooltipped::Yes);
168 }
169 if let Some(block) = block.get_field_block("effect") {
170 validate_effect(block, data, &mut sc, Tooltipped::Yes);
171 }
172 }
173 "Execute" => {
174 if let Some(block) = block.get_field_block("effect") {
175 validate_effect(block, data, &mut sc, Tooltipped::No);
176 } else {
177 err(ErrorKey::Gui)
178 .msg(format!("scripted gui `{key}` has no effect block"))
179 .loc(&code.name)
180 .loc_msg(key, "scripted gui here")
181 .push();
182 }
183 }
184 "ExecuteTooltip" => {
185 if let Some(block) = block.get_field_block("effect") {
186 validate_effect(block, data, &mut sc, Tooltipped::Yes);
187 } else {
188 warn(ErrorKey::Gui)
189 .msg(format!("scripted gui `{key}` has no effect block"))
190 .loc(&code.name)
191 .loc_msg(key, "scripted gui here")
192 .push();
193 }
194 }
195 "IsShown" => {
196 if let Some(block) = block.get_field_block("is_shown") {
197 validate_trigger(block, data, &mut sc, Tooltipped::No);
198 }
199 }
200 "IsShownTooltip" => {
201 if let Some(block) = block.get_field_block("is_shown") {
202 validate_trigger(block, data, &mut sc, Tooltipped::Yes);
203 }
204 }
205 "IsValid" => {
206 if let Some(block) = block.get_field_block("is_valid") {
207 validate_trigger(block, data, &mut sc, Tooltipped::No);
208 }
209 }
210 "IsValidTooltip" => {
211 if let Some(block) = block.get_field_block("is_valid") {
212 validate_trigger(block, data, &mut sc, Tooltipped::Yes);
213 }
214 }
215 _ => unreachable!(),
217 }
218 }
219 }
220}
221
222fn deduce_scope(
224 chain: &CodeChain,
225 data: &Everything,
226 context_sc: &mut ScopeContext,
227 dc: &DataContext,
228) -> Scopes {
229 if chain.codes.last().is_some_and(|code| code.name.is("MakeScope")) {
234 let chain = chain.without_last();
235 let rtype =
236 validate_datatypes(&chain, data, context_sc, dc, Datatype::Unknown, None, None, true);
237 scope_from_datatype(rtype).unwrap_or(Scopes::all())
238 } else {
239 Scopes::all()
240 }
241}