Merge branch 'imbris/sync' into 'master'

Fix sync issues

Closes #359 and #377

See merge request veloren/veloren!675
This commit is contained in:
Imbris 2019-12-21 04:26:38 +00:00
commit ddfa2b4600
49 changed files with 1677 additions and 720 deletions

431
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,6 @@ uvth = "3.1.1"
image = "0.22.3"
num_cpus = "1.10.1"
log = "0.4.8"
specs = "0.14.2"
specs = "0.15.1"
vek = { version = "0.9.9", features = ["serde"] }
hashbrown = { version = "0.6.2", features = ["serde", "nightly"] }

View File

@ -5,7 +5,7 @@ pub mod error;
// Reexports
pub use crate::error::Error;
pub use specs::{join::Join, saveload::Marker, Entity as EcsEntity, ReadStorage};
pub use specs::{join::Join, saveload::Marker, Entity as EcsEntity, ReadStorage, WorldExt};
use common::{
comp::{self, ControlEvent, Controller, ControllerInputs, InventoryManip},
@ -14,7 +14,8 @@ use common::{
ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG,
},
net::PostBox,
state::{State, Uid},
state::State,
sync::{Uid, WorldSyncExt},
terrain::{block::Block, TerrainChunk, TerrainChunkSize},
vol::RectVolSize,
ChatType,
@ -78,9 +79,9 @@ impl Client {
// Wait for initial sync
let (state, entity, server_info, world_map) = match postbox.next_message() {
Some(ServerMsg::InitialSync {
ecs_state,
entity_uid,
entity_package,
server_info,
time_of_day,
// world_map: /*(map_size, world_map)*/map_size,
}) => {
// TODO: Voxygen should display this.
@ -94,11 +95,10 @@ impl Client {
);
}
let state = State::from_state_package(ecs_state);
let entity = state
.ecs()
.entity_from_uid(entity_uid)
.ok_or(Error::ServerWentMad)?;
// Initialize `State`
let mut state = State::default();
let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day;
// assert_eq!(world_map.len(), map_size.x * map_size.y);
let map_size = Vec2::new(1024, 1024);
@ -537,7 +537,7 @@ impl Client {
self.last_ping_delta = Instant::now()
.duration_since(self.last_server_ping)
.as_secs_f64()
.as_secs_f64();
}
ServerMsg::ChatMsg { chat_type, message } => {
frontend_events.push(Event::Chat { chat_type, message })
@ -549,14 +549,25 @@ impl Client {
return Err(Error::Other("Failed to find entity from uid.".to_owned()));
}
}
ServerMsg::TimeOfDay(time_of_day) => {
*self.state.ecs_mut().write_resource() = time_of_day;
}
ServerMsg::EcsSync(sync_package) => {
self.state.ecs_mut().sync_with_package(sync_package)
self.state.ecs_mut().apply_sync_package(sync_package);
}
ServerMsg::CreateEntity(entity_package) => {
self.state.ecs_mut().apply_entity_package(entity_package);
}
ServerMsg::DeleteEntity(entity) => {
if let Some(entity) = self.state.ecs().entity_from_uid(entity) {
if entity != self.entity {
let _ = self.state.ecs_mut().delete_entity(entity);
}
if self
.state
.read_component_cloned::<Uid>(self.entity)
.map(|u| u.into())
!= Some(entity)
{
self.state
.ecs_mut()
.delete_entity_and_clear_from_uid_allocator(entity);
}
}
ServerMsg::EntityPos { entity, pos } => {

View File

@ -5,10 +5,9 @@ authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>", "Maciej Ćwięka <mc
edition = "2018"
[dependencies]
sphynx = { git = "https://gitlab.com/veloren/sphynx.git", features = ["serde1"], rev = "ac4adf54d181339a789907acd27f61fe81daa455" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
specs = { version = "0.14.2", features = ["serde", "nightly"] }
specs = { version = "0.15.1", features = ["serde", "nightly", "storage-event-control"] }
vek = { version = "0.9.9", features = ["serde"] }
dot_vox = "4.0.0"
image = "0.22.3"
@ -30,8 +29,7 @@ parking_lot = "0.9.0"
crossbeam = "=0.7.2"
notify = "5.0.0-pre.1"
indexmap = "1.3.0"
# TODO: remove when upgrading to specs 0.15
hibitset = "0.5.3"
sum_type = "0.2.0"
[dev-dependencies]
criterion = "0.3"

View File

@ -33,6 +33,23 @@ impl Body {
_ => false,
}
}
// Note: this might need to be refined to something more complex for realistic
// behavior with less cylindrical bodies (e.g. wolfs)
pub fn radius(&self) -> f32 {
// TODO: Improve these values (some might be reliant on more info in inner type)
match self {
Body::Humanoid(_) => 0.5,
Body::QuadrupedSmall(_) => 0.6,
Body::QuadrupedMedium(_) => 0.9,
Body::BirdMedium(_) => 0.5,
Body::FishMedium(_) => 0.5,
Body::Dragon(_) => 2.5,
Body::BirdSmall(_) => 0.2,
Body::FishSmall(_) => 0.2,
Body::BipedLarge(_) => 1.0,
Body::Object(_) => 0.3,
}
}
}
impl Component for Body {

View File

@ -1,6 +1,6 @@
use crate::sync::Uid;
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
use sphynx::Uid;
use std::time::Duration;
use vek::*;

View File

@ -1,4 +1,4 @@
use crate::state::Uid;
use crate::sync::Uid;
use specs::{Component, FlaggedStorage, NullStorage};
use specs_idvs::IDVStorage;
use vek::*;

View File

@ -1,5 +1,4 @@
use crate::comp;
use crate::state::Uid;
use crate::{comp, sync::Uid};
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
use std::time::Duration;
@ -15,6 +14,7 @@ pub enum Effect {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Projectile {
pub owner: Uid,
// TODO: use SmallVec for these effects
pub hit_ground: Vec<Effect>,
pub hit_wall: Vec<Effect>,
pub hit_entity: Vec<Effect>,

View File

@ -1,4 +1,4 @@
use crate::{comp, state::Uid};
use crate::{comp, sync::Uid};
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;

View File

@ -1,9 +1,8 @@
use crate::comp;
use crate::{comp, sync::Uid};
use comp::item::Tool;
use parking_lot::Mutex;
use serde::Deserialize;
use specs::Entity as EcsEntity;
use sphynx::Uid;
use std::{collections::VecDeque, ops::DerefMut};
use vek::*;

View File

@ -20,6 +20,7 @@ pub mod pathfinding;
pub mod ray;
pub mod region;
pub mod state;
pub mod sync;
pub mod sys;
pub mod terrain;
pub mod util;

View File

@ -1,25 +1,13 @@
use crate::{comp, state};
use crate::{comp, sync};
use serde_derive::{Deserialize, Serialize};
use std::marker::PhantomData;
use sum_type::sum_type;
// Automatically derive From<T> for EcsResPacket
// for each variant EcsResPacket::T(T).
sphynx::sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsResPacket {
Time(state::Time),
TimeOfDay(state::TimeOfDay),
}
}
impl sphynx::ResPacket for EcsResPacket {}
// Automatically derive From<T> for EcsCompPacket
// for each variant EcsCompPacket::T(T.)
sphynx::sum_type! {
sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsCompPacket {
Pos(comp::Pos),
Vel(comp::Vel),
Ori(comp::Ori),
Body(comp::Body),
Player(comp::Player),
CanBuild(comp::CanBuild),
@ -30,19 +18,15 @@ sphynx::sum_type! {
MountState(comp::MountState),
Mounting(comp::Mounting),
Mass(comp::Mass),
Projectile(comp::Projectile),
Gravity(comp::Gravity),
Sticky(comp::Sticky),
}
}
// Automatically derive From<T> for EcsCompPhantom
// for each variant EcsCompPhantom::T(PhantomData<T>).
sphynx::sum_type! {
sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsCompPhantom {
Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>),
Ori(PhantomData<comp::Ori>),
Body(PhantomData<comp::Body>),
Player(PhantomData<comp::Player>),
CanBuild(PhantomData<comp::CanBuild>),
@ -53,11 +37,60 @@ sphynx::sum_type! {
MountState(PhantomData<comp::MountState>),
Mounting(PhantomData<comp::Mounting>),
Mass(PhantomData<comp::Mass>),
Projectile(PhantomData<comp::Projectile>),
Gravity(PhantomData<comp::Gravity>),
Sticky(PhantomData<comp::Sticky>),
}
}
impl sphynx::CompPacket for EcsCompPacket {
impl sync::CompPacket for EcsCompPacket {
type Phantom = EcsCompPhantom;
fn apply_insert(self, entity: specs::Entity, world: &specs::World) {
match self {
EcsCompPacket::Body(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Player(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CanBuild(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::MountState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mounting(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Gravity(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
}
}
fn apply_modify(self, entity: specs::Entity, world: &specs::World) {
match self {
EcsCompPacket::Body(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Player(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CanBuild(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::MountState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mounting(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Gravity(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
}
}
fn apply_remove(phantom: Self::Phantom, entity: specs::Entity, world: &specs::World) {
match phantom {
EcsCompPhantom::Body(_) => sync::handle_remove::<comp::Body>(entity, world),
EcsCompPhantom::Player(_) => sync::handle_remove::<comp::Player>(entity, world),
EcsCompPhantom::CanBuild(_) => sync::handle_remove::<comp::CanBuild>(entity, world),
EcsCompPhantom::Stats(_) => sync::handle_remove::<comp::Stats>(entity, world),
EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world)
}
EcsCompPhantom::Item(_) => sync::handle_remove::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world),
EcsCompPhantom::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world),
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),
EcsCompPhantom::Gravity(_) => sync::handle_remove::<comp::Gravity>(entity, world),
EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world),
}
}
}

View File

@ -4,7 +4,7 @@ pub mod server;
// Reexports
pub use self::client::ClientMsg;
pub use self::ecs_packet::{EcsCompPacket, EcsResPacket};
pub use self::ecs_packet::EcsCompPacket;
pub use self::server::{RequestStateError, ServerError, ServerInfo, ServerMsg};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View File

@ -1,6 +1,6 @@
use super::{ClientState, EcsCompPacket, EcsResPacket};
use super::{ClientState, EcsCompPacket};
use crate::{
comp,
comp, state, sync,
terrain::{Block, TerrainChunk},
ChatType,
};
@ -26,9 +26,9 @@ pub struct ServerInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerMsg {
InitialSync {
ecs_state: sphynx::StatePackage<EcsCompPacket, EcsResPacket>,
entity_uid: u64,
entity_package: sync::EntityPackage<EcsCompPacket>,
server_info: ServerInfo,
time_of_day: state::TimeOfDay,
// world_map: Vec2<usize>, /*, Vec<u32>)*/
},
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
@ -40,7 +40,9 @@ pub enum ServerMsg {
message: String,
},
SetPlayerEntity(u64),
EcsSync(sphynx::SyncPackage<EcsCompPacket, EcsResPacket>),
TimeOfDay(state::TimeOfDay),
EcsSync(sync::SyncPackage<EcsCompPacket>),
CreateEntity(sync::EntityPackage<EcsCompPacket>),
DeleteEntity(u64),
EntityPos {
entity: u64,

View File

@ -1,8 +1,7 @@
use crate::comp::{Pos, Vel};
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
use hibitset::BitSetLike;
use indexmap::IndexMap;
use specs::{BitSet, Entities, Join, ReadStorage};
use specs::{hibitset::BitSetLike, BitSet, Entities, Join, ReadStorage};
use vek::*;
pub enum Event {
@ -104,6 +103,16 @@ impl RegionMap {
// TODO special case large entities
pub fn tick(&mut self, pos: ReadStorage<Pos>, vel: ReadStorage<Vel>, entities: Entities) {
self.tick += 1;
// Clear events within each region
for i in 0..self.regions.len() {
self.regions
.get_index_mut(i)
.map(|(_, v)| v)
.unwrap()
.events
.clear();
}
// Add any untracked entites
for (pos, id) in (&pos, &entities, !&self.tracked_entities)
.join()
@ -118,14 +127,6 @@ impl RegionMap {
let mut regions_to_remove = Vec::new();
for i in 0..self.regions.len() {
// 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(),
@ -215,6 +216,47 @@ impl RegionMap {
pub fn key_pos(key: Vec2<i32>) -> Vec2<i32> {
key.map(|e| e << REGION_LOG2)
}
/// Finds the region where a given entity is located using a given position to speed up the search
pub fn find_region(&self, entity: specs::Entity, pos: Vec3<f32>) -> Option<Vec2<i32>> {
let id = entity.id();
// Compute key for most likely region
let key = Self::pos_key(pos.map(|e| e as i32));
// Get region
if let Some(region) = self.regions.get(&key) {
if region.entities().contains(id) {
return Some(key);
} else {
// Check neighbors
for i in 0..8 {
if let Some(idx) = region.neighbors[i] {
let (key, region) = self.regions.get_index(idx).unwrap();
if region.entities().contains(id) {
return Some(*key);
}
}
}
}
} else {
// Check neighbors
for i in 0..8 {
let key = key + NEIGHBOR_OFFSETS[i];
if let Some(region) = self.regions.get(&key) {
if region.entities().contains(id) {
return Some(key);
}
}
}
}
// Scan though all regions
for (key, region) in self.iter() {
if region.entities().contains(id) {
return Some(key);
}
}
None
}
fn key_index(&self, key: Vec2<i32>) -> Option<usize> {
self.regions.get_full(&key).map(|(i, _, _)| i)
}

View File

@ -1,11 +1,8 @@
// Reexports
pub use sphynx::Uid;
use crate::{
comp,
event::{EventBus, LocalEvent, ServerEvent, SfxEventItem},
msg::{EcsCompPacket, EcsResPacket},
region::RegionMap,
sync::WorldSyncExt,
sys,
terrain::{Block, TerrainChunk, TerrainGrid},
vol::WriteVol,
@ -14,12 +11,10 @@ use hashbrown::{HashMap, HashSet};
use rayon::{ThreadPool, ThreadPoolBuilder};
use serde_derive::{Deserialize, Serialize};
use specs::{
saveload::Marker,
shred::{Fetch, FetchMut},
storage::{MaskedStorage as EcsMaskedStorage, Storage as EcsStorage},
Component, DispatcherBuilder, Entity as EcsEntity, Join,
Component, DispatcherBuilder, Entity as EcsEntity, WorldExt,
};
use sphynx;
use std::{sync::Arc, time::Duration};
use vek::*;
@ -28,7 +23,7 @@ use vek::*;
const DAY_CYCLE_FACTOR: f64 = 24.0 * 2.0;
/// A resource that stores the time of day.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct TimeOfDay(pub f64);
/// A resource that stores the tick (i.e: physics) time.
@ -89,7 +84,7 @@ impl TerrainChanges {
/// A type used to represent game state stored on both the client and the server. This includes
/// things like entity components, terrain data, and global states like weather, time of day, etc.
pub struct State {
ecs: sphynx::World<EcsCompPacket, EcsResPacket>,
ecs: specs::World,
// Avoid lifetime annotation by storing a thread pool instead of the whole dispatcher
thread_pool: Arc<ThreadPool>,
}
@ -98,44 +93,32 @@ impl Default for State {
/// Create a new `State`.
fn default() -> Self {
Self {
ecs: sphynx::World::new(specs::World::new(), Self::setup_sphynx_world),
ecs: Self::setup_ecs_world(),
thread_pool: Arc::new(ThreadPoolBuilder::new().build().unwrap()),
}
}
}
impl State {
/// Create a new `State` from an ECS state package.
pub fn from_state_package(
state_package: sphynx::StatePackage<EcsCompPacket, EcsResPacket>,
) -> Self {
Self {
ecs: sphynx::World::from_state_package(
specs::World::new(),
Self::setup_sphynx_world,
state_package,
),
thread_pool: Arc::new(ThreadPoolBuilder::new().build().unwrap()),
}
}
// Create a new Sphynx ECS world.
/// Creates ecs world and registers all the common components and resources
// TODO: Split up registering into server and client (e.g. move EventBus<ServerEvent> to the server)
fn setup_sphynx_world(ecs: &mut sphynx::World<EcsCompPacket, EcsResPacket>) {
fn setup_ecs_world() -> specs::World {
let mut ecs = specs::World::new();
// Uids for sync
ecs.register_sync_marker();
// Register server -> all clients synced components.
ecs.register_synced::<comp::Body>();
ecs.register_synced::<comp::Player>();
ecs.register_synced::<comp::Stats>();
ecs.register_synced::<comp::CanBuild>();
ecs.register_synced::<comp::LightEmitter>();
ecs.register_synced::<comp::Item>();
ecs.register_synced::<comp::Scale>();
ecs.register_synced::<comp::Mounting>();
ecs.register_synced::<comp::MountState>();
ecs.register_synced::<comp::Mass>();
ecs.register_synced::<comp::Sticky>();
ecs.register_synced::<comp::Gravity>();
ecs.register_synced::<comp::Projectile>();
ecs.register::<comp::Body>();
ecs.register::<comp::Player>();
ecs.register::<comp::Stats>();
ecs.register::<comp::CanBuild>();
ecs.register::<comp::LightEmitter>();
ecs.register::<comp::Item>();
ecs.register::<comp::Scale>();
ecs.register::<comp::Mounting>();
ecs.register::<comp::MountState>();
ecs.register::<comp::Mass>();
ecs.register::<comp::Sticky>();
ecs.register::<comp::Gravity>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();
@ -151,6 +134,7 @@ impl State {
ecs.register::<comp::Inventory>();
// Register server-local components
// TODO: only register on the server
ecs.register::<comp::Last<comp::Pos>>();
ecs.register::<comp::Last<comp::Vel>>();
ecs.register::<comp::Last<comp::Ori>>();
@ -158,23 +142,26 @@ impl State {
ecs.register::<comp::Agent>();
ecs.register::<comp::ForceUpdate>();
ecs.register::<comp::InventoryUpdate>();
ecs.register::<comp::Inventory>();
ecs.register::<comp::Admin>();
ecs.register::<comp::Waypoint>();
ecs.register::<comp::Projectile>();
// Register synced resources used by the ECS.
ecs.insert_synced(TimeOfDay(0.0));
ecs.insert(TimeOfDay(0.0));
// Register unsynced resources used by the ECS.
ecs.add_resource(Time(0.0));
ecs.add_resource(DeltaTime(0.0));
ecs.add_resource(TerrainGrid::new().unwrap());
ecs.add_resource(BlockChange::default());
ecs.add_resource(TerrainChanges::default());
ecs.add_resource(EventBus::<ServerEvent>::default());
ecs.add_resource(EventBus::<LocalEvent>::default());
ecs.add_resource(EventBus::<SfxEventItem>::default());
ecs.add_resource(RegionMap::new());
ecs.insert(Time(0.0));
ecs.insert(DeltaTime(0.0));
ecs.insert(TerrainGrid::new().unwrap());
ecs.insert(BlockChange::default());
ecs.insert(TerrainChanges::default());
// TODO: only register on the server
ecs.insert(EventBus::<ServerEvent>::default());
ecs.insert(EventBus::<LocalEvent>::default());
ecs.insert(EventBus::<SfxEventItem>::default());
ecs.insert(RegionMap::new());
ecs
}
/// Register a component with the state's ECS.
@ -207,12 +194,12 @@ impl State {
}
/// Get a reference to the internal ECS world.
pub fn ecs(&self) -> &sphynx::World<EcsCompPacket, EcsResPacket> {
pub fn ecs(&self) -> &specs::World {
&self.ecs
}
/// Get a mutable reference to the internal ECS world.
pub fn ecs_mut(&mut self) -> &mut sphynx::World<EcsCompPacket, EcsResPacket> {
pub fn ecs_mut(&mut self) -> &mut specs::World {
&mut self.ecs
}
@ -319,68 +306,6 @@ impl State {
// Beyond a delta time of MAX_DELTA_TIME, start lagging to avoid skipping important physics events.
self.ecs.write_resource::<DeltaTime>().0 = dt.as_secs_f32().min(MAX_DELTA_TIME);
// Mounted entities. We handle this here because we need access to the Uid registry and I
// forgot how to access that within a system. Anyhow, here goes.
for (entity, mount_state) in (
&self.ecs.entities(),
&mut self.ecs.write_storage::<comp::MountState>(),
)
.join()
{
match mount_state {
comp::MountState::Unmounted => {}
comp::MountState::MountedBy(mounter) => {
if let Some((controller, mounter)) =
self.ecs.entity_from_uid(mounter.id()).and_then(|mounter| {
self.ecs
.read_storage::<comp::Controller>()
.get(mounter)
.cloned()
.map(|x| (x, mounter))
})
{
let pos = self.ecs.read_storage::<comp::Pos>().get(entity).copied();
let ori = self.ecs.read_storage::<comp::Ori>().get(entity).copied();
let vel = self.ecs.read_storage::<comp::Vel>().get(entity).copied();
if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) {
let _ = self
.ecs
.write_storage()
.insert(mounter, comp::Pos(pos.0 + Vec3::unit_z() * 1.0));
let _ = self.ecs.write_storage().insert(mounter, ori);
let _ = self.ecs.write_storage().insert(mounter, vel);
}
let _ = self
.ecs
.write_storage::<comp::Controller>()
.insert(entity, controller);
} else {
*mount_state = comp::MountState::Unmounted;
}
}
}
}
let mut to_unmount = Vec::new();
for (entity, comp::Mounting(mountee)) in (
&self.ecs.entities(),
&self.ecs.read_storage::<comp::Mounting>(),
)
.join()
{
if self
.ecs
.entity_from_uid(mountee.id())
.filter(|mountee| self.ecs.is_alive(*mountee))
.is_none()
{
to_unmount.push(entity);
}
}
for entity in to_unmount {
self.ecs.write_storage::<comp::Mounting>().remove(entity);
}
// Run RegionMap tick to update entity region occupancy
self.ecs.write_resource::<RegionMap>().tick(
self.ecs.read_storage::<comp::Pos>(),
@ -395,7 +320,7 @@ impl State {
// TODO: Consider alternative ways to do this
add_foreign_systems(&mut dispatch_builder);
// This dispatches all the systems in parallel.
dispatch_builder.build().dispatch(&self.ecs.res);
dispatch_builder.build().dispatch(&self.ecs);
self.ecs.maintain();

14
common/src/sync/mod.rs Normal file
View File

@ -0,0 +1,14 @@
// Note: Currently only one-way sync is supported until a usecase for two-way sync arises
mod packet;
mod sync_ext;
mod track;
mod uid;
// Reexports
pub use packet::{
handle_insert, handle_modify, handle_remove, CompPacket, EntityPackage, StatePackage,
SyncPackage,
};
pub use sync_ext::WorldSyncExt;
pub use track::UpdateTracker;
pub use uid::{Uid, UidAllocator};

125
common/src/sync/packet.rs Normal file
View File

@ -0,0 +1,125 @@
use super::{track::UpdateTracker, uid::Uid};
use log::error;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use specs::{Component, Entity, Join, ReadStorage, World, WorldExt};
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
marker::PhantomData,
};
/// Implemented by type that carries component data for insertion and modification
/// The assocatied `Phantom` type only carries information about which component type is of
/// interest and is used to transmit deletion events
pub trait CompPacket: Clone + Debug + Send + 'static {
type Phantom: Clone + Debug + Serialize + DeserializeOwned;
fn apply_insert(self, entity: Entity, world: &World);
fn apply_modify(self, entity: Entity, world: &World);
fn apply_remove(phantom: Self::Phantom, entity: Entity, world: &World);
}
/// Useful for implementing CompPacket trait
pub fn handle_insert<C: Component>(comp: C, entity: Entity, world: &World) {
if let Err(err) = world.write_storage::<C>().insert(entity, comp) {
error!("Error inserting component: {:?}", err);
};
}
/// Useful for implementing CompPacket trait
pub fn handle_modify<C: Component>(comp: C, entity: Entity, world: &World) {
let _ = world
.write_storage::<C>()
.get_mut(entity)
.map(|c| *c = comp);
}
/// Useful for implementing CompPacket trait
pub fn handle_remove<C: Component>(entity: Entity, world: &World) {
let _ = world.write_storage::<C>().remove(entity);
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum CompUpdateKind<P: CompPacket> {
Inserted(P),
Modified(P),
Removed(P::Phantom),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EntityPackage<P: CompPacket> {
pub uid: u64,
pub comps: Vec<P>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StatePackage<P: CompPacket> {
pub entities: Vec<EntityPackage<P>>,
}
impl<P: CompPacket> Default for StatePackage<P> {
fn default() -> Self {
Self {
entities: Vec::new(),
}
}
}
impl<P: CompPacket> StatePackage<P> {
pub fn new() -> Self {
Self::default()
}
pub fn with_entities<C: Component + Clone + Send + Sync>(
mut self,
mut entities: Vec<EntityPackage<P>>,
) -> Self {
self.entities.append(&mut entities);
self
}
pub fn with_entity(mut self, entry: EntityPackage<P>) -> Self {
self.entities.push(entry);
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SyncPackage<P: CompPacket> {
pub comp_updates: Vec<(u64, CompUpdateKind<P>)>,
pub created_entities: Vec<u64>,
pub deleted_entities: Vec<u64>,
}
impl<P: CompPacket> SyncPackage<P> {
pub fn new<'a>(
uids: &ReadStorage<'a, Uid>,
uid_tracker: &UpdateTracker<Uid>,
filter: impl Join + Copy,
deleted_entities: Vec<u64>,
) -> Self {
// Add created and deleted entities
let created_entities = (uids, filter, uid_tracker.inserted())
.join()
.map(|(uid, _, _)| (*uid).into())
.collect();
Self {
comp_updates: Vec::new(),
created_entities,
deleted_entities,
}
}
pub fn with_component<'a, C: Component + Clone + Send + Sync>(
mut self,
uids: &ReadStorage<'a, Uid>,
tracker: &UpdateTracker<C>,
storage: &ReadStorage<'a, C>,
filter: impl Join + Copy,
) -> Self
where
P: From<C>,
C: TryFrom<P>,
P::Phantom: From<PhantomData<C>>,
P::Phantom: TryInto<PhantomData<C>>,
C::Storage: specs::storage::Tracked,
{
tracker.get_updates_for(uids, storage, filter, &mut self.comp_updates);
self
}
}

164
common/src/sync/sync_ext.rs Normal file
View File

@ -0,0 +1,164 @@
use super::{
packet::{CompPacket, CompUpdateKind, EntityPackage, StatePackage, SyncPackage},
track::UpdateTracker,
uid::{Uid, UidAllocator},
};
use log::error;
use specs::{
saveload::{MarkedBuilder, MarkerAllocator},
world::Builder,
WorldExt,
};
pub trait WorldSyncExt {
fn register_sync_marker(&mut self);
fn register_synced<C: specs::Component + Clone + Send + Sync>(&mut self)
where
C::Storage: Default + specs::storage::Tracked;
fn register_tracker<C: specs::Component + Clone + Send + Sync>(&mut self)
where
C::Storage: Default + specs::storage::Tracked;
fn create_entity_synced(&mut self) -> specs::EntityBuilder;
fn delete_entity_and_clear_from_uid_allocator(&mut self, uid: u64);
fn uid_from_entity(&self, entity: specs::Entity) -> Option<Uid>;
fn entity_from_uid(&self, uid: u64) -> Option<specs::Entity>;
fn apply_entity_package<P: CompPacket>(
&mut self,
entity_package: EntityPackage<P>,
) -> specs::Entity;
fn apply_state_package<P: CompPacket>(&mut self, state_package: StatePackage<P>);
fn apply_sync_package<P: CompPacket>(&mut self, package: SyncPackage<P>);
}
impl WorldSyncExt for specs::World {
fn register_sync_marker(&mut self) {
self.register_synced::<Uid>();
// TODO: Consider only having allocator server side for now
self.insert(UidAllocator::new());
}
fn register_synced<C: specs::Component + Clone + Send + Sync>(&mut self)
where
C::Storage: Default + specs::storage::Tracked,
{
self.register::<C>();
self.register_tracker::<C>();
}
fn register_tracker<C: specs::Component + Clone + Send + Sync>(&mut self)
where
C::Storage: Default + specs::storage::Tracked,
{
let tracker = UpdateTracker::<C>::new(self);
self.insert(tracker);
}
fn create_entity_synced(&mut self) -> specs::EntityBuilder {
self.create_entity().marked::<super::Uid>()
}
/// Get the UID of an entity
fn uid_from_entity(&self, entity: specs::Entity) -> Option<Uid> {
self.read_storage::<Uid>().get(entity).copied()
}
/// Get the UID of an entity
fn entity_from_uid(&self, uid: u64) -> Option<specs::Entity> {
self.read_resource::<UidAllocator>()
.retrieve_entity_internal(uid)
}
fn apply_entity_package<P: CompPacket>(
&mut self,
entity_package: EntityPackage<P>,
) -> specs::Entity {
let EntityPackage { uid, comps } = entity_package;
let entity = create_entity_with_uid(self, uid);
for packet in comps {
packet.apply_insert(entity, self)
}
entity
}
fn delete_entity_and_clear_from_uid_allocator(&mut self, uid: u64) {
// Clear from uid allocator
let maybe_entity = self.write_resource::<UidAllocator>().remove_entity(uid);
if let Some(entity) = maybe_entity {
if let Err(err) = self.delete_entity(entity) {
error!("Failed to delete entity: {:?}", err);
}
}
}
fn apply_state_package<P: CompPacket>(&mut self, state_package: StatePackage<P>) {
let StatePackage { entities } = state_package;
// Apply state package entities
for entity_package in entities {
self.apply_entity_package(entity_package);
}
// TODO: determine if this is needed
// Initialize entities
//self.maintain();
}
fn apply_sync_package<P: CompPacket>(&mut self, package: SyncPackage<P>) {
// Take ownership of the fields
let SyncPackage {
comp_updates,
created_entities,
deleted_entities,
} = package;
// Attempt to create entities
for entity_uid in created_entities {
create_entity_with_uid(self, entity_uid);
}
// Update components
for (entity_uid, update) in comp_updates {
if let Some(entity) = self
.read_resource::<UidAllocator>()
.retrieve_entity_internal(entity_uid)
{
match update {
CompUpdateKind::Inserted(packet) => {
packet.apply_insert(entity, self);
}
CompUpdateKind::Modified(packet) => {
packet.apply_modify(entity, self);
}
CompUpdateKind::Removed(phantom) => {
P::apply_remove(phantom, entity, self);
}
}
}
}
// Attempt to delete entities that were marked for deletion
for entity_uid in deleted_entities {
self.delete_entity_and_clear_from_uid_allocator(entity_uid);
}
}
}
// Private utilities
fn create_entity_with_uid(specs_world: &mut specs::World, entity_uid: u64) -> specs::Entity {
let existing_entity = specs_world
.read_resource::<UidAllocator>()
.retrieve_entity_internal(entity_uid);
match existing_entity {
Some(entity) => entity,
None => {
let entity_builder = specs_world.create_entity();
let uid = entity_builder
.world
.write_resource::<UidAllocator>()
.allocate(entity_builder.entity, Some(entity_uid));
entity_builder.with(uid).build()
}
}
}

128
common/src/sync/track.rs Normal file
View File

@ -0,0 +1,128 @@
use super::{
packet::{CompPacket, CompUpdateKind},
uid::Uid,
};
use specs::{BitSet, Component, Entity, Join, ReadStorage, World, WorldExt};
use std::{
convert::{TryFrom, TryInto},
marker::PhantomData,
};
pub struct UpdateTracker<C: Component> {
reader_id: specs::ReaderId<specs::storage::ComponentEvent>,
inserted: BitSet,
modified: BitSet,
removed: BitSet,
phantom: PhantomData<C>,
}
impl<C: Component> UpdateTracker<C>
where
C::Storage: specs::storage::Tracked,
{
pub fn new(specs_world: &mut World) -> Self {
Self {
reader_id: specs_world.write_storage::<C>().register_reader(),
inserted: BitSet::new(),
modified: BitSet::new(),
removed: BitSet::new(),
phantom: PhantomData,
}
}
pub fn inserted(&self) -> &BitSet {
&self.inserted
}
pub fn modified(&self) -> &BitSet {
&self.modified
}
pub fn removed(&self) -> &BitSet {
&self.removed
}
pub fn record_changes<'a>(&mut self, storage: &specs::ReadStorage<'a, C>) {
self.inserted.clear();
self.modified.clear();
self.removed.clear();
for event in storage.channel().read(&mut self.reader_id) {
match event {
specs::storage::ComponentEvent::Inserted(id) => {
// If previously removed/modified we don't need to know that anymore
self.removed.remove(*id);
self.modified.remove(*id);
self.inserted.add(*id);
}
specs::storage::ComponentEvent::Modified(id) => {
// We don't care about modification if the component was just added
if !self.inserted.contains(*id) {
debug_assert!(!self.removed.contains(*id)); // Theoretically impossible
self.modified.add(*id);
}
}
specs::storage::ComponentEvent::Removed(id) => {
// Don't need to know that it was inserted/modified if it was subsequently removed
self.inserted.remove(*id);
self.modified.remove(*id);
self.removed.add(*id);
}
};
}
}
}
impl<C: Component + Clone + Send + Sync> UpdateTracker<C> {
pub fn add_packet_for<'a, P>(
&self,
storage: &ReadStorage<'a, C>,
entity: Entity,
packets: &mut Vec<P>,
) where
P: CompPacket,
P: From<C>,
C: TryFrom<P>,
P::Phantom: From<PhantomData<C>>,
P::Phantom: TryInto<PhantomData<C>>,
C::Storage: specs::storage::Tracked,
{
if let Some(comp) = storage.get(entity) {
packets.push(P::from(comp.clone()));
}
}
pub fn get_updates_for<'a, P>(
&self,
uids: &specs::ReadStorage<'a, Uid>,
storage: &specs::ReadStorage<'a, C>,
entity_filter: impl Join + Copy,
buf: &mut Vec<(u64, CompUpdateKind<P>)>,
) where
P: CompPacket,
P: From<C>,
C: TryFrom<P>,
P::Phantom: From<PhantomData<C>>,
P::Phantom: TryInto<PhantomData<C>>,
C::Storage: specs::storage::Tracked,
{
// Generate inserted updates
for (uid, comp, _, _) in (uids, storage, &self.inserted, entity_filter).join() {
buf.push((
(*uid).into(),
CompUpdateKind::Inserted(P::from(comp.clone())),
));
}
// Generate modified updates
for (uid, comp, _, _) in (uids, storage, &self.modified, entity_filter).join() {
buf.push((
(*uid).into(),
CompUpdateKind::Modified(P::from(comp.clone())),
));
}
// Generate removed updates
for (uid, _, _) in (uids, &self.removed, entity_filter).join() {
buf.push((
(*uid).into(),
CompUpdateKind::Removed(P::Phantom::from(PhantomData::<C>)),
));
}
}
}

92
common/src/sync/uid.rs Normal file
View File

@ -0,0 +1,92 @@
use serde_derive::{Deserialize, Serialize};
use specs::{
saveload::{Marker, MarkerAllocator},
world::EntitiesRes,
Component, Entity, FlaggedStorage, Join, ReadStorage, VecStorage,
};
use std::{collections::HashMap, fmt, u64};
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Uid(pub u64);
impl Into<u64> for Uid {
fn into(self) -> u64 {
self.0
}
}
impl From<u64> for Uid {
fn from(uid: u64) -> Self {
Self(uid)
}
}
impl fmt::Display for Uid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Component for Uid {
type Storage = FlaggedStorage<Self, VecStorage<Self>>;
}
impl Marker for Uid {
type Identifier = u64;
type Allocator = UidAllocator;
fn id(&self) -> u64 {
self.0
}
fn update(&mut self, update: Self) {
assert_eq!(self.0, update.0);
}
}
pub struct UidAllocator {
index: u64,
mapping: HashMap<u64, Entity>,
}
impl UidAllocator {
pub fn new() -> Self {
Self {
index: 0,
mapping: HashMap::new(),
}
}
// Useful for when a single entity is deleted because it doesn't reconstruct the entire hashmap
pub fn remove_entity(&mut self, id: u64) -> Option<Entity> {
self.mapping.remove(&id)
}
}
impl Default for UidAllocator {
fn default() -> Self {
Self::new()
}
}
impl MarkerAllocator<Uid> for UidAllocator {
fn allocate(&mut self, entity: Entity, id: Option<u64>) -> Uid {
let id = id.unwrap_or_else(|| {
let id = self.index;
self.index += 1;
id
});
self.mapping.insert(id, entity);
Uid(id)
}
fn retrieve_entity_internal(&self, id: u64) -> Option<Entity> {
self.mapping.get(&id).copied()
}
fn maintain(&mut self, entities: &EntitiesRes, storage: &ReadStorage<Uid>) {
self.mapping = (entities, storage)
.join()
.map(|(e, m)| (m.id(), e))
.collect();
}
}

View File

@ -1,6 +1,5 @@
use crate::comp::{
Agent, CharacterState, Controller, ControllerInputs, MountState, MovementState::Glide, Pos,
Stats,
Agent, CharacterState, Controller, MountState, MovementState::Glide, Pos, Stats,
};
use crate::pathfinding::WorldPath;
use crate::terrain::TerrainGrid;
@ -60,7 +59,7 @@ impl<'a> System<'a> for Sys {
controller.reset();
let mut inputs = ControllerInputs::default();
let mut inputs = &mut controller.inputs;
match agent {
Agent::Traveler { path } => {
@ -147,10 +146,13 @@ impl<'a> System<'a> for Sys {
let dist = Vec2::<f32>::from(target_pos.0 - pos.0).magnitude();
if target_stats.is_dead {
choose_new = true;
} else if dist < MIN_ATTACK_DIST
&& dist > 0.001
&& rand::random::<f32>() < 0.3
{
} else if dist < 0.001 {
// Probably can only happen when entities are at a different z-level
// since at the same level repulsion would keep them apart.
// Distinct from the first if block since we may want to change the
// behavior for this case.
choose_new = true;
} else if dist < MIN_ATTACK_DIST {
// Fight (and slowly move closer)
inputs.move_dir =
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.01;
@ -203,7 +205,8 @@ impl<'a> System<'a> for Sys {
}
}
controller.inputs = inputs;
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
}
}
}

View File

@ -1,10 +1,11 @@
use crate::{
comp::{
ActionState::*, CharacterState, Controller, HealthChange, HealthSource, Item, ItemKind,
Ori, Pos, Stats,
ActionState::*, Body, CharacterState, Controller, HealthChange, HealthSource, Item,
ItemKind, Ori, Pos, Scale, Stats,
},
event::{EventBus, LocalEvent, ServerEvent},
state::{DeltaTime, Uid},
state::DeltaTime,
sync::Uid,
};
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration;
@ -12,7 +13,8 @@ use vek::*;
const BLOCK_EFFICIENCY: f32 = 0.9;
const ATTACK_RANGE: f32 = 4.0;
const ATTACK_RANGE: f32 = 3.5;
const ATTACK_ANGLE: f32 = 45.0;
const BLOCK_ANGLE: f32 = 180.0;
/// This system is responsible for handling accepted inputs like moving or attacking
@ -26,9 +28,11 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Uid>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Controller>,
ReadStorage<'a, Body>,
ReadStorage<'a, Stats>,
WriteStorage<'a, CharacterState>,
WriteStorage<'a, Stats>,
);
fn run(
@ -41,20 +45,23 @@ impl<'a> System<'a> for Sys {
uids,
positions,
orientations,
scales,
controllers,
mut character_states,
bodies,
stats,
mut character_states,
): Self::SystemData,
) {
let mut server_emitter = server_bus.emitter();
let mut _local_emitter = local_bus.emitter();
// Attacks
for (entity, uid, pos, ori, _, stat) in (
for (entity, uid, pos, ori, scale_maybe, _, attacker_stats) in (
&entities,
&uids,
&positions,
&orientations,
scales.maybe(),
&controllers,
&stats,
)
@ -63,7 +70,7 @@ impl<'a> System<'a> for Sys {
let recover_duration = if let Some(Item {
kind: ItemKind::Tool { kind, .. },
..
}) = stat.equipment.main
}) = attacker_stats.equipment.main
{
kind.attack_recover_duration()
} else {
@ -91,13 +98,15 @@ impl<'a> System<'a> for Sys {
if deal_damage {
if let Some(Attack { .. }) = &character_states.get(entity).map(|c| c.action) {
// Go through all other entities
for (b, uid_b, pos_b, ori_b, character_b, stat_b) in (
for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in (
&entities,
&uids,
&positions,
&orientations,
scales.maybe(),
&character_states,
&stats,
&bodies,
)
.join()
{
@ -106,16 +115,21 @@ impl<'a> System<'a> for Sys {
let pos_b2: Vec2<f32> = Vec2::from(pos_b.0);
let ori2 = Vec2::from(ori.0);
// Scales
let scale = scale_maybe.map_or(1.0, |s| s.0);
let scale_b = scale_b_maybe.map_or(1.0, |s| s.0);
let rad_b = body_b.radius() * scale_b;
// Check if it is a hit
if entity != b
&& !stat_b.is_dead
&& pos.0.distance_squared(pos_b.0) < ATTACK_RANGE.powi(2)
// TODO: Use size instead of 1.0
&& ori2.angle_between(pos_b2 - pos2) < (2.0 / pos2.distance(pos_b2)).atan()
&& !stats_b.is_dead
// Spherical wedge shaped attack field
&& pos.0.distance_squared(pos_b.0) < (rad_b + scale * ATTACK_RANGE).powi(2)
&& ori2.angle_between(pos_b2 - pos2) < ATTACK_ANGLE.to_radians() / 2.0 + (rad_b / pos2.distance(pos_b2)).atan()
{
// Weapon gives base damage
let mut dmg = if let Some(ItemKind::Tool { power, .. }) =
stat.equipment.main.as_ref().map(|i| &i.kind)
attacker_stats.equipment.main.as_ref().map(|i| &i.kind)
{
*power as i32
} else {
@ -124,8 +138,8 @@ impl<'a> System<'a> for Sys {
// Block
if character_b.action.is_block()
&& ori_b.0.angle_between(pos.0 - pos_b.0).to_degrees()
< BLOCK_ANGLE / 2.0
&& ori_b.0.angle_between(pos.0 - pos_b.0)
< BLOCK_ANGLE.to_radians() / 2.0
{
dmg = (dmg as f32 * (1.0 - BLOCK_EFFICIENCY)) as i32
}

View File

@ -7,12 +7,12 @@ use crate::{
},
event::{Emitter, EventBus, LocalEvent, ServerEvent},
state::DeltaTime,
sync::{Uid, UidAllocator},
};
use specs::{
saveload::{Marker, MarkerAllocator},
Entities, Entity, Join, Read, ReadStorage, System, WriteStorage,
};
use sphynx::{Uid, UidAllocator};
use std::time::Duration;
use vek::*;

View File

@ -2,6 +2,7 @@ pub mod agent;
mod cleanup;
pub mod combat;
pub mod controller;
mod mount;
pub mod movement;
pub mod phys;
mod projectile;
@ -13,6 +14,7 @@ use specs::DispatcherBuilder;
// System names
const AGENT_SYS: &str = "agent_sys";
const CONTROLLER_SYS: &str = "controller_sys";
const MOUNT_SYS: &str = "mount_sys";
const PHYS_SYS: &str = "phys_sys";
const MOVEMENT_SYS: &str = "movement_sys";
const PROJECTILE_SYS: &str = "projectile_sys";
@ -23,13 +25,20 @@ const CLEANUP_SYS: &str = "cleanup_sys";
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(agent::Sys, AGENT_SYS, &[]);
dispatch_builder.add(controller::Sys, CONTROLLER_SYS, &[AGENT_SYS]);
dispatch_builder.add(mount::Sys, MOUNT_SYS, &[CONTROLLER_SYS]);
dispatch_builder.add(movement::Sys, MOVEMENT_SYS, &[]);
dispatch_builder.add(combat::Sys, COMBAT_SYS, &[CONTROLLER_SYS]);
dispatch_builder.add(stats::Sys, STATS_SYS, &[COMBAT_SYS]);
dispatch_builder.add(
phys::Sys,
PHYS_SYS,
&[CONTROLLER_SYS, MOVEMENT_SYS, COMBAT_SYS, STATS_SYS],
&[
CONTROLLER_SYS,
MOUNT_SYS,
MOVEMENT_SYS,
COMBAT_SYS,
STATS_SYS,
],
);
dispatch_builder.add(projectile::Sys, PROJECTILE_SYS, &[PHYS_SYS]);
dispatch_builder.add(cleanup::Sys, CLEANUP_SYS, &[PHYS_SYS]);

78
common/src/sys/mount.rs Normal file
View File

@ -0,0 +1,78 @@
use crate::{
comp::{Controller, MountState, Mounting, Ori, Pos, Vel},
sync::UidAllocator,
};
use specs::{
saveload::{Marker, MarkerAllocator},
Entities, Join, Read, System, WriteStorage,
};
use vek::*;
/// This system is responsible for controlling mounts
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Read<'a, UidAllocator>,
Entities<'a>,
WriteStorage<'a, Controller>,
WriteStorage<'a, MountState>,
WriteStorage<'a, Mounting>,
WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
);
fn run(
&mut self,
(
uid_allocator,
entities,
mut controllers,
mut mount_state,
mut mountings,
mut positions,
mut velocities,
mut orientations,
): Self::SystemData,
) {
// Mounted entities.
for (entity, mut mount_states) in (&entities, &mut mount_state.restrict_mut()).join() {
match mount_states.get_unchecked() {
MountState::Unmounted => {}
MountState::MountedBy(mounter_uid) => {
if let Some((controller, mounter)) = uid_allocator
.retrieve_entity_internal(mounter_uid.id())
.and_then(|mounter| controllers.get(mounter).cloned().map(|x| (x, mounter)))
{
// TODO: consider joining on these? (remember we can use .maybe())
let pos = positions.get(entity).copied();
let ori = orientations.get(entity).copied();
let vel = velocities.get(entity).copied();
if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) {
let _ = positions.insert(mounter, Pos(pos.0 + Vec3::unit_z() * 1.0));
let _ = orientations.insert(mounter, ori);
let _ = velocities.insert(mounter, vel);
}
let _ = controllers.insert(entity, controller);
} else {
*(mount_states.get_mut_unchecked()) = MountState::Unmounted;
}
}
}
}
let mut to_unmount = Vec::new();
for (entity, Mounting(mountee_uid)) in (&entities, &mountings).join() {
if uid_allocator
.retrieve_entity_internal(mountee_uid.id())
.filter(|mountee| entities.is_alive(*mountee))
.is_none()
{
to_unmount.push(entity);
}
}
for entity in to_unmount {
mountings.remove(entity);
}
}
}

View File

@ -5,10 +5,10 @@ use crate::{
},
event::{EventBus, ServerEvent},
state::DeltaTime,
sync::Uid,
terrain::TerrainGrid,
};
use specs::prelude::*;
use sphynx::Uid;
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use std::time::Duration;
use vek::*;
@ -173,10 +173,14 @@ impl<'a> System<'a> for Sys {
if Vec2::<f32>::from(wall_dir).magnitude_squared() > 0.001 {
Vec2::from(wall_dir).normalized()
} else {
Vec2::from(vel.0)
Vec2::from(inputs.move_dir)
}
} else {
} else if let Glide = character.movement {
// Note: non-gliding forces will also affect velocity and thus orientation
// producing potentially unexpected changes in direction
Vec2::from(vel.0)
} else {
Vec2::from(inputs.move_dir)
};
if ori_dir.magnitude_squared() > 0.0001

View File

@ -3,11 +3,11 @@ use {
comp::{Body, Gravity, Mass, Mounting, Ori, PhysicsState, Pos, Scale, Sticky, Vel},
event::{EventBus, ServerEvent},
state::DeltaTime,
sync::Uid,
terrain::{Block, TerrainGrid},
vol::ReadVol,
},
specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage},
sphynx::Uid,
vek::*,
};
@ -102,6 +102,7 @@ impl<'a> System<'a> for Sys {
let scale = scale.map(|s| s.0).unwrap_or(1.0);
// Basic collision with terrain
// TODO: rename this, not just the player entity
let player_rad = 0.3 * scale; // half-width of the player's AABB
let player_height = 1.5 * scale;
@ -342,16 +343,20 @@ impl<'a> System<'a> for Sys {
}
// Apply pushback
for (pos, scale, mass, vel, _, _, physics) in (
for (pos, scale, mass, vel, _, _, _, physics) in (
&positions,
scales.maybe(),
masses.maybe(),
&mut velocities,
&bodies,
!&mountings,
stickies.maybe(),
&mut physics_states,
)
.join()
.filter(|(_, _, _, _, _, _, sticky, physics)| {
sticky.is_none() || (physics.on_wall.is_none() && !physics.on_ground)
})
{
physics.touch_entity = None;

View File

@ -23,8 +23,25 @@ impl<'a> System<'a> for Sys {
) {
let mut server_event_emitter = server_event_bus.emitter();
for (entity, mut stat) in (&entities, &mut stats).join() {
if stat.should_die() && !stat.is_dead {
// Increment last change timer
stats.set_event_emission(false); // avoid unnecessary syncing
for stat in (&mut stats).join() {
stat.health.last_change.0 += f64::from(dt.0);
}
stats.set_event_emission(true);
// Mutates all stats every tick causing the server to resend this component for every entity every tick
for (entity, mut stats) in (&entities, &mut stats.restrict_mut()).join() {
let (set_dead, level_up) = {
let stat = stats.get_unchecked();
(
stat.should_die() && !stat.is_dead,
stat.exp.current() >= stat.exp.maximum(),
)
};
if set_dead {
let stat = stats.get_mut_unchecked();
server_event_emitter.emit(ServerEvent::Destroy {
entity,
cause: stat.health.last_change.1.cause,
@ -33,9 +50,8 @@ impl<'a> System<'a> for Sys {
stat.is_dead = true;
}
stat.health.last_change.0 += f64::from(dt.0);
if stat.exp.current() >= stat.exp.maximum() {
if level_up {
let stat = stats.get_mut_unchecked();
while stat.exp.current() >= stat.exp.maximum() {
stat.exp.change_by(-(stat.exp.maximum() as i64));
stat.exp.change_maximum_by(25);

View File

@ -11,7 +11,7 @@ world = { package = "veloren-world", path = "../world" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
log = "0.4.8"
specs = "0.14.2"
specs = { version = "0.15.1", features = ["shred-derive"] }
vek = "0.9.9"
uvth = "3.1.1"
lazy_static = "1.4.0"
@ -27,5 +27,3 @@ prometheus = "0.7"
prometheus-static-metric = "0.2"
rouille = "3.0.0"
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
# TODO: remove when upgrading to specs 0.15
hibitset = "0.5.3"

View File

@ -11,15 +11,17 @@ use common::{
npc::{get_npc_name, NpcKind},
pathfinding::WorldPath,
state::TimeOfDay,
sync::WorldSyncExt,
terrain::{Block, BlockKind, TerrainChunkSize},
vol::RectVolSize,
};
use rand::Rng;
use specs::{Builder, Entity as EcsEntity, Join};
use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
use vek::*;
use world::util::Sampler;
use lazy_static::lazy_static;
use log::error;
use scan_fmt::{scan_fmt, scan_fmt_some};
/// Struct representing a command that a user can run from server chat.
@ -1117,7 +1119,9 @@ fn handle_remove_lights(
let size = to_delete.len();
for entity in to_delete {
let _ = server.state.ecs_mut().delete_entity_synced(entity);
if let Err(err) = server.state.delete_entity_recorded(entity) {
error!("Failed to delete light: {:?}", err);
}
}
server.notify_client(

View File

@ -19,6 +19,7 @@ use crate::{
chunk_generator::ChunkGenerator,
client::{Client, RegionSubscription},
cmd::CHAT_COMMANDS,
sys::sentinel::{DeletedEntities, TrackedComps},
};
use common::{
assets, comp,
@ -26,14 +27,18 @@ use common::{
event::{EventBus, ServerEvent},
msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg},
net::PostOffice,
state::{BlockChange, State, TimeOfDay, Uid},
state::{BlockChange, State, TimeOfDay},
sync::{Uid, WorldSyncExt},
terrain::{block::Block, TerrainChunkSize, TerrainGrid},
vol::{ReadVol, RectVolSize, Vox},
};
use log::debug;
use log::{debug, error};
use metrics::ServerMetrics;
use rand::Rng;
use specs::{join::Join, world::EntityBuilder as EcsEntityBuilder, Builder, Entity as EcsEntity};
use specs::{
join::Join, world::EntityBuilder as EcsEntityBuilder, Builder, Entity as EcsEntity, RunNow,
SystemData, WorldExt,
};
use std::{
i32,
sync::Arc,
@ -83,25 +88,18 @@ impl Server {
/// Create a new `Server`
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
let mut state = State::default();
state
.ecs_mut()
.add_resource(EventBus::<ServerEvent>::default());
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
// TODO: anything but this
state.ecs_mut().add_resource(AuthProvider::new());
state.ecs_mut().add_resource(Tick(0));
state.ecs_mut().add_resource(ChunkGenerator::new());
// System timers
state
.ecs_mut()
.add_resource(sys::EntitySyncTimer::default());
state.ecs_mut().add_resource(sys::MessageTimer::default());
state
.ecs_mut()
.add_resource(sys::SubscriptionTimer::default());
state
.ecs_mut()
.add_resource(sys::TerrainSyncTimer::default());
state.ecs_mut().add_resource(sys::TerrainTimer::default());
state.ecs_mut().insert(AuthProvider::new());
state.ecs_mut().insert(Tick(0));
state.ecs_mut().insert(ChunkGenerator::new());
// System timers for performance monitoring
state.ecs_mut().insert(sys::EntitySyncTimer::default());
state.ecs_mut().insert(sys::MessageTimer::default());
state.ecs_mut().insert(sys::SentinelTimer::default());
state.ecs_mut().insert(sys::SubscriptionTimer::default());
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default());
// Server-only components
state.ecs_mut().register::<RegionSubscription>();
state.ecs_mut().register::<Client>();
@ -153,11 +151,16 @@ impl Server {
};
// set the spawn point we calculated above
state.ecs_mut().add_resource(SpawnPoint(spawn_point));
state.ecs_mut().insert(SpawnPoint(spawn_point));
// Set starting time for the server.
state.ecs_mut().write_resource::<TimeOfDay>().0 = settings.start_time;
// Register trackers
sys::sentinel::register_trackers(&mut state.ecs_mut());
state.ecs_mut().insert(DeletedEntities::default());
let this = Self {
state,
world: Arc::new(world),
@ -301,8 +304,6 @@ impl Server {
let server_settings = &self.server_settings;
let mut todo_remove = None;
match event {
ServerEvent::Explosion { pos, radius } => {
const RAYS: usize = 500;
@ -372,19 +373,20 @@ impl Server {
}
ServerEvent::Destroy { entity, cause } => {
let ecs = state.ecs();
// Chat message
if let Some(player) = ecs.read_storage::<comp::Player>().get(entity) {
if let Some(player) = state.ecs().read_storage::<comp::Player>().get(entity) {
let msg = if let comp::HealthSource::Attack { by } = cause {
ecs.entity_from_uid(by.into()).and_then(|attacker| {
ecs.read_storage::<comp::Player>().get(attacker).map(
|attacker_alias| {
state.ecs().entity_from_uid(by.into()).and_then(|attacker| {
state
.ecs()
.read_storage::<comp::Player>()
.get(attacker)
.map(|attacker_alias| {
format!(
"{} was killed by {}",
&player.alias, &attacker_alias.alias
)
},
)
})
})
} else {
None
@ -394,28 +396,48 @@ impl Server {
state.notify_registered_clients(ServerMsg::kill(msg));
}
// Give EXP to the killer if entity had stats
let mut stats = ecs.write_storage::<comp::Stats>();
if let Some(entity_stats) = stats.get(entity).cloned() {
if let comp::HealthSource::Attack { by } = cause {
ecs.entity_from_uid(by.into()).map(|attacker| {
if let Some(attacker_stats) = stats.get_mut(attacker) {
// TODO: Discuss whether we should give EXP by Player Killing or not.
attacker_stats
.exp
.change_by((entity_stats.level.level() * 10) as i64);
}
});
{
// Give EXP to the killer if entity had stats
let mut stats = state.ecs().write_storage::<comp::Stats>();
if let Some(entity_stats) = stats.get(entity).cloned() {
if let comp::HealthSource::Attack { by } = cause {
state.ecs().entity_from_uid(by.into()).map(|attacker| {
if let Some(attacker_stats) = stats.get_mut(attacker) {
// TODO: Discuss whether we should give EXP by Player Killing or not.
attacker_stats
.exp
.change_by((entity_stats.level.level() * 10) as i64);
}
});
}
}
}
if let Some(client) = ecs.write_storage::<Client>().get_mut(entity) {
let _ = ecs.write_storage().insert(entity, comp::Vel(Vec3::zero()));
let _ = ecs.write_storage().insert(entity, comp::ForceUpdate);
let mut remove = true;
if let Some(client) = state.ecs().write_storage::<Client>().get_mut(entity) {
remove = false;
state
.ecs()
.write_storage()
.insert(entity, comp::Vel(Vec3::zero()))
.err()
.map(|err| error!("Failed to set zero vel on dead client: {:?}", err));
state
.ecs()
.write_storage()
.insert(entity, comp::ForceUpdate)
.err()
.map(|err| {
error!("Failed to insert ForceUpdate on dead client: {:?}", err)
});
client.force_state(ClientState::Dead);
} else {
todo_remove = Some(entity.clone());
}
if remove {
if let Err(err) = state.delete_entity_recorded(entity) {
error!("Failed to delete destroyed entity: {:?}", err);
}
}
}
@ -449,7 +471,9 @@ impl Server {
};
if let Some(item_entity) = item_entity {
let _ = state.ecs_mut().delete_entity_synced(item_entity);
if let Err(err) = state.delete_entity_recorded(item_entity) {
error!("Failed to delete picked up item entity: {:?}", err);
}
}
state.write_component(entity, comp::InventoryUpdate);
@ -605,8 +629,8 @@ impl Server {
{
let not_mounting_yet = if let Some(comp::MountState::Unmounted) = state
.ecs()
.write_storage::<comp::MountState>()
.get_mut(mountee)
.read_storage::<comp::MountState>()
.get(mountee)
.cloned()
{
true
@ -720,7 +744,7 @@ impl Server {
main,
&server_settings,
);
Self::initialize_region_subscription(state, entity);
sys::subscription::initialize_region_subscription(state.ecs(), entity);
}
ServerEvent::CreateNpc {
@ -738,8 +762,8 @@ impl Server {
}
ServerEvent::ClientDisconnect(entity) => {
if let Err(err) = state.ecs_mut().delete_entity_synced(entity) {
debug!("Failed to delete disconnected client: {:?}", err);
if let Err(err) = state.delete_entity_recorded(entity) {
error!("Failed to delete disconnected client: {:?}", err);
}
frontend_events.push(Event::ClientDisconnected { entity });
@ -753,11 +777,6 @@ impl Server {
chat_commands.push((entity, cmd));
}
}
// TODO: is this needed?
if let Some(entity) = todo_remove {
let _ = state.ecs_mut().delete_entity_synced(entity);
}
}
// Generate requested chunks.
@ -816,8 +835,12 @@ impl Server {
// 3) Handle inputs from clients
frontend_events.append(&mut self.handle_new_connections()?);
// Run message recieving sys before the systems in common for decreased latency (e.g. run before controller system)
sys::message::Sys.run_now(&self.state.ecs());
let before_tick_4 = Instant::now();
// 4) Tick the client's LocalState.
// 4) Tick the server's LocalState.
self.state.tick(dt, sys::add_server_systems);
let before_handle_events = Instant::now();
@ -832,11 +855,6 @@ impl Server {
let before_tick_6 = Instant::now();
// 6) Synchronise clients with the new state of the world.
// TODO: Remove sphynx
// Sync 'logical' state using Sphynx.
let sync_package = self.state.ecs_mut().next_sync_package();
self.state
.notify_registered_clients(ServerMsg::EcsSync(sync_package));
// Remove NPCs that are outside the view distances of all players
// This is done by removing NPCs in unloaded chunks
@ -853,7 +871,9 @@ impl Server {
.collect::<Vec<_>>()
};
for entity in to_delete {
let _ = self.state.ecs_mut().delete_entity_synced(entity);
if let Err(err) = self.state.delete_entity_recorded(entity) {
error!("Failed to delete agent outside the terrain: {:?}", err);
}
}
let before_tick_7 = Instant::now();
@ -864,6 +884,7 @@ impl Server {
.read_resource::<sys::EntitySyncTimer>()
.nanos as i64;
let message_nanos = self.state.ecs().read_resource::<sys::MessageTimer>().nanos as i64;
let sentinel_nanos = self.state.ecs().read_resource::<sys::SentinelTimer>().nanos as i64;
let subscription_nanos = self
.state
.ecs()
@ -877,24 +898,28 @@ impl Server {
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().nanos as i64;
let total_sys_nanos = entity_sync_nanos
+ message_nanos
+ sentinel_nanos
+ subscription_nanos
+ terrain_sync_nanos
+ terrain_nanos;
self.metrics
.tick_time
.with_label_values(&["input"])
.set((before_tick_4 - before_tick_1).as_nanos() as i64);
.set((before_tick_4 - before_tick_1).as_nanos() as i64 - message_nanos);
self.metrics
.tick_time
.with_label_values(&["state tick"])
.set((before_handle_events - before_tick_4).as_nanos() as i64 - total_sys_nanos);
.set(
(before_handle_events - before_tick_4).as_nanos() as i64
- (total_sys_nanos - message_nanos),
);
self.metrics
.tick_time
.with_label_values(&["handle server events"])
.set((before_tick_6 - before_handle_events).as_nanos() as i64);
self.metrics
.tick_time
.with_label_values(&["sphynx sync"])
.with_label_values(&["entity deletion"])
.set((before_tick_7 - before_tick_6).as_nanos() as i64);
self.metrics
.tick_time
@ -971,7 +996,8 @@ impl Server {
.create_entity_synced()
.with(client)
.build();
// Return the state of the current world (all of the components that Sphynx tracks).
// Send client all the tracked components currently attached to its entity as well
// as synced resources (currently only `TimeOfDay`)
log::debug!("Starting initial sync with client.");
self.state
.ecs()
@ -979,9 +1005,11 @@ impl Server {
.get_mut(entity)
.unwrap()
.notify(ServerMsg::InitialSync {
ecs_state: self.state.ecs().gen_state_package(),
entity_uid: self.state.ecs().uid_from_entity(entity).unwrap().into(), // Can't fail.
// Send client their entity
entity_package: TrackedComps::fetch(&self.state.ecs())
.create_entity_package(entity),
server_info: self.server_info.clone(),
time_of_day: *self.state.ecs().read_resource(),
// world_map: (WORLD_SIZE/*, self.world.sim().get_map()*/),
});
log::debug!("Done initial sync with client.");
@ -993,83 +1021,6 @@ impl Server {
Ok(frontend_events)
}
/// Initialize region subscription
fn initialize_region_subscription(state: &mut State, entity: specs::Entity) {
let mut subscription = None;
if let (Some(client_pos), Some(client_vd), Some(client)) = (
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),
state.ecs().write_storage::<Client>().get_mut(entity),
) {
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()
{
client.notify(ServerMsg::EntityPos {
entity: uid.into(),
pos,
});
if let Some(vel) = vel.copied() {
client.notify(ServerMsg::EntityVel {
entity: uid.into(),
vel,
});
}
if let Some(ori) = ori.copied() {
client.notify(ServerMsg::EntityOri {
entity: uid.into(),
ori,
});
}
if let Some(character_state) = character_state.copied() {
client.notify(ServerMsg::EntityCharacterState {
entity: uid.into(),
character_state,
});
}
}
}
subscription = Some(RegionSubscription {
fuzzy_chunk,
regions,
});
}
if let Some(subscription) = subscription {
state.write_component(entity, subscription);
}
}
pub fn notify_client(&self, entity: EcsEntity, msg: ServerMsg) {
if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
client.notify(msg)
@ -1131,6 +1082,10 @@ trait StateExt {
stats: comp::Stats,
body: comp::Body,
) -> EcsEntityBuilder;
fn delete_entity_recorded(
&mut self,
entity: EcsEntity,
) -> Result<(), specs::error::WrongGeneration>;
}
impl StateExt for State {
@ -1191,4 +1146,28 @@ impl StateExt for State {
client.notify(msg.clone())
}
}
fn delete_entity_recorded(
&mut self,
entity: EcsEntity,
) -> Result<(), specs::error::WrongGeneration> {
let (maybe_uid, maybe_pos) = (
self.ecs().read_storage::<Uid>().get(entity).copied(),
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),
);
let res = self.ecs_mut().delete_entity(entity);
if res.is_ok() {
if let (Some(uid), Some(pos)) = (maybe_uid, maybe_pos) {
let region_key = self
.ecs()
.read_resource::<common::region::RegionMap>()
.find_region(entity, pos.0)
.expect("Failed to find region containing entity during entity deletion");
self.ecs()
.write_resource::<DeletedEntities>()
.record_deleted_entity(uid, region_key);
}
}
res
}
}

View File

@ -1,4 +1,7 @@
use super::SysTimer;
use super::{
sentinel::{DeletedEntities, ReadTrackers, TrackedComps},
SysTimer,
};
use crate::{
client::{Client, RegionSubscription},
Tick,
@ -7,7 +10,8 @@ use common::{
comp::{CharacterState, ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel},
msg::ServerMsg,
region::{Event as RegionEvent, RegionMap},
state::Uid,
state::TimeOfDay,
sync::Uid,
};
use specs::{
Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage,
@ -19,6 +23,7 @@ impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, Tick>,
ReadExpect<'a, TimeOfDay>,
ReadExpect<'a, RegionMap>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Uid>,
@ -35,6 +40,9 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Client>,
WriteStorage<'a, ForceUpdate>,
WriteStorage<'a, InventoryUpdate>,
Write<'a, DeletedEntities>,
TrackedComps<'a>,
ReadTrackers<'a>,
);
fn run(
@ -42,6 +50,7 @@ impl<'a> System<'a> for Sys {
(
entities,
tick,
time_of_day,
region_map,
mut timer,
uids,
@ -58,6 +67,9 @@ impl<'a> System<'a> for Sys {
mut clients,
mut force_updates,
mut inventory_updates,
mut deleted_entities,
tracked_comps,
trackers,
): Self::SystemData,
) {
timer.start();
@ -68,8 +80,8 @@ impl<'a> System<'a> for Sys {
// 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??
// - For each entity entered event, iterate through the client list and check if they are subscribed to the source (hash calc per subscribed client per entity event), if not subscribed to the source send a entity creation message to that client
// - 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 event)
// 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
@ -77,6 +89,8 @@ impl<'a> System<'a> for Sys {
// Sync physics
// via iterating through regions
for (key, region) in region_map.iter() {
// Assemble subscriber list for this region by iterating through clients and checking
// if they are subscribed to this region
let mut subscribers = (&mut clients, &entities, &subscriptions, &positions)
.join()
.filter_map(|(client, entity, subscription, pos)| {
@ -91,6 +105,10 @@ impl<'a> System<'a> for Sys {
for event in region.events() {
match event {
RegionEvent::Entered(id, maybe_key) => {
// Don't process newly created entities here (redundant network messages)
if trackers.uid.inserted().contains(*id) {
continue;
}
let entity = entities.entity(*id);
if let Some((uid, pos, vel, ori, character_state)) =
uids.get(entity).and_then(|uid| {
@ -105,14 +123,18 @@ impl<'a> System<'a> for Sys {
})
})
{
let create_msg = ServerMsg::CreateEntity(
tracked_comps.create_entity_package(entity),
);
for (client, regions, client_entity, _) in &mut subscribers {
if maybe_key
.as_ref()
.map(|key| !regions.contains(key))
.unwrap_or(true)
// Client doesn't need to know about itself
&& *client_entity != entity
// Client doesn't need to know about itself
{
client.notify(create_msg.clone());
send_initial_unsynced_components(
client,
uid,
@ -142,39 +164,59 @@ impl<'a> System<'a> for Sys {
}
}
let mut send_msg =
|msg: ServerMsg, entity: EcsEntity, pos: Pos, force_update, throttle: bool| {
for (client, _, client_entity, client_pos) in &mut subscribers {
match force_update {
None if client_entity == &entity => {}
_ => {
let distance_sq = client_pos.0.distance_squared(pos.0);
// Sync tracked components
// Get deleted entities in this region from DeletedEntities
let sync_msg = ServerMsg::EcsSync(
trackers.create_sync_package(
&tracked_comps,
region.entities(),
deleted_entities
.take_deleted_in_region(key)
.unwrap_or_else(|| Vec::new()),
),
);
for (client, _, _, _) in &mut subscribers {
client.notify(sync_msg.clone());
}
// Throttle update rate based on distance to player
// TODO: more entities will be farther away so it could be more
// efficient to reverse the order of these checks
let update = if !throttle || 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) % 16 == 0
} else {
(tick + entity.id() as u64) % 32 == 0
};
if update {
client.notify(msg.clone());
}
}
let mut send_msg = |msg: ServerMsg,
entity: EcsEntity,
pos: Pos,
force_update: Option<&ForceUpdate>,
throttle: bool| {
for (client, _, client_entity, client_pos) in &mut subscribers {
let update = if client_entity == &entity {
// Don't send client physics updates about itself unless force update is set
force_update.is_some()
} else if !throttle {
// Update rate not thottled by distance
true
} else {
// Throttle update rate based on distance to client
let distance_sq = client_pos.0.distance_squared(pos.0);
// More entities farther away so checks start there
if distance_sq > 300.0f32.powi(2) {
(tick + entity.id() as u64) % 32 == 0
} else if distance_sq > 250.0f32.powi(2) {
(tick + entity.id() as u64) % 16 == 0
} else if distance_sq > 200.0f32.powi(2) {
(tick + entity.id() as u64) % 8 == 0
} else if distance_sq > 150.0f32.powi(2) {
(tick + entity.id() as u64) % 4 == 0
} else if distance_sq > 100.0f32.powi(2) {
(tick + entity.id() as u64) % 2 == 0
} else {
true // Closer than 100 blocks
}
}
};
};
if update {
client.notify(msg.clone());
}
}
};
// Sync physics components
for (_, entity, &uid, &pos, maybe_vel, maybe_ori, character_state, force_update) in (
region.entities(),
&entities,
@ -257,6 +299,27 @@ impl<'a> System<'a> for Sys {
}
}
// Handle entity deletion in regions that don't exist in RegionMap (theoretically none)
for (region_key, deleted) in deleted_entities.take_remaining_deleted() {
for client in
(&mut clients, &subscriptions)
.join()
.filter_map(|(client, subscription)| {
if client.is_ingame() && subscription.regions.contains(&region_key) {
Some(client)
} else {
None
}
})
{
for uid in &deleted {
client.notify(ServerMsg::DeleteEntity(*uid));
}
}
}
// TODO: Sync clients that don't have a position?
// Sync inventories
for (client, inventory, _) in (&mut clients, &inventories, &inventory_updates).join() {
client.notify(ServerMsg::InventoryUpdate(inventory.clone()));
@ -266,6 +329,13 @@ impl<'a> System<'a> for Sys {
force_updates.clear();
inventory_updates.clear();
// Sync resources
// TODO: doesn't really belong in this system (rename system or create another system?)
let tof_msg = ServerMsg::TimeOfDay(*time_of_day);
for client in (&mut clients).join() {
client.notify(tof_msg.clone());
}
timer.end();
}
}

View File

@ -1,7 +1,7 @@
use super::SysTimer;
use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT};
use common::{
comp::{Admin, Body, CanBuild, Controller, Ori, Player, Pos, Vel},
comp::{Admin, Body, CanBuild, Controller, ForceUpdate, Ori, Player, Pos, Vel},
event::{EventBus, ServerEvent},
msg::{validate_chat_msg, ChatMsgValidationError, MAX_BYTES_CHAT_MSG},
msg::{ClientMsg, ClientState, RequestStateError, ServerMsg},
@ -25,6 +25,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Body>,
ReadStorage<'a, CanBuild>,
ReadStorage<'a, Admin>,
ReadStorage<'a, ForceUpdate>,
WriteExpect<'a, AuthProvider>,
Write<'a, BlockChange>,
WriteStorage<'a, Pos>,
@ -46,6 +47,7 @@ impl<'a> System<'a> for Sys {
bodies,
can_build,
admins,
force_updates,
mut accounts,
mut block_changes,
mut positions,
@ -218,9 +220,11 @@ impl<'a> System<'a> for Sys {
},
ClientMsg::PlayerPhysics { pos, vel, ori } => match client.client_state {
ClientState::Character => {
let _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel);
let _ = orientations.insert(entity, ori);
if force_updates.get(entity).is_none() {
let _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel);
let _ = orientations.insert(entity, ori);
}
}
// Only characters can send positions.
_ => client.error_state(RequestStateError::Impossible),

View File

@ -1,5 +1,6 @@
pub mod entity_sync;
pub mod message;
pub mod sentinel;
pub mod subscription;
pub mod terrain;
pub mod terrain_sync;
@ -9,23 +10,29 @@ use std::{marker::PhantomData, time::Instant};
pub type EntitySyncTimer = SysTimer<entity_sync::Sys>;
pub type MessageTimer = SysTimer<message::Sys>;
pub type SentinelTimer = SysTimer<sentinel::Sys>;
pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
// System names
const ENTITY_SYNC_SYS: &str = "server_entity_sync_sys";
const SENTINEL_SYS: &str = "sentinel_sys";
const SUBSCRIPTION_SYS: &str = "server_subscription_sys";
const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys";
const MESSAGE_SYS: &str = "server_message_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
// TODO: makes some of these dependent on systems in common like the phys system
dispatch_builder.add(sentinel::Sys, SENTINEL_SYS, &[]);
dispatch_builder.add(subscription::Sys, SUBSCRIPTION_SYS, &[]);
dispatch_builder.add(entity_sync::Sys, ENTITY_SYNC_SYS, &[SUBSCRIPTION_SYS]);
dispatch_builder.add(
entity_sync::Sys,
ENTITY_SYNC_SYS,
&[SUBSCRIPTION_SYS, SENTINEL_SYS],
);
dispatch_builder.add(terrain_sync::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(terrain::Sys, TERRAIN_SYNC_SYS, &[TERRAIN_SYS]);
dispatch_builder.add(message::Sys, MESSAGE_SYS, &[]);
}
/// Used to keep track of how much time each system takes

229
server/src/sys/sentinel.rs Normal file
View File

@ -0,0 +1,229 @@
use super::SysTimer;
use common::{
comp::{
Body, CanBuild, Gravity, Item, LightEmitter, Mass, MountState, Mounting, Player, Scale,
Stats, Sticky,
},
msg::EcsCompPacket,
sync::{EntityPackage, SyncPackage, Uid, UpdateTracker, WorldSyncExt},
};
use hashbrown::HashMap;
use specs::{
shred::ResourceId, Entity as EcsEntity, Join, ReadExpect, ReadStorage, System, SystemData,
World, Write, WriteExpect,
};
use vek::*;
/// Always watching
/// This system will monitor specific components for insertion, removal, and modification
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Write<'a, SysTimer<Self>>,
TrackedComps<'a>,
WriteTrackers<'a>,
);
fn run(&mut self, (mut timer, comps, mut trackers): Self::SystemData) {
timer.start();
record_changes(&comps, &mut trackers);
timer.end();
}
}
// Probably more difficult than it needs to be :p
#[derive(SystemData)]
pub struct TrackedComps<'a> {
pub uid: ReadStorage<'a, Uid>,
pub body: ReadStorage<'a, Body>,
pub player: ReadStorage<'a, Player>,
pub stats: ReadStorage<'a, Stats>,
pub can_build: ReadStorage<'a, CanBuild>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
pub item: ReadStorage<'a, Item>,
pub scale: ReadStorage<'a, Scale>,
pub mounting: ReadStorage<'a, Mounting>,
pub mount_state: ReadStorage<'a, MountState>,
pub mass: ReadStorage<'a, Mass>,
pub sticky: ReadStorage<'a, Sticky>,
pub gravity: ReadStorage<'a, Gravity>,
}
impl<'a> TrackedComps<'a> {
pub fn create_entity_package(&self, entity: EcsEntity) -> EntityPackage<EcsCompPacket> {
let uid = self
.uid
.get(entity)
.copied()
.expect("No uid to create an entity package")
.0;
let mut comps = Vec::new();
self.body.get(entity).copied().map(|c| comps.push(c.into()));
self.player
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.stats
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.can_build
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.light_emitter
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.item.get(entity).cloned().map(|c| comps.push(c.into()));
self.scale
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.mounting
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.mount_state
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.mass.get(entity).copied().map(|c| comps.push(c.into()));
self.sticky
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.gravity
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
EntityPackage { uid, comps }
}
}
#[derive(SystemData)]
pub struct ReadTrackers<'a> {
pub uid: ReadExpect<'a, UpdateTracker<Uid>>,
pub body: ReadExpect<'a, UpdateTracker<Body>>,
pub player: ReadExpect<'a, UpdateTracker<Player>>,
pub stats: ReadExpect<'a, UpdateTracker<Stats>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
pub item: ReadExpect<'a, UpdateTracker<Item>>,
pub scale: ReadExpect<'a, UpdateTracker<Scale>>,
pub mounting: ReadExpect<'a, UpdateTracker<Mounting>>,
pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>,
pub mass: ReadExpect<'a, UpdateTracker<Mass>>,
pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>,
pub gravity: ReadExpect<'a, UpdateTracker<Gravity>>,
}
impl<'a> ReadTrackers<'a> {
pub fn create_sync_package(
&self,
comps: &TrackedComps,
filter: impl Join + Copy,
deleted_entities: Vec<u64>,
) -> SyncPackage<EcsCompPacket> {
SyncPackage::new(&comps.uid, &self.uid, filter, deleted_entities)
.with_component(&comps.uid, &*self.body, &comps.body, filter)
.with_component(&comps.uid, &*self.player, &comps.player, filter)
.with_component(&comps.uid, &*self.stats, &comps.stats, filter)
.with_component(&comps.uid, &*self.can_build, &comps.can_build, filter)
.with_component(
&comps.uid,
&*self.light_emitter,
&comps.light_emitter,
filter,
)
.with_component(&comps.uid, &*self.item, &comps.item, filter)
.with_component(&comps.uid, &*self.scale, &comps.scale, filter)
.with_component(&comps.uid, &*self.mounting, &comps.mounting, filter)
.with_component(&comps.uid, &*self.mount_state, &comps.mount_state, filter)
.with_component(&comps.uid, &*self.mass, &comps.mass, filter)
.with_component(&comps.uid, &*self.sticky, &comps.sticky, filter)
.with_component(&comps.uid, &*self.gravity, &comps.gravity, filter)
}
}
#[derive(SystemData)]
pub struct WriteTrackers<'a> {
uid: WriteExpect<'a, UpdateTracker<Uid>>,
body: WriteExpect<'a, UpdateTracker<Body>>,
player: WriteExpect<'a, UpdateTracker<Player>>,
stats: WriteExpect<'a, UpdateTracker<Stats>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
item: WriteExpect<'a, UpdateTracker<Item>>,
scale: WriteExpect<'a, UpdateTracker<Scale>>,
mounting: WriteExpect<'a, UpdateTracker<Mounting>>,
mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
mass: WriteExpect<'a, UpdateTracker<Mass>>,
sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
gravity: WriteExpect<'a, UpdateTracker<Gravity>>,
}
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
// Update trackers
trackers.uid.record_changes(&comps.uid);
trackers.body.record_changes(&comps.body);
trackers.player.record_changes(&comps.player);
trackers.stats.record_changes(&comps.stats);
trackers.can_build.record_changes(&comps.can_build);
trackers.light_emitter.record_changes(&comps.light_emitter);
trackers.item.record_changes(&comps.item);
trackers.scale.record_changes(&comps.scale);
trackers.mounting.record_changes(&comps.mounting);
trackers.mount_state.record_changes(&comps.mount_state);
trackers.mass.record_changes(&comps.mass);
trackers.sticky.record_changes(&comps.sticky);
trackers.gravity.record_changes(&comps.gravity);
}
pub fn register_trackers(world: &mut World) {
world.register_tracker::<Uid>();
world.register_tracker::<Body>();
world.register_tracker::<Player>();
world.register_tracker::<Stats>();
world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>();
world.register_tracker::<Item>();
world.register_tracker::<Scale>();
world.register_tracker::<Mounting>();
world.register_tracker::<MountState>();
world.register_tracker::<Mass>();
world.register_tracker::<Sticky>();
world.register_tracker::<Gravity>();
}
/// Deleted entities grouped by region
pub struct DeletedEntities {
map: HashMap<Vec2<i32>, Vec<u64>>,
}
impl Default for DeletedEntities {
fn default() -> Self {
Self {
map: HashMap::new(),
}
}
}
impl DeletedEntities {
pub fn record_deleted_entity(&mut self, uid: Uid, region_key: Vec2<i32>) {
self.map
.entry(region_key)
.or_insert(Vec::new())
.push(uid.into());
}
pub fn take_deleted_in_region(&mut self, key: Vec2<i32>) -> Option<Vec<u64>> {
self.map.remove(&key)
}
pub fn get_deleted_in_region(&mut self, key: Vec2<i32>) -> Option<&Vec<u64>> {
self.map.get(&key)
}
pub fn take_remaining_deleted(&mut self) -> Vec<(Vec2<i32>, Vec<u64>)> {
// TODO: don't allocate
self.map.drain().collect()
}
}

View File

@ -1,14 +1,21 @@
use super::SysTimer;
use super::{
sentinel::{DeletedEntities, TrackedComps},
SysTimer,
};
use crate::client::{self, Client, RegionSubscription};
use common::{
comp::{CharacterState, Ori, Player, Pos, Vel},
msg::ServerMsg,
region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap},
state::Uid,
sync::Uid,
terrain::TerrainChunkSize,
vol::RectVolSize,
};
use specs::{Entities, Join, ReadExpect, ReadStorage, System, Write, WriteStorage};
use log::{debug, error};
use specs::{
Entities, Join, ReadExpect, ReadStorage, System, SystemData, World, WorldExt, Write,
WriteStorage,
};
use vek::*;
/// This system will update region subscriptions based on client positions
@ -26,6 +33,8 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Player>,
WriteStorage<'a, Client>,
WriteStorage<'a, RegionSubscription>,
Write<'a, DeletedEntities>,
TrackedComps<'a>,
);
fn run(
@ -42,6 +51,8 @@ impl<'a> System<'a> for Sys {
players,
mut clients,
mut subscriptions,
mut deleted_entities,
tracked_comps,
): Self::SystemData,
) {
timer.start();
@ -66,8 +77,15 @@ impl<'a> System<'a> for Sys {
&entities,
)
.join()
.filter_map(|(c, s, pos, player, e)| player.view_distance.map(|v| (c, s, pos, v, e)))
.filter_map(|(client, s, pos, player, e)| {
if client.is_ingame() {
player.view_distance.map(|v| (client, s, pos, v, e))
} else {
None
}
})
{
// Calculate current chunk
let chunk = (Vec2::<f32>::from(pos.0))
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
// Only update regions when moving to a new chunk
@ -85,7 +103,7 @@ impl<'a> System<'a> for Sys {
.reduce_or()
{
// Update current chunk
subscription.fuzzy_chunk = (Vec2::<f32>::from(pos.0))
subscription.fuzzy_chunk = Vec2::<f32>::from(pos.0)
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
// Use the largest side length as our chunk size
let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
@ -106,19 +124,24 @@ impl<'a> System<'a> for Sys {
// Iterate through regions to remove
for key in regions_to_remove.drain(..) {
// Remove region from this clients set of subscribed regions
// Remove region from this client's set of subscribed regions
subscription.regions.remove(&key);
// Tell the client to delete the entities in that region if it exists in the RegionMap
if let Some(region) = region_map.get(key) {
// Process entity left events since they won't be processed during phsyics sync because this region is no longer subscribed to
// Process entity left events since they won't be processed during entity sync because this region is no longer subscribed to
// TODO: consider changing system ordering??
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
// Doesn't overlap with entity deletion in sync packages
// because the uid would not be available if the entity was
// deleted
if let Some(&uid) = uids.get(entities.entity(*id)) {
if !maybe_key
.as_ref()
// Don't need to check that this isn't also in the regions to remove since the entity will be removed when we get to that one
.map(|key| subscription.regions.contains(key))
.unwrap_or(false)
{
@ -128,10 +151,19 @@ impl<'a> System<'a> for Sys {
}
}
}
// Tell client to delete entities in the region
for (&uid, _) in (&uids, region.entities()).join() {
client.notify(ServerMsg::DeleteEntity(uid.into()))
client.notify(ServerMsg::DeleteEntity(uid.into()));
}
}
// Send deleted entities since they won't be processed for this client in entity sync
for uid in deleted_entities
.get_deleted_in_region(key)
.iter()
.flat_map(|v| v.iter())
{
client.notify(ServerMsg::DeleteEntity(*uid));
}
}
for key in regions_in_vd(
@ -139,10 +171,11 @@ impl<'a> System<'a> for Sys {
(vd as f32 * chunk_size)
+ (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
) {
// Send client intial info about the entities in this region
if subscription.regions.insert(key) {
// Send client intial info about the entities in this region if it was not
// already within the set of subscribed regions
if subscription.regions.insert(key.clone()) {
if let Some(region) = region_map.get(key) {
for (uid, pos, vel, ori, character_state, _, _) in (
for (uid, pos, vel, ori, character_state, _, entity) in (
&uids,
&positions,
velocities.maybe(),
@ -154,6 +187,11 @@ impl<'a> System<'a> for Sys {
.join()
.filter(|(_, _, _, _, _, _, e)| *e != client_entity)
{
// Send message to create entity and tracked components
client.notify(ServerMsg::CreateEntity(
tracked_comps.create_entity_package(entity),
));
// Send message to create physics components
super::entity_sync::send_initial_unsynced_components(
client,
uid,
@ -172,3 +210,69 @@ impl<'a> System<'a> for Sys {
timer.end();
}
}
/// Initialize region subscription
pub fn initialize_region_subscription(world: &World, entity: specs::Entity) {
if let (Some(client_pos), Some(client_vd), Some(client)) = (
world.read_storage::<Pos>().get(entity),
world
.read_storage::<Player>()
.get(entity)
.map(|pl| pl.view_distance)
.and_then(|v| v),
world.write_storage::<Client>().get_mut(entity),
) {
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(),
);
let region_map = world.read_resource::<RegionMap>();
let tracked_comps = TrackedComps::fetch(world);
for key in &regions {
if let Some(region) = region_map.get(*key) {
for (uid, pos, vel, ori, character_state, _, entity) in (
&tracked_comps.uid,
&world.read_storage::<Pos>(), // We assume all these entities have a position
world.read_storage::<Vel>().maybe(),
world.read_storage::<Ori>().maybe(),
world.read_storage::<CharacterState>().maybe(),
region.entities(),
&world.entities(),
)
.join()
{
// Send message to create entity and tracked components
client.notify(ServerMsg::CreateEntity(
tracked_comps.create_entity_package(entity),
));
// Send message to create physics components
super::entity_sync::send_initial_unsynced_components(
client,
uid,
pos,
vel,
ori,
character_state,
);
}
}
}
if let Err(err) = world.write_storage().insert(
entity,
RegionSubscription {
fuzzy_chunk,
regions,
},
) {
error!("Failed to insert region subscription component: {:?}", err);
}
} else {
debug!("Failed to initialize region subcription. Couldn't retrieve all the neccesary components on the provided entity");
}
}

View File

@ -27,7 +27,7 @@ conrod_winit = { git = "https://gitlab.com/veloren/conrod.git" }
euc = "0.3.0"
# ECS
specs = "0.14.2"
specs = "0.15.1"
# Mathematics
vek = { version = "0.9.9", features = ["serde"] }

View File

@ -8,7 +8,7 @@ use common::{
event::{EventBus, SfxEvent, SfxEventItem},
};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join};
use specs::{Entity as EcsEntity, Join, WorldExt};
use std::time::{Duration, Instant};
use vek::*;

View File

@ -9,6 +9,7 @@ use common::{
event::{EventBus, SfxEvent, SfxEventItem},
};
use serde::Deserialize;
use specs::WorldExt;
use vek::*;
#[derive(Deserialize)]

View File

@ -7,6 +7,7 @@ use conrod_core::{
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
use specs::WorldExt;
use vek::*;
widget_ids! {

View File

@ -7,6 +7,7 @@ use conrod_core::{
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
use specs::WorldExt;
use std::time::{Duration, Instant};
use vek::*;

View File

@ -49,7 +49,7 @@ use conrod_core::{
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
};
use specs::Join;
use specs::{Join, WorldExt};
use std::collections::VecDeque;
use vek::*;
@ -112,6 +112,7 @@ widget_ids! {
velocity,
loaded_distance,
time,
entity_count,
// Game Version
version,
@ -872,11 +873,19 @@ impl Hud {
.font_id(self.fonts.cyri)
.font_size(14)
.set(self.ids.time, ui_widgets);
// Number of entities
let entity_count = client.state().ecs().entities().join().count();
Text::new(&format!("Entity count: {}", entity_count))
.color(TEXT_COLOR)
.down_from(self.ids.time, 5.0)
.font_id(self.fonts.cyri)
.font_size(14)
.set(self.ids.entity_count, ui_widgets);
// Help Window
Text::new("Press 'F1' to show Keybindings")
.color(TEXT_COLOR)
.down_from(self.ids.time, 5.0)
.down_from(self.ids.entity_count, 5.0)
.font_id(self.fonts.cyri)
.font_size(14)
.set(self.ids.help_info, ui_widgets);

View File

@ -7,7 +7,7 @@ use conrod_core::{
widget_ids, /*, Color*/
Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
};
use specs::Join;
use specs::{Join, WorldExt};
use client::{self, Client};

View File

@ -21,6 +21,7 @@ use common::{
terrain::BlockKind,
};
use log::error;
use specs::WorldExt;
use vek::*;
struct Skybox {

View File

@ -26,7 +26,7 @@ use common::{
};
use hashbrown::HashMap;
use log::trace;
use specs::{Entity as EcsEntity, Join};
use specs::{Entity as EcsEntity, Join, WorldExt};
use vek::*;
const DAMAGE_FADE_COEFFICIENT: f64 = 5.0;

View File

@ -22,7 +22,7 @@ use common::{
terrain::{BlockKind, TerrainChunk},
vol::ReadVol,
};
use specs::Join;
use specs::{Join, WorldExt};
use vek::*;
// TODO: Don't hard-code this.

View File

@ -17,7 +17,7 @@ use common::{
ChatType,
};
use log::error;
use specs::Join;
use specs::{Join, WorldExt};
use std::{cell::RefCell, rc::Rc, time::Duration};
use vek::*;

View File

@ -342,7 +342,7 @@ impl Ui {
// moving origin to top-left corner (from middle).
let min_x = self.ui.win_w / 2.0 + l;
let min_y = self.ui.win_h / 2.0 - b - h;
Aabr {
let intersection = Aabr {
min: Vec2 {
x: (min_x * scale_factor) as u16,
y: (min_y * scale_factor) as u16,
@ -352,7 +352,13 @@ impl Ui {
y: ((min_y + h) * scale_factor) as u16,
},
}
.intersection(window_scissor)
.intersection(window_scissor);
if intersection.is_valid() {
intersection
} else {
Aabr::new_empty(Vec2::zero())
}
};
if new_scissor != current_scissor {
// Finish the current command.
@ -750,7 +756,7 @@ impl Ui {
}
fn default_scissor(renderer: &Renderer) -> Aabr<u16> {
let (screen_w, screen_h) = renderer.get_resolution().map(|e| e as u16).into_tuple();
let (screen_w, screen_h) = renderer.get_resolution().into_tuple();
Aabr {
min: Vec2 { x: 0, y: 0 },
max: Vec2 {