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#[derive(Clone, Debug, Reflect)]
14pub struct PlacementConfigEntry {
15 pub label: String,
16 pub category: String,
18 pub description: String,
19 pub network: Option<NetworkPiece>,
21 pub role: Option<String>,
23 pub icon: Option<Handle<Texture>>,
25 pub asset: Handle<GltfAsset>,
26 pub asset_cut: Option<Handle<GltfAsset>>,
28 pub extent: UVec2,
29 pub anchors_in: Vec<PlacementAnchor>,
31 pub anchors_out: Vec<PlacementAnchor>,
33 pub module: bool,
36 pub interior: Option<InteriorArea>,
38 order: usize
40}
41
42#[derive(Clone, Copy, Debug, Reflect)]
44pub struct InteriorArea {
45 pub center: Vec2,
46 pub radius: f32,
47 pub floor: f32
49}
50
51#[derive(Clone, Debug, Reflect)]
53pub struct NetworkPiece {
54 pub name: String,
56 pub junction: bool
58}
59
60#[derive(Clone, Debug, Reflect)]
62pub struct PlacementAnchor {
63 pub name: String,
64 pub pos: Vec2
65}
66
67struct 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#[derive(Resource, Default)]
83pub struct PlacementConfig {
84 pending_entries: Vec<PendingEntry>,
85 pub labels: Vec<String>,
86 pub entries: Vec<PlacementConfigEntry>,
87 pub categories: Vec<String>
89}
90impl PlacementConfig {
91 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 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 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 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 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}