use bevy::{platform::collections::HashMap, prelude::*, time::Real};
use iroh::EndpointId;
use iroh_gossip::proto::TopicId;
use super::{
Authority, Codec, InRoom, NetId, NetIds, Outbox, Registry, Remote, Replica, Shared, Snapshots,
wire,
};
use crate::{
net::{Payload, proto::MAX_ITEMS},
node::{Iroh, ToNet},
room::{PeerJoined, RoomTopic, Rooms},
};
const MANIFEST_EVERY: f64 = 1.0;
pub(crate) fn on_remove_shared(
remove: On<Remove, Shared>,
q: Query<(&NetId, &Replica), Without<Remote>>,
mut outbox: ResMut<Outbox>,
) {
if let Ok((id, replica)) = q.get(remove.entity) {
outbox.despawned.push((
*id,
replica.lamport + 1,
replica.announced.iter().copied().collect(),
));
}
}
pub(crate) fn on_remove_source<C: Codec>(
remove: On<Remove, C::Source>,
q: Query<(&NetId, &Replica), (With<Shared>, Without<Remote>)>,
registry: Option<Res<Registry>>,
mut outbox: ResMut<Outbox>,
) {
if let Ok((id, replica)) = q.get(remove.entity)
&& let Some(key) = registry.and_then(|r| r.key_of::<C::Source>())
{
outbox.removed.push((
*id,
replica.lamport + 1,
key,
replica.announced.iter().copied().collect(),
));
}
}
pub(crate) fn queue_snapshots(
mut joined: MessageReader<PeerJoined>,
rooms: Query<&RoomTopic>,
mut snapshots: ResMut<Snapshots>,
) {
for PeerJoined { room, id, .. } in joined.read() {
if let Ok(topic) = rooms.get(*room) {
snapshots.pending.push((topic.0, *id, None));
}
}
}
struct Local {
entity: Entity,
id: NetId,
authority: Authority,
topics: Vec<TopicId>,
}
fn topics_for(world: &World, entity: Entity, joined: &[(TopicId, Entity)]) -> Vec<TopicId> {
match world.get::<InRoom>(entity) {
Some(InRoom(room)) => joined
.iter()
.filter(|(_, e)| e == room)
.map(|(t, _)| *t)
.collect(),
None => joined.iter().map(|(t, _)| *t).collect(),
}
}
fn broadcast<P: Payload>(iroh: &Iroh, topic: TopicId, payload: &P) {
if let Ok(body) = payload.to_body() {
iroh.send(ToNet::Broadcast {
topic,
kind: P::KIND,
body,
});
}
}
fn send_to<P: Payload>(iroh: &Iroh, topic: TopicId, to: EndpointId, payload: &P) {
if let Ok(body) = payload.to_body() {
iroh.send(ToNet::SendTo {
topic,
to,
kind: P::KIND,
body,
});
}
}
fn encode_all(world: &World, registry: &Registry, entity: Entity) -> Vec<wire::Component> {
registry
.entries()
.iter()
.filter_map(|e| e.codec.encode(world, entity).map(|bytes| (e.key, bytes)))
.collect()
}
pub(crate) fn send(
world: &mut World,
mut manifests: bevy::ecs::system::Local<HashMap<TopicId, f64>>,
) {
let Some(me) = world.get_resource::<Iroh>().map(|i| i.id()) else {
return;
};
let joined: Vec<(TopicId, Entity)> = world.resource::<Rooms>().joined().collect();
let now = world.resource::<Time<Real>>().elapsed_secs_f64();
let fresh: Vec<Entity> = world
.query_filtered::<Entity, (With<Shared>, Without<Replica>)>()
.iter(world)
.collect();
for entity in fresh {
let id = *world.get::<NetId>(entity).expect("Shared requires NetId");
let mut e = world.entity_mut(entity);
e.insert(Replica::local());
if !e.contains::<super::Owner>() {
e.insert(super::Owner(me));
}
world.resource_mut::<NetIds>().insert(id, entity);
}
let locals: Vec<Local> = world
.query_filtered::<(Entity, &NetId, &Shared), (With<Replica>, Without<Remote>)>()
.iter(world)
.map(|(entity, id, shared)| Local {
entity,
id: *id,
authority: shared.authority,
topics: Vec::new(),
})
.collect::<Vec<_>>()
.into_iter()
.map(|mut l| {
l.topics = topics_for(world, l.entity, &joined);
l
})
.collect();
world.resource_scope(|world, registry: Mut<Registry>| {
let mut spawns: Vec<(TopicId, wire::Spawn, Entity)> = Vec::new();
for l in &locals {
let replica = world.get::<Replica>(l.entity).unwrap();
let missing: Vec<TopicId> = l
.topics
.iter()
.copied()
.filter(|t| !replica.announced.contains(t))
.collect();
if missing.is_empty() {
continue;
}
let spawn = wire::Spawn {
id: l.id,
owner: me,
authority: l.authority,
lamport: replica.lamport,
components: encode_all(world, ®istry, l.entity),
};
for topic in missing {
spawns.push((topic, spawn.clone(), l.entity));
}
}
let iroh = world.resource::<Iroh>();
for (topic, spawn, _) in &spawns {
broadcast(iroh, *topic, spawn);
}
for (topic, _, entity) in spawns {
world
.get_mut::<Replica>(entity)
.unwrap()
.announced
.insert(topic);
}
});
let mut changed: HashMap<Entity, Vec<wire::Component>> = HashMap::new();
world.resource_scope(|world, mut registry: Mut<Registry>| {
let mut out = Vec::new();
for entry in registry.entries_mut() {
if now - entry.last_sent < entry.rate.interval() {
continue;
}
entry.last_sent = now;
out.clear();
entry.codec.collect(world, &mut out);
for (entity, bytes) in out.drain(..) {
changed.entry(entity).or_default().push((entry.key, bytes));
}
}
});
let mut updates: HashMap<TopicId, Vec<wire::EntityUpdate>> = HashMap::new();
for l in &locals {
let Some(components) = changed.remove(&l.entity) else {
continue;
};
let mut replica = world.get_mut::<Replica>(l.entity).unwrap();
replica.lamport += 1;
let lamport = replica.lamport;
let announced: Vec<TopicId> = l
.topics
.iter()
.copied()
.filter(|t| replica.announced.contains(t))
.collect();
for topic in announced {
updates.entry(topic).or_default().push(wire::EntityUpdate {
id: l.id,
lamport,
components: components.clone(),
});
}
}
let mut outbox = std::mem::take(&mut *world.resource_mut::<Outbox>());
for (id, ..) in &outbox.despawned {
world.resource_mut::<NetIds>().remove(*id);
}
let despawned_ids: Vec<NetId> = outbox.despawned.iter().map(|(id, ..)| *id).collect();
outbox
.removed
.retain(|(id, ..)| !despawned_ids.contains(id));
let mut manifest_out: Vec<(TopicId, wire::Manifest)> = Vec::new();
for (topic, _) in &joined {
let due = manifests
.get(topic)
.is_none_or(|last| now - last >= MANIFEST_EVERY);
if !due {
continue;
}
manifests.insert(*topic, now);
let entries: Vec<(NetId, u64)> = locals
.iter()
.filter(|l| {
world
.get::<Replica>(l.entity)
.is_some_and(|r| r.announced.contains(topic))
})
.map(|l| (l.id, world.get::<Replica>(l.entity).unwrap().lamport))
.collect();
if !entries.is_empty() {
manifest_out.push((*topic, wire::Manifest { entries }));
}
}
let pending = std::mem::take(&mut world.resource_mut::<Snapshots>().pending);
let mut snapshot_out: Vec<(TopicId, EndpointId, wire::Snapshot)> = Vec::new();
world.resource_scope(|world, registry: Mut<Registry>| {
for (topic, to, ids) in pending {
let spawns: Vec<wire::Spawn> = locals
.iter()
.filter(|l| ids.as_ref().is_none_or(|ids| ids.contains(&l.id)))
.filter(|l| {
world
.get::<Replica>(l.entity)
.is_some_and(|r| r.announced.contains(&topic))
})
.map(|l| wire::Spawn {
id: l.id,
owner: me,
authority: l.authority,
lamport: world.get::<Replica>(l.entity).unwrap().lamport,
components: encode_all(world, ®istry, l.entity),
})
.collect();
if !spawns.is_empty() {
snapshot_out.push((topic, to, wire::Snapshot { spawns }));
}
}
});
let iroh = world.resource::<Iroh>();
for (topic, entities) in updates {
for chunk in entities.chunks(MAX_ITEMS) {
broadcast(
iroh,
topic,
&wire::Update {
entities: chunk.to_vec(),
},
);
}
}
for (id, lamport, key, topics) in outbox.removed {
for topic in topics {
broadcast(iroh, topic, &wire::Remove { id, lamport, key });
}
}
for (id, lamport, topics) in outbox.despawned {
for topic in topics {
broadcast(iroh, topic, &wire::Despawn { id, lamport });
}
}
for (topic, manifest) in manifest_out {
for chunk in manifest.entries.chunks(MAX_ITEMS) {
broadcast(
iroh,
topic,
&wire::Manifest {
entries: chunk.to_vec(),
},
);
}
}
for (topic, to, snapshot) in snapshot_out {
for chunk in snapshot.spawns.chunks(MAX_ITEMS) {
send_to(
iroh,
topic,
to,
&wire::Snapshot {
spawns: chunk.to_vec(),
},
);
}
}
}