tiger_lib/vic3/data/
terrain.rs

1use crate::block::{BV, Block};
2use crate::context::ScopeContext;
3use crate::db::{Db, DbKind};
4use crate::everything::Everything;
5use crate::game::GameFlags;
6use crate::item::{Item, ItemLoader, LoadAsFile, Recursive};
7use crate::modif::validate_modifs;
8use crate::pdxfile::PdxEncoding;
9use crate::report::{ErrorKey, untidy, warn};
10use crate::scopes::Scopes;
11use crate::token::Token;
12use crate::util::SmartJoin;
13use crate::validator::Validator;
14use crate::vic3::modif::ModifKinds;
15use crate::vic3::tables::misc::TERRAIN_KEYS;
16
17#[derive(Clone, Debug)]
18pub struct Terrain {}
19
20inventory::submit! {
21    ItemLoader::Normal(GameFlags::Vic3, Item::Terrain, Terrain::add)
22}
23
24impl Terrain {
25    pub fn add(db: &mut Db, key: Token, block: Block) {
26        db.add(Item::Terrain, key, block, Box::new(Self {}));
27    }
28}
29
30impl DbKind for Terrain {
31    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
32        let mut vd = Validator::new(block, data);
33
34        data.verify_exists(Item::Localization, key);
35        vd.multi_field_item("label", Item::TerrainLabel);
36
37        vd.field_script_value_rooted("weight", Scopes::Province);
38        vd.multi_field_validated_key_block("textures", |key, block, data| {
39            let mut vd = Validator::new(block, data);
40            vd.validated_blocks(|block, data| {
41                let mut vd = Validator::new(block, data);
42                let mut sc = ScopeContext::new(Scopes::Province, key);
43                sc.define_name("state", Scopes::State, key);
44                sc.define_name("region", Scopes::StateRegion, key);
45                vd.field_script_value("weight", &mut sc);
46                vd.field_item("path", Item::File);
47                vd.field_item("effect", Item::Entity);
48            });
49        });
50
51        vd.field_numeric("combat_width");
52        vd.field_numeric("risk");
53
54        vd.field_validated_block("materials", |block, data| {
55            let mut vd = Validator::new(block, data);
56            vd.unknown_value_fields(|key, value| {
57                data.verify_exists(Item::TerrainMaterial, key);
58                value.expect_number();
59            });
60        });
61        vd.field_numeric("pollution_mask_strength");
62        vd.field_numeric("devastation_mask_strength");
63
64        // deliberately not validated because it's only for debug
65        vd.field("debug_color");
66
67        // undocumented
68        vd.field_item("created_material", Item::TerrainMaterial);
69    }
70}
71
72#[derive(Clone, Debug)]
73pub struct TerrainLabel {}
74
75inventory::submit! {
76    ItemLoader::Normal(GameFlags::Vic3, Item::TerrainLabel, TerrainLabel::add)
77}
78
79impl TerrainLabel {
80    pub fn add(db: &mut Db, key: Token, block: Block) {
81        db.add(Item::TerrainLabel, key, block, Box::new(Self {}));
82    }
83}
84
85impl DbKind for TerrainLabel {
86    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
87        let mut vd = Validator::new(block, data);
88
89        data.verify_exists(Item::Localization, key);
90        let loca = format!("{key}_desc");
91        data.verify_exists_implied(Item::Localization, &loca, key);
92
93        // TerrainLabel might have to be renamed if there are more options here
94        vd.field_choice("type", &["terrain"]);
95        vd.field_choice("modifier_key", TERRAIN_KEYS);
96        vd.replaced_field("modifiers", "modifier");
97        vd.field_validated_block("modifier", |block, data| {
98            let vd = Validator::new(block, data);
99            validate_modifs(block, data, ModifKinds::UnitCombat, vd);
100        });
101    }
102}
103
104#[derive(Clone, Debug)]
105pub struct TerrainManipulator {}
106
107inventory::submit! {
108    ItemLoader::Normal(GameFlags::Vic3, Item::TerrainManipulator, TerrainManipulator::add)
109}
110
111impl TerrainManipulator {
112    pub fn add(db: &mut Db, key: Token, block: Block) {
113        // Skip the common/terrain_manipulators/provinces/ files.
114        // TODO: should be a way to tell fileset to skip subdirectories
115        if key.loc.pathname().components().count() > 3 {
116            return;
117        }
118        db.add(Item::TerrainManipulator, key, block, Box::new(Self {}));
119    }
120}
121
122impl DbKind for TerrainManipulator {
123    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
124        let mut vd = Validator::new(block, data);
125
126        vd.field_item("created_terrain", Item::Terrain);
127        vd.field_item("terrain_mask", Item::TerrainMask);
128        vd.field_item("preferred_terrain", Item::Terrain);
129        // Same as in BuildingType
130        vd.field_choice("city_type", &["none", "city", "farm", "mine", "port", "wood"]);
131        vd.field_bool("coastal");
132
133        vd.field_validated_key_block("toggle_map_object_layers", |key, block, data| {
134            let mut vd = Validator::new(block, data);
135            if let Some(array) =
136                data.get_defined_array_warn(key, "NGraphics|DYNAMIC_MAP_OBJECT_LAYERS")
137            {
138                for layer in array.iter_values_warn() {
139                    vd.field_validated(layer.as_str(), validate_layer);
140                }
141            }
142        });
143    }
144}
145
146#[derive(Clone, Debug)]
147pub struct TerrainMaterial {}
148
149inventory::submit! {
150    ItemLoader::Full(GameFlags::Vic3, Item::TerrainMaterial, PdxEncoding::Utf8OptionalBom, ".settings", LoadAsFile::Yes, Recursive::No, TerrainMaterial::add)
151}
152
153impl TerrainMaterial {
154    // This gets the whole file as a Block
155    #[allow(clippy::needless_pass_by_value)] // needs to match the ::add function signature
156    pub fn add(db: &mut Db, _key: Token, block: Block) {
157        // Structure is { { material } { material } ... } { ... }
158        for block in block.iter_blocks_warn() {
159            for block in block.iter_blocks_warn() {
160                // docs say that the id field uniquely identifies a material,
161                // but the name is the one actually used to look them up.
162                if let Some(name) = block.get_field_value("name") {
163                    db.add(Item::TerrainMaterial, name.clone(), block.clone(), Box::new(Self {}));
164                } else {
165                    untidy(ErrorKey::FieldMissing).msg("texture with no name").loc(block).push();
166                }
167            }
168        }
169    }
170}
171
172impl DbKind for TerrainMaterial {
173    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
174        let mut vd = Validator::new(block, data);
175
176        vd.field_value("name");
177        vd.field_value("id");
178
179        for field in &["diffuse", "normal", "material"] {
180            vd.req_field(field);
181            if let Some(token) = vd.field_value(field) {
182                let path = block.loc.pathname().smart_join_parent(token.as_str());
183                data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
184            }
185        }
186
187        vd.req_field("mask");
188        vd.field_value("mask");
189    }
190}
191
192#[derive(Clone, Debug)]
193pub struct TerrainMask {}
194
195impl TerrainMask {
196    #[allow(clippy::needless_pass_by_value)] // TODO: need drain_blocks_warn()
197    pub fn add_json(db: &mut Db, block: Block) {
198        // The masks are deeply nested in a json that looks like this:
199        // { "masks": [ { ... }, { ... } ] }
200        let mut count = 0;
201        for block in block.iter_blocks_warn() {
202            count += 1;
203            if count == 2 {
204                warn(ErrorKey::Validation).msg("expected only one block").loc(block).push();
205            }
206            if let Some(block) = block.get_field_block("masks") {
207                for block in block.iter_blocks_warn() {
208                    if let Some(token) = block.get_field_value("key") {
209                        db.add(Item::TerrainMask, token.clone(), block.clone(), Box::new(Self {}));
210                    } else {
211                        warn(ErrorKey::FieldMissing).msg("mask with no key").loc(block).push();
212                    }
213                }
214            }
215        }
216    }
217}
218
219impl DbKind for TerrainMask {
220    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
221        let mut vd = Validator::new(block, data);
222        vd.field_value("key");
223        vd.req_field("filename");
224        if let Some(token) = vd.field_value("filename") {
225            let path = key.loc.pathname().smart_join_parent(token.as_str());
226            data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
227        }
228    }
229}
230
231fn validate_layer(bv: &BV, data: &Everything) {
232    match bv {
233        BV::Value(token) => {
234            if !token.is("show_above_default") && !token.is("show_below_default") {
235                let msg = "unknown layer position `{token}`";
236                warn(ErrorKey::UnknownField).msg(msg).loc(token).push();
237            }
238        }
239        BV::Block(block) => {
240            let mut vd = Validator::new(block, data);
241            vd.req_field_one_of(&["show_above", "show_below"]);
242            vd.field_numeric("show_above");
243            vd.field_numeric("show_below");
244        }
245    }
246}