Skip to main content

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            && (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"); // TODO
128    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                        // TODO: check that the chain eventually resolves to a block
220                        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"); // TODO
238        vd.field_value("torso"); // TODO
239    });
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"); // TODO
256            vd.field_value("torso"); // TODO
257        });
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            // This is a block in ck3, a value otherwise
336            vd.field("position_node"); // TODO
337
338            vd.field_list_numeric_exactly("look_at", 3);
339            // This is a block in ck3, a value otherwise
340            vd.field("look_at_node"); // TODO
341
342            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}