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            if self.special_gene {
213                let msg = "adding a group to a gene under special_genes will make the ruler designer crash";
214                fatal(ErrorKey::Crash).msg(msg).loc(token).push();
215            }
216        }
217        vd.unknown_block_fields(|_, block| {
218            validate_morph_gene(block, data);
219        });
220    }
221
222    fn has_property(
223        &self,
224        _key: &Token,
225        _block: &Block,
226        property: &str,
227        _data: &Everything,
228    ) -> bool {
229        self.templates.contains(property)
230    }
231
232    fn validate_property_use(
233        &self,
234        _key: &Token,
235        block: &Block,
236        property: &Token,
237        caller: &str,
238        data: &Everything,
239    ) {
240        validate_portrait_modifier_use(block, data, property, caller);
241    }
242
243    fn validate_use(
244        &self,
245        _key: &Token,
246        _block: &Block,
247        data: &Everything,
248        call_key: &Token,
249        call_block: &Block,
250    ) {
251        let mut vd = Validator::new(call_block, data);
252        let mut count = 0;
253        for token in vd.values() {
254            if count % 2 == 0 {
255                if !token.is("") && !self.templates.contains(token) {
256                    let msg = format!("Gene template {token} not found in category {call_key}");
257                    err(ErrorKey::MissingItem).msg(msg).loc(token).push();
258                }
259            } else if let Some(i) = token.expect_integer() {
260                if !(0..=256).contains(&i) {
261                    warn(ErrorKey::Range).msg("expected value from 0 to 256").loc(token).push();
262                }
263            }
264            count += 1;
265            if count > 4 {
266                let msg = "too many values in this gene";
267                err(ErrorKey::Validation).msg(msg).loc(token).push();
268                break;
269            }
270        }
271        if count < 4 {
272            let msg = "too few values in this gene";
273            err(ErrorKey::Validation).msg(msg).loc(call_block).push();
274        }
275    }
276
277    fn merge_in(&mut self, other: Box<dyn DbKind>) {
278        if let Some(other) = other.as_any().downcast_ref::<Self>() {
279            for key in &other.templates {
280                if let Some(already) = self.templates.get(key.as_str()) {
281                    dup_error(key, already, "morph gene template");
282                }
283                self.templates.insert(key.clone());
284            }
285        }
286    }
287}
288
289fn validate_portrait_modifier_use(
290    block: &Block,
291    data: &Everything,
292    property: &Token,
293    caller: &str,
294) {
295    // get template
296    if let Some(block) = block.get_field_block(property.as_str()) {
297        // loop over body types
298        for field in BODY_TYPES {
299            // get weighted settings
300            if let Some(block) = block.get_field_block(field) {
301                for (_, token) in block.iter_assignments() {
302                    if token.is("empty") {
303                        continue;
304                    }
305                    let loca = format!("PORTRAIT_MODIFIER_{caller}_{token}");
306                    if !data.item_exists(Item::Localization, &loca) {
307                        let msg = format!("missing localization key {loca}");
308                        warn(ErrorKey::MissingLocalization)
309                            .msg(msg)
310                            .loc(property)
311                            .loc_msg(token, "this setting")
312                            .push();
313                    }
314                }
315            }
316        }
317    }
318}
319
320#[derive(Clone, Debug)]
321pub struct AccessoryGene {
322    templates: TigerHashSet<Token>,
323}
324
325impl AccessoryGene {
326    pub fn add(db: &mut Db, key: Token, block: Block) {
327        let mut templates = TigerHashSet::default();
328        for (key, _) in block.iter_definitions() {
329            if key.is("ugliness_feature_categories") {
330                continue;
331            }
332            if let Some(other) = templates.get(key.as_str()) {
333                dup_error(key, other, "accessory gene template");
334            }
335            templates.insert(key.clone());
336        }
337        db.add(Item::GeneCategory, key, block, Box::new(Self { templates }));
338    }
339
340    pub fn has_template_setting(
341        _key: &Token,
342        block: &Block,
343        _data: &Everything,
344        template: &str,
345        setting: &str,
346    ) -> bool {
347        if template == "ugliness_feature_categories" {
348            return false;
349        }
350        if let Some(block) = block.get_field_block(template) {
351            for field in BODY_TYPES {
352                // get weighted settings
353                if let Some(block) = block.get_field_block(field) {
354                    for (_, token) in block.iter_assignments() {
355                        if token.is("empty") {
356                            continue;
357                        }
358                        if token.is(setting) {
359                            return true;
360                        }
361                    }
362                }
363            }
364        }
365        false
366    }
367}
368
369impl DbKind for AccessoryGene {
370    #[cfg(feature = "ck3")]
371    fn add_subitems(&self, _key: &Token, block: &Block, db: &mut Db) {
372        for (key, block) in block.iter_definitions() {
373            if key.is("ugliness_feature_categories") {
374                continue;
375            }
376
377            if let Some(tags) = block.get_field_value("set_tags") {
378                for tag in tags.split(',') {
379                    db.add_flag(Item::AccessoryTag, tag);
380                }
381            }
382        }
383    }
384
385    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
386        let mut vd = Validator::new(block, data);
387
388        vd.field_bool("inheritable");
389        vd.field_value("group");
390
391        if Game::is_imperator() {
392            vd.req_field("index");
393            vd.field_integer("index");
394        }
395
396        vd.unknown_block_fields(|_, block| {
397            validate_accessory_gene(block, data);
398        });
399    }
400
401    fn has_property(
402        &self,
403        _key: &Token,
404        _block: &Block,
405        property: &str,
406        _data: &Everything,
407    ) -> bool {
408        self.templates.contains(property)
409    }
410
411    fn validate_property_use(
412        &self,
413        _key: &Token,
414        block: &Block,
415        property: &Token,
416        caller: &str,
417        data: &Everything,
418    ) {
419        validate_portrait_modifier_use(block, data, property, caller);
420    }
421
422    fn validate_use(
423        &self,
424        _key: &Token,
425        _block: &Block,
426        data: &Everything,
427        call_key: &Token,
428        call_block: &Block,
429    ) {
430        let mut vd = Validator::new(call_block, data);
431        let mut count = 0;
432        for token in vd.values() {
433            if count % 2 == 0 {
434                if !token.is("") && !self.templates.contains(token) {
435                    let msg = format!("Gene template {token} not found in category {call_key}");
436                    err(ErrorKey::MissingItem).msg(msg).loc(token).push();
437                }
438            } else if let Some(i) = token.expect_integer() {
439                if !(0..=256).contains(&i) {
440                    warn(ErrorKey::Range).msg("expected value from 0 to 256").loc(token).push();
441                }
442            }
443            count += 1;
444            if count > 4 {
445                let msg = "too many values in this gene";
446                err(ErrorKey::Validation).msg(msg).loc(token).push();
447                break;
448            }
449        }
450        if count < 4 {
451            let msg = "too few values in this gene";
452            err(ErrorKey::Validation).msg(msg).loc(call_block).push();
453        }
454    }
455
456    fn merge_in(&mut self, other: Box<dyn DbKind>) {
457        if let Some(other) = other.as_any().downcast_ref::<Self>() {
458            for key in &other.templates {
459                if let Some(already) = self.templates.get(key.as_str()) {
460                    dup_error(key, already, "morph gene template");
461                }
462                self.templates.insert(key.clone());
463            }
464        }
465    }
466}
467
468fn validate_age_field(bv: &BV, data: &Everything) {
469    match bv {
470        BV::Value(token) => data.verify_exists(Item::GeneAgePreset, token),
471        BV::Block(block) => validate_age(block, data),
472    }
473}
474
475fn validate_age(block: &Block, data: &Everything) {
476    let mut vd = Validator::new(block, data);
477    vd.req_field("mode");
478    vd.req_field("curve");
479
480    vd.field_value("mode"); // TODO
481    vd.field_validated_block("curve", validate_curve);
482}
483
484fn validate_curve(block: &Block, data: &Everything) {
485    let mut vd = Validator::new(block, data);
486    for block in vd.blocks() {
487        validate_curve_range(block, data);
488    }
489}
490
491fn validate_hsv_curve(block: &Block, data: &Everything) {
492    let mut vd = Validator::new(block, data);
493    for block in vd.blocks() {
494        validate_hsv_curve_range(block, data);
495    }
496}
497
498fn validate_curve_range(block: &Block, data: &Everything) {
499    let mut vd = Validator::new(block, data);
500    let mut count = 0;
501    for token in vd.values() {
502        if let Some(v) = token.expect_number() {
503            count += 1;
504            #[allow(clippy::collapsible_else_if)]
505            if count == 1 {
506                if !(0.0..=1.0).contains(&v) {
507                    let msg = "expected number from 0.0 to 1.0";
508                    err(ErrorKey::Range).msg(msg).loc(token).push();
509                }
510            } else {
511                if !(-1.0..=1.0).contains(&v) {
512                    let msg = "expected number from -1.0 to 1.0";
513                    err(ErrorKey::Range).msg(msg).loc(token).push();
514                }
515            }
516        }
517    }
518    if count != 2 {
519        err(ErrorKey::Validation).msg("expected exactly 2 numbers").loc(block).push();
520    }
521}
522
523fn validate_hsv_curve_range(block: &Block, data: &Everything) {
524    let mut found_first = false;
525    let mut found_second = false;
526
527    for item in block.iter_items() {
528        if item.is_field() {
529            warn(ErrorKey::Validation).msg("unexpected key").loc(item).push();
530        } else if !found_first {
531            if let Some(token) = item.expect_value() {
532                if let Some(v) = token.expect_number() {
533                    found_first = true;
534                    if !(0.0..=1.0).contains(&v) {
535                        let msg = "expected number from 0.0 to 1.0";
536                        err(ErrorKey::Range).msg(msg).loc(token).push();
537                    }
538                }
539            }
540        } else if !found_second {
541            if 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}
561
562fn validate_morph_gene(block: &Block, data: &Everything) {
563    let mut vd = Validator::new(block, data);
564    vd.req_field("index");
565    vd.field_integer("index"); // TODO: verify unique indices
566    vd.field_bool("generic");
567    vd.field_bool("visible");
568    vd.field_value("positive_mirror"); // TODO
569    vd.field_value("negative_mirror"); // TODO
570    #[cfg(feature = "imperator")]
571    vd.field_value("set_tags");
572
573    for field in BODY_TYPES {
574        vd.field_validated(field, |bv, data| {
575            match bv {
576                BV::Value(token) => {
577                    // TODO: if it refers to another field, check that following the chain of fields eventually reaches a block
578                    if !BODY_TYPES.contains(&token.as_str()) {
579                        let msg = format!("expected one of {}", BODY_TYPES.join(", "));
580                        warn(ErrorKey::Choice).msg(msg).loc(token).push();
581                    }
582                }
583                BV::Block(block) => {
584                    let mut vd = Validator::new(block, data);
585                    vd.multi_field_validated_block("setting", validate_gene_setting);
586                    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
587                    vd.multi_field_validated_block("decal", validate_gene_decal);
588                    #[cfg(feature = "imperator")]
589                    vd.multi_field_validated_block("decal", validate_gene_decal_imperator);
590                    vd.multi_field_validated_block("texture_override", validate_texture_override);
591
592                    if Game::is_imperator() {
593                        vd.field_validated_block("hair_hsv_shift_curve", validate_hsv_curve);
594                        vd.field_validated_block("eye_hsv_shift_curve", validate_hsv_curve);
595                        vd.field_validated_block("skin_hsv_shift_curve", validate_hsv_curve);
596                    } else {
597                        vd.field_validated_block("hair_hsv_shift_curve", validate_shift_curve);
598                        vd.field_validated_block("eye_hsv_shift_curve", validate_shift_curve);
599                        vd.field_validated_block("skin_hsv_shift_curve", validate_shift_curve);
600                    }
601                }
602            }
603        });
604    }
605}
606
607fn validate_accessory_gene(block: &Block, data: &Everything) {
608    let mut vd = Validator::new(block, data);
609    vd.req_field("index");
610    vd.field_integer("index"); // TODO: verify unique indices
611    vd.field_value("set_tags");
612    vd.field_bool("allow_game_entity_override"); // undocumented
613
614    for field in BODY_TYPES {
615        vd.field_validated(field, |bv, data| {
616            match bv {
617                BV::Value(token) => {
618                    // TODO: if it refers to another field, check that following the chain of fields eventually reaches a block
619                    if !BODY_TYPES.contains(&token.as_str()) {
620                        let msg = format!("expected one of {}", BODY_TYPES.join(", "));
621                        warn(ErrorKey::Choice).msg(msg).loc(token).push();
622                    }
623                }
624                BV::Block(block) => {
625                    let mut vd = Validator::new(block, data);
626                    vd.integer_keys(|_weight, bv| match bv {
627                        BV::Value(token) => {
628                            if !token.is("empty") && !token.is("0") {
629                                data.verify_exists(Item::Accessory, token);
630                            }
631                        }
632                        BV::Block(block) => {
633                            for token in block.iter_values_warn() {
634                                data.verify_exists(Item::Accessory, token);
635                            }
636                        }
637                    });
638                }
639            }
640        });
641    }
642}
643
644fn validate_gene_setting(block: &Block, data: &Everything) {
645    let mut vd = Validator::new(block, data);
646    vd.req_field("attribute");
647    vd.req_field_one_of(&["value", "curve"]);
648    vd.field_item("attribute", Item::GeneAttribute);
649    vd.field_validated("value", |bv, data| match bv {
650        BV::Value(value) => {
651            value.expect_number();
652        }
653        BV::Block(block) => {
654            let mut vd = Validator::new(block, data);
655            vd.req_field("min");
656            vd.req_field("max");
657            vd.field_numeric("min");
658            vd.field_numeric("max");
659        }
660    });
661    vd.field_validated_block("curve", validate_curve);
662    #[cfg(feature = "imperator")]
663    vd.multi_field_validated_block("animation_curve", validate_curve);
664
665    vd.field_validated("age", validate_age_field);
666    if let Some(token) = vd.field_value("required_tags") {
667        for tag in token.split(',') {
668            if tag.starts_with("not(") {
669                let real_tag = &tag.split('(')[1].split(')')[0];
670                data.verify_exists(Item::AccessoryTag, real_tag);
671            } else {
672                data.verify_exists(Item::AccessoryTag, &tag);
673            }
674        }
675    }
676}
677
678#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
679fn validate_gene_decal(block: &Block, data: &Everything) {
680    let mut vd = Validator::new(block, data);
681    vd.req_field("body_part");
682    vd.req_field("textures");
683    #[cfg(any(feature = "ck3", feature = "vic3"))]
684    vd.req_field("priority");
685    vd.field_value("body_part"); // TODO
686    vd.multi_field_validated_block("textures", validate_decal_textures);
687    vd.multi_field_validated_block("alpha_curve", validate_curve);
688    vd.multi_field_validated_block("uv_tiling", validate_uv_tiling);
689    vd.multi_field_validated_block("blend_modes", validate_blend_modes);
690    vd.field_integer("priority");
691    vd.field_validated("age", validate_age_field);
692    vd.field_choice("decal_apply_order", &["pre_skin_color", "post_skin_color"]);
693}
694
695#[cfg(feature = "imperator")]
696fn validate_gene_decal_imperator(block: &Block, data: &Everything) {
697    let mut vd = Validator::new(block, data);
698    vd.req_field("type");
699    vd.req_field("atlas_pos");
700    vd.field_choice("type", &["skin", "paint"]);
701    vd.field_list_integers_exactly("atlas_pos", 2);
702    vd.multi_field_validated_block("alpha_curve", validate_curve);
703}
704
705#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
706fn validate_decal_textures(block: &Block, data: &Everything) {
707    let mut vd = Validator::new(block, data);
708    // TODO: validate that it's a dds? What properties should the dds have?
709    vd.field_item("diffuse", Item::File);
710    vd.field_item("normal", Item::File);
711    vd.field_item("specular", Item::File);
712    vd.field_item("properties", Item::File);
713}
714
715fn validate_texture_override(block: &Block, data: &Everything) {
716    let mut vd = Validator::new(block, data);
717    vd.req_field("weight");
718    vd.field_integer("weight");
719    // TODO: validate that it's a dds? What properties should the dds have?
720    vd.field_item("diffuse", Item::File);
721    vd.field_item("normal", Item::File);
722    vd.field_item("specular", Item::File);
723    vd.field_item("properties", Item::File);
724}
725
726#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
727fn validate_blend_modes(block: &Block, data: &Everything) {
728    let mut vd = Validator::new(block, data);
729    let choices = &["overlay", "replace", "hard_light", "multiply"];
730    vd.field_choice("diffuse", choices);
731    vd.field_choice("normal", choices);
732    vd.field_choice("properties", choices);
733}
734
735#[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
736fn validate_uv_tiling(block: &Block, data: &Everything) {
737    let mut vd = Validator::new(block, data);
738    vd.req_tokens_integers_exactly(2);
739}
740
741fn validate_shift_curve(block: &Block, data: &Everything) {
742    let mut vd = Validator::new(block, data);
743    vd.req_field("curve");
744    vd.field_validated_block("curve", validate_hsv_curve);
745    vd.field_validated("age", validate_age_field);
746}