mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Make clients subscribed to nearby regions and only send physics updates from those regions.
This commit is contained in:
parent
24d1f6d970
commit
b09bddda79
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -3625,6 +3625,8 @@ dependencies = [
|
||||
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hibitset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"portpicker 0.1.0 (git+https://github.com/wusyong/portpicker-rs?branch=fix_ipv6)",
|
||||
@ -3637,6 +3639,7 @@ dependencies = [
|
||||
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"specs 0.14.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"specs-idvs 0.1.0 (git+https://gitlab.com/veloren/specs-idvs.git)",
|
||||
"uvth 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vek 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"veloren-common 0.4.0",
|
||||
|
@ -501,6 +501,11 @@ impl Client {
|
||||
ServerMsg::EcsSync(sync_package) => {
|
||||
self.state.ecs_mut().sync_with_package(sync_package)
|
||||
}
|
||||
ServerMsg::DeleteEntity(entity) => {
|
||||
if let Some(entity) = self.state.ecs().entity_from_uid(entity) {
|
||||
let _ = self.state.ecs_mut().delete_entity(entity);
|
||||
}
|
||||
}
|
||||
ServerMsg::EntityPos { entity, pos } => {
|
||||
if let Some(entity) = self.state.ecs().entity_from_uid(entity) {
|
||||
self.state.write_component(entity, pos);
|
||||
|
@ -41,6 +41,7 @@ pub enum ServerMsg {
|
||||
},
|
||||
SetPlayerEntity(u64),
|
||||
EcsSync(sphynx::SyncPackage<EcsCompPacket, EcsResPacket>),
|
||||
DeleteEntity(u64),
|
||||
EntityPos {
|
||||
entity: u64,
|
||||
pos: comp::Pos,
|
||||
|
@ -1,38 +1,64 @@
|
||||
use crate::comp::{Pos, Vel};
|
||||
use hashbrown::hash_map::DefaultHashBuilder;
|
||||
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
|
||||
use hibitset::BitSetLike;
|
||||
use indexmap::IndexMap;
|
||||
use specs::{BitSet, Entities, Join, ReadStorage};
|
||||
use specs::{BitSet, Entities, Entity as EcsEntity, Join, ReadStorage};
|
||||
use vek::*;
|
||||
|
||||
pub enum Event {
|
||||
// Contains the key of the region the entity moved to
|
||||
Left(u32, Option<Vec2<i32>>),
|
||||
// Contains the key of the region the entity came from
|
||||
Entered(u32, Option<Vec2<i32>>),
|
||||
}
|
||||
|
||||
/// Region consisting of a bitset of entities within it
|
||||
struct Region<S> {
|
||||
pub struct Region {
|
||||
// Use specs bitset for simplicity (and joinability)
|
||||
bitset: BitSet,
|
||||
// Indices of neighboring regions
|
||||
neighbors: [Option<usize>; 8],
|
||||
// Keep track of subscribers
|
||||
subscribers: Vec<S>,
|
||||
// TODO consider SmallVec for these
|
||||
// Entites that left or entered this region
|
||||
events: Vec<Event>,
|
||||
}
|
||||
impl<S> Region<S> {
|
||||
fn with_entity(entity: u32) -> Self {
|
||||
let mut bitset = BitSet::new();
|
||||
bitset.add(entity);
|
||||
impl Region {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
bitset,
|
||||
bitset: BitSet::new(),
|
||||
neighbors: [None; 8],
|
||||
subscribers: Vec::new(),
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
/// Checks if the region contains no entities and no events
|
||||
fn removable(&self) -> bool {
|
||||
self.bitset.is_empty() && self.events.is_empty()
|
||||
}
|
||||
fn add(&mut self, id: u32, from: Option<Vec2<i32>>) {
|
||||
self.bitset.add(id);
|
||||
self.events.push(Event::Entered(id, from));
|
||||
}
|
||||
fn remove(&mut self, id: u32, to: Option<Vec2<i32>>) {
|
||||
self.bitset.remove(id);
|
||||
self.events.push(Event::Left(id, to));
|
||||
}
|
||||
pub fn events(&self) -> &[Event] {
|
||||
&self.events
|
||||
}
|
||||
pub fn entities(&self) -> &BitSet {
|
||||
&self.bitset
|
||||
}
|
||||
}
|
||||
|
||||
/// How far can an entity roam outside its region before it is switched over to the neighboring one
|
||||
/// In units of blocks (i.e. world pos)
|
||||
/// Used to prevent rapid switching of entities between regions
|
||||
const TETHER_LENGTH: u32 = 16;
|
||||
/// Region Size in chunks
|
||||
const REGION_SIZE: u16 = 16;
|
||||
const REGION_LOG2: u8 = 4;
|
||||
pub const TETHER_LENGTH: u32 = 16;
|
||||
/// Region Size in blocks
|
||||
pub const REGION_SIZE: u32 = 16 * 32;
|
||||
/// Shift between region to world pos
|
||||
/// TODO: don't use this :P
|
||||
const REGION_LOG2: u8 = 9;
|
||||
/// Offsets to iterate though neighbors
|
||||
/// Counter-clockwise order
|
||||
const NEIGHBOR_OFFSETS: [Vec2<i32>; 8] = [
|
||||
@ -49,11 +75,11 @@ const NEIGHBOR_OFFSETS: [Vec2<i32>; 8] = [
|
||||
// TODO generic region size (16x16 for now)
|
||||
// TODO compare to sweep and prune approach
|
||||
/// A region system that tracks where entities are
|
||||
pub struct RegionMap<S> {
|
||||
pub struct RegionMap {
|
||||
// Tree?
|
||||
// Sorted Vec? (binary search lookup)
|
||||
// Sort into multiple vecs (say 32) using lower bits of morton code, then binary search via upper bits? <-- sounds very promising to me (might not be super good though?)
|
||||
regions: IndexMap<Vec2<i32>, Region<S>, DefaultHashBuilder>,
|
||||
regions: IndexMap<Vec2<i32>, Region, DefaultHashBuilder>,
|
||||
// If an entity isn't here it needs to be added to a region
|
||||
tracked_entities: BitSet,
|
||||
// Re-useable vecs
|
||||
@ -61,24 +87,24 @@ pub struct RegionMap<S> {
|
||||
entities_to_move: Vec<(usize, u32, Vec3<i32>)>,
|
||||
// (region, entity)
|
||||
entities_to_remove: Vec<(usize, u32)>,
|
||||
// Track the current tick, used to enable not checking everything every tick
|
||||
tick: u64,
|
||||
}
|
||||
impl<S> RegionMap<S> {
|
||||
impl RegionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
regions: IndexMap::default(),
|
||||
tracked_entities: BitSet::new(),
|
||||
entities_to_move: Vec::new(),
|
||||
entities_to_remove: Vec::new(),
|
||||
// rate is depedent on the rate the caller calls region_manager.tick()
|
||||
tick: 0,
|
||||
}
|
||||
}
|
||||
// TODO maintain within a system
|
||||
pub fn maintain(
|
||||
&mut self,
|
||||
pos: ReadStorage<Pos>,
|
||||
vel: ReadStorage<Vel>,
|
||||
entities: Entities,
|
||||
tick: u64,
|
||||
) {
|
||||
// TODO maintain within a system?
|
||||
// TODO special case large entities
|
||||
pub fn tick(&mut self, pos: ReadStorage<Pos>, vel: ReadStorage<Vel>, entities: Entities) {
|
||||
self.tick += 1;
|
||||
// Add any untracked entites
|
||||
for (pos, id) in (&pos, &entities, !&self.tracked_entities)
|
||||
.join()
|
||||
@ -86,14 +112,22 @@ impl<S> RegionMap<S> {
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
// Add entity
|
||||
self.add_entity(id, pos.0.map(|e| e as i32));
|
||||
self.tracked_entities.add(id);
|
||||
self.add_entity(id, pos.0.map(|e| e as i32), None);
|
||||
}
|
||||
|
||||
self.entities_to_move.clear();
|
||||
self.entities_to_remove.clear();
|
||||
let mut regions_to_remove = Vec::new();
|
||||
|
||||
for i in 0..self.regions.len() {
|
||||
for (maybe_pos, maybe_vel, entity) in (
|
||||
// Clear events within each region
|
||||
self.regions
|
||||
.get_index_mut(i)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.events
|
||||
.clear();
|
||||
|
||||
for (maybe_pos, _maybe_vel, id) in (
|
||||
pos.maybe(),
|
||||
vel.maybe(),
|
||||
&self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset,
|
||||
@ -118,14 +152,14 @@ impl<S> RegionMap<S> {
|
||||
> TETHER_LENGTH
|
||||
{
|
||||
// Switch
|
||||
self.entities_to_move.push((i, entity, pos));
|
||||
self.entities_to_move.push((i, id, pos));
|
||||
}
|
||||
}
|
||||
// Remove any non-existant entities (or just ones that lost their position component)
|
||||
// TODO: distribute this between ticks
|
||||
None => {
|
||||
// TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs?
|
||||
self.entities_to_remove.push((i, entity));
|
||||
self.entities_to_remove.push((i, id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,63 +171,65 @@ impl<S> RegionMap<S> {
|
||||
.get_index(i)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.bitset
|
||||
.is_empty()
|
||||
.removable()
|
||||
{
|
||||
self.remove_index(i);
|
||||
regions_to_remove.push(i);
|
||||
}
|
||||
}
|
||||
for index in regions_to_remove {
|
||||
self.remove_index(index);
|
||||
}
|
||||
|
||||
// Mutate
|
||||
// Note entity moving is outside the whole loop so that the same entity is not checked twice (this may be fine though...)
|
||||
while let Some((i, entity, pos)) = self.entities_to_move.pop() {
|
||||
self.regions
|
||||
.get_index_mut(i)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.bitset
|
||||
.remove(entity);
|
||||
self.add_entity_untracked(entity, pos);
|
||||
}
|
||||
for (i, entity) in self.entities_to_remove.drain(..) {
|
||||
self.regions
|
||||
.get_index_mut(i)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.bitset
|
||||
.remove(entity);
|
||||
}
|
||||
while let Some((i, id, pos)) = self.entities_to_move.pop() {
|
||||
let (prev_key, region) = self.regions.get_index_mut(i).map(|(k, v)| (*k, v)).unwrap();
|
||||
region.remove(id, Some(Self::pos_key(pos)));
|
||||
|
||||
// Maintain subscriptions ???
|
||||
self.add_entity(id, pos, Some(prev_key));
|
||||
}
|
||||
fn add_entity(&mut self, id: u32, pos: Vec3<i32>) {
|
||||
self.tracked_entities.add(id);
|
||||
self.add_entity_untracked(id, pos);
|
||||
for (i, id) in self.entities_to_remove.drain(..) {
|
||||
self.regions
|
||||
.get_index_mut(i)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.remove(id, None);
|
||||
self.tracked_entities.remove(id);
|
||||
}
|
||||
fn add_entity_untracked(&mut self, id: u32, pos: Vec3<i32>) {
|
||||
}
|
||||
pub fn add(&mut self, entity: EcsEntity, pos: Vec3<f32>) {
|
||||
self.add_entity(entity.id(), pos.map(|e| e as i32), None);
|
||||
}
|
||||
fn add_entity(&mut self, id: u32, pos: Vec3<i32>, from: Option<Vec2<i32>>) {
|
||||
let key = Self::pos_key(pos);
|
||||
if let Some(region) = self.regions.get_mut(&key) {
|
||||
region.bitset.add(id);
|
||||
region.add(id, from);
|
||||
return;
|
||||
}
|
||||
|
||||
self.insert(key, id);
|
||||
let index = self.insert(key);
|
||||
self.regions
|
||||
.get_index_mut(index)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.add(id, None);
|
||||
}
|
||||
fn pos_key<P: Into<Vec2<i32>>>(pos: P) -> Vec2<i32> {
|
||||
pos.into().map(|e| e >> REGION_LOG2)
|
||||
}
|
||||
fn key_pos(key: Vec2<i32>) -> Vec2<i32> {
|
||||
pub fn key_pos(key: Vec2<i32>) -> Vec2<i32> {
|
||||
key.map(|e| e << REGION_LOG2)
|
||||
}
|
||||
fn key_index(&self, key: Vec2<i32>) -> Option<usize> {
|
||||
self.regions.get_full(&key).map(|(i, _, _)| i)
|
||||
}
|
||||
//fn key_index(&self, key: Vec2<i32>) -> Option<usize> {
|
||||
// self.regions.get_full(&key).map(|(i, _, _)| i)
|
||||
//}
|
||||
fn index_key(&self, index: usize) -> Option<Vec2<i32>> {
|
||||
self.regions.get_index(index).map(|(k, _)| k).copied()
|
||||
}
|
||||
/// Adds a new region
|
||||
fn insert(&mut self, key: Vec2<i32>, entity: u32) {
|
||||
let (index, old_region) = self.regions.insert_full(key, Region::with_entity(entity));
|
||||
/// Returns the index of the region in the index map
|
||||
fn insert(&mut self, key: Vec2<i32>) -> usize {
|
||||
let (index, old_region) = self.regions.insert_full(key, Region::new());
|
||||
if old_region.is_some() {
|
||||
panic!("Inserted a region that already exists!!!(this should never need to occur");
|
||||
}
|
||||
@ -213,19 +249,22 @@ impl<S> RegionMap<S> {
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.neighbors = neighbors;
|
||||
|
||||
index
|
||||
}
|
||||
/// Remove a region using its key
|
||||
fn remove(&mut self, key: Vec2<i32>) {
|
||||
if let Some(index) = self.key_index(key) {
|
||||
self.remove_index(index);
|
||||
}
|
||||
}
|
||||
//fn remove(&mut self, key: Vec2<i32>) {
|
||||
// if let Some(index) = self.key_index(key) {
|
||||
// self.remove_index(index);
|
||||
// }
|
||||
//}
|
||||
/// Add a region using its key
|
||||
fn remove_index(&mut self, index: usize) {
|
||||
// Remap neighbor indices for neighbors of the region that will be moved from the end of the index map
|
||||
if index != self.regions.len() - 1 {
|
||||
let moved_neighbors = self
|
||||
.regions
|
||||
.get_index(index)
|
||||
.get_index(self.regions.len() - 1)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap()
|
||||
.neighbors;
|
||||
@ -238,6 +277,7 @@ impl<S> RegionMap<S> {
|
||||
.neighbors[(i + 4) % 8] = Some(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(region) = self
|
||||
.regions
|
||||
.swap_remove_index(index)
|
||||
@ -258,75 +298,58 @@ impl<S> RegionMap<S> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Returns a region given a key
|
||||
pub fn get(&self, key: Vec2<i32>) -> Option<&Region> {
|
||||
self.regions.get(&key)
|
||||
}
|
||||
// Returns an iterator of (Position, Region)
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Vec2<i32>, &Region)> {
|
||||
self.regions.iter().map(|(key, r)| (*key, r))
|
||||
}
|
||||
}
|
||||
|
||||
/*pub struct RegionManager<S> {
|
||||
region_map: RegionMap<S>
|
||||
// If an entity isn't here it needs to be added to a region
|
||||
tracked_entities: BitSet,
|
||||
// Note vd is in blocks in this case
|
||||
pub fn region_in_vd(key: Vec2<i32>, pos: Vec3<f32>, vd: f32) -> bool {
|
||||
let vd_extended = vd + TETHER_LENGTH as f32 * 2.0f32.sqrt();
|
||||
|
||||
let min_region_pos = RegionMap::key_pos(key).map(|e| e as f32);
|
||||
// Should be diff to closest point on the square (which can be in the middle of an edge)
|
||||
let diff = (min_region_pos - Vec2::from(pos)).map(|e| {
|
||||
if e < 0.0 {
|
||||
(e + REGION_SIZE as f32).min(0.0)
|
||||
} else {
|
||||
e
|
||||
}
|
||||
impl<S> RegionManager {
|
||||
// TODO maintain within a system?
|
||||
pub fn maintain(&mut self, pos: ReadStorage<Pos>, vel: ReadStorage<Vel>, entities: Entities, tick: u64) {
|
||||
let Self {
|
||||
ref mut region_map,
|
||||
ref mut tracked_entities,
|
||||
} =
|
||||
// Add any untracked entites
|
||||
for (pos, e, _) in (&pos, &entities, !&self.tracked_entities).join() {
|
||||
let id = e.id();
|
||||
// Add entity
|
||||
self.add_entity(id, pos.0.map(|e| e as i32));
|
||||
});
|
||||
|
||||
diff.magnitude_squared() < vd_extended.powi(2)
|
||||
}
|
||||
// Iterate through regions
|
||||
for i in 0..self.regions.len() {
|
||||
for (maybe_pos, maybe_vel, entity) in
|
||||
(pos.maybe(), vel.maybe(), &self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset).join()
|
||||
{
|
||||
match maybe_pos {
|
||||
// Switch regions for entities which need switching
|
||||
// TODO don't check every tick (use velocity) (and use id to stagger)
|
||||
// Starting parameters at v = 0 check every 100 ticks
|
||||
// tether_length^2 / vel^2 (with a max of every tick)
|
||||
Some(pos) => {
|
||||
let pos = pos.0.map(|e| e as i32);
|
||||
let current_region = self.index_key(i).unwrap();
|
||||
let key = Self::pos_key(pos);
|
||||
// Consider switching
|
||||
// Caculate distance outside border
|
||||
if key != current_region
|
||||
&& (Vec2::<i32>::from(pos) - Self::key_pos(current_region))
|
||||
.map(|e| e.abs() as u32)
|
||||
.reduce_max()
|
||||
> TETHER_LENGTH
|
||||
{
|
||||
// Switch
|
||||
self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity);
|
||||
self.add_entity_untracked(entity, pos);
|
||||
}
|
||||
}
|
||||
// Remove any non-existant entities (or just ones that lost their position component)
|
||||
// TODO: distribute this between ticks
|
||||
None => {
|
||||
// TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs?
|
||||
self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity);
|
||||
|
||||
// Note vd is in blocks in this case
|
||||
pub fn regions_in_vd(pos: Vec3<f32>, vd: f32) -> HashSet<Vec2<i32>> {
|
||||
let mut set = HashSet::new();
|
||||
|
||||
let pos_xy = Vec2::<f32>::from(pos);
|
||||
let vd_extended = vd + TETHER_LENGTH as f32 * 2.0f32.sqrt();
|
||||
|
||||
let max = RegionMap::pos_key(pos_xy.map(|e| (e + vd_extended) as i32));
|
||||
let min = RegionMap::pos_key(pos_xy.map(|e| (e - vd_extended) as i32));
|
||||
|
||||
for x in min.x..=max.x {
|
||||
for y in min.y..=max.y {
|
||||
let key = Vec2::new(x, y);
|
||||
|
||||
if region_in_vd(key, pos, vd) {
|
||||
set.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove region if it is empty
|
||||
// TODO: distribute this betweeen ticks
|
||||
if self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset.is_empty() {
|
||||
self.remove_index(i);
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain subscriptions ???
|
||||
|
||||
}
|
||||
}*/
|
||||
// Iterator designed for use in collision systems
|
||||
// Iterates through all regions yielding them along with half of their neighbors
|
||||
// ..................
|
||||
|
||||
/*fn interleave_i32_with_zeros(mut x: i32) -> i64 {
|
||||
x = (x ^ (x << 16)) & 0x0000ffff0000ffff;
|
||||
|
@ -5,6 +5,7 @@ use crate::{
|
||||
comp,
|
||||
event::{EventBus, LocalEvent, ServerEvent},
|
||||
msg::{EcsCompPacket, EcsResPacket},
|
||||
region::RegionMap,
|
||||
sys,
|
||||
terrain::{Block, TerrainChunk, TerrainGrid},
|
||||
vol::WriteVol,
|
||||
@ -172,6 +173,7 @@ impl State {
|
||||
ecs.add_resource(TerrainChanges::default());
|
||||
ecs.add_resource(EventBus::<ServerEvent>::default());
|
||||
ecs.add_resource(EventBus::<LocalEvent>::default());
|
||||
ecs.add_resource(RegionMap::new());
|
||||
}
|
||||
|
||||
/// Register a component with the state's ECS.
|
||||
@ -387,6 +389,13 @@ impl State {
|
||||
|
||||
self.ecs.maintain();
|
||||
|
||||
// Run RegionMap tick to update enitity region occupancy
|
||||
self.ecs.write_resource::<RegionMap>().tick(
|
||||
self.ecs.read_storage::<comp::Pos>(),
|
||||
self.ecs.read_storage::<comp::Vel>(),
|
||||
self.ecs.entities(),
|
||||
);
|
||||
|
||||
// Apply terrain changes
|
||||
let mut terrain = self.ecs.write_resource::<TerrainGrid>();
|
||||
self.ecs
|
||||
|
@ -8,6 +8,8 @@ edition = "2018"
|
||||
common = { package = "veloren-common", path = "../common" }
|
||||
world = { package = "veloren-world", path = "../world" }
|
||||
|
||||
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
|
||||
|
||||
log = "0.4.8"
|
||||
specs = "0.14.2"
|
||||
vek = "0.9.9"
|
||||
@ -25,3 +27,6 @@ prometheus = "0.7"
|
||||
prometheus-static-metric = "0.2"
|
||||
rouille = "3.0.0"
|
||||
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
|
||||
indexmap = "1.2.0"
|
||||
# TODO: remove when upgrading to specs 0.15
|
||||
hibitset = "0.5.3"
|
||||
|
@ -2,8 +2,12 @@ use common::{
|
||||
msg::{ClientMsg, ClientState, RequestStateError, ServerMsg},
|
||||
net::PostBox,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
|
||||
use indexmap::IndexMap;
|
||||
use specs::Entity as EcsEntity;
|
||||
use specs::{Component, FlaggedStorage};
|
||||
use specs_idvs::IDVStorage;
|
||||
use vek::*;
|
||||
|
||||
pub struct Client {
|
||||
pub client_state: ClientState,
|
||||
@ -31,13 +35,13 @@ impl Client {
|
||||
}
|
||||
|
||||
pub struct Clients {
|
||||
clients: HashMap<EcsEntity, Client>,
|
||||
clients: IndexMap<EcsEntity, Client, DefaultHashBuilder>,
|
||||
}
|
||||
|
||||
impl Clients {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
clients: HashMap::new(),
|
||||
clients: IndexMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,17 +58,40 @@ impl Clients {
|
||||
}
|
||||
|
||||
pub fn get_mut<'a>(&'a mut self, entity: &EcsEntity) -> Option<&'a mut Client> {
|
||||
self.clients.get_mut(entity)
|
||||
self.clients.get_mut(entit:y)
|
||||
}
|
||||
|
||||
pub fn remove<'a>(&'a mut self, entity: &EcsEntity) -> Option<Client> {
|
||||
self.clients.remove(entity)
|
||||
}
|
||||
|
||||
pub fn get_client_index_ingame<'a>(&'a mut self, entity: &EcsEntity) -> Option<usize> {
|
||||
self.clients.get_full(entity).and_then(|(i, _, c)| {
|
||||
if c.client_state == ClientState::Spectator
|
||||
|| c.client_state == ClientState::Character
|
||||
|| c.client_state == ClientState::Dead
|
||||
{
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//pub fn get_index_mut<'a>(&'a mut self, index: u32) -> Option<&'a mut Client> {
|
||||
// self.clients.get_index_mut(index)
|
||||
//}
|
||||
|
||||
pub fn remove_if<F: FnMut(EcsEntity, &mut Client) -> bool>(&mut self, mut f: F) {
|
||||
self.clients.retain(|entity, client| !f(*entity, client));
|
||||
}
|
||||
|
||||
pub fn notify_index(&mut self, index: usize, msg: ServerMsg) {
|
||||
if let Some((_, client)) = self.clients.get_index_mut(index) {
|
||||
client.notify(msg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify(&mut self, entity: EcsEntity, msg: ServerMsg) {
|
||||
if let Some(client) = self.clients.get_mut(&entity) {
|
||||
client.notify(msg);
|
||||
@ -138,3 +165,18 @@ impl Clients {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distance from fuzzy_chunk before snapping to current chunk
|
||||
pub const CHUNK_FUZZ: u32 = 2;
|
||||
// Distance out of the range of a region before removing it from subscriptions
|
||||
pub const REGION_FUZZ: u32 = 16;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RegionSubscription {
|
||||
pub fuzzy_chunk: Vec2<i32>,
|
||||
pub regions: HashSet<Vec2<i32>>,
|
||||
}
|
||||
|
||||
impl Component for RegionSubscription {
|
||||
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ pub use crate::{error::Error, input::Input, settings::ServerSettings};
|
||||
|
||||
use crate::{
|
||||
auth_provider::AuthProvider,
|
||||
client::{Client, Clients},
|
||||
client::{Client, Clients, RegionSubscription},
|
||||
cmd::CHAT_COMMANDS,
|
||||
};
|
||||
use common::{
|
||||
@ -86,6 +86,10 @@ pub struct Server {
|
||||
server_info: ServerInfo,
|
||||
metrics: ServerMetrics,
|
||||
|
||||
// Tick count used for throttling network updates
|
||||
// Note this doesn't account for dt (so update rate changes with tick rate)
|
||||
tick: u64,
|
||||
|
||||
// TODO: anything but this
|
||||
accounts: AuthProvider,
|
||||
}
|
||||
@ -102,6 +106,7 @@ impl Server {
|
||||
state
|
||||
.ecs_mut()
|
||||
.add_resource(EventBus::<ServerEvent>::default());
|
||||
state.ecs_mut().register::<RegionSubscription>();
|
||||
|
||||
// Set starting time for the server.
|
||||
state.ecs_mut().write_resource::<TimeOfDay>().0 = settings.start_time;
|
||||
@ -128,6 +133,7 @@ impl Server {
|
||||
},
|
||||
metrics: ServerMetrics::new(settings.metrics_address)
|
||||
.expect("Failed to initialize server metrics submodule."),
|
||||
tick: 0,
|
||||
accounts: AuthProvider::new(),
|
||||
server_settings: settings.clone(),
|
||||
};
|
||||
@ -547,6 +553,7 @@ impl Server {
|
||||
|
||||
/// Execute a single server tick, handle input and update the game state by the given duration.
|
||||
pub fn tick(&mut self, _input: Input, dt: Duration) -> Result<Vec<Event>, Error> {
|
||||
self.tick += 1;
|
||||
// This tick function is the centre of the Veloren universe. Most server-side things are
|
||||
// managed from here, and as such it's important that it stays organised. Please consult
|
||||
// the core developers before making significant changes to this code. Here is the
|
||||
@ -792,6 +799,7 @@ impl Server {
|
||||
}
|
||||
|
||||
// Remove NPCs that are outside the view distances of all players
|
||||
// This is done by removing NPCs in unloaded chunks
|
||||
let to_delete = {
|
||||
let terrain = self.state.terrain();
|
||||
(
|
||||
@ -1116,6 +1124,7 @@ impl Server {
|
||||
}),
|
||||
&server_settings,
|
||||
);
|
||||
Self::initialize_region_subscription(state, client, entity);
|
||||
}
|
||||
ClientState::Character => {
|
||||
client.error_state(RequestStateError::Already)
|
||||
@ -1329,13 +1338,52 @@ impl Server {
|
||||
// Save player metadata (for example the username).
|
||||
state.write_component(entity, player);
|
||||
|
||||
// Sync physics of all entities
|
||||
for (&uid, &pos, vel, ori, character_state) in (
|
||||
// Tell the client its request was successful.
|
||||
client.allow_state(ClientState::Registered);
|
||||
}
|
||||
|
||||
/// Initialize region subscription, entity should be the client's entity
|
||||
fn initialize_region_subscription(
|
||||
state: &mut State,
|
||||
client: &mut Client,
|
||||
entity: specs::Entity,
|
||||
) {
|
||||
let mut subscription = None;
|
||||
|
||||
if let (Some(client_pos), Some(client_vd)) = (
|
||||
state.ecs().read_storage::<comp::Pos>().get(entity),
|
||||
state
|
||||
.ecs()
|
||||
.read_storage::<comp::Player>()
|
||||
.get(entity)
|
||||
.map(|pl| pl.view_distance)
|
||||
.and_then(|v| v),
|
||||
) {
|
||||
use common::region::RegionMap;
|
||||
|
||||
let fuzzy_chunk = (Vec2::<f32>::from(client_pos.0))
|
||||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
|
||||
let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
|
||||
let regions = common::region::regions_in_vd(
|
||||
client_pos.0,
|
||||
(client_vd as f32 * chunk_size) as f32
|
||||
+ (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
|
||||
);
|
||||
|
||||
for (_, region) in state
|
||||
.ecs()
|
||||
.read_resource::<RegionMap>()
|
||||
.iter()
|
||||
.filter(|(key, _)| regions.contains(key))
|
||||
{
|
||||
// Sync physics of all entities in this region
|
||||
for (&uid, &pos, vel, ori, character_state, _) in (
|
||||
&state.ecs().read_storage::<Uid>(),
|
||||
&state.ecs().read_storage::<comp::Pos>(), // We assume all these entities have a position
|
||||
state.ecs().read_storage::<comp::Vel>().maybe(),
|
||||
state.ecs().read_storage::<comp::Ori>().maybe(),
|
||||
state.ecs().read_storage::<comp::CharacterState>().maybe(),
|
||||
region.entities(),
|
||||
)
|
||||
.join()
|
||||
{
|
||||
@ -1362,128 +1410,296 @@ impl Server {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the client its request was successful.
|
||||
client.allow_state(ClientState::Registered);
|
||||
subscription = Some(RegionSubscription {
|
||||
fuzzy_chunk,
|
||||
regions,
|
||||
});
|
||||
}
|
||||
if let Some(subscription) = subscription {
|
||||
state.write_component(entity, subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync client states with the most up to date information.
|
||||
fn sync_clients(&mut self) {
|
||||
// Sync 'logical' state using Sphynx.
|
||||
self.clients
|
||||
.notify_registered(ServerMsg::EcsSync(self.state.ecs_mut().next_sync_package()));
|
||||
use common::region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap};
|
||||
//use hibitset::BitSetLike;
|
||||
|
||||
let ecs = self.state.ecs_mut();
|
||||
let clients = &mut self.clients;
|
||||
|
||||
// Sync 'logical' state using Sphynx.
|
||||
clients.notify_registered(ServerMsg::EcsSync(ecs.next_sync_package()));
|
||||
|
||||
// To update subscriptions
|
||||
// 1. Iterate through clients
|
||||
// 2. Calculate current chunk position
|
||||
// 3. If chunk is the same return, otherwise continue (use fuzzyiness)
|
||||
// 4. Iterate through subscribed regions
|
||||
// 5. Check if region is still in range (use fuzzyiness)
|
||||
// 6. If not in range
|
||||
// - remove from hashset
|
||||
// - inform client of which entities to remove
|
||||
// 7. Determine list of regions that are in range and iterate through it
|
||||
// - check if in hashset (hash calc) if not add it
|
||||
let mut regions_to_remove = Vec::new();
|
||||
for (entity, subscription, pos, vd) in (
|
||||
&ecs.entities(),
|
||||
&mut ecs.write_storage::<RegionSubscription>(),
|
||||
&ecs.read_storage::<comp::Pos>(),
|
||||
&ecs.read_storage::<comp::Player>(),
|
||||
)
|
||||
.join()
|
||||
.filter_map(|(e, s, pos, player)| player.view_distance.map(|v| (e, s, pos, v)))
|
||||
{
|
||||
let chunk = (Vec2::<f32>::from(pos.0))
|
||||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
|
||||
if chunk != subscription.fuzzy_chunk
|
||||
&& (subscription
|
||||
.fuzzy_chunk
|
||||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
|
||||
(e as f32 + 0.5) * sz as f32
|
||||
})
|
||||
- Vec2::from(pos.0))
|
||||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
|
||||
e.abs() > (sz / 2 + client::CHUNK_FUZZ) as f32
|
||||
})
|
||||
.reduce_or()
|
||||
{
|
||||
let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
|
||||
for key in &subscription.regions {
|
||||
if !region_in_vd(
|
||||
*key,
|
||||
pos.0,
|
||||
(vd as f32 * chunk_size)
|
||||
+ (client::CHUNK_FUZZ as f32 + client::REGION_FUZZ as f32 + chunk_size)
|
||||
* 2.0f32.sqrt(),
|
||||
) {
|
||||
regions_to_remove.push(*key);
|
||||
}
|
||||
}
|
||||
|
||||
let mut client = clients.get_mut(&entity);
|
||||
|
||||
for key in regions_to_remove.drain(..) {
|
||||
subscription.regions.remove(&key);
|
||||
// Inform the client to delete these entities
|
||||
if let (Some(ref mut client), Some(region)) =
|
||||
(&mut client, ecs.read_resource::<RegionMap>().get(key))
|
||||
{
|
||||
// Process entity left events since they won't be processed below because this region is no longer subscribed to
|
||||
for event in region.events() {
|
||||
match event {
|
||||
RegionEvent::Entered(_, _) => {} // These don't need to be processed because this region is being thrown out anyway
|
||||
RegionEvent::Left(id, maybe_key) => {
|
||||
// Lookup UID for entity
|
||||
if let Some(&uid) =
|
||||
ecs.read_storage::<Uid>().get(ecs.entities().entity(*id))
|
||||
{
|
||||
if !maybe_key
|
||||
.as_ref()
|
||||
.map(|key| subscription.regions.contains(key))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
client.notify(ServerMsg::DeleteEntity(uid.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (&uid, _) in (&ecs.read_storage::<Uid>(), region.entities()).join() {
|
||||
client.notify(ServerMsg::DeleteEntity(uid.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key in regions_in_vd(
|
||||
pos.0,
|
||||
(vd as f32 * chunk_size)
|
||||
+ (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
|
||||
) {
|
||||
if subscription.regions.insert(key) {
|
||||
// TODO: send the client initial infromation for all the entities in this region
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To send entity updates
|
||||
// 1. Iterate through regions
|
||||
// 2. Iterate through region subscribers (ie clients)
|
||||
// - Collect a list of entity ids for clients who are subscribed to this region (hash calc to check each)
|
||||
// 3. Iterate through events from that region
|
||||
// - For each entity left event, iterate through the client list and check if they are subscribed to the destination (hash calc per subscribed client per entity left event)
|
||||
// - Do something with entity entered events when sphynx is removed??
|
||||
// 4. Iterate through entities in that region
|
||||
// 5. Inform clients of the component changes for that entity
|
||||
// - Throttle update rate base on distance to each client
|
||||
|
||||
// Sync physics
|
||||
for (entity, &uid, &pos, force_update) in (
|
||||
// via iterating through regions
|
||||
for (key, region) in ecs.read_resource::<RegionMap>().iter() {
|
||||
let subscriptions = ecs.read_storage::<RegionSubscription>();
|
||||
let subscribers = (
|
||||
&ecs.entities(),
|
||||
&subscriptions,
|
||||
&ecs.read_storage::<comp::Pos>(),
|
||||
)
|
||||
.join()
|
||||
.filter_map(|(entity, subscription, pos)| {
|
||||
if subscription.regions.contains(&key) {
|
||||
clients
|
||||
.get_client_index_ingame(&entity)
|
||||
.map(|index| (index, &subscription.regions, entity, *pos))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for event in region.events() {
|
||||
match event {
|
||||
RegionEvent::Entered(_, _) => {} // TODO use this
|
||||
RegionEvent::Left(id, maybe_key) => {
|
||||
// Lookup UID for entity
|
||||
if let Some(&uid) =
|
||||
ecs.read_storage::<Uid>().get(ecs.entities().entity(*id))
|
||||
{
|
||||
for (client_index, regions, _, _) in &subscribers {
|
||||
if !maybe_key
|
||||
.as_ref()
|
||||
.map(|key| regions.contains(key))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
clients.notify_index(
|
||||
*client_index,
|
||||
ServerMsg::DeleteEntity(uid.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tick = self.tick;
|
||||
let send_msg = |msg: ServerMsg,
|
||||
entity: EcsEntity,
|
||||
pos: comp::Pos,
|
||||
force_update,
|
||||
clients: &mut Clients| {
|
||||
for (index, _, client_entity, client_pos) in &subscribers {
|
||||
match force_update {
|
||||
None if client_entity == &entity => {}
|
||||
_ => {
|
||||
let distance_sq = client_pos.0.distance_squared(pos.0);
|
||||
|
||||
// Throttle update rate based on distance to player
|
||||
let update = if distance_sq < 100.0f32.powi(2) {
|
||||
true // Closer than 100.0 blocks
|
||||
} else if distance_sq < 150.0f32.powi(2) {
|
||||
tick + entity.id() as u64 % 2 == 0
|
||||
} else if distance_sq < 200.0f32.powi(2) {
|
||||
tick + entity.id() as u64 % 4 == 0
|
||||
} else if distance_sq < 250.0f32.powi(2) {
|
||||
tick + entity.id() as u64 % 8 == 0
|
||||
} else if distance_sq < 300.0f32.powi(2) {
|
||||
tick + entity.id() as u64 % 8 == 0
|
||||
} else {
|
||||
tick + entity.id() as u64 % 16 == 0
|
||||
};
|
||||
|
||||
if update {
|
||||
clients.notify_index(*index, msg.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (_, entity, &uid, &pos, maybe_vel, maybe_ori, character_state, force_update) in (
|
||||
region.entities(),
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Uid>(),
|
||||
&ecs.read_storage::<comp::Pos>(),
|
||||
ecs.read_storage::<comp::Vel>().maybe(),
|
||||
ecs.read_storage::<comp::Ori>().maybe(),
|
||||
ecs.read_storage::<comp::CharacterState>().maybe(),
|
||||
ecs.read_storage::<comp::ForceUpdate>().maybe(),
|
||||
)
|
||||
.join()
|
||||
{
|
||||
let clients = &mut self.clients;
|
||||
|
||||
let in_vd = |entity| {
|
||||
if let (Some(client_pos), Some(client_vd)) = (
|
||||
ecs.read_storage::<comp::Pos>().get(entity),
|
||||
ecs.read_storage::<comp::Player>()
|
||||
.get(entity)
|
||||
.map(|pl| pl.view_distance)
|
||||
.and_then(|v| v),
|
||||
) {
|
||||
{
|
||||
// Check if the entity is in the client's range
|
||||
Vec2::from(pos.0 - client_pos.0)
|
||||
.map2(TerrainChunkSize::RECT_SIZE, |d: f32, sz| {
|
||||
(d.abs() as u32 / sz).checked_sub(2).unwrap_or(0)
|
||||
})
|
||||
.magnitude_squared()
|
||||
< client_vd.pow(2)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mut last_pos = ecs.write_storage::<comp::Last<comp::Pos>>();
|
||||
let mut last_vel = ecs.write_storage::<comp::Last<comp::Vel>>();
|
||||
let mut last_ori = ecs.write_storage::<comp::Last<comp::Ori>>();
|
||||
let mut last_character_state = ecs.write_storage::<comp::Last<comp::CharacterState>>();
|
||||
let mut last_character_state =
|
||||
ecs.write_storage::<comp::Last<comp::CharacterState>>();
|
||||
|
||||
if let Some(client_pos) = ecs.read_storage::<comp::Pos>().get(entity) {
|
||||
if last_pos
|
||||
.get(entity)
|
||||
.map(|&l| l.0 != *client_pos)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let _ = last_pos.insert(entity, comp::Last(*client_pos));
|
||||
let msg = ServerMsg::EntityPos {
|
||||
if last_pos.get(entity).map(|&l| l.0 != pos).unwrap_or(true) {
|
||||
let _ = last_pos.insert(entity, comp::Last(pos));
|
||||
send_msg(
|
||||
ServerMsg::EntityPos {
|
||||
entity: uid.into(),
|
||||
pos: *client_pos,
|
||||
};
|
||||
match force_update {
|
||||
Some(_) => clients.notify_ingame_if(msg, in_vd),
|
||||
None => clients.notify_ingame_if_except(entity, msg, in_vd),
|
||||
}
|
||||
}
|
||||
pos,
|
||||
},
|
||||
entity,
|
||||
pos,
|
||||
force_update,
|
||||
clients,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(client_vel) = ecs.read_storage::<comp::Vel>().get(entity) {
|
||||
if last_vel
|
||||
.get(entity)
|
||||
.map(|&l| l.0 != *client_vel)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let _ = last_vel.insert(entity, comp::Last(*client_vel));
|
||||
let msg = ServerMsg::EntityVel {
|
||||
if let Some(&vel) = maybe_vel {
|
||||
if last_vel.get(entity).map(|&l| l.0 != vel).unwrap_or(true) {
|
||||
let _ = last_vel.insert(entity, comp::Last(vel));
|
||||
send_msg(
|
||||
ServerMsg::EntityVel {
|
||||
entity: uid.into(),
|
||||
vel: *client_vel,
|
||||
};
|
||||
match force_update {
|
||||
Some(_) => clients.notify_ingame_if(msg, in_vd),
|
||||
None => clients.notify_ingame_if_except(entity, msg, in_vd),
|
||||
}
|
||||
vel,
|
||||
},
|
||||
entity,
|
||||
pos,
|
||||
force_update,
|
||||
clients,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(client_ori) = ecs.read_storage::<comp::Ori>().get(entity) {
|
||||
if last_ori
|
||||
.get(entity)
|
||||
.map(|&l| l.0 != *client_ori)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let _ = last_ori.insert(entity, comp::Last(*client_ori));
|
||||
let msg = ServerMsg::EntityOri {
|
||||
if let Some(&ori) = maybe_ori {
|
||||
if last_ori.get(entity).map(|&l| l.0 != ori).unwrap_or(true) {
|
||||
let _ = last_ori.insert(entity, comp::Last(ori));
|
||||
send_msg(
|
||||
ServerMsg::EntityOri {
|
||||
entity: uid.into(),
|
||||
ori: *client_ori,
|
||||
};
|
||||
match force_update {
|
||||
Some(_) => clients.notify_ingame_if(msg, in_vd),
|
||||
None => clients.notify_ingame_if_except(entity, msg, in_vd),
|
||||
}
|
||||
ori,
|
||||
},
|
||||
entity,
|
||||
pos,
|
||||
force_update,
|
||||
clients,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(client_character_state) =
|
||||
ecs.read_storage::<comp::CharacterState>().get(entity)
|
||||
{
|
||||
if let Some(&character_state) = character_state {
|
||||
if last_character_state
|
||||
.get(entity)
|
||||
.map(|&l| !client_character_state.is_same_state(&l.0))
|
||||
.map(|&l| !character_state.is_same_state(&l.0))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let _ =
|
||||
last_character_state.insert(entity, comp::Last(*client_character_state));
|
||||
let msg = ServerMsg::EntityCharacterState {
|
||||
let _ = last_character_state.insert(entity, comp::Last(character_state));
|
||||
send_msg(
|
||||
ServerMsg::EntityCharacterState {
|
||||
entity: uid.into(),
|
||||
character_state: *client_character_state,
|
||||
};
|
||||
match force_update {
|
||||
Some(_) => clients.notify_ingame_if(msg, in_vd),
|
||||
None => clients.notify_ingame_if_except(entity, msg, in_vd),
|
||||
character_state,
|
||||
},
|
||||
entity,
|
||||
pos,
|
||||
force_update,
|
||||
clients,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user