Skip to main content

wde_terrain_grid/core/
entries.rs

1use wde_logger::prelude::*;
2
3use bevy::prelude::*;
4use wde_editor::prelude::*;
5use wde_gltf::prelude::*;
6use wde_renderer::prelude::*;
7use wde_scene::prelude::*;
8
9const PLACEMENT_CONFIG_VERSION: u32 = 1;
10const PLACEMENT_CONFIG_PATH: &str = "core/config/placement.json";
11
12/// Entry for a single entity in the placement configuration.
13#[derive(Clone, Debug, Reflect)]
14pub struct PlacementConfigEntry {
15    pub label: String,
16    /// Menu category, in the order categories first appear in the config.
17    pub category: String,
18    pub description: String,
19    /// Network (e.g. `tunnel`, `path`) this piece belongs to, drawn with the line tool.
20    pub network: Option<NetworkPiece>,
21    /// Gameplay role, e.g. `control_room` for the building defining a base.
22    pub role: Option<String>,
23    /// Menu icon, registered in [UITextures].
24    pub icon: Option<Handle<Texture>>,
25    pub asset: Handle<GltfAsset>,
26    /// Optional cutaway variant (open top), shown when [BuildingCutaway](crate::prelude::BuildingCutaway) is enabled.
27    pub asset_cut: Option<Handle<GltfAsset>>,
28    pub extent: UVec2,
29    /// Tunnel connection points (`%P%AnchorIn*` properties).
30    pub anchors_in: Vec<PlacementAnchor>,
31    /// Path connection points (`%P%AnchorOut*` properties).
32    pub anchors_out: Vec<PlacementAnchor>,
33    /// Placed inside the interior of another building (`"placement": "module"`), not on the
34    /// ground.
35    pub module: bool,
36    /// Floor where modules can be placed (`%P%Interior` property).
37    pub interior: Option<InteriorArea>,
38    /// Position in the config file, so entries keep that order whatever their load order.
39    order: usize
40}
41
42/// Disc of a building floor where modules can be placed, in local space.
43#[derive(Clone, Copy, Debug, Reflect)]
44pub struct InteriorArea {
45    pub center: Vec2,
46    pub radius: f32,
47    /// Height of the floor.
48    pub floor: f32
49}
50
51/// Role of a piece in a network drawn by the line tool.
52#[derive(Clone, Debug, Reflect)]
53pub struct NetworkPiece {
54    /// Network name, shared by its straight and junction pieces.
55    pub name: String,
56    /// Junction (4 connections) or straight piece (2 connections along local X).
57    pub junction: bool
58}
59
60/// Named connection point of an entry, in local grid space (x maps to world X, y to world Z).
61#[derive(Clone, Debug, Reflect)]
62pub struct PlacementAnchor {
63    pub name: String,
64    pub pos: Vec2
65}
66
67/// Entry waiting for its model to load (its footprint and anchors come from the glTF).
68struct PendingEntry {
69    order: usize,
70    label: String,
71    category: String,
72    description: String,
73    network: Option<NetworkPiece>,
74    role: Option<String>,
75    icon: Option<Handle<Texture>>,
76    asset: Handle<GltfAsset>,
77    asset_cut: Option<Handle<GltfAsset>>,
78    module: bool
79}
80
81/// Configuration for the placement of entities in the terrain grid.
82#[derive(Resource, Default)]
83pub struct PlacementConfig {
84    pending_entries: Vec<PendingEntry>,
85    pub labels: Vec<String>,
86    pub entries: Vec<PlacementConfigEntry>,
87    /// Categories in config order.
88    pub categories: Vec<String>
89}
90impl PlacementConfig {
91    /// Whether some entries still wait for their model.
92    pub fn is_loading(&self) -> bool {
93        !self.pending_entries.is_empty()
94    }
95}
96
97pub struct PlacementPlugin;
98impl Plugin for PlacementPlugin {
99    fn build(&self, app: &mut App) {
100        app.init_resource::<PlacementConfig>()
101            .add_systems(Startup, read_placement_file)
102            .add_systems(Update, process_pending_entries);
103    }
104}
105
106fn read_placement_file(
107    asset_server: Res<AssetServer>,
108    mut placement_config_struct: ResMut<PlacementConfig>,
109    mut ui_textures: Option<ResMut<UITextures>>
110) {
111    // Load placement configuration file
112    let placement_config = match serialize::parse_file(PLACEMENT_CONFIG_PATH) {
113        Ok(config) => config,
114        Err(err) => {
115            error!(
116                "Failed to load placement configuration file at '{}': {}",
117                PLACEMENT_CONFIG_PATH, err
118            );
119            return;
120        }
121    };
122    let placement_config = placement_config.data;
123
124    // Check version
125    if placement_config["version"].as_u64().unwrap_or(0) != PLACEMENT_CONFIG_VERSION as u64 {
126        error!(
127            "Unsupported placement configuration version: {}. Expected version: {}",
128            placement_config["version"], PLACEMENT_CONFIG_VERSION
129        );
130        return;
131    }
132
133    // Load entities
134    let config_entities = placement_config["entities"].as_array();
135    let mut entries = Vec::new();
136    let mut categories: Vec<String> = Vec::new();
137    for (order, entity) in config_entities.unwrap_or(&vec![]).iter().enumerate() {
138        // Parse entity configuration
139        let (label, model_path) = match (entity["label"].as_str(), entity["model"].as_str()) {
140            (Some(label), Some(model_path)) => (label.to_string(), model_path.to_string()),
141            _ => {
142                error!(
143                    "Invalid entity configuration in placement config: {:?}",
144                    entity
145                );
146                continue;
147            }
148        };
149        let asset_cut = entity["model_cut"]
150            .as_str()
151            .map(|path| asset_server.load(path.to_string()));
152        let category = entity["category"].as_str().unwrap_or("Other").to_string();
153        if !categories.contains(&category) {
154            categories.push(category.clone());
155        }
156        let icon = entity["icon"].as_str().map(|path| {
157            let icon: Handle<Texture> = asset_server.load(path.to_string());
158            if let Some(ui_textures) = ui_textures.as_mut() {
159                ui_textures.register_texture(icon.clone());
160            }
161            icon
162        });
163        entries.push(PendingEntry {
164            order,
165            label,
166            category,
167            description: entity["description"]
168                .as_str()
169                .unwrap_or_default()
170                .to_string(),
171            role: entity["role"].as_str().map(str::to_string),
172            module: entity["placement"].as_str() == Some("module"),
173            network: entity["network"].as_str().map(|name| NetworkPiece {
174                name: name.to_string(),
175                junction: entity["piece"].as_str() == Some("junction")
176            }),
177            icon,
178            asset: asset_server.load(&model_path),
179            asset_cut
180        });
181    }
182    placement_config_struct.pending_entries = entries;
183    placement_config_struct.categories = categories;
184}
185
186fn process_pending_entries(
187    mut placement_config_struct: ResMut<PlacementConfig>,
188    gltf_assets: Res<Assets<GltfAsset>>
189) {
190    if placement_config_struct.pending_entries.is_empty() {
191        return;
192    }
193    let mut processed_entries = Vec::new();
194    let mut processed_labels = Vec::new();
195    let mut retry_entries = Vec::new();
196    for pending in placement_config_struct.pending_entries.drain(..) {
197        if let Some(gltf_asset) = gltf_assets.get(&pending.asset) {
198            let properties = &gltf_asset.properties;
199            let extent = match properties.get("Footprint") {
200                Some((_pos, scale)) => {
201                    UVec2::new((scale.x).round() as u32, (scale.z).round() as u32)
202                }
203                None => {
204                    error!(
205                        "Extent not specified for label '{}', defaulting to (1, 1).",
206                        pending.label
207                    );
208                    UVec2::new(1, 1)
209                }
210            };
211            let mut anchors_in = Vec::new();
212            let mut anchors_out = Vec::new();
213            for (anchor_name, (pos, _scale)) in properties.iter() {
214                let anchor = PlacementAnchor {
215                    name: anchor_name.clone(),
216                    pos: Vec2::new(pos.x, pos.z)
217                };
218                if anchor_name.starts_with("AnchorIn") {
219                    anchors_in.push(anchor);
220                } else if anchor_name.starts_with("AnchorOut") {
221                    anchors_out.push(anchor);
222                }
223            }
224            let interior = properties.get("Interior").map(|(pos, scale)| InteriorArea {
225                center: pos.xz(),
226                radius: scale.x,
227                floor: pos.y
228            });
229            processed_labels.push(pending.label.clone());
230            processed_entries.push(PlacementConfigEntry {
231                label: pending.label,
232                category: pending.category,
233                description: pending.description,
234                network: pending.network,
235                role: pending.role,
236                icon: pending.icon,
237                asset: pending.asset,
238                asset_cut: pending.asset_cut,
239                extent,
240                anchors_in,
241                anchors_out,
242                module: pending.module,
243                interior,
244                order: pending.order
245            });
246        } else {
247            retry_entries.push(pending);
248        }
249    }
250    placement_config_struct.entries.extend(processed_entries);
251    placement_config_struct.entries.sort_by_key(|e| e.order);
252    placement_config_struct.labels.extend(processed_labels);
253    placement_config_struct.pending_entries = retry_entries;
254}