1#![allow(clippy::result_large_err)] use bevy::prelude::*;
11use core::sync::atomic::{AtomicUsize, Ordering};
12use wyrd::core::{HostTime, ONE, ZERO};
13use wyrd::graph::Weave;
14use wyrd::runtime::{BindError, HandleError, HostPathId, Outbox, Runtime, SenseId};
15
16#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
18pub enum WyrdSet {
19 Sample,
21 Loom,
23 Apply,
25}
26
27pub struct WyrdInstance {
29 label: String,
30 runtime: Runtime,
31 tick: u64,
32}
33
34impl WyrdInstance {
35 pub fn new(label: impl Into<String>, weave: Weave) -> Result<Self, BindError> {
37 let runtime = Runtime::bind(weave, Default::default())?;
38 Ok(Self {
39 label: label.into(),
40 runtime,
41 tick: 0,
42 })
43 }
44
45 pub fn label(&self) -> &str {
47 &self.label
48 }
49
50 pub fn tick(&self) -> u64 {
52 self.tick
53 }
54
55 pub fn runtime(&self) -> &Runtime {
57 &self.runtime
58 }
59
60 pub fn runtime_mut(&mut self) -> &mut Runtime {
62 &mut self.runtime
63 }
64
65 pub fn outbox(&self) -> Outbox<'_> {
67 self.runtime.outbox()
68 }
69
70 pub fn sense_id(&self, name: &str) -> Option<SenseId> {
72 self.runtime.sense_id(name)
73 }
74
75 pub fn path_id(&self, path: &str) -> Option<HostPathId> {
77 self.runtime.path_id(path)
78 }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86pub struct WyrdInstanceId {
87 owner: usize,
88 index: usize,
89 generation: u64,
90}
91
92struct WyrdInstanceSlot {
93 generation: u64,
94 instance: Option<WyrdInstance>,
95 next_free: Option<usize>,
96}
97
98static NEXT_WORLD_OWNER: AtomicUsize = AtomicUsize::new(1);
100
101fn next_world_owner() -> usize {
102 NEXT_WORLD_OWNER
103 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |owner| {
104 owner.checked_add(1)
105 })
106 .expect("WyrdWorld owner token space exhausted")
107}
108
109#[derive(Resource)]
111pub struct WyrdWorld {
112 owner: usize,
113 slots: Vec<WyrdInstanceSlot>,
114 free_head: Option<usize>,
115 len: usize,
116}
117
118impl Default for WyrdWorld {
119 fn default() -> Self {
120 Self {
121 owner: next_world_owner(),
122 slots: Vec::new(),
123 free_head: None,
124 len: 0,
125 }
126 }
127}
128
129impl WyrdWorld {
130 pub fn insert(&mut self, instance: WyrdInstance) -> WyrdInstanceId {
132 let (index, generation) = if let Some(index) = self.free_head {
133 let slot = &mut self.slots[index];
134 self.free_head = slot.next_free.take();
135 debug_assert!(slot.instance.is_none());
136 slot.instance = Some(instance);
137 (index, slot.generation)
138 } else {
139 let index = self.slots.len();
140 self.slots.push(WyrdInstanceSlot {
141 generation: 0,
142 instance: Some(instance),
143 next_free: None,
144 });
145 (index, 0)
146 };
147 self.len += 1;
148 WyrdInstanceId {
149 owner: self.owner,
150 index,
151 generation,
152 }
153 }
154
155 pub fn remove(&mut self, id: WyrdInstanceId) -> Option<WyrdInstance> {
160 if id.owner != self.owner {
161 return None;
162 }
163 let slot = self.slots.get_mut(id.index)?;
164 if slot.generation != id.generation {
165 return None;
166 }
167 let instance = slot.instance.take()?;
168 self.len -= 1;
169
170 if let Some(generation) = slot.generation.checked_add(1) {
171 slot.generation = generation;
172 slot.next_free = self.free_head;
173 self.free_head = Some(id.index);
174 } else {
175 slot.next_free = None;
176 }
177
178 Some(instance)
179 }
180
181 pub fn get(&self, id: WyrdInstanceId) -> Option<&WyrdInstance> {
183 if id.owner != self.owner {
184 return None;
185 }
186 let slot = self.slots.get(id.index)?;
187 if slot.generation == id.generation {
188 slot.instance.as_ref()
189 } else {
190 None
191 }
192 }
193
194 pub fn get_mut(&mut self, id: WyrdInstanceId) -> Option<&mut WyrdInstance> {
196 if id.owner != self.owner {
197 return None;
198 }
199 let slot = self.slots.get_mut(id.index)?;
200 if slot.generation == id.generation {
201 slot.instance.as_mut()
202 } else {
203 None
204 }
205 }
206
207 pub fn iter(&self) -> impl Iterator<Item = (WyrdInstanceId, &WyrdInstance)> {
209 self.slots.iter().enumerate().filter_map(|(index, slot)| {
210 slot.instance.as_ref().map(|instance| {
211 (
212 WyrdInstanceId {
213 owner: self.owner,
214 index,
215 generation: slot.generation,
216 },
217 instance,
218 )
219 })
220 })
221 }
222
223 pub fn iter_mut(&mut self) -> impl Iterator<Item = (WyrdInstanceId, &mut WyrdInstance)> {
225 let owner = self.owner;
226 self.slots
227 .iter_mut()
228 .enumerate()
229 .filter_map(move |(index, slot)| {
230 let generation = slot.generation;
231 slot.instance.as_mut().map(|instance| {
232 (
233 WyrdInstanceId {
234 owner,
235 index,
236 generation,
237 },
238 instance,
239 )
240 })
241 })
242 }
243
244 pub fn len(&self) -> usize {
246 self.len
247 }
248
249 pub fn is_empty(&self) -> bool {
251 self.len == 0
252 }
253}
254
255#[derive(Resource, Clone, Copy, Debug)]
258pub struct AndDoorBinding {
259 pub plate_a: SenseId,
261 pub plate_b: SenseId,
263 pub door_path: HostPathId,
265 pub instance: WyrdInstanceId,
267}
268
269#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq)]
271pub struct Door {
272 pub open: bool,
274}
275
276#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
280pub struct WyrdSignalConfirm {
281 pub path: HostPathId,
283 pub truthy: bool,
285}
286
287pub struct WyrdPlugin;
289
290impl Plugin for WyrdPlugin {
291 fn build(&self, app: &mut App) {
292 app.init_resource::<WyrdWorld>()
293 .add_message::<WyrdSignalConfirm>()
294 .configure_sets(
295 Update,
296 (WyrdSet::Sample, WyrdSet::Loom, WyrdSet::Apply).chain(),
297 )
298 .add_systems(Update, loom_all.in_set(WyrdSet::Loom));
299 }
300}
301
302pub fn loom_all(mut world: ResMut<WyrdWorld>) {
307 for (_, inst) in world.iter_mut() {
308 inst.tick = inst.tick.wrapping_add(1);
309 inst.runtime.begin_frame(HostTime { tick: inst.tick });
310 inst.runtime.loom();
311 }
312}
313
314#[inline]
316pub fn set_sense_bool(inst: &mut WyrdInstance, id: SenseId, on: bool) -> Result<(), HandleError> {
317 let v = if on { ONE } else { ZERO };
318 inst.runtime.port_writer().set_sense(id, v)
319}
320
321pub fn signal_truthy(inst: &WyrdInstance, path: HostPathId) -> Result<bool, HandleError> {
326 inst.runtime.path_name(path)?;
327 Ok(inst
328 .runtime
329 .outbox()
330 .signals()
331 .iter()
332 .find(|s| s.path == path)
333 .map(|s| wyrd::core::is_truthy(s.value))
334 .unwrap_or(false))
335}
336
337pub fn apply_signal_bool(
342 inst: &WyrdInstance,
343 path: HostPathId,
344 slot: &mut bool,
345) -> Result<bool, HandleError> {
346 let next = signal_truthy(inst, path)?;
347 let changed = *slot != next;
348 *slot = next;
349 Ok(changed)
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use wyrd::core::{KnotKind, SignalDomain};
356
357 fn and_door_weave() -> Weave {
358 let mut b = Weave::builder("door").unwrap();
359 let pa = b
360 .knot("plate_a", KnotKind::signal_in(SignalDomain::Bool))
361 .unwrap();
362 let pb = b
363 .knot("plate_b", KnotKind::signal_in(SignalDomain::Bool))
364 .unwrap();
365 let both = b.knot("both", KnotKind::and2()).unwrap();
366 let door = b
367 .knot(
368 "door",
369 KnotKind::signal_out("door.open", SignalDomain::Bool),
370 )
371 .unwrap();
372 let from = b.output(&pa, "out").unwrap();
373 let to = b.input(&both, "in_0").unwrap();
374 b.connect(from, to).unwrap();
375 let from = b.output(&pb, "out").unwrap();
376 let to = b.input(&both, "in_1").unwrap();
377 b.connect(from, to).unwrap();
378 let from = b.output(&both, "out").unwrap();
379 let to = b.input(&door, "in").unwrap();
380 b.connect(from, to).unwrap();
381 b.build().unwrap()
382 }
383
384 #[derive(Resource, Default)]
385 struct DoorOpen(bool);
386
387 #[derive(Resource, Clone, Copy)]
388 struct PlateState {
389 a: bool,
390 b: bool,
391 }
392
393 fn sample_plates(
394 plates: Option<Res<PlateState>>,
395 binding: Res<AndDoorBinding>,
396 mut world: ResMut<WyrdWorld>,
397 ) {
398 let Some(plates) = plates else {
399 return;
400 };
401 let Some(inst) = world.get_mut(binding.instance) else {
402 return;
403 };
404 set_sense_bool(inst, binding.plate_a, plates.a).expect("bound plate_a handle");
405 set_sense_bool(inst, binding.plate_b, plates.b).expect("bound plate_b handle");
406 }
407
408 fn apply_door(binding: Res<AndDoorBinding>, world: Res<WyrdWorld>, mut door: ResMut<DoorOpen>) {
409 let Some(inst) = world.get(binding.instance) else {
410 return;
411 };
412 door.0 = signal_truthy(inst, binding.door_path).expect("bound door path");
413 }
414
415 fn apply_door_component(
416 binding: Res<AndDoorBinding>,
417 world: Res<WyrdWorld>,
418 mut q: Query<&mut Door>,
419 mut confirms: MessageWriter<WyrdSignalConfirm>,
420 ) {
421 let Some(inst) = world.get(binding.instance) else {
422 return;
423 };
424 for mut door in &mut q {
425 if apply_signal_bool(inst, binding.door_path, &mut door.open).expect("bound door path")
426 {
427 confirms.write(WyrdSignalConfirm {
428 path: binding.door_path,
429 truthy: door.open,
430 });
431 }
432 }
433 }
434
435 #[test]
436 fn headless_app_and_door() {
437 let mut app = App::new();
438 app.add_plugins(MinimalPlugins)
439 .add_plugins(WyrdPlugin)
440 .init_resource::<DoorOpen>();
441
442 let weave = and_door_weave();
443 let inst = WyrdInstance::new("demo", weave).unwrap();
444 assert!(inst.sense_id("nope").is_none());
445 assert!(inst.path_id("nope").is_none());
446 let plate_a = inst.sense_id("plate_a").unwrap();
447 let plate_b = inst.sense_id("plate_b").unwrap();
448 let door_path = inst.path_id("door.open").unwrap();
449 let foreign = WyrdInstance::new("foreign", and_door_weave()).unwrap();
450 assert_eq!(
451 signal_truthy(&inst, foreign.path_id("door.open").unwrap()),
452 Err(HandleError::ForeignRuntime {
453 handle: "host path"
454 })
455 );
456 let mut open = true;
457 assert_eq!(
458 apply_signal_bool(&inst, foreign.path_id("door.open").unwrap(), &mut open),
459 Err(HandleError::ForeignRuntime {
460 handle: "host path"
461 })
462 );
463 assert!(open, "a rejected handle must not mutate host state");
464
465 let instance = app.world_mut().resource_mut::<WyrdWorld>().insert(inst);
466 let binding = AndDoorBinding {
467 plate_a,
468 plate_b,
469 door_path,
470 instance,
471 };
472 app.insert_resource(binding);
473 app.add_systems(Update, sample_plates.in_set(WyrdSet::Sample));
474 app.add_systems(Update, apply_door.in_set(WyrdSet::Apply));
475
476 app.update();
477 assert!(!app.world().resource::<DoorOpen>().0);
478
479 app.world_mut()
480 .insert_resource(PlateState { a: true, b: false });
481 app.update();
482 assert!(!app.world().resource::<DoorOpen>().0);
483
484 app.world_mut()
485 .insert_resource(PlateState { a: true, b: true });
486 app.update();
487 assert!(app.world().resource::<DoorOpen>().0);
488
489 let removed = app
490 .world_mut()
491 .resource_mut::<WyrdWorld>()
492 .remove(binding.instance)
493 .unwrap();
494 let removed_tick = removed.tick();
495 app.update();
496 assert_eq!(removed.tick(), removed_tick);
497 }
498
499 #[derive(Resource, Default)]
500 struct ConfirmLog(Vec<WyrdSignalConfirm>);
501
502 fn log_confirms(mut reader: MessageReader<WyrdSignalConfirm>, mut log: ResMut<ConfirmLog>) {
503 for m in reader.read() {
504 log.0.push(*m);
505 }
506 }
507
508 #[test]
509 fn door_component_and_confirmation_message() {
510 let mut app = App::new();
511 app.add_plugins(MinimalPlugins)
512 .add_plugins(WyrdPlugin)
513 .init_resource::<ConfirmLog>();
514
515 let weave = and_door_weave();
516 let inst = WyrdInstance::new("demo", weave).unwrap();
517 let plate_a = inst.sense_id("plate_a").unwrap();
518 let plate_b = inst.sense_id("plate_b").unwrap();
519 let door_path = inst.path_id("door.open").unwrap();
520 let instance = app.world_mut().resource_mut::<WyrdWorld>().insert(inst);
521 let binding = AndDoorBinding {
522 plate_a,
523 plate_b,
524 door_path,
525 instance,
526 };
527 let door_path = binding.door_path;
528 app.insert_resource(binding);
529 app.world_mut().spawn(Door { open: false });
530 app.add_systems(Update, sample_plates.in_set(WyrdSet::Sample));
531 app.add_systems(Update, apply_door_component.in_set(WyrdSet::Apply));
532 app.add_systems(Update, log_confirms.after(WyrdSet::Apply));
533
534 app.world_mut()
535 .insert_resource(PlateState { a: true, b: true });
536 app.update();
537
538 let door = app
539 .world_mut()
540 .query::<&Door>()
541 .single(app.world())
542 .expect("door entity");
543 assert!(door.open);
544
545 {
546 let log = app.world().resource::<ConfirmLog>();
547 assert!(
548 log.0.iter().any(|c| c.path == door_path && c.truthy),
549 "expected WyrdSignalConfirm for door.open"
550 );
551 }
552
553 app.world_mut()
554 .resource_mut::<WyrdWorld>()
555 .remove(binding.instance)
556 .expect("bound instance");
557 app.update();
558 assert_eq!(app.world().resource::<ConfirmLog>().0.len(), 1);
559 }
560
561 #[test]
562 fn instance_state_is_exposed_through_accessors() {
563 let mut inst = WyrdInstance::new("demo", and_door_weave()).unwrap();
564 assert_eq!(inst.label(), "demo");
565 assert_eq!(inst.tick(), 0);
566 assert!(inst.runtime().sense_id("plate_a").is_some());
567 assert!(inst.runtime_mut().sense_id("plate_b").is_some());
568 assert!(inst.outbox().signals().is_empty());
569 }
570
571 #[test]
572 fn generational_ids_survive_removal_and_reject_stale_handles() {
573 let mut world = WyrdWorld::default();
574 let first = world.insert(WyrdInstance::new("first", and_door_weave()).unwrap());
575 let second = world.insert(WyrdInstance::new("second", and_door_weave()).unwrap());
576
577 let removed = world.remove(first).unwrap();
578 assert_eq!(removed.label(), "first");
579 assert_eq!(world.get(second).unwrap().label(), "second");
580 assert!(world.get(first).is_none());
581
582 let replacement = world.insert(WyrdInstance::new("replacement", and_door_weave()).unwrap());
583 assert_eq!(replacement.index, first.index);
584 assert_ne!(replacement.generation, first.generation);
585 assert!(world.get(first).is_none());
586 assert!(world.remove(first).is_none());
587 assert_eq!(world.get(replacement).unwrap().label(), "replacement");
588 assert_eq!(world.len(), 2);
589 assert_eq!(world.iter().count(), 2);
590 let iter_ids: Vec<_> = world.iter().map(|(id, _)| id).collect();
591 assert!(iter_ids.iter().all(|id| world.get(*id).is_some()));
592 let iter_mut_ids: Vec<_> = world.iter_mut().map(|(id, _)| id).collect();
593 assert!(iter_mut_ids.iter().all(|id| world.get(*id).is_some()));
594 assert!(!world.is_empty());
595 }
596
597 #[test]
598 fn instance_ids_are_rejected_by_other_worlds() {
599 let mut first_world = WyrdWorld::default();
600 let mut second_world = WyrdWorld::default();
601 let first_id = first_world.insert(WyrdInstance::new("first", and_door_weave()).unwrap());
602 let second_id = second_world.insert(WyrdInstance::new("second", and_door_weave()).unwrap());
603
604 assert!(second_world.get(first_id).is_none());
605 assert!(second_world.get_mut(first_id).is_none());
606 assert!(second_world.remove(first_id).is_none());
607 assert_eq!(second_world.get(second_id).unwrap().label(), "second");
608 }
609
610 #[test]
611 fn replacing_world_resource_invalidates_old_ids() {
612 let mut app = App::new();
613 app.init_resource::<WyrdWorld>();
614 let old_id = app
615 .world_mut()
616 .resource_mut::<WyrdWorld>()
617 .insert(WyrdInstance::new("old", and_door_weave()).unwrap());
618
619 app.insert_resource(WyrdWorld::default());
620 let new_id = app
621 .world_mut()
622 .resource_mut::<WyrdWorld>()
623 .insert(WyrdInstance::new("new", and_door_weave()).unwrap());
624
625 let world = app.world().resource::<WyrdWorld>();
626 assert!(world.get(old_id).is_none());
627 assert_eq!(world.get(new_id).unwrap().label(), "new");
628 }
629
630 #[test]
631 fn generation_overflow_retires_slot() {
632 let mut world = WyrdWorld::default();
633 let original = world.insert(WyrdInstance::new("original", and_door_weave()).unwrap());
634 world.slots[original.index].generation = u64::MAX;
635 let final_id = WyrdInstanceId {
636 owner: world.owner,
637 index: original.index,
638 generation: u64::MAX,
639 };
640
641 world.remove(final_id).unwrap();
642 let replacement = world.insert(WyrdInstance::new("replacement", and_door_weave()).unwrap());
643
644 assert_ne!(replacement.index, final_id.index);
645 assert!(world.get(final_id).is_none());
646 assert_eq!(world.get(replacement).unwrap().label(), "replacement");
647 }
648}