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 if token.is("game") || token.is("none") {
59 caller = "";
60 }
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 if !block.has_key(token.as_str()) {
70 let msg = "portrait modifier not defined";
71 warn(ErrorKey::MissingItem).msg(msg).loc(token).push();
72 }
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 if let Some(template) = block.get_field_value("template") {
91 if let Some((key, block)) =
92 data.get_key_block(Item::GeneCategory, gene.as_str())
93 {
94 if AccessoryGene::has_template_setting(
95 key,
96 block,
97 data,
98 template.as_str(),
99 property,
100 ) {
101 return true;
102 }
103 }
104 }
105 }
106 }
107 false
108 }
109 }
110}
111
112fn validate_portrait_modifier(
113 key: &Token,
114 block: &Block,
115 data: &Everything,
116 mut caller: &str,
117 sc: &mut ScopeContext,
118) {
119 let mut vd = Validator::new(block, data);
120 vd.field_choice("usage", &["customization", "game", "both"]);
121 if let Some(token) = block.get_field_value("usage") {
122 if token.is("game") {
123 caller = "";
124 }
125 }
126 if !Game::is_imperator() && !caller.is_empty() {
127 let loca = format!("PORTRAIT_MODIFIER_{caller}_{key}");
128 data.verify_exists_implied(Item::Localization, &loca, key);
129 }
130 vd.field_list("outfit_tags"); vd.field_bool("require_outfit_tags");
132 vd.field_bool("ignore_outfit_tags");
133
134 vd.field_trigger("is_valid_custom", Tooltipped::No, sc);
135
136 vd.multi_field_validated_block("dna_modifiers", validate_dna_modifiers);
137
138 #[cfg(feature = "vic3")]
139 if Game::is_vic3() {
140 sc.define_name("character", Scopes::Character, key);
141 sc.define_name("pop", Scopes::Pop, key);
142 }
143 vd.multi_field_validated_block_sc("weight", sc, validate_modifiers_with_base);
144
145 #[cfg(feature = "ck3")]
146 if Game::is_ck3() {
147 vd.field_bool("skip_if_overridden");
148 }
149}
150
151fn validate_add_accessory_modifiers(
152 block: &Block,
153 data: &Everything,
154 caller: &str,
155 sc: &mut ScopeContext,
156) {
157 let mut vd = Validator::new(block, data);
158 vd.field_item("gene", Item::GeneCategory);
159 if let Some(category) = block.get_field_value("gene") {
160 if let Some(template) = vd.field_value("template") {
161 Gene::verify_has_template(category.as_str(), template, data);
162 if !caller.is_empty() {
163 data.database.validate_property_use(
164 Item::GeneCategory,
165 category,
166 data,
167 template,
168 caller,
169 );
170 }
171 }
172 }
173 vd.field_trigger("is_valid_custom", Tooltipped::No, sc);
174 vd.multi_field_validated_block_sc("weight", sc, validate_modifiers_with_base);
175}
176
177#[derive(Clone, Debug)]
178pub struct PortraitAnimation {}
179
180inventory::submit! {
181 ItemLoader::Normal(GameFlags::all(), Item::PortraitAnimation, PortraitAnimation::add)
182}
183
184impl PortraitAnimation {
185 pub fn add(db: &mut Db, key: Token, block: Block) {
186 db.add(Item::PortraitAnimation, key, block, Box::new(Self {}));
187 }
188}
189
190const BODY_TYPES: &[&str] = &[
191 "male",
192 "female",
193 "boy",
194 "girl",
195 #[cfg(any(feature = "vic3", feature = "eu5"))]
196 "adolescent_boy",
197 #[cfg(any(feature = "vic3", feature = "eu5"))]
198 "adolescent_girl",
199 #[cfg(any(feature = "vic3", feature = "imperator", feature = "eu5"))]
200 "infant",
201];
202
203impl DbKind for PortraitAnimation {
204 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
205 let mut vd = Validator::new(block, data);
206 data.verify_exists(Item::Localization, key);
207
208 if Game::is_ck3() {
209 vd.field_validated_block("default", validate_animation);
210 }
211 let has_default = Game::is_ck3() && block.has_key("default");
212
213 vd.field_bool("barbershop");
214
215 for field in BODY_TYPES {
216 if !has_default {
217 vd.req_field(field);
218 }
219 vd.field_validated(field, |bv, data| {
220 match bv {
221 BV::Value(token) => {
222 if !BODY_TYPES.contains(&token.as_str()) {
224 warn(ErrorKey::Validation).msg("unknown body type").loc(token).push();
225 }
226 }
227 BV::Block(block) => {
228 validate_animation(block, data);
229 }
230 }
231 });
232 }
233 }
234}
235
236fn validate_animation(block: &Block, data: &Everything) {
237 let mut vd = Validator::new(block, data);
238 vd.field_validated_block("default", |block, data| {
239 let mut vd = Validator::new(block, data);
240 vd.field_value("head"); vd.field_value("torso"); });
243
244 if Game::is_ck3() {
245 vd.field_bool("force");
246 }
247
248 vd.multi_field_validated_block("portrait_modifier", |block, data| {
249 let mut vd = Validator::new(block, data);
250 vd.field_trigger_rooted("trigger", Tooltipped::No, Scopes::Character);
251 validate_portrait_modifiers(block, data, vd);
252 });
253
254 vd.unknown_block_fields(|_, block| {
255 let mut vd = Validator::new(block, data);
256 vd.field_validated_block("animation", |block, data| {
257 let mut vd = Validator::new(block, data);
258 vd.field_value("head"); vd.field_value("torso"); });
261 vd.field_validated_key_block("weight", |key, block, data| {
262 let mut sc = ScopeContext::new(Scopes::Character, key);
263 sc.define_name("age", Scopes::Value, key);
264 sc.define_name("culture", Scopes::Culture, key);
265 sc.define_name("current_weight", Scopes::Value, key);
266 sc.define_name("ai_boldness", Scopes::Value, key);
267 sc.define_name("ai_compassion", Scopes::Value, key);
268 sc.define_name("ai_greed", Scopes::Value, key);
269 sc.define_name("ai_honor", Scopes::Value, key);
270 sc.define_name("ai_rationality", Scopes::Value, key);
271 sc.define_name("ai_vengefulness", Scopes::Value, key);
272 sc.define_name("ai_zeal", Scopes::Value, key);
273 validate_modifiers_with_base(block, data, &mut sc);
274 });
275 vd.field_validated_block("portrait_modifier", |block, data| {
276 let vd = Validator::new(block, data);
277 validate_portrait_modifiers(block, data, vd);
278 });
279 vd.field_item("portrait_modifier_pack", Item::PortraitModifierPack);
280 });
281}
282
283#[derive(Clone, Debug)]
284pub struct PortraitModifierPack {}
285
286inventory::submit! {
287 ItemLoader::Full(GameFlags::all(), Item::PortraitModifierPack, PdxEncoding::Utf8Bom, ".modifierpack", LoadAsFile::No, Recursive::No, PortraitModifierPack::add)
288}
289
290impl PortraitModifierPack {
291 pub fn add(db: &mut Db, key: Token, block: Block) {
292 db.add(Item::PortraitModifierPack, key, block, Box::new(Self {}));
293 }
294}
295
296impl DbKind for PortraitModifierPack {
297 fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
298 let mut vd = Validator::new(block, data);
299
300 vd.multi_field_validated_block("portrait_modifier", |block, data| {
301 let mut vd = Validator::new(block, data);
302 vd.field_trigger_rooted("trigger", Tooltipped::No, Scopes::Character);
303 validate_portrait_modifiers(block, data, vd);
304 });
305 }
306}
307
308fn validate_portrait_modifiers(_block: &Block, data: &Everything, mut vd: Validator) {
309 vd.unknown_value_fields(|key, value| {
310 data.verify_exists(Item::PortraitModifierGroup, key);
311 if !data.item_has_property(Item::PortraitModifierGroup, key.as_str(), value.as_str()) {
312 let msg = format!("portrait modifier {value} not found in group {key}");
313 err(ErrorKey::MissingItem).msg(msg).loc(value).push();
314 }
315 });
316}
317
318#[derive(Clone, Debug)]
319pub struct PortraitCamera {}
320
321inventory::submit! {
322 ItemLoader::Normal(GameFlags::all(), Item::PortraitCamera, PortraitCamera::add)
323}
324
325impl PortraitCamera {
326 pub fn add(db: &mut Db, key: Token, block: Block) {
327 db.add(Item::PortraitCamera, key, block, Box::new(Self {}));
328 }
329}
330
331impl DbKind for PortraitCamera {
332 fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
333 let mut vd = Validator::new(block, data);
334
335 vd.field_validated_block("camera", |block, data| {
336 let mut vd = Validator::new(block, data);
337 vd.field_list_numeric_exactly("position", 3);
338 vd.field("position_node"); vd.field_list_numeric_exactly("look_at", 3);
342 vd.field("look_at_node"); vd.field_numeric("fov");
346 vd.field_list_numeric_exactly("camera_near_far", 2);
347 });
348
349 if let Some(token) = vd.field_value("unknown") {
350 if token.as_str().contains('/') {
351 data.verify_exists(Item::File, token);
352 } else {
353 data.verify_exists(Item::PortraitCamera, token);
354 }
355 }
356 }
357}
358
359pub fn validate_dna_modifiers(block: &Block, data: &Everything) {
360 let mut vd = Validator::new(block, data);
361
362 #[cfg(feature = "imperator")]
363 let modes = &["add", "replace", "modify", "replace_template"];
364 #[cfg(not(feature = "imperator"))]
365 let modes = &["add", "replace", "modify", "modify_multiply"];
366
367 vd.multi_field_validated_block("morph", |block, data| {
368 let mut vd = Validator::new(block, data);
369 vd.field_choice("mode", modes);
370 vd.field_item("gene", Item::GeneCategory);
371 if let Some(category) = block.get_field_value("gene") {
372 if let Some(template) = vd.field_value("template") {
373 Gene::verify_has_template(category.as_str(), template, data);
374 }
375 }
376 vd.field_script_value_no_breakdown_rooted("value", Scopes::Character);
377 vd.field_validated_block("range", |block, data| {
378 validate_numeric_range(block, data, 0.0, 1.0, Severity::Warning, Confidence::Weak);
379 });
380 });
381 vd.multi_field_validated_block("color", |block, data| {
382 let mut vd = Validator::new(block, data);
383 vd.field_choice("mode", modes);
384 vd.field_item("gene", Item::GeneCategory);
385 vd.field_script_value_rooted("x", Scopes::Character);
386 vd.field_script_value_rooted("y", Scopes::Character);
387 });
388 vd.multi_field_validated_block("accessory", |block, data| {
389 let mut vd = Validator::new(block, data);
390 vd.field_choice("mode", modes);
391 vd.field_item("gene", Item::GeneCategory);
392 if let Some(category) = block.get_field_value("gene") {
393 if let Some(template) = vd.field_value("template") {
394 Gene::verify_has_template(category.as_str(), template, data);
395 }
396 }
397 vd.field_script_value_no_breakdown_rooted("value", Scopes::Character);
398 vd.field_validated_block("range", |block, data| {
399 validate_numeric_range(block, data, 0.0, 1.0, Severity::Warning, Confidence::Weak);
400 });
401 vd.field_item("accessory", Item::Accessory);
402 vd.field_choice("type", &["male", "female", "boy", "girl"]);
403 });
404}