1use crate::block::{BV, Block};
2use crate::context::ScopeContext;
3use crate::data::genes::{AccessoryGene, Gene};
4use crate::db::{Db, DbKind};
5use crate::everything::Everything;
6use crate::game::{Game, GameFlags};
7use crate::item::{Item, ItemLoader, LoadAsFile, Recursive};
8use crate::pdxfile::PdxEncoding;
9use crate::report::{Confidence, ErrorKey, Severity, err, warn};
10use crate::scopes::Scopes;
11use crate::token::Token;
12use crate::tooltipped::Tooltipped;
13use crate::validate::{validate_modifiers_with_base, validate_numeric_range};
14use crate::validator::Validator;
15
16#[derive(Clone, Debug)]
17pub struct PortraitModifierGroup {}
18
19inventory::submit! {
20 ItemLoader::Normal(GameFlags::jomini(), Item::PortraitModifierGroup, PortraitModifierGroup::add)
21}
22
23impl PortraitModifierGroup {
24 pub fn add(db: &mut Db, key: Token, block: Block) {
25 db.add(Item::PortraitModifierGroup, key, block, Box::new(Self {}));
26 }
27}
28
29impl DbKind for PortraitModifierGroup {
30 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
31 let mut vd = Validator::new(block, data);
32
33 let mut sc = ScopeContext::new(Scopes::Character, key);
35 sc.define_name("age", Scopes::Value, key);
36 sc.define_name("culture", Scopes::Culture, key);
37 sc.define_name("current_weight", Scopes::Value, key);
38 #[cfg(feature = "ck3")]
39 if Game::is_ck3() {
40 sc.define_name("highest_held_title_tier", Scopes::Value, key);
41 sc.define_name("faith", Scopes::Faith, key);
42 sc.define_name("government", Scopes::GovernmentType, key);
43 sc.define_name("prowess", Scopes::Value, key);
44 sc.define_name("ruler_designer", Scopes::Bool, key);
45 }
46 sc.define_name("female", Scopes::Bool, key);
47 sc.define_name("weight_for_portrait", Scopes::Value, key);
48 sc.define_name("year_of_birth", Scopes::Value, key);
49
50 vd.field_choice("usage", &["customization", "game", "both", "none"]);
51 vd.field_integer("interface_position");
52 vd.field_integer("priority");
53 vd.field_value("user_data");
54 vd.field_choice("selection_behavior", &["weighted_random", "max"]);
55
56 let mut caller = key.as_str();
57 if let Some(token) = block.get_field_value("usage")
58 && (token.is("game") || token.is("none"))
59 {
60 caller = "";
61 }
62
63 if !Game::is_imperator() && !caller.is_empty() {
64 let loca = format!("PORTRAIT_MODIFIER_{key}");
65 data.verify_exists_implied(Item::Localization, &loca, key);
66 }
67
68 if let Some(token) = vd.field_value("fallback")
69 && !block.has_key(token.as_str())
70 {
71 let msg = "portrait modifier not defined";
72 warn(ErrorKey::MissingItem).msg(msg).loc(token).push();
73 }
74 vd.multi_field_validated_block("add_accessory_modifiers", |block, data| {
75 validate_add_accessory_modifiers(block, data, caller, &mut sc);
76 });
77 vd.unknown_block_fields(|key, block| {
78 validate_portrait_modifier(key, block, data, caller, &mut sc);
79 });
80 }
81
82 fn has_property(&self, _key: &Token, block: &Block, property: &str, data: &Everything) -> bool {
83 if property == "fallback" || property == "add_accessory_modifiers" {
84 false
85 } else if block.get_field_block(property).is_some() {
86 true
87 } else {
88 for block in block.get_field_blocks("add_accessory_modifiers") {
89 if let Some(gene) = block.get_field_value("gene")
90 && let Some(template) = block.get_field_value("template")
91 && let Some((key, block)) =
92 data.get_key_block(Item::GeneCategory, gene.as_str())
93 && AccessoryGene::has_template_setting(
94 key,
95 block,
96 data,
97 template.as_str(),
98 property,
99 )
100 {
101 return true;
102 }
103 }
104 false
105 }
106 }
107}
108
109fn validate_portrait_modifier(
110 key: &Token,
111 block: &Block,
112 data: &Everything,
113 mut caller: &str,
114 sc: &mut ScopeContext,
115) {
116 let mut vd = Validator::new(block, data);
117 vd.field_choice("usage", &["customization", "game", "both"]);
118 if let Some(token) = block.get_field_value("usage")
119 && token.is("game")
120 {
121 caller = "";
122 }
123 if !Game::is_imperator() && !caller.is_empty() {
124 let loca = format!("PORTRAIT_MODIFIER_{caller}_{key}");
125 data.verify_exists_implied(Item::Localization, &loca, key);
126 }
127 vd.field_list("outfit_tags"); vd.field_bool("require_outfit_tags");
129 vd.field_bool("ignore_outfit_tags");
130
131 vd.field_trigger("is_valid_custom", Tooltipped::No, sc);
132
133 vd.multi_field_validated_block("dna_modifiers", validate_dna_modifiers);
134
135 #[cfg(feature = "vic3")]
136 if Game::is_vic3() {
137 sc.define_name("character", Scopes::Character, key);
138 sc.define_name("pop", Scopes::Pop, key);
139 }
140 vd.multi_field_validated_block_sc("weight", sc, validate_modifiers_with_base);
141
142 #[cfg(feature = "ck3")]
143 if Game::is_ck3() {
144 vd.field_bool("skip_if_overridden");
145 }
146}
147
148fn validate_add_accessory_modifiers(
149 block: &Block,
150 data: &Everything,
151 caller: &str,
152 sc: &mut ScopeContext,
153) {
154 let mut vd = Validator::new(block, data);
155 vd.field_item("gene", Item::GeneCategory);
156 if let Some(category) = block.get_field_value("gene")
157 && let Some(template) = vd.field_value("template")
158 {
159 Gene::verify_has_template(category.as_str(), template, data);
160 if !caller.is_empty() {
161 data.database.validate_property_use(
162 Item::GeneCategory,
163 category,
164 data,
165 template,
166 caller,
167 );
168 }
169 }
170 vd.field_trigger("is_valid_custom", Tooltipped::No, sc);
171 vd.multi_field_validated_block_sc("weight", sc, validate_modifiers_with_base);
172}
173
174#[derive(Clone, Debug)]
175pub struct PortraitAnimation {}
176
177inventory::submit! {
178 ItemLoader::Normal(GameFlags::all(), Item::PortraitAnimation, PortraitAnimation::add)
179}
180
181impl PortraitAnimation {
182 pub fn add(db: &mut Db, key: Token, block: Block) {
183 db.add(Item::PortraitAnimation, key, block, Box::new(Self {}));
184 }
185}
186
187const BODY_TYPES: &[&str] = &[
188 "male",
189 "female",
190 "boy",
191 "girl",
192 #[cfg(any(feature = "vic3", feature = "eu5"))]
193 "adolescent_boy",
194 #[cfg(any(feature = "vic3", feature = "eu5"))]
195 "adolescent_girl",
196 #[cfg(any(feature = "vic3", feature = "imperator", feature = "eu5"))]
197 "infant",
198];
199
200impl DbKind for PortraitAnimation {
201 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
202 let mut vd = Validator::new(block, data);
203 data.verify_exists(Item::Localization, key);
204
205 if Game::is_ck3() {
206 vd.field_validated_block("default", validate_animation);
207 }
208 let has_default = Game::is_ck3() && block.has_key("default");
209
210 vd.field_bool("barbershop");
211
212 for field in BODY_TYPES {
213 if !has_default {
214 vd.req_field(field);
215 }
216 vd.field_validated(field, |bv, data| {
217 match bv {
218 BV::Value(token) => {
219 if !BODY_TYPES.contains(&token.as_str()) {
221 warn(ErrorKey::Validation).msg("unknown body type").loc(token).push();
222 }
223 }
224 BV::Block(block) => {
225 validate_animation(block, data);
226 }
227 }
228 });
229 }
230 }
231}
232
233fn validate_animation(block: &Block, data: &Everything) {
234 let mut vd = Validator::new(block, data);
235 vd.field_validated_block("default", |block, data| {
236 let mut vd = Validator::new(block, data);
237 vd.field_value("head"); vd.field_value("torso"); });
240
241 if Game::is_ck3() {
242 vd.field_bool("force");
243 }
244
245 vd.multi_field_validated_block("portrait_modifier", |block, data| {
246 let mut vd = Validator::new(block, data);
247 vd.field_trigger_rooted("trigger", Tooltipped::No, Scopes::Character);
248 validate_portrait_modifiers(block, data, vd);
249 });
250
251 vd.unknown_block_fields(|_, block| {
252 let mut vd = Validator::new(block, data);
253 vd.field_validated_block("animation", |block, data| {
254 let mut vd = Validator::new(block, data);
255 vd.field_value("head"); vd.field_value("torso"); });
258 vd.field_validated_key_block("weight", |key, block, data| {
259 let mut sc = ScopeContext::new(Scopes::Character, key);
260 sc.define_name("age", Scopes::Value, key);
261 sc.define_name("culture", Scopes::Culture, key);
262 sc.define_name("current_weight", Scopes::Value, key);
263 sc.define_name("ai_boldness", Scopes::Value, key);
264 sc.define_name("ai_compassion", Scopes::Value, key);
265 sc.define_name("ai_greed", Scopes::Value, key);
266 sc.define_name("ai_honor", Scopes::Value, key);
267 sc.define_name("ai_rationality", Scopes::Value, key);
268 sc.define_name("ai_vengefulness", Scopes::Value, key);
269 sc.define_name("ai_zeal", Scopes::Value, key);
270 validate_modifiers_with_base(block, data, &mut sc);
271 });
272 vd.field_validated_block("portrait_modifier", |block, data| {
273 let vd = Validator::new(block, data);
274 validate_portrait_modifiers(block, data, vd);
275 });
276 vd.field_item("portrait_modifier_pack", Item::PortraitModifierPack);
277 });
278}
279
280#[derive(Clone, Debug)]
281pub struct PortraitModifierPack {}
282
283inventory::submit! {
284 ItemLoader::Full(GameFlags::all(), Item::PortraitModifierPack, PdxEncoding::Utf8Bom, ".modifierpack", LoadAsFile::No, Recursive::No, PortraitModifierPack::add)
285}
286
287impl PortraitModifierPack {
288 pub fn add(db: &mut Db, key: Token, block: Block) {
289 db.add(Item::PortraitModifierPack, key, block, Box::new(Self {}));
290 }
291}
292
293impl DbKind for PortraitModifierPack {
294 fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
295 let mut vd = Validator::new(block, data);
296
297 vd.multi_field_validated_block("portrait_modifier", |block, data| {
298 let mut vd = Validator::new(block, data);
299 vd.field_trigger_rooted("trigger", Tooltipped::No, Scopes::Character);
300 validate_portrait_modifiers(block, data, vd);
301 });
302 }
303}
304
305fn validate_portrait_modifiers(_block: &Block, data: &Everything, mut vd: Validator) {
306 vd.unknown_value_fields(|key, value| {
307 data.verify_exists(Item::PortraitModifierGroup, key);
308 if !data.item_has_property(Item::PortraitModifierGroup, key.as_str(), value.as_str()) {
309 let msg = format!("portrait modifier {value} not found in group {key}");
310 err(ErrorKey::MissingItem).msg(msg).loc(value).push();
311 }
312 });
313}
314
315#[derive(Clone, Debug)]
316pub struct PortraitCamera {}
317
318inventory::submit! {
319 ItemLoader::Normal(GameFlags::all(), Item::PortraitCamera, PortraitCamera::add)
320}
321
322impl PortraitCamera {
323 pub fn add(db: &mut Db, key: Token, block: Block) {
324 db.add(Item::PortraitCamera, key, block, Box::new(Self {}));
325 }
326}
327
328impl DbKind for PortraitCamera {
329 fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
330 let mut vd = Validator::new(block, data);
331
332 vd.field_validated_block("camera", |block, data| {
333 let mut vd = Validator::new(block, data);
334 vd.field_list_numeric_exactly("position", 3);
335 vd.field("position_node"); vd.field_list_numeric_exactly("look_at", 3);
339 vd.field("look_at_node"); vd.field_numeric("fov");
343 vd.field_list_numeric_exactly("camera_near_far", 2);
344 });
345
346 if let Some(token) = vd.field_value("unknown") {
347 if token.as_str().contains('/') {
348 data.verify_exists(Item::File, token);
349 } else {
350 data.verify_exists(Item::PortraitCamera, token);
351 }
352 }
353 }
354}
355
356pub fn validate_dna_modifiers(block: &Block, data: &Everything) {
357 let mut vd = Validator::new(block, data);
358
359 #[cfg(feature = "imperator")]
360 let modes = &["add", "replace", "modify", "replace_template"];
361 #[cfg(not(feature = "imperator"))]
362 let modes = &["add", "replace", "modify", "modify_multiply"];
363
364 vd.multi_field_validated_block("morph", |block, data| {
365 let mut vd = Validator::new(block, data);
366 vd.field_choice("mode", modes);
367 vd.field_item("gene", Item::GeneCategory);
368 if let Some(category) = block.get_field_value("gene")
369 && let Some(template) = vd.field_value("template")
370 {
371 Gene::verify_has_template(category.as_str(), template, data);
372 }
373 vd.field_script_value_no_breakdown_rooted("value", Scopes::Character);
374 vd.field_validated_block("range", |block, data| {
375 validate_numeric_range(block, data, 0.0, 1.0, Severity::Warning, Confidence::Weak);
376 });
377 });
378 vd.multi_field_validated_block("color", |block, data| {
379 let mut vd = Validator::new(block, data);
380 vd.field_choice("mode", modes);
381 vd.field_item("gene", Item::GeneCategory);
382 vd.field_script_value_rooted("x", Scopes::Character);
383 vd.field_script_value_rooted("y", Scopes::Character);
384 });
385 vd.multi_field_validated_block("accessory", |block, data| {
386 let mut vd = Validator::new(block, data);
387 vd.field_choice("mode", modes);
388 vd.field_item("gene", Item::GeneCategory);
389 if let Some(category) = block.get_field_value("gene")
390 && let Some(template) = vd.field_value("template")
391 {
392 Gene::verify_has_template(category.as_str(), template, data);
393 }
394 vd.field_script_value_no_breakdown_rooted("value", Scopes::Character);
395 vd.field_validated_block("range", |block, data| {
396 validate_numeric_range(block, data, 0.0, 1.0, Severity::Warning, Confidence::Weak);
397 });
398 vd.field_item("accessory", Item::Accessory);
399 vd.field_choice("type", &["male", "female", "boy", "girl"]);
400 });
401}