unlit3dx/handle.rs
1//! The handle every spawn hands back.
2//!
3//! A spawned world — a sub-world on the host thread, a threaded world on its
4//! own — is reached the same way: a bounded command channel that never
5//! blocks. [`crate::handle::WorldHandle`] is that uniform handle. The spawn
6//! functions return it instead of a bare entity, so a caller queues work
7//! without a second lookup for the proxy component on the host.
8
9use unlit_ecs::Entity;
10
11use crate::channel::SnapshotReader;
12use crate::job::JobSender;
13use crate::thread::{ThreadStatus, WorldState};
14
15/// A cloneable handle to a spawned world.
16///
17/// It carries the host entity the world's proxy component lives on, a
18/// cloneable command sender, and the state the world publishes about itself.
19/// It can be handed to the world that queues work without the host's `World`
20/// in hand. Cloning it does not spawn anything: the world lives on until the
21/// host drops the proxy entity.
22#[derive(Clone)]
23pub struct WorldHandle {
24 /// The host entity the world's proxy component lives on.
25 pub entity: Entity,
26 /// The command queue. Queueing through [`send`](JobSender::send) never
27 /// blocks: a full or stopped queue reports
28 /// [`SendError`](crate::job::SendError) and hands the job back.
29 pub sender: JobSender,
30 /// What the world is doing, and how it ended.
31 ///
32 /// A world publishes its state, so a reader that arrives after the end
33 /// still learns how the world ended, and reading never takes the answer
34 /// away from another reader.
35 pub state: SnapshotReader<WorldState>,
36 status: ThreadStatus,
37}
38
39impl WorldHandle {
40 /// Bind a handle to the entity carrying the world's proxy.
41 ///
42 /// A runner of the caller's own builds its handle this way: the world it
43 /// drives shares its thread, so it stays running until that runner ends
44 /// it.
45 #[must_use]
46 pub fn new(entity: Entity, sender: JobSender) -> Self {
47 Self::with_status(entity, sender, ThreadStatus::detached())
48 }
49
50 /// Bind a handle to a world the spawn functions drive themselves.
51 #[must_use]
52 pub(crate) fn with_status(entity: Entity, sender: JobSender, status: ThreadStatus) -> Self {
53 Self {
54 entity,
55 sender,
56 state: status.state(),
57 status,
58 }
59 }
60
61 /// Whether the world has stopped or died.
62 ///
63 /// It reads the same state the world publishes, so a world of any kind
64 /// answers it: a threaded world when its thread ends, a stepped world
65 /// when its tick ends or its owner despawns it, and a sub-world when its
66 /// host stops it or drops it.
67 #[must_use]
68 pub fn is_finished(&self) -> bool {
69 self.status.is_finished()
70 }
71
72 /// Ask the world to stop, without waiting for it.
73 ///
74 /// The world ends on its driver's next turn — the thread's next loop, the
75 /// host's next [`step_stepped_world`](crate::world::step_stepped_world) or
76 /// [`drain_sub_world`](crate::host::drain_sub_world) — and only then does
77 /// its queue close. A job sent in between is accepted and then dropped,
78 /// so a caller that must know the job ran reads [`state`](Self::state)
79 /// rather than trusting the send.
80 pub fn stop(&self) {
81 self.status.stop();
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use unlit_ecs::World;
88
89 /// A handle queues jobs into the world it was spawned for.
90 #[test]
91 fn a_handle_queues_into_its_world() {
92 let mut host = World::new();
93 let handle = crate::host::spawn_sub_world(&mut host, "nested", 8);
94 let ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
95 let seen = std::sync::Arc::clone(&ran);
96 handle
97 .sender
98 .send(Box::new(move |_: &mut World| {
99 seen.store(true, std::sync::atomic::Ordering::SeqCst);
100 }))
101 .expect("the queue has room");
102 let mut world_row = host
103 .root_scope()
104 .query_one::<&mut World>(handle.entity)
105 .expect("the nested world");
106
107 let nested = world_row.get();
108 crate::host::drain_sub_world(nested);
109 assert!(ran.load(std::sync::atomic::Ordering::SeqCst));
110 }
111}