Skip to main content

tiger_lib/data/
genes.rs

1use crate::block::{BV, Block};
2use crate::db::{Db, DbKind};
3use crate::everything::Everything;
4use crate::game::{Game, GameFlags};
5use crate::helpers::{TigerHashSet, dup_error};
6use crate::item::{Item, ItemLoader};
7#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
8use crate::report::{Confidence, Severity};
9use crate::report::{ErrorKey, err, fatal, warn};
10use crate::token::Token;
11#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
12use crate::validate::validate_numeric_range;
13use crate::validator::Validator;
14
15const BODY_TYPES: &[&str] = &[
16    "male",
17    "female",
18    "boy",
19    "girl",
20    #[cfg(feature = "eu5")]
21    "adolescent_boy",
22    #[cfg(feature = "eu5")]
23    "adolescent_girl",
24    #[cfg(any(feature = "imperator", feature = "eu5"))]
25    "infant",
26];
27
28#[derive(Clone, Debug)]
29pub struct Gene {}
30
31inventory::submit! {
32    ItemLoader::Normal(GameFlags::jomini(), Item::GeneCategory, Gene::add)
33}
34
35impl Gene {
36    pub fn add(db: &mut Db, key: Token, mut block: Block) {
37        match key.as_str() {
38            "color_genes" => {
39                for (k, b) in block.drain_definitions_warn() {
40                    ColorGene::add(db, k, b);
41                }
42            }
43            "age_presets" => {
44                for (k, b) in block.drain_definitions_warn() {
45                    AgePresetGene::add(db, k, b);
46                }
47            }
48            "decal_atlases" => {
49                for (_k, _b) in block.drain_definitions_warn() {
50                    // TODO: no examples in vanilla
51                }
52            }
53            "morph_genes" => {
54                for (k, b) in block.drain_definitions_warn() {
55                    MorphGene::add(db, k, b, false);
56                }
57            }
58            "accessory_genes" => {
59                for (k, b) in block.drain_definitions_warn() {
60                    AccessoryGene::add(db, k, b);
61                }
62            }
63            "special_genes" => {
64                for (k, mut b) in block.drain_definitions_warn() {
65                    match k.as_str() {
66                        "morph_genes" => {
67                            for (k, b) in b.drain_definitions_warn() {
68                                MorphGene::add(db, k, b, true);
69                            }
70                        }
71                        "accessory_genes" => {
72                            for (k, b) in b.drain_definitions_warn() {
73                                AccessoryGene::add(db, k, b);
74                            }
75                        }
76                        _ => warn(ErrorKey::ParseError).msg("unknown gene type").loc(k).push(),
77                    }
78                }
79            }
80            _ => warn(ErrorKey::ParseError).msg("unknown gene type").loc(key).push(),
81        }
82    }
83
84    pub fn verify_has_template(category: &str, template: &Token, data: &Everything) {
85        if !data.item_has_property(Item::GeneCategory, category, template.as_str()) {
86            let msg = format!("gene {category} does not have template {template}");
87            err(ErrorKey::MissingItem).msg(msg).loc(template).push();
88        }
89    }
90}
91
92#[derive(Clone, Debug)]
93pub struct ColorGene {}
94
95impl ColorGene {
96    pub fn add(db: &mut Db, key: Token, block: Block) {
97        db.add(Item::GeneCategory, key, block, Box::new(Self {}));
98    }
99}
100
101impl DbKind for ColorGene {
102    #[allow(unused_variables)] // vic3 does not use key
103    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
104        let mut vd = Validator::new(block, data);
105        if Game::is_ck3() {
106            data.verify_exists(Item::Localization, key);
107        }
108
109        if Game::is_ck3() {
110            vd.req_field("group");
111        }
112
113        if Game::is_imperator() {
114            vd.req_field("index");
115            vd.field_integer("index");
116            vd.field_value("max_blend");
117        }
118
119        vd.req_field("color");
120        #[cfg(any(feature = "ck3", feature = "vic3"))]
121        vd.req_field("blend_range");
122
123        vd.field_item("sync_inheritance_with", Item::GeneCategory);
124        vd.field_value("group"); // TODO
125        vd.field_value("color"); // TODO
126
127        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
128        vd.field_validated_block("blend_range", |block, data| {
129            validate_numeric_range(block, data, 0.0, 1.0, Severity::Warning, Confidence::Weak);
130        });
131    }
132
133    fn validate_use(
134        &self,
135        _key: &Token,
136        _block: &Block,
137        data: &Everything,
138        _call_key: &Token,
139        call_block: &Block,
140    ) {
141        let mut vd = Validator::new(call_block, data);
142        vd.req_tokens_numbers_exactly(4);
143    }
144}
145
146#[derive(Clone, Debug)]
147pub struct AgePresetGene {}
148
149impl AgePresetGene {
150    pub fn add(db: &mut Db, key: Token, block: Block) {
151        db.add(Item::GeneAgePreset, key, block, Box::new(Self {}));
152    }
153}
154
155impl DbKind for AgePresetGene {
156    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
157        validate_age(block, data);
158    }
159
160    fn validate_use(
161        &self,
162        _key: &Token,
163        _block: &Block,
164        _data: &Everything,
165        call_key: &Token,
166        _call_block: &Block,
167    ) {
168        warn(ErrorKey::Validation).msg("cannot define age preset genes").loc(call_key).push();
169    }
170}
171
172#[derive(Clone, Debug)]
173pub struct MorphGene {
174    special_gene: bool,
175    templates: TigerHashSet<Token>,
176}
177
178impl MorphGene {
179    pub fn add(db: &mut Db, key: Token, block: Block, special_gene: bool) {
180        let mut templates = TigerHashSet::default();
181        for (key, _block) in block.iter_definitions() {
182            if key.is("ugliness_feature_categories") {
183                continue;
184            }
185            if let Some(other) = templates.get(key.as_str()) {
186                dup_error(key, other, "morph gene template");
187            }
188            templates.insert(key.clone());
189        }
190        db.add(Item::GeneCategory, key, block, Box::new(Self { special_gene, templates }));
191    }
192}
193
194impl DbKind for MorphGene {
195    #[allow(unused_variables)] // vic3 does not use key
196    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
197        let mut vd = Validator::new(block, data);
198
199        if Game::is_ck3() {
200            data.verify_exists(Item::Localization, key);
201        }
202
203        if Game::is_imperator() {
204            vd.req_field("index");
205            vd.field_integer("index");
206        }
207
208        vd.field_list("ugliness_feature_categories"); // TODO: options
209        vd.field_bool("can_have_portrait_extremity_shift");
210        // TODO value?
211        if let Some(token) = vd.field_value("group")
212            && self.special_gene
213        {
214            let msg =
215                "adding a group to a gene under special_genes will make the ruler designer crash";
216            fatal(ErrorKey::Crash).msg(msg).loc(token).push();
217        }
218        vd.unknown_block_fields(|_, block| {
219            validate_morph_gene(block, data);
220        });
221    }
222
223    fn has_property(
224        &self,
225        _key: &Token,
226        _block: &Block,
227        property: &str,
228        _data: &Everything,
229    ) -> bool {
230        self.templates.contains(property)
231    }
232
233    fn validate_property_use(
234        &self,
235        _key: &Token,
236        block: &Block,
237        property: &Token,
238        caller: &str,
239        data: &Everything,
240    ) {
241        validate_portrait_modifier_use(block, data, property, caller);
242    }
243
244    fn validate_use(
245        &self,
246        _key: &Token,
247        _block: &Block,
248        data: &Everything,
249        call_key: &Token,
250        call_block: &Block,
251    ) {
252        let mut vd = Validator::new(call_block, data);
253        let mut count = 0;
254        for token in vd.values() {
255            if count % 2 == 0 {
256                if !token.is("") && !self.templates.contains(token) {
257                    let msg = format!("Gene template {token} not found in category {call_key}");
258                    err(ErrorKey::MissingItem).msg(msg).loc(token).push();
259                }
260            } else if let Some(i) = token.expect_integer()
261                && !(0..=256).contains(&i)
262            {
263                warn(ErrorKey::Range).msg("expected value from 0 to 256").loc(token).push();
264            }
265            count += 1;
266            if count > 4 {
267                let msg = "too many values in this gene";
268                err(ErrorKey::Validation).msg(msg).loc(token).push();
269                break;
270            }
271        }
272        if count < 4 {
273            let msg = "too few values in this gene";
274            err(ErrorKey::Validation).msg(msg).loc(call_block).push();
275        }
276    }
277
278    fn merge_in(&mut self, other: Box<dyn DbKind>) {
279        if let Some(other) = other.as_any().downcast_ref::<Self>() {
280            for key in &other.templates {
281                if let Some(already) = self.templates.get(key.as_str()) {
282                    dup_error(key, already, "morph gene template");
283                }
284                self.templates.insert(key.clone());
285            }
286        }
287    }
288}
289
290fn validate_portrait_modifier_use(
291    block: &Block,
292    data: &Everything,
293    property: &Token,
294    caller: &str,
295) {
296    // get template
297    if let Some(block) = block.get_field_block(property.as_str()) {
298        // loop over body types
299        for field in BODY_TYPES {
300            // get weighted settings
301            if let Some(block) = block.get_field_block(field) {
302                for (_, token) in block.iter_assignments() {
303                    if token.is("empty") {
304                        continue;
305                    }
306                    let loca = format!("PORTRAIT_MODIFIER_{caller}_{token}");
307                    if !data.item_exists(Item::Localization, &loca) {
308                        let msg = format!("missing localization key {loca}");
309                        warn(ErrorKey::MissingLocalization)
310                            .msg(msg)
311                            .loc(property)
312                            .loc_msg(token, "this setting")
313                            .push();
314                    }
315                }
316            }
317        }
318    }
319}
320
321#[derive(Clone, Debug)]
322pub struct AccessoryGene {
323    templates: TigerHashSet<Token>,
324}
325
326impl AccessoryGene {
327    pub fn add(db: &mut Db, key: Token, block: Block) {
328        let mut templates = TigerHashSet::default();
329        for (key, _) in block.iter_definitions() {
330            if key.is("ugliness_feature_categories") {
331                continue;
332            }
333            if let Some(other) = templates.get(key.as_str()) {
334                dup_error(key, other, "accessory gene template");
335            }
336            templates.insert(key.clone());
337        }
338        db.add(Item::GeneCategory, key, block, Box::new(Self { templates }));
339    }
340
341    pub fn has_template_setting(
342        _key: &Token,
343        block: &Block,
344        _data: &Everything,
345        template: &str,
346        setting: &str,
347    ) -> bool {
348        if template == "ugliness_feature_categories" {
349            return false;
350        }
351        if let Some(block) = block.get_field_block(template) {
352            for field in BODY_TYPES {
353                // get weighted settings
354                if let Some(block) = block.get_field_block(field) {
355                    for (_, token) in block.iter_assignments() {
356                        if token.is("empty") {
357                            continue;
358                        }
359                        if token.is(setting) {
360                            return true;
361                        }
362                    }
363                }
364            }
365        }
366        false
367    }
368}
369
370impl DbKind for AccessoryGene {
371    #[cfg(feature = "ck3")]
372    fn add_subitems(&self, _key: &Token, block: &Block, db: &mut Db) {
373        for (key, block) in block.iter_definitions() {
374            if key.is("ugliness_feature_categories") {
375                continue;
376            }
377
378            if let Some(tags) = block.get_field_value("set_tags") {
379                for tag in tags.split(',') {
380                    db.add_flag(Item::AccessoryTag, tag);
381                }
382            }
383        }
384    }
385
386    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
387        let mut vd = Validator::new(block, data);
388
389        vd.field_bool("inheritable");
390        vd.field_value("group");
391
392        if Game::is_imperator() {
393            vd.req_field("index");
394            vd.field_integer("index");
395        }
396
397        vd.unknown_block_fields(|_, block| {
398            validate_accessory_gene(block, data);
399        });
400    }
401
402    fn has_property(
403        &self,
404        _key: &Token,
405        _block: &Block,
406        property: &str,
407        _data: &Everything,
408    ) -> bool {
409        self.templates.contains(property)
410    }
411
412    fn validate_property_use(
413        &self,
414        _key: &Token,
415        block: &Block,
416        property: &Token,
417        caller: &str,
418        data: &Everything,
419    ) {
420        validate_portrait_modifier_use(block, data, property, caller);
421    }
422
423    fn validate_use(
424        &self,
425        _key: &Token,
426        _block: &Block,
427        data: &Everything,
428        call_key: &Token,
429        call_block: &Block,
430    ) {
431        let mut vd = Validator::new(call_block, data);
432        let mut count = 0;
433        for token in vd.values() {
434            if count % 2 == 0 {
435                if !token.is("") && !self.templates.contains(token) {
436                    let msg = format!("Gene template {token} not found in category {call_key}");
437                    err(ErrorKey::MissingItem).msg(msg).loc(token).push();
438                }
439            } else if let Some(i) = token.expect_integer()
440                && !(0..=256).contains(&i)
441            {
442                warn(ErrorKey::Range).msg("expected value from 0 to 256").loc(token).push();
443            }
444            count += 1;
445            if count > 4 {
446                let msg = "too many values in this gene";
447                err(ErrorKey::Validation).msg(msg).loc(token).push();
448                break;
449            }
450        }
451        if count < 4 {
452            let msg = "too few values in this gene";
453            err(ErrorKey::Validation).msg(msg).loc(call_block).push();
454        }
455    }
456
457    fn merge_in(&mut self, other: Box<dyn DbKind>) {
458        if let Some(other) = other.as_any().downcast_ref::<Self>() {
459            for key in &other.templates {
460                if let Some(already) = self.templates.get(key.as_str()) {
461                    dup_error(key, already, "morph gene template");
462                }
463                self.templates.insert(key.clone());
464            }
465        }
466    }
467}
468
469fn validate_age_field(bv: &BV, data: &Everything) {
470    match bv {
471        BV::Value(token) => data.verify_exists(Item::GeneAgePreset, token),
472        BV::Block(block) => validate_age(block, data),
473    }
474}
475
476fn validate_age(block: &Block, data: &Everything) {
477    let mut vd = Validator::new(block, data);
478    vd.req_field("mode");
479    vd.req_field("curve");
480
481    vd.field_value("mode"); // TODO
482    vd.field_validated_block("curve", validate_curve);
483}
484
485fn validate_curve(block: &Block, data: &Everything) {
486    let mut vd = Validator::new(block, data);
487    for block in vd.blocks() {
488        validate_curve_range(block, data);
489    }
490}
491
492fn validate_hsv_curve(block: &Block, data: &Everything) {
493    let mut vd = Validator::new(block, data);
494    for block in vd.blocks() {
495        validate_hsv_curve_range(block, data);
496    }
497}
498
499fn validate_curve_range(block: &Block, data: &Everything) {
500    let mut vd = Validator::new(block, data);
501    let mut count = 0;
502    for token in vd.values() {
503        if let Some(v) = token.expect_number() {
504            count += 1;
505            #[allow(clippy::collapsible_else_if)]
506            if count == 1 {
507                if !(0.0..=1.0).contains(&v) {
508                    let msg = "expected number from 0.0 to 1.0";
509                    err(ErrorKey::Range).msg(msg).loc(token).push();
510                }
511            } else {
512                if !(-1.0..=1.0).contains(&v) {
513                    let msg = "expected number from -1.0 to 1.0";
514                    err(ErrorKey::Range).msg(msg).loc(token).push();
515                }
516            }
517        }
518    }
519    if count != 2 {
520        err(ErrorKey::Validation).msg("expected exactly 2 numbers").loc(block).push();
521    }
522}
523
524fn validate_hsv_curve_range(block: &Block, data: &Everything) {
525    let mut found_first = false;
526    let mut found_second = false;
527
528    for item in block.iter_items() {
529        if item.is_field() {
530            warn(ErrorKey::Validation).msg("unexpected key").loc(item).push();
531        } else if !found_first {
532            if let Some(token) = item.expect_value()
533                && let Some(v) = token.expect_number()
534            {
535                found_first = true;
536                if !(0.0..=1.0).contains(&v) {
537                    let msg = "expected number from 0.0 to 1.0";
538                    err(ErrorKey::Range).msg(msg).loc(token).push();
539                }
540            }
541        } else if !found_second && let Some(block) = item.expect_block() {
542            found_second = true;
543            let mut count = 0;
544            let mut vd = Validator::new(block, data);
545            for token in vd.values() {
546                if let Some(v) = token.expect_number() {
547                    count += 1;
548                    if !(-1.0..=1.0).contains(&v) {
549                        let msg = "expected number from -1.0 to 1.0";
550                        err(ErrorKey::Range).msg(msg).loc(token).push();
551                    }
552                }
553            }
554            if count != 3 {
555                err(ErrorKey::Validation).msg("expected exactly 3 numbers").loc(block).push();
556            }
557        }
558    }
559}
560
561fn validate_morph_gene(block: &Block, data: &Everything) {
562    let mut vd = Validator::new(block, data);
563    vd.req_field("index");
564    vd.field_integer("index"); // TODO: verify unique indices
565    vd.field_bool("generic");
566    vd.field_bool("visible");
567    vd.field_value("positive_mirror"); // TODO
568    vd.field_value("negative_mirror"); // TODO
569    #[cfg(feature = "imperator")]
570    vd.field_value("set_tags");
571
572    for field in BODY_TYPES {
573        vd.field_validated(field, |bv, data| {
574            match bv {
575                BV::Value(token) => {
576                    // TODO: if it refers to another field, check that following the chain of fields eventually reaches a block
577                    if !BODY_TYPES.contains(&token.as_str()) {
578                        let msg = format!("expected one of {}", BODY_TYPES.join(", "));
579                        warn(ErrorKey::Choice).msg(msg).loc(token).push();
580                    }
581                }
582                BV::Block(block) => {
583                    let mut vd = Validator::new(block, data);
584                    vd.multi_field_validated_block("setting", validate_gene_setting);
585                    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
586                    vd.multi_field_validated_block("decal", validate_gene_decal);
587                    #[cfg(feature = "imperator")]
588                    vd.multi_field_validated_block("decal", validate_gene_decal_imperator);
589                    vd.multi_field_validated_block("texture_override", validate_texture_override);
590
591                    if Game::is_imperator() {
592                        vd.field_validated_block("hair_hsv_shift_curve", validate_hsv_curve);
593                        vd.field_validated_block("eye_hsv_shift_curve", validate_hsv_curve);
594                        vd.field_validated_block("skin_hsv_shift_curve", validate_hsv_curve);
595                    } else {
596                        vd.field_validated_block("hair_hsv_shift_curve", validate_shift_curve);
597                        vd.field_validated_block("eye_hsv_shift_curve", validate_shift_curve);
598                        vd.field_validated_block("skin_hsv_shift_curve", validate_shift_curve);
599                    }
600                }
601            }
602        });
603    }
604}
605
606fn validate_accessory_gene(block: &Block, data: &Everything) {
607    let mut vd = Validator::new(block, data);
608    vd.req_field("index");
609    vd.field_integer("index"); // TODO: verify unique indices
610    vd.field_value("set_tags");
611    vd.field_bool("allow_game_entity_override"); // undocumented
612
613    for field in BODY_TYPES {
614        vd.field_validated(field, |bv, data| {
615            match bv {
616                BV::Value(token) => {
617                    // TODO: if it refers to another field, check that following the chain of fields eventually reaches a block
618                    if !BODY_TYPES.contains(&token.as_str()) {
619                        let msg = format!("expected one of {}", BODY_TYPES.join(", "));
620                        warn(ErrorKey::Choice).msg(msg).loc(token).push();
621                    }
622                }
623                BV::Block(block) => {
624                    let mut vd = Validator::new(block, data);
625                    vd.integer_keys(|_weight, bv| match bv {
626                        BV::Value(token) => {
627                            if !token.is("empty") && !token.is("0") {
628                                data.verify_exists(Item::Accessory, token);
629                            }
630                        }
631                        BV::Block(block) => {
632                            for token in block.iter_values_warn() {
633                                data.verify_exists(Item::Accessory, token);
634                            }
635                        }
636                    });
637                }
638            }
639        });
640    }
641}
642
643fn validate_gene_setting(block: &Block, data: &Everything) {
644    let mut vd = Validator::new(block, data);
645    vd.req_field("attribute");
646    vd.req_field_one_of(&["value", "curve"]);
647    vd.field_item("attribute", Item::GeneAttribute);
648    vd.field_validated("value", |bv, data| match bv {
649        BV::Value(value) => {
650            value.expect_number();
651        }
652        BV::Block(block) => {
653            let mut vd = Validator::new(block, data);
654            vd.req_field("min");
655            vd.req_field("max");
656            vd.field_numeric("min");
657            vd.field_numeric("max");
658        }
659    });
660    vd.field_validated_block("curve", validate_curve);
661    #[cfg(feature = "imperator")]
662    vd.multi_field_validated_block("animation_curve", validate_curve);
663
664    vd.field_validated("age", validate_age_field);
665    if let Some(token) = vd.field_value("required_tags") {
666        for tag in token.split(',') {
667            if tag.starts_with("not(") {
668                let real_tag = &tag.split('(')[1].split(')')[0];
669                data.verify_exists(Item::AccessoryTag, real_tag);
670            } else {
671                data.verify_exists(Item::AccessoryTag, &tag);
672            }
673        }
674    }
675}
676
677#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
678fn validate_gene_decal(block: &Block, data: &Everything) {
679    let mut vd = Validator::new(block, data);
680    vd.req_field("body_part");
681    vd.req_field("textures");
682    #[cfg(any(feature = "ck3", feature = "vic3"))]
683    vd.req_field("priority");
684    vd.field_value("body_part"); // TODO
685    vd.multi_field_validated_block("textures", validate_decal_textures);
686    vd.multi_field_validated_block("alpha_curve", validate_curve);
687    vd.multi_field_validated_block("uv_tiling", validate_uv_tiling);
688    vd.multi_field_validated_block("blend_modes", validate_blend_modes);
689    vd.field_integer("priority");
690    vd.field_validated("age", validate_age_field);
691    vd.field_choice("decal_apply_order", &["pre_skin_color", "post_skin_color"]);
692}
693
694#[cfg(feature = "imperator")]
695fn validate_gene_decal_imperator(block: &Block, data: &Everything) {
696    let mut vd = Validator::new(block, data);
697    vd.req_field("type");
698    vd.req_field("atlas_pos");
699    vd.field_choice("type", &["skin", "paint"]);
700    vd.field_list_integers_exactly("atlas_pos", 2);
701    vd.multi_field_validated_block("alpha_curve", validate_curve);
702}
703
704#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
705fn validate_decal_textures(block: &Block, data: &Everything) {
706    let mut vd = Validator::new(block, data);
707    // TODO: validate that it's a dds? What properties should the dds have?
708    vd.field_item("diffuse", Item::File);
709    vd.field_item("normal", Item::File);
710    vd.field_item("specular", Item::File);
711    vd.field_item("properties", Item::File);
712}
713
714fn validate_texture_override(block: &Block, data: &Everything) {
715    let mut vd = Validator::new(block, data);
716    vd.req_field("weight");
717    vd.field_integer("weight");
718    // TODO: validate that it's a dds? What properties should the dds have?
719    vd.field_item("diffuse", Item::File);
720    vd.field_item("normal", Item::File);
721    vd.field_item("specular", Item::File);
722    vd.field_item("properties", Item::File);
723}
724
725#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
726fn validate_blend_modes(block: &Block, data: &Everything) {
727    let mut vd = Validator::new(block, data);
728    let choices = &["overlay", "replace", "hard_light", "multiply"];
729    vd.field_choice("diffuse", choices);
730    vd.field_choice("normal", choices);
731    vd.field_choice("properties", choices);
732}
733
734#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
735fn validate_uv_tiling(block: &Block, data: &Everything) {
736    let mut vd = Validator::new(block, data);
737    vd.req_tokens_integers_exactly(2);
738}
739
740fn validate_shift_curve(block: &Block, data: &Everything) {
741    let mut vd = Validator::new(block, data);
742    vd.req_field("curve");
743    vd.field_validated_block("curve", validate_hsv_curve);
744    vd.field_validated("age", validate_age_field);
745}