diff --git a/crates/messages/src/lib.rs b/crates/messages/src/lib.rs index 6554d987..b8440fe9 100644 --- a/crates/messages/src/lib.rs +++ b/crates/messages/src/lib.rs @@ -4,7 +4,7 @@ pub use game::{FromGame, JoinError, Readiness, ToGame}; pub use players::{ BorrowedFromPlayers, ChatMessage, ChatMessageError, EntityNet, FromPlayers, HealthDelta, - ToPlayers, MAX_CHAT_LEN, + NetEntityIndex, ToPlayers, MAX_CHAT_LEN, }; pub use server::{FromServer, GameOpenError, ToServer}; diff --git a/crates/messages/src/players/entity.rs b/crates/messages/src/players/entity.rs index d9291b06..43627d39 100644 --- a/crates/messages/src/players/entity.rs +++ b/crates/messages/src/players/entity.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "bevy")] +use bevy::ecs::entity::Entity; use bincode::{Decode, Encode}; use de_types::player::Player; @@ -5,7 +7,7 @@ use de_types::player::Player; #[derive(Clone, Copy, Debug, Encode, Decode, Hash, PartialEq, Eq)] pub struct EntityNet { player: Player, - index: u32, + index: NetEntityIndex, } impl EntityNet { @@ -15,11 +17,31 @@ impl EntityNet { /// instance. /// /// * `index` - locally unique index of the entity. - pub fn new(player: Player, index: u32) -> Self { + pub fn new(player: Player, index: NetEntityIndex) -> Self { Self { player, index } } - pub fn index(&self) -> u32 { + pub fn player(&self) -> Player { + self.player + } + + pub fn index(&self) -> NetEntityIndex { self.index } } + +#[derive(Clone, Copy, Debug, Encode, Decode, Hash, PartialEq, Eq)] +pub struct NetEntityIndex(u32); + +impl From for u32 { + fn from(index: NetEntityIndex) -> u32 { + index.0 + } +} + +#[cfg(feature = "bevy")] +impl From for NetEntityIndex { + fn from(entity: Entity) -> Self { + Self(entity.index()) + } +} diff --git a/crates/messages/src/players/mod.rs b/crates/messages/src/players/mod.rs index 2a3cb56c..d66719e8 100644 --- a/crates/messages/src/players/mod.rs +++ b/crates/messages/src/players/mod.rs @@ -1,7 +1,7 @@ use bincode::{Decode, Encode}; pub use chat::{ChatMessage, ChatMessageError, MAX_CHAT_LEN}; use de_types::{objects::ActiveObjectType, player::Player}; -pub use entity::EntityNet; +pub use entity::{EntityNet, NetEntityIndex}; pub use geom::{TransformNet, Vec2Net, Vec3Net, Vec4Net}; pub use path::{PathError, PathNet}; diff --git a/crates/multiplayer/src/lib.rs b/crates/multiplayer/src/lib.rs index f4e4726c..ba943acf 100644 --- a/crates/multiplayer/src/lib.rs +++ b/crates/multiplayer/src/lib.rs @@ -24,7 +24,7 @@ pub use crate::{ messages::{MessagesSet, ToPlayersEvent}, netstate::NetState, playermsg::{ - GameNetSet, NetEntities, NetRecvDespawnActiveEvent, NetRecvHealthEvent, + GameNetSet, NetEntities, NetEntityCommands, NetRecvDespawnActiveEvent, NetRecvHealthEvent, NetRecvSpawnActiveEvent, }, }; diff --git a/crates/multiplayer/src/playermsg.rs b/crates/multiplayer/src/playermsg.rs index bc9482b2..77ecb030 100644 --- a/crates/multiplayer/src/playermsg.rs +++ b/crates/multiplayer/src/playermsg.rs @@ -4,7 +4,7 @@ use bevy::{ prelude::*, }; use de_core::{gconfig::GameConfig, schedule::PreMovement, state::AppState}; -use de_messages::{EntityNet, ToPlayers}; +use de_messages::{EntityNet, NetEntityIndex, ToPlayers}; use de_types::{objects::ActiveObjectType, player::Player}; use crate::messages::{FromPlayersEvent, MessagesSet}; @@ -148,7 +148,7 @@ impl<'w> NetEntities<'w> { /// It is assumed that the entity exists. pub fn local_net_id(&self, entity: Entity) -> EntityNet { let player = self.config.locals().playable(); - EntityNet::new(player, entity.index()) + EntityNet::new(player, entity.into()) } } @@ -159,6 +159,10 @@ pub struct NetEntityCommands<'w> { } impl<'w> NetEntityCommands<'w> { + pub fn remove_player(&mut self, player: Player) -> Option { + self.map.remove_player(player) + } + fn register(&mut self, remote: EntityNet, local: Entity) { self.map.register(remote, local) } @@ -170,7 +174,7 @@ impl<'w> NetEntityCommands<'w> { fn local_id(&self, entity: EntityNet) -> Option { self.map .translate_remote(entity) - .or_else(|| self.entities.resolve_from_id(entity.index())) + .or_else(|| self.entities.resolve_from_id(entity.index().into())) } } @@ -178,7 +182,7 @@ impl<'w> NetEntityCommands<'w> { /// entities. #[derive(Resource)] struct EntityIdMapRes { - remote_to_local: AHashMap, + remote_to_local: AHashMap, local_to_remote: AHashMap, } @@ -202,8 +206,10 @@ impl EntityIdMapRes { /// /// Panics if the remote entity is already registered. fn register(&mut self, remote: EntityNet, local: Entity) { - let result = self.remote_to_local.insert(remote, local); - assert!(result.is_none()); + self.remote_to_local + .entry(remote.player()) + .or_default() + .insert(remote.index(), local); let result = self.local_to_remote.insert(local, remote); assert!(result.is_none()); } @@ -216,7 +222,8 @@ impl EntityIdMapRes { /// /// Panics if the entity is not registered. fn deregister(&mut self, remote: EntityNet) -> Entity { - let local = self.remote_to_local.remove(&remote).unwrap(); + let player_entities = self.remote_to_local.get_mut(&remote.player()).unwrap(); + let local = player_entities.remove(remote.index()).unwrap(); self.local_to_remote.remove(&local).unwrap(); local } @@ -230,7 +237,57 @@ impl EntityIdMapRes { /// Translates remote entity ID to a local entity ID in case the entity is /// not locally simulated. fn translate_remote(&self, remote: EntityNet) -> Option { - self.remote_to_local.get(&remote).copied() + self.remote_to_local + .get(&remote.player()) + .and_then(|h| h.translate(remote.index())) + } + + /// Removes entity mapping for the player. + /// + /// This should not be called unless the player leaves the multiplayer + /// game. + fn remove_player(&mut self, player: Player) -> Option { + let Some(map) = self.remote_to_local.remove(&player) else { + return None; + }; + + for local in map.locals() { + self.local_to_remote.remove(&local).unwrap(); + } + + Some(map) + } +} + +/// Mapping from remote entity indices to local ECS entities. +#[derive(Default)] +pub struct PlayerNetToLocal(AHashMap); + +impl PlayerNetToLocal { + /// Inserts a new remote to local entity link. + /// + /// # Panics + /// + /// Panics if the remote entity is already registered. + fn insert(&mut self, remote: NetEntityIndex, local: Entity) { + let result = self.0.insert(remote, local); + debug_assert!(result.is_none()); + } + + /// Removes a remote to local entity link and returns the local entity if + /// it was registered. + fn remove(&mut self, index: NetEntityIndex) -> Option { + self.0.remove(&index) + } + + /// Translates a remote entity to a local entity. + fn translate(&self, remote: NetEntityIndex) -> Option { + self.0.get(&remote).copied() + } + + /// Returns an iterator over all local entities from the mapping. + pub fn locals(&self) -> impl Iterator + '_ { + self.0.values().copied() } } diff --git a/crates/spawner/src/despawner.rs b/crates/spawner/src/despawner.rs index 03ac4a90..b7713f98 100644 --- a/crates/spawner/src/despawner.rs +++ b/crates/spawner/src/despawner.rs @@ -6,10 +6,12 @@ use de_audio::spatial::{PlaySpatialAudioEvent, Sound}; use de_core::gconfig::GameConfig; use de_core::{objects::ObjectTypeComponent, player::PlayerComponent, state::AppState}; use de_messages::ToPlayers; -use de_multiplayer::{NetEntities, NetRecvDespawnActiveEvent, ToPlayersEvent}; +use de_multiplayer::{ + NetEntities, NetEntityCommands, NetRecvDespawnActiveEvent, PeerLeftEvent, ToPlayersEvent, +}; use de_types::objects::{ActiveObjectType, ObjectType}; -use crate::ObjectCounter; +use crate::{ObjectCounter, SpawnerSet}; pub(crate) struct DespawnerPlugin; @@ -22,11 +24,16 @@ impl Plugin for DespawnerPlugin { despawn_active_remote .run_if(on_event::()) .before(despawn_active), + despawn_active_peer_left + .run_if(on_event::()) + .after(despawn_active_remote) + .before(despawn_active), despawn_active.before(despawn), despawn, ) .run_if(in_state(AppState::InGame)) - .in_set(DespawnerSet::Despawn), + .in_set(DespawnerSet::Despawn) + .after(SpawnerSet::Spawner), ) .add_event::() .add_event::() @@ -84,6 +91,20 @@ fn despawn_active_remote( } } +fn despawn_active_peer_left( + mut net_commands: NetEntityCommands, + mut peer_left_events: EventReader, + mut event_writer: EventWriter, +) { + for event in peer_left_events.iter() { + if let Some(entity_map) = net_commands.remove_player(event.id()) { + for entity in entity_map.locals() { + event_writer.send(DespawnActiveEvent(entity)); + } + } + } +} + fn despawn_active( mut counter: ResMut, entities: Query<(&PlayerComponent, &ObjectTypeComponent, &Transform)>,