| 1 |
#![expect( |
| 2 |
unsafe_code, |
| 3 |
reason = "Unsafe code is needed to work with dynamic components" |
| 4 |
)] |
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
use std::{alloc::Layout, collections::HashMap, io::Write, ptr::NonNull}; |
| 13 |
|
| 14 |
use bevy::{ |
| 15 |
ecs::{ |
| 16 |
component::{ |
| 17 |
ComponentCloneBehavior, ComponentDescriptor, ComponentId, ComponentInfo, StorageType, |
| 18 |
}, |
| 19 |
event::EventKey, |
| 20 |
observer::{Observer, ObserverRunner}, |
| 21 |
query::{ComponentAccessKind, QueryData}, |
| 22 |
world::FilteredEntityMut, |
| 23 |
}, |
| 24 |
prelude::*, |
| 25 |
ptr::{Aligned, OwningPtr, PtrMut}, |
| 26 |
}; |
| 27 |
|
| 28 |
const PROMPT: &str = " |
| 29 |
Commands: |
| 30 |
comp, c Create new components |
| 31 |
spawn, s Spawn entities |
| 32 |
query, q Query for entities |
| 33 |
event, e Register dynamic events and observers |
| 34 |
emit, t Trigger a dynamic event |
| 35 |
Enter a command with no parameters for usage."; |
| 36 |
|
| 37 |
const COMPONENT_PROMPT: &str = " |
| 38 |
comp, c Create new components |
| 39 |
Enter a comma separated list of type names optionally followed by a size in u64s. |
| 40 |
e.g. CompA 3, CompB, CompC 2"; |
| 41 |
|
| 42 |
const ENTITY_PROMPT: &str = " |
| 43 |
spawn, s Spawn entities |
| 44 |
Enter a comma separated list of components optionally followed by values. |
| 45 |
e.g. CompA 0 1 0, CompB, CompC 1"; |
| 46 |
|
| 47 |
const QUERY_PROMPT: &str = " |
| 48 |
query, q Query for entities |
| 49 |
Enter a query to fetch and update entities |
| 50 |
Components with read or write access will be displayed with their values |
| 51 |
Components with write access will have their fields incremented by one |
| 52 |
|
| 53 |
Accesses: 'A' with, '&A' read, '&mut A' write |
| 54 |
Operators: '||' or, ',' and, '?' optional |
| 55 |
|
| 56 |
e.g. &A || &B, &mut C, D, ?E"; |
| 57 |
|
| 58 |
const EVENT_PROMPT: &str = " |
| 59 |
event, e Register dynamic events and observers |
| 60 |
Enter a comma separated list of event names. |
| 61 |
Each event gets a dynamic observer that prints when fired. |
| 62 |
e.g. OnDamage, OnHeal, OnDeath"; |
| 63 |
|
| 64 |
const EMIT_PROMPT: &str = " |
| 65 |
emit, t Trigger a dynamic event |
| 66 |
Enter the name of a previously registered event. |
| 67 |
e.g. OnDamage"; |
| 68 |
|
| 69 |
fn main() { |
| 70 |
let mut world = World::new(); |
| 71 |
let mut lines = std::io::stdin().lines(); |
| 72 |
let mut component_names = HashMap::<String, ComponentId>::new(); |
| 73 |
let mut component_info = HashMap::<ComponentId, ComponentInfo>::new(); |
| 74 |
let mut event_names = HashMap::<String, EventKey>::new(); |
| 75 |
|
| 76 |
println!("{PROMPT}"); |
| 77 |
loop { |
| 78 |
print!("\n> "); |
| 79 |
let _ = std::io::stdout().flush(); |
| 80 |
let Some(Ok(line)) = lines.next() else { |
| 81 |
return; |
| 82 |
}; |
| 83 |
|
| 84 |
if line.is_empty() { |
| 85 |
return; |
| 86 |
}; |
| 87 |
|
| 88 |
let Some((first, rest)) = line.trim().split_once(|c: char| c.is_whitespace()) else { |
| 89 |
match &line.chars().next() { |
| 90 |
Some('c') => println!("{COMPONENT_PROMPT}"), |
| 91 |
Some('s') => println!("{ENTITY_PROMPT}"), |
| 92 |
Some('q') => println!("{QUERY_PROMPT}"), |
| 93 |
Some('e') => println!("{EVENT_PROMPT}"), |
| 94 |
Some('t') => println!("{EMIT_PROMPT}"), |
| 95 |
_ => println!("{PROMPT}"), |
| 96 |
} |
| 97 |
continue; |
| 98 |
}; |
| 99 |
|
| 100 |
match &first[0..1] { |
| 101 |
"c" => { |
| 102 |
rest.split(',').for_each(|component| { |
| 103 |
let mut component = component.split_whitespace(); |
| 104 |
let Some(name) = component.next() else { |
| 105 |
return; |
| 106 |
}; |
| 107 |
let size = match component.next().map(str::parse) { |
| 108 |
Some(Ok(size)) => size, |
| 109 |
_ => 0, |
| 110 |
}; |
| 111 |
|
| 112 |
|
| 113 |
let id = world.register_component_with_descriptor(unsafe { |
| 114 |
ComponentDescriptor::new_with_layout( |
| 115 |
name.to_string(), |
| 116 |
StorageType::Table, |
| 117 |
Layout::array::<u64>(size).unwrap(), |
| 118 |
None, |
| 119 |
true, |
| 120 |
false, |
| 121 |
ComponentCloneBehavior::Default, |
| 122 |
None, |
| 123 |
) |
| 124 |
}); |
| 125 |
let Some(info) = world.components().get_info(id) else { |
| 126 |
return; |
| 127 |
}; |
| 128 |
component_names.insert(name.to_string(), id); |
| 129 |
component_info.insert(id, info.clone()); |
| 130 |
println!("Component {} created with id: {}", name, id.index()); |
| 131 |
}); |
| 132 |
} |
| 133 |
"s" => { |
| 134 |
let mut to_insert_ids = Vec::new(); |
| 135 |
let mut to_insert_data = Vec::new(); |
| 136 |
rest.split(',').for_each(|component| { |
| 137 |
let mut component = component.split_whitespace(); |
| 138 |
let Some(name) = component.next() else { |
| 139 |
return; |
| 140 |
}; |
| 141 |
|
| 142 |
|
| 143 |
let Some(&id) = component_names.get(name) else { |
| 144 |
println!("Component {name} does not exist"); |
| 145 |
return; |
| 146 |
}; |
| 147 |
|
| 148 |
|
| 149 |
let info = world.components().get_info(id).unwrap(); |
| 150 |
let len = info.layout().size() / size_of::<u64>(); |
| 151 |
let mut values: Vec<u64> = component |
| 152 |
.take(len) |
| 153 |
.filter_map(|value| value.parse::<u64>().ok()) |
| 154 |
.collect(); |
| 155 |
values.resize(len, 0); |
| 156 |
|
| 157 |
|
| 158 |
to_insert_ids.push(id); |
| 159 |
to_insert_data.push(values); |
| 160 |
}); |
| 161 |
|
| 162 |
let mut entity = world.spawn_empty(); |
| 163 |
|
| 164 |
|
| 165 |
let to_insert_ptr = to_owning_ptrs(&mut to_insert_data); |
| 166 |
|
| 167 |
|
| 168 |
|
| 169 |
|
| 170 |
unsafe { |
| 171 |
entity.insert_by_ids(&to_insert_ids, to_insert_ptr.into_iter()); |
| 172 |
} |
| 173 |
|
| 174 |
println!("Entity spawned with id: {}", entity.id()); |
| 175 |
} |
| 176 |
"q" => { |
| 177 |
let mut builder = QueryBuilder::<FilteredEntityMut>::new(&mut world); |
| 178 |
parse_query(rest, &mut builder, &component_names); |
| 179 |
let mut query = builder.build(); |
| 180 |
query.iter_mut(&mut world).for_each(|filtered_entity| { |
| 181 |
let terms = filtered_entity |
| 182 |
.access() |
| 183 |
.try_iter_access() |
| 184 |
.unwrap() |
| 185 |
.map(|component_access| { |
| 186 |
let id = *component_access.index(); |
| 187 |
let ptr = filtered_entity.get_by_id(id).unwrap(); |
| 188 |
let info = component_info.get(&id).unwrap(); |
| 189 |
let len = info.layout().size() / size_of::<u64>(); |
| 190 |
|
| 191 |
|
| 192 |
|
| 193 |
|
| 194 |
let data = unsafe { |
| 195 |
std::slice::from_raw_parts_mut( |
| 196 |
ptr.assert_unique().as_ptr().cast::<u64>(), |
| 197 |
len, |
| 198 |
) |
| 199 |
}; |
| 200 |
|
| 201 |
|
| 202 |
if matches!(component_access, ComponentAccessKind::Exclusive(_)) { |
| 203 |
data.iter_mut().for_each(|data| { |
| 204 |
*data += 1; |
| 205 |
}); |
| 206 |
} |
| 207 |
|
| 208 |
format!("{}: {:?}", info.name(), data[0..len].to_vec()) |
| 209 |
}) |
| 210 |
.collect::<Vec<_>>() |
| 211 |
.join(", "); |
| 212 |
|
| 213 |
println!("{}: {}", filtered_entity.id(), terms); |
| 214 |
}); |
| 215 |
} |
| 216 |
"e" => { |
| 217 |
rest.split(',').for_each(|event| { |
| 218 |
let name = event.trim(); |
| 219 |
if name.is_empty() { |
| 220 |
return; |
| 221 |
} |
| 222 |
|
| 223 |
|
| 224 |
|
| 225 |
let event_component_id = world.register_component_with_descriptor(unsafe { |
| 226 |
ComponentDescriptor::new_with_layout( |
| 227 |
format!("event:{name}"), |
| 228 |
StorageType::Table, |
| 229 |
Layout::new::<()>(), |
| 230 |
None, |
| 231 |
false, |
| 232 |
false, |
| 233 |
ComponentCloneBehavior::Ignore, |
| 234 |
None, |
| 235 |
) |
| 236 |
}); |
| 237 |
|
| 238 |
let event_key = unsafe { EventKey::new(event_component_id) }; |
| 239 |
event_names.insert(name.to_string(), event_key); |
| 240 |
|
| 241 |
|
| 242 |
let runner: ObserverRunner = |mut world, _observer, ctx, _event, _trigger| { |
| 243 |
println!(" Observer fired!"); |
| 244 |
if let Some(mut counts) = world.get_resource_mut::<EventFireCount>() { |
| 245 |
*counts.0.entry(ctx.event_key).or_insert(0) += 1; |
| 246 |
} |
| 247 |
}; |
| 248 |
|
| 249 |
|
| 250 |
let observer = |
| 251 |
unsafe { Observer::with_dynamic_runner(runner).with_event_key(event_key) }; |
| 252 |
world.spawn(observer); |
| 253 |
|
| 254 |
println!( |
| 255 |
"Event '{name}' registered (key: {}) with a dynamic observer", |
| 256 |
event_component_id.index() |
| 257 |
); |
| 258 |
}); |
| 259 |
|
| 260 |
|
| 261 |
world.init_resource::<EventFireCount>(); |
| 262 |
} |
| 263 |
"t" => { |
| 264 |
let name = rest.trim(); |
| 265 |
let Some(&event_key) = event_names.get(name) else { |
| 266 |
println!( |
| 267 |
"Event '{name}' does not exist. Register it first with 'event {name}'" |
| 268 |
); |
| 269 |
continue; |
| 270 |
}; |
| 271 |
|
| 272 |
let mut event_data = (); |
| 273 |
let mut trigger_data = (); |
| 274 |
|
| 275 |
unsafe { |
| 276 |
world.trigger_dynamic( |
| 277 |
event_key, |
| 278 |
PtrMut::from(&mut event_data), |
| 279 |
PtrMut::from(&mut trigger_data), |
| 280 |
); |
| 281 |
} |
| 282 |
|
| 283 |
let count = world |
| 284 |
.get_resource::<EventFireCount>() |
| 285 |
.map_or(0, |c| c.0.get(&event_key).copied().unwrap_or(0)); |
| 286 |
println!("Event '{name}' triggered ({count} fires)"); |
| 287 |
} |
| 288 |
_ => continue, |
| 289 |
} |
| 290 |
} |
| 291 |
} |
| 292 |
|
| 293 |
|
| 294 |
#[derive(Resource, Default)] |
| 295 |
struct EventFireCount(HashMap<EventKey, u32>); |
| 296 |
|
| 297 |
|
| 298 |
|
| 299 |
fn to_owning_ptrs(components: &mut [Vec<u64>]) -> Vec<OwningPtr<'_, Aligned>> { |
| 300 |
components |
| 301 |
.iter_mut() |
| 302 |
.map(|data| { |
| 303 |
let ptr = data.as_mut_ptr(); |
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
unsafe { |
| 308 |
let non_null = NonNull::new_unchecked(ptr.cast()); |
| 309 |
OwningPtr::new(non_null) |
| 310 |
} |
| 311 |
}) |
| 312 |
.collect() |
| 313 |
} |
| 314 |
|
| 315 |
fn parse_term<Q: QueryData>( |
| 316 |
str: &str, |
| 317 |
builder: &mut QueryBuilder<Q>, |
| 318 |
components: &HashMap<String, ComponentId>, |
| 319 |
) { |
| 320 |
let mut matched = false; |
| 321 |
let str = str.trim(); |
| 322 |
match str.chars().next() { |
| 323 |
|
| 324 |
Some('?') => { |
| 325 |
builder.optional(|b| parse_term(&str[1..], b, components)); |
| 326 |
matched = true; |
| 327 |
} |
| 328 |
|
| 329 |
Some('&') => { |
| 330 |
let mut parts = str.split_whitespace(); |
| 331 |
let first = parts.next().unwrap(); |
| 332 |
if first == "&mut" { |
| 333 |
if let Some(str) = parts.next() |
| 334 |
&& let Some(&id) = components.get(str) |
| 335 |
{ |
| 336 |
builder.mut_id(id); |
| 337 |
matched = true; |
| 338 |
}; |
| 339 |
} else if let Some(&id) = components.get(&first[1..]) { |
| 340 |
builder.ref_id(id); |
| 341 |
matched = true; |
| 342 |
} |
| 343 |
} |
| 344 |
|
| 345 |
Some(_) => { |
| 346 |
if let Some(&id) = components.get(str) { |
| 347 |
builder.with_id(id); |
| 348 |
matched = true; |
| 349 |
} |
| 350 |
} |
| 351 |
None => {} |
| 352 |
}; |
| 353 |
|
| 354 |
if !matched { |
| 355 |
println!("Unable to find component: {str}"); |
| 356 |
} |
| 357 |
} |
| 358 |
|
| 359 |
fn parse_query<Q: QueryData>( |
| 360 |
str: &str, |
| 361 |
builder: &mut QueryBuilder<Q>, |
| 362 |
components: &HashMap<String, ComponentId>, |
| 363 |
) { |
| 364 |
let str = str.split(','); |
| 365 |
str.for_each(|term| { |
| 366 |
let sub_terms: Vec<_> = term.split("||").collect(); |
| 367 |
if sub_terms.len() == 1 { |
| 368 |
parse_term(sub_terms[0], builder, components); |
| 369 |
} else { |
| 370 |
builder.or(|b| { |
| 371 |
sub_terms |
| 372 |
.iter() |
| 373 |
.for_each(|term| parse_term(term, b, components)); |
| 374 |
}); |
| 375 |
} |
| 376 |
}); |
| 377 |
} |