tiger_lib/data/
portrait.rs

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        // TODO: could the root be Scopes::None here?
34        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"); // TODO
131    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                        // TODO: check that the chain eventually resolves to a block
223                        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"); // TODO
241        vd.field_value("torso"); // TODO
242    });
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"); // TODO
259            vd.field_value("torso"); // TODO
260        });
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            // This is a block in ck3, a value otherwise
339            vd.field("position_node"); // TODO
340
341            vd.field_list_numeric_exactly("look_at", 3);
342            // This is a block in ck3, a value otherwise
343            vd.field("look_at_node"); // TODO
344
345            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}