Merge branch 'imbris/regions' into 'master'

Fix ghost bug :) [Regional system for sending entity updates to clients]

See merge request veloren/veloren!555
This commit is contained in:
Imbris 2019-10-24 04:39:13 +00:00
commit 4b1fb112de
29 changed files with 2241 additions and 1262 deletions

10
Cargo.lock generated
View File

@ -1577,6 +1577,11 @@ dependencies = [
"tiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "indexmap"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "inflate"
version = "0.3.4"
@ -3592,7 +3597,9 @@ dependencies = [
"dot_vox 4.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"find_folder 0.3.0 (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)",
"image 0.22.2 (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)",
"lz4-compress 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -3618,6 +3625,7 @@ 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)",
"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)",
@ -3630,6 +3638,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",
@ -4144,6 +4153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
"checksum image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "545f000e8aa4e569e93f49c446987133452e0091c2494ac3efd3606aa3d309f2"
"checksum image 0.22.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ee0665404aa0f2ad154021777b785878b0e5b1c1da030455abc3d9ed257c2c67"
"checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3"
"checksum inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4"
"checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
"checksum inotify 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8"

View File

@ -65,7 +65,7 @@ fn main() {
client.send_chat(msg)
}
let events = match client.tick(comp::Controller::default(), clock.get_last_delta()) {
let events = match client.tick(comp::ControllerInputs::default(), clock.get_last_delta()) {
Ok(events) => events,
Err(err) => {
error!("Error: {:?}", err);

View File

@ -8,7 +8,7 @@ pub use crate::error::Error;
pub use specs::{join::Join, saveload::Marker, Entity as EcsEntity, ReadStorage};
use common::{
comp,
comp::{self, ControlEvent, Controller, ControllerInputs, InventoryManip},
msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, RequestStateError,
ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG,
@ -201,22 +201,33 @@ impl Client {
// Can't fail
}
pub fn use_inventory_slot(&mut self, x: usize) {
self.postbox.send_message(ClientMsg::UseInventorySlot(x))
pub fn use_inventory_slot(&mut self, slot: usize) {
self.postbox
.send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Use(slot),
)));
}
pub fn swap_inventory_slots(&mut self, a: usize, b: usize) {
self.postbox
.send_message(ClientMsg::SwapInventorySlots(a, b))
.send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Swap(a, b),
)));
}
pub fn drop_inventory_slot(&mut self, x: usize) {
self.postbox.send_message(ClientMsg::DropInventorySlot(x))
pub fn drop_inventory_slot(&mut self, slot: usize) {
self.postbox
.send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Drop(slot),
)));
}
pub fn pick_up(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.ecs().read_storage::<Uid>().get(entity).copied() {
self.postbox.send_message(ClientMsg::PickUp(uid.id()));
self.postbox
.send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Pickup(uid),
)));
}
}
@ -228,6 +239,18 @@ impl Client {
.is_some()
}
pub fn mount(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.ecs().read_storage::<Uid>().get(entity).copied() {
self.postbox
.send_message(ClientMsg::ControlEvent(ControlEvent::Mount(uid)));
}
}
pub fn unmount(&mut self) {
self.postbox
.send_message(ClientMsg::ControlEvent(ControlEvent::Unmount));
}
pub fn view_distance(&self) -> Option<u32> {
self.view_distance
}
@ -283,16 +306,15 @@ impl Client {
}
pub fn collect_block(&mut self, pos: Vec3<i32>) {
self.postbox.send_message(ClientMsg::CollectBlock(pos));
self.postbox
.send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Collect(pos),
)));
}
/// Execute a single client tick, handle input and update the game state by the given duration.
#[allow(dead_code)]
pub fn tick(
&mut self,
controller: comp::Controller,
dt: Duration,
) -> Result<Vec<Event>, Error> {
pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result<Vec<Event>, Error> {
// This tick function is the centre of the Veloren universe. Most client-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
@ -310,8 +332,15 @@ impl Client {
// 1) Handle input from frontend.
// Pass character actions from frontend input to the player's entity.
if let ClientState::Character | ClientState::Dead = self.client_state {
self.state.write_component(self.entity, controller.clone());
self.postbox.send_message(ClientMsg::Controller(controller));
self.state.write_component(
self.entity,
Controller {
inputs: inputs.clone(),
events: Vec::new(),
},
);
self.postbox
.send_message(ClientMsg::ControllerInputs(inputs));
}
// 2) Build up a list of events for this frame, to be passed to the frontend.
@ -319,7 +348,7 @@ impl Client {
// Prepare for new events
{
let ecs = self.state.ecs_mut();
let ecs = self.state.ecs();
for (entity, _) in (&ecs.entities(), &ecs.read_storage::<comp::Body>()).join() {
let mut last_character_states =
ecs.write_storage::<comp::Last<comp::CharacterState>>();
@ -343,7 +372,7 @@ impl Client {
// 3) Update client local data
// 4) Tick the client's LocalState
self.state.tick(dt);
self.state.tick(dt, |_| {});
// 5) Terrain
let pos = self
@ -501,6 +530,13 @@ 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) {
if entity != self.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);

View File

@ -28,6 +28,9 @@ find_folder = "0.3.0"
parking_lot = "0.9.0"
crossbeam = "0.7.2"
notify = "5.0.0-pre.1"
indexmap = "1.2.0"
# TODO: remove when upgrading to specs 0.15
hibitset = "0.5.3"
[dev-dependencies]
criterion = "0.3"

View File

@ -7,14 +7,14 @@ use vek::*;
pub enum ControlEvent {
Mount(Uid),
Unmount,
InventoryManip(InventoryManip),
//Respawn,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Controller {
pub struct ControllerInputs {
pub primary: bool,
pub secondary: bool,
pub move_dir: Vec2<f32>,
pub look_dir: Vec3<f32>,
pub sit: bool,
pub jump: bool,
pub roll: bool,
@ -23,6 +23,14 @@ pub struct Controller {
pub climb_down: bool,
pub wall_leap: bool,
pub respawn: bool,
pub move_dir: Vec2<f32>,
pub look_dir: Vec3<f32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Controller {
pub inputs: ControllerInputs,
// TODO: consider SmallVec
pub events: Vec<ControlEvent>,
}
@ -60,3 +68,12 @@ pub struct Mounting(pub Uid);
impl Component for Mounting {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum InventoryManip {
Pickup(Uid),
Collect(Vec3<i32>),
Use(usize),
Swap(usize, usize),
Drop(usize),
}

View File

@ -18,7 +18,9 @@ pub use admin::Admin;
pub use agent::Agent;
pub use body::{humanoid, object, quadruped, quadruped_medium, Body};
pub use character_state::{ActionState, CharacterState, MovementState};
pub use controller::{ControlEvent, Controller, MountState, Mounting};
pub use controller::{
ControlEvent, Controller, ControllerInputs, InventoryManip, MountState, Mounting,
};
pub use inputs::CanBuild;
pub use inventory::{item, Inventory, InventoryUpdate, Item};
pub use last::Last;

View File

@ -30,6 +30,7 @@ pub enum ServerEvent {
entity: EcsEntity,
cause: comp::HealthSource,
},
InventoryManip(EcsEntity, comp::InventoryManip),
Respawn(EcsEntity),
Shoot {
entity: EcsEntity,
@ -46,6 +47,22 @@ pub enum ServerEvent {
Mount(EcsEntity, EcsEntity),
Unmount(EcsEntity),
Possess(Uid, Uid),
CreatePlayer {
entity: EcsEntity,
name: String,
body: comp::Body,
main: Option<comp::Item>,
},
CreateNpc {
pos: comp::Pos,
stats: comp::Stats,
body: comp::Body,
agent: comp::Agent,
scale: comp::Scale,
},
ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String),
}
pub struct EventBus<E> {

View File

@ -19,6 +19,7 @@ pub mod figure;
pub mod msg;
pub mod npc;
pub mod ray;
pub mod region;
pub mod state;
pub mod sys;
pub mod terrain;

View File

@ -14,12 +14,12 @@ pub enum ClientMsg {
body: comp::Body,
main: Option<comp::item::Tool>,
},
Controller(comp::Controller),
ControllerInputs(comp::ControllerInputs),
ControlEvent(comp::ControlEvent),
RequestState(ClientState),
SetViewDistance(u32),
BreakBlock(Vec3<i32>),
PlaceBlock(Vec3<i32>, Block),
CollectBlock(Vec3<i32>),
Ping,
Pong,
ChatMsg {
@ -31,10 +31,6 @@ pub enum ClientMsg {
vel: comp::Vel,
ori: comp::Ori,
},
UseInventorySlot(usize),
SwapInventorySlots(usize, usize),
DropInventorySlot(usize),
PickUp(u64),
TerrainChunkRequest {
key: Vec2<i32>,
},

View File

@ -41,6 +41,7 @@ pub enum ServerMsg {
},
SetPlayerEntity(u64),
EcsSync(sphynx::SyncPackage<EcsCompPacket, EcsResPacket>),
DeleteEntity(u64),
EntityPos {
entity: u64,
pos: comp::Pos,

360
common/src/region.rs Normal file
View File

@ -0,0 +1,360 @@
use crate::comp::{Pos, Vel};
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
use hibitset::BitSetLike;
use indexmap::IndexMap;
use specs::{BitSet, Entities, 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
pub struct Region {
// Use specs bitset for simplicity (and joinability)
bitset: BitSet,
// Indices of neighboring regions
neighbors: [Option<usize>; 8],
// TODO consider SmallVec for these
// Entites that left or entered this region
events: Vec<Event>,
}
impl Region {
fn new() -> Self {
Self {
bitset: BitSet::new(),
neighbors: [None; 8],
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
pub const TETHER_LENGTH: u32 = 16;
/// Bitshift between region and world pos, i.e. log2(REGION_SIZE)
const REGION_LOG2: u8 = 9;
/// Region Size in blocks
pub const REGION_SIZE: u32 = 1 << REGION_LOG2;
/// Offsets to iterate though neighbors
/// Counter-clockwise order
const NEIGHBOR_OFFSETS: [Vec2<i32>; 8] = [
Vec2::new(0, 1),
Vec2::new(-1, 1),
Vec2::new(-1, 0),
Vec2::new(-1, -1),
Vec2::new(0, -1),
Vec2::new(1, -1),
Vec2::new(1, 0),
Vec2::new(1, 1),
];
// 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 {
// 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, DefaultHashBuilder>,
// If an entity isn't here it needs to be added to a region
tracked_entities: BitSet,
// Re-useable vecs
// (src, entity, pos)
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 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?
// 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()
.map(|(pos, e, _)| (pos, e.id()))
.collect::<Vec<_>>()
{
// Add entity
self.tracked_entities.add(id);
self.add_entity(id, pos.0.map(|e| e as i32), None);
}
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(),
&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.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, id));
}
}
}
// Remove region if it is empty
// TODO: distribute this betweeen ticks
let (key, region) = self.regions.get_index(i).unwrap();
if region.removable() {
regions_to_remove.push(*key);
}
}
// 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, 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)));
self.add_entity(id, pos, Some(prev_key));
}
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);
}
for key in regions_to_remove.into_iter() {
// Check that the region is still removable
if self.regions.get(&key).unwrap().removable() {
// Note we have to use key's here since the index can change when others are removed
self.remove(key);
}
}
}
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.add(id, from);
return;
}
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)
}
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 index_key(&self, index: usize) -> Option<Vec2<i32>> {
self.regions.get_index(index).map(|(k, _)| k).copied()
}
/// Adds a new region
/// 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");
}
// Add neighbors and add to neighbors
let mut neighbors = [None; 8];
for i in 0..8 {
if let Some((idx, _, region)) = self.regions.get_full_mut(&(key + NEIGHBOR_OFFSETS[i]))
{
// Add neighbor to the new region
neighbors[i] = Some(idx);
// Add new region to neighbor
region.neighbors[(i + 4) % 8] = Some(index);
}
}
self.regions
.get_index_mut(index)
.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);
}
}
/// 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(self.regions.len() - 1)
.map(|(_, v)| v)
.unwrap()
.neighbors;
for i in 0..8 {
if let Some(idx) = moved_neighbors[i] {
self.regions
.get_index_mut(idx)
.map(|(_, v)| v)
.unwrap()
.neighbors[(i + 4) % 8] = Some(index);
}
}
}
if let Some(region) = self
.regions
.swap_remove_index(index)
.map(|(_, region)| region)
{
if !region.bitset.is_empty() {
panic!("Removed region containing entities");
}
// Remove from neighbors
for i in 0..8 {
if let Some(idx) = region.neighbors[i] {
self.regions
.get_index_mut(idx)
.map(|(_, v)| v)
.unwrap()
.neighbors[(i + 4) % 8] = None;
}
}
}
}
// 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))
}
}
// 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
}
});
diff.magnitude_squared() < vd_extended.powi(2)
}
// 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);
}
}
}
set
}
// 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;
x = (x ^ (x << 8)) & 0x00ff00ff00ff00ff;
x = (x ^ (x << 4)) & 0x0f0f0f0f0f0f0f0f;
x = (x ^ (x << 2)) & 0x3333333333333333;
x = (x ^ (x << 1)) & 0x5555555555555555;
x
}
fn morton_code(pos: Vec2<i32>) -> i64 {
interleave_i32_with_zeros(pos.x) | (interleave_i32_with_zeros(pos.y) << 1)
}*/

View File

@ -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.
@ -307,7 +309,7 @@ impl State {
}
/// Execute a single tick, simulating the game state by the given duration.
pub fn tick(&mut self, dt: Duration) {
pub fn tick(&mut self, dt: Duration, add_foreign_systems: impl Fn(&mut DispatcherBuilder)) {
// Change the time accordingly.
self.ecs.write_resource::<TimeOfDay>().0 += dt.as_secs_f64() * DAY_CYCLE_FACTOR;
self.ecs.write_resource::<Time>().0 += dt.as_secs_f64();
@ -378,10 +380,19 @@ impl State {
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>(),
self.ecs.read_storage::<comp::Vel>(),
self.ecs.entities(),
);
// Run systems to update the world.
// Create and run a dispatcher for ecs systems.
let mut dispatch_builder = DispatcherBuilder::new().with_pool(self.thread_pool.clone());
sys::add_local_systems(&mut dispatch_builder);
// 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);

View File

@ -47,6 +47,8 @@ impl<'a> System<'a> for Sys {
controller.reset();
let mut inputs = &mut controller.inputs;
match agent {
Agent::Wanderer(bearing) => {
*bearing += Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
@ -55,7 +57,7 @@ impl<'a> System<'a> for Sys {
- pos.0 * 0.0002;
if bearing.magnitude_squared() > 0.001 {
controller.move_dir = bearing.normalized();
inputs.move_dir = bearing.normalized();
}
}
Agent::Pet { target, offset } => {
@ -65,12 +67,12 @@ impl<'a> System<'a> for Sys {
let tgt_pos = tgt_pos.0 + *offset;
if tgt_pos.z > pos.0.z + 1.0 {
controller.jump = true;
inputs.jump = true;
}
// Move towards the target.
let dist: f32 = Vec2::from(tgt_pos - pos.0).magnitude();
controller.move_dir = if dist > 5.0 {
inputs.move_dir = if dist > 5.0 {
Vec2::from(tgt_pos - pos.0).normalized()
} else if dist < 1.5 && dist > 0.001 {
Vec2::from(pos.0 - tgt_pos).normalized()
@ -78,7 +80,7 @@ impl<'a> System<'a> for Sys {
Vec2::zero()
};
}
_ => controller.move_dir = Vec2::zero(),
_ => inputs.move_dir = Vec2::zero(),
}
// Change offset occasionally.
@ -102,28 +104,28 @@ impl<'a> System<'a> for Sys {
)
})
{
controller.look_dir = target_pos.0 - pos.0;
inputs.look_dir = target_pos.0 - pos.0;
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 {
// Fight (and slowly move closer)
controller.move_dir =
inputs.move_dir =
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.01;
controller.primary = true;
inputs.primary = true;
} else if dist < SIGHT_DIST {
controller.move_dir =
inputs.move_dir =
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.96;
if rand::random::<f32>() < 0.02 {
controller.roll = true;
inputs.roll = true;
}
if target_character.movement == Glide && target_pos.0.z > pos.0.z + 5.0
{
controller.glide = true;
controller.jump = true;
inputs.glide = true;
inputs.jump = true;
}
} else {
choose_new = true;
@ -134,7 +136,7 @@ impl<'a> System<'a> for Sys {
* 0.1
- *bearing * 0.005;
controller.move_dir = if bearing.magnitude_squared() > 0.001 {
inputs.move_dir = if bearing.magnitude_squared() > 0.001 {
bearing.normalized()
} else {
Vec2::zero()

View File

@ -65,48 +65,50 @@ impl<'a> System<'a> for Sys {
)
.join()
{
let mut inputs = &mut controller.inputs;
if stats.is_dead {
// Respawn
if controller.respawn {
if inputs.respawn {
server_emitter.emit(ServerEvent::Respawn(entity));
}
continue;
}
// Move
controller.move_dir = if controller.move_dir.magnitude_squared() > 1.0 {
controller.move_dir.normalized()
inputs.move_dir = if inputs.move_dir.magnitude_squared() > 1.0 {
inputs.move_dir.normalized()
} else {
controller.move_dir
inputs.move_dir
};
if character.movement == Stand && controller.move_dir.magnitude_squared() > 0.0 {
if character.movement == Stand && inputs.move_dir.magnitude_squared() > 0.0 {
character.movement = Run;
} else if character.movement == Run && controller.move_dir.magnitude_squared() == 0.0 {
} else if character.movement == Run && inputs.move_dir.magnitude_squared() == 0.0 {
character.movement = Stand;
}
// Look
controller.look_dir = controller
inputs.look_dir = inputs
.look_dir
.try_normalized()
.unwrap_or(controller.move_dir.into());
.unwrap_or(inputs.move_dir.into());
// Glide
// TODO: Check for glide ability/item
if controller.glide
if inputs.glide
&& !physics.on_ground
&& (character.action == Idle || character.action.is_wield())
&& character.movement == Jump
&& body.is_humanoid()
{
character.movement = Glide;
} else if !controller.glide && character.movement == Glide {
} else if !inputs.glide && character.movement == Glide {
character.movement = Jump;
}
// Sit
if controller.sit
if inputs.sit
&& physics.on_ground
&& character.action == Idle
&& character.movement != Sit
@ -114,13 +116,13 @@ impl<'a> System<'a> for Sys {
{
character.movement = Sit;
} else if character.movement == Sit
&& (controller.move_dir.magnitude_squared() > 0.0 || !physics.on_ground)
&& (inputs.move_dir.magnitude_squared() > 0.0 || !physics.on_ground)
{
character.movement = Run;
}
// Wield
if controller.primary
if inputs.primary
&& character.action == Idle
&& (character.movement == Stand || character.movement == Run)
{
@ -135,7 +137,7 @@ impl<'a> System<'a> for Sys {
power,
..
}) => {
if controller.primary
if inputs.primary
&& (character.movement == Stand
|| character.movement == Run
|| character.movement == Jump)
@ -146,7 +148,7 @@ impl<'a> System<'a> for Sys {
character.action = Idle;
server_emitter.emit(ServerEvent::Shoot {
entity,
dir: controller.look_dir,
dir: inputs.look_dir,
body: comp::Body::Object(comp::object::Body::Arrow),
light: None,
gravity: Some(comp::Gravity(0.3)),
@ -174,7 +176,7 @@ impl<'a> System<'a> for Sys {
..
}) => {
// Melee Attack
if controller.primary
if inputs.primary
&& (character.movement == Stand
|| character.movement == Run
|| character.movement == Jump)
@ -189,7 +191,7 @@ impl<'a> System<'a> for Sys {
}
}
// Magical Bolt
if controller.secondary
if inputs.secondary
&& (
character.movement == Stand
//|| character.movement == Run
@ -204,7 +206,7 @@ impl<'a> System<'a> for Sys {
};
server_emitter.emit(ServerEvent::Shoot {
entity,
dir: controller.look_dir,
dir: inputs.look_dir,
body: comp::Body::Object(comp::object::Body::BoltFire),
gravity: Some(comp::Gravity(0.0)),
light: Some(comp::LightEmitter {
@ -232,7 +234,7 @@ impl<'a> System<'a> for Sys {
}
Some(Item::Tool { .. }) => {
// Melee Attack
if controller.primary
if inputs.primary
&& (character.movement == Stand
|| character.movement == Run
|| character.movement == Jump)
@ -248,27 +250,27 @@ impl<'a> System<'a> for Sys {
}
// Block
if controller.secondary
if inputs.secondary
&& (character.movement == Stand || character.movement == Run)
&& character.action.is_wield()
{
character.action = Block {
time_left: Duration::from_secs(5),
};
} else if !controller.secondary && character.action.is_block() {
} else if !inputs.secondary && character.action.is_block() {
character.action = Wield {
time_left: Duration::default(),
};
}
}
Some(Item::Debug(item::Debug::Boost)) => {
if controller.primary {
if inputs.primary {
local_emitter.emit(LocalEvent::Boost {
entity,
vel: controller.look_dir * 7.0,
vel: inputs.look_dir * 7.0,
});
}
if controller.secondary {
if inputs.secondary {
// Go upward
local_emitter.emit(LocalEvent::Boost {
entity,
@ -277,7 +279,7 @@ impl<'a> System<'a> for Sys {
}
}
Some(Item::Debug(item::Debug::Possess)) => {
if controller.primary
if inputs.primary
&& (character.movement == Stand
|| character.movement == Run
|| character.movement == Jump)
@ -289,7 +291,7 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Shoot {
entity,
gravity: Some(comp::Gravity(0.1)),
dir: controller.look_dir,
dir: inputs.look_dir,
body: comp::Body::Object(comp::object::Body::ArrowSnake),
light: Some(comp::LightEmitter {
col: (0.0, 1.0, 0.3).into(),
@ -310,14 +312,14 @@ impl<'a> System<'a> for Sys {
}
}
// Block
if controller.secondary
if inputs.secondary
&& (character.movement == Stand || character.movement == Run)
&& character.action.is_wield()
{
character.action = Block {
time_left: Duration::from_secs(5),
};
} else if !controller.secondary && character.action.is_block() {
} else if !inputs.secondary && character.action.is_block() {
character.action = Wield {
time_left: Duration::default(),
};
@ -325,7 +327,7 @@ impl<'a> System<'a> for Sys {
}
None => {
// Attack
if controller.primary
if inputs.primary
&& (character.movement == Stand
|| character.movement == Run
|| character.movement == Jump)
@ -341,7 +343,7 @@ impl<'a> System<'a> for Sys {
}
// Roll
if controller.roll
if inputs.roll
&& (character.action == Idle || character.action.is_wield())
&& character.movement == Run
&& physics.on_ground
@ -352,23 +354,19 @@ impl<'a> System<'a> for Sys {
}
// Jump
if controller.jump
&& physics.on_ground
&& vel.0.z <= 0.0
&& !character.movement.is_roll()
{
if inputs.jump && physics.on_ground && vel.0.z <= 0.0 && !character.movement.is_roll() {
local_emitter.emit(LocalEvent::Jump(entity));
}
// Wall leap
if controller.wall_leap {
if inputs.wall_leap {
if let (Some(_wall_dir), Climb) = (physics.on_wall, character.movement) {
//local_emitter.emit(LocalEvent::WallLeap { entity, wall_dir });
}
}
// Process controller events
for event in std::mem::replace(&mut controller.events, Vec::new()) {
for event in controller.events.drain(..) {
match event {
ControlEvent::Mount(mountee_uid) => {
if let Some(mountee_entity) =
@ -378,6 +376,9 @@ impl<'a> System<'a> for Sys {
}
}
ControlEvent::Unmount => server_emitter.emit(ServerEvent::Unmount(entity)),
ControlEvent::InventoryManip(manip) => {
server_emitter.emit(ServerEvent::InventoryManip(entity, manip))
} //ControlEvent::Respawn => server_emitter.emit(ServerEvent::Unmount(entity)),
}
}
}

View File

@ -97,11 +97,13 @@ impl<'a> System<'a> for Sys {
continue;
}
let inputs = &controller.inputs;
if character.movement.is_roll() {
vel.0 = Vec3::new(0.0, 0.0, vel.0.z)
+ (vel.0 * Vec3::new(1.0, 1.0, 0.0)
+ 1.5
* controller
* inputs
.move_dir
.try_normalized()
.unwrap_or(Vec2::from(vel.0).try_normalized().unwrap_or_default()))
@ -110,7 +112,7 @@ impl<'a> System<'a> for Sys {
}
if character.action.is_block() || character.action.is_attack() {
vel.0 += Vec2::broadcast(dt.0)
* controller.move_dir
* inputs.move_dir
* match physics.on_ground {
true if vel.0.magnitude_squared() < BLOCK_SPEED.powf(2.0) => BLOCK_ACCEL,
_ => 0.0,
@ -118,7 +120,7 @@ impl<'a> System<'a> for Sys {
} else {
// Move player according to move_dir
vel.0 += Vec2::broadcast(dt.0)
* controller.move_dir
* inputs.move_dir
* match (physics.on_ground, &character.movement) {
(true, Run) if vel.0.magnitude_squared() < HUMANOID_SPEED.powf(2.0) => {
HUMANOID_ACCEL
@ -148,7 +150,7 @@ impl<'a> System<'a> for Sys {
|| character.action.is_attack()
|| character.action.is_block()
{
Vec2::from(controller.look_dir).normalized()
Vec2::from(inputs.look_dir).normalized()
} else if let (Climb, Some(wall_dir)) = (character.movement, physics.on_wall) {
if Vec2::<f32>::from(wall_dir).magnitude_squared() > 0.001 {
Vec2::from(wall_dir).normalized()
@ -198,12 +200,12 @@ impl<'a> System<'a> for Sys {
// Climb
if let (true, Some(_wall_dir)) = (
(controller.climb | controller.climb_down) && vel.0.z <= CLIMB_SPEED,
(inputs.climb | inputs.climb_down) && vel.0.z <= CLIMB_SPEED,
physics.on_wall,
) {
if controller.climb_down && !controller.climb {
if inputs.climb_down && !inputs.climb {
vel.0 -= dt.0 * vel.0.map(|e| e.abs().powf(1.5) * e.signum() * 6.0);
} else if controller.climb && !controller.climb_down {
} else if inputs.climb && !inputs.climb_down {
vel.0.z = (vel.0.z + dt.0 * GRAVITY * 1.25).min(CLIMB_SPEED);
} else {
vel.0.z = vel.0.z + dt.0 * GRAVITY * 1.5;

View File

@ -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"
@ -24,4 +26,6 @@ crossbeam = "0.7.2"
prometheus = "0.7"
prometheus-static-metric = "0.2"
rouille = "3.0.0"
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
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

@ -0,0 +1,70 @@
use common::terrain::TerrainChunk;
use crossbeam::channel;
use hashbrown::{hash_map::Entry, HashMap};
use specs::Entity as EcsEntity;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use vek::*;
use world::{ChunkSupplement, World};
type ChunkGenResult = (
Vec2<i32>,
Result<(TerrainChunk, ChunkSupplement), EcsEntity>,
);
pub struct ChunkGenerator {
chunk_tx: channel::Sender<ChunkGenResult>,
chunk_rx: channel::Receiver<ChunkGenResult>,
pending_chunks: HashMap<Vec2<i32>, Arc<AtomicBool>>,
}
impl ChunkGenerator {
pub fn new() -> Self {
let (chunk_tx, chunk_rx) = channel::unbounded();
Self {
chunk_tx,
chunk_rx,
pending_chunks: HashMap::new(),
}
}
pub fn generate_chunk(
&mut self,
entity: EcsEntity,
key: Vec2<i32>,
thread_pool: &mut uvth::ThreadPool,
world: Arc<World>,
) {
let v = if let Entry::Vacant(v) = self.pending_chunks.entry(key) {
v
} else {
return;
};
let cancel = Arc::new(AtomicBool::new(false));
v.insert(Arc::clone(&cancel));
let chunk_tx = self.chunk_tx.clone();
thread_pool.execute(move || {
let payload = world
.generate_chunk(key, || cancel.load(Ordering::Relaxed))
.map_err(|_| entity);
let _ = chunk_tx.send((key, payload));
});
}
pub fn recv_new_chunk(&mut self) -> Option<ChunkGenResult> {
if let Ok((key, res)) = self.chunk_rx.try_recv() {
self.pending_chunks.remove(&key);
// TODO: do anything else if res is an Err?
Some((key, res))
} else {
None
}
}
pub fn pending_chunks<'a>(&'a self) -> impl Iterator<Item = Vec2<i32>> + 'a {
self.pending_chunks.keys().copied()
}
pub fn cancel_if_pending(&mut self, key: Vec2<i32>) {
if let Some(cancel) = self.pending_chunks.remove(&key) {
cancel.store(true, Ordering::Relaxed);
}
}
}

View File

@ -2,8 +2,10 @@ use common::{
msg::{ClientMsg, ClientState, RequestStateError, ServerMsg},
net::PostBox,
};
use hashbrown::HashMap;
use specs::Entity as EcsEntity;
use hashbrown::HashSet;
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
use vek::*;
pub struct Client {
pub client_state: ClientState,
@ -11,10 +13,29 @@ pub struct Client {
pub last_ping: f64,
}
impl Component for Client {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
impl Client {
pub fn notify(&mut self, msg: ServerMsg) {
self.postbox.send_message(msg);
}
pub fn is_registered(&self) -> bool {
match self.client_state {
ClientState::Registered
| ClientState::Spectator
| ClientState::Dead
| ClientState::Character => true,
_ => false,
}
}
pub fn is_ingame(&self) -> bool {
match self.client_state {
ClientState::Spectator | ClientState::Character | ClientState::Dead => true,
_ => false,
}
}
pub fn allow_state(&mut self, new_state: ClientState) {
self.client_state = new_state;
self.postbox
@ -30,111 +51,17 @@ impl Client {
}
}
pub struct Clients {
clients: HashMap<EcsEntity, Client>,
// 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 Clients {
pub fn empty() -> Self {
Self {
clients: HashMap::new(),
}
}
pub fn len(&mut self) -> usize {
self.clients.len()
}
pub fn add(&mut self, entity: EcsEntity, client: Client) {
self.clients.insert(entity, client);
}
pub fn get<'a>(&'a self, entity: &EcsEntity) -> Option<&'a Client> {
self.clients.get(entity)
}
pub fn get_mut<'a>(&'a mut self, entity: &EcsEntity) -> Option<&'a mut Client> {
self.clients.get_mut(entity)
}
pub fn remove<'a>(&'a mut self, entity: &EcsEntity) -> Option<Client> {
self.clients.remove(entity)
}
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(&mut self, entity: EcsEntity, msg: ServerMsg) {
if let Some(client) = self.clients.get_mut(&entity) {
client.notify(msg);
}
}
pub fn notify_registered(&mut self, msg: ServerMsg) {
for client in self.clients.values_mut() {
if client.client_state != ClientState::Connected {
client.notify(msg.clone());
}
}
}
pub fn notify_ingame(&mut self, msg: ServerMsg) {
for client in self.clients.values_mut() {
if client.client_state == ClientState::Spectator
|| client.client_state == ClientState::Character
|| client.client_state == ClientState::Dead
{
client.notify(msg.clone());
}
}
}
pub fn notify_ingame_if<F: FnMut(EcsEntity) -> bool>(&mut self, msg: ServerMsg, mut f: F) {
for (_entity, client) in self.clients.iter_mut().filter(|(e, _)| f(**e)) {
if client.client_state == ClientState::Spectator
|| client.client_state == ClientState::Character
|| client.client_state == ClientState::Dead
{
client.notify(msg.clone());
}
}
}
pub fn notify_registered_except(&mut self, except_entity: EcsEntity, msg: ServerMsg) {
for (entity, client) in self.clients.iter_mut() {
if client.client_state != ClientState::Connected && *entity != except_entity {
client.notify(msg.clone());
}
}
}
pub fn notify_ingame_except(&mut self, except_entity: EcsEntity, msg: ServerMsg) {
for (entity, client) in self.clients.iter_mut() {
if (client.client_state == ClientState::Spectator
|| client.client_state == ClientState::Character
|| client.client_state == ClientState::Dead)
&& *entity != except_entity
{
client.notify(msg.clone());
}
}
}
pub fn notify_ingame_if_except<F: FnMut(EcsEntity) -> bool>(
&mut self,
except_entity: EcsEntity,
msg: ServerMsg,
mut f: F,
) {
for (entity, client) in self.clients.iter_mut().filter(|(e, _)| f(**e)) {
if (client.client_state == ClientState::Spectator
|| client.client_state == ClientState::Character
|| client.client_state == ClientState::Dead)
&& *entity != except_entity
{
client.notify(msg.clone());
}
}
}
impl Component for RegionSubscription {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}

View File

@ -2,7 +2,7 @@
//! To implement a new command, add an instance of `ChatCommand` to `CHAT_COMMANDS`
//! and provide a handler function.
use crate::Server;
use crate::{Server, StateExt};
use chrono::{NaiveTime, Timelike};
use common::{
comp,
@ -62,7 +62,7 @@ impl ChatCommand {
pub fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) {
if self.needs_admin {
if !server.entity_is_admin(entity) {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!(
"You don't have permission to use '/{}'.",
@ -243,7 +243,7 @@ fn handle_jump(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
.write_component(entity, comp::Pos(current_pos.0 + Vec3::new(x, y, z)));
server.state.write_component(entity, comp::ForceUpdate);
}
None => server.clients.notify(
None => server.notify_client(
entity,
ServerMsg::private(String::from("You have no position.")),
),
@ -263,15 +263,13 @@ fn handle_goto(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
.write_component(entity, comp::Pos(Vec3::new(x, y, z)));
server.state.write_component(entity, comp::ForceUpdate);
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("You have no position.")),
);
}
} else {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
@ -299,7 +297,7 @@ fn handle_time(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
Err(_) => match NaiveTime::parse_from_str(n, "%H:%M") {
Ok(time) => time,
Err(_) => {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("'{}' is not a valid time.", n)),
);
@ -319,7 +317,7 @@ fn handle_time(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
Some(time) => format!("It is {}", time.format("%H:%M").to_string()),
None => String::from("Unknown Time"),
};
server.clients.notify(entity, ServerMsg::private(msg));
server.notify_client(entity, ServerMsg::private(msg));
return;
}
};
@ -327,7 +325,7 @@ fn handle_time(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
server.state.ecs_mut().write_resource::<TimeOfDay>().0 =
new_time.num_seconds_from_midnight() as f64;
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!(
"Time changed to: {}",
@ -340,19 +338,19 @@ fn handle_health(server: &mut Server, entity: EcsEntity, args: String, action: &
if let Ok(hp) = scan_fmt!(&args, action.arg_fmt, u32) {
if let Some(stats) = server
.state
.ecs_mut()
.ecs()
.write_storage::<comp::Stats>()
.get_mut(entity)
{
stats.health.set_to(hp, comp::HealthSource::Command);
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("You have no health.")),
);
}
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("You must specify health amount!")),
);
@ -368,9 +366,7 @@ fn handle_alias(server: &mut Server, entity: EcsEntity, args: String, action: &C
.get_mut(entity)
.map(|player| player.alias = alias);
} else {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
@ -388,31 +384,28 @@ fn handle_tp(server: &mut Server, entity: EcsEntity, args: String, action: &Chat
server.state.write_component(entity, pos);
server.state.write_component(entity, comp::ForceUpdate);
}
None => server.clients.notify(
None => server.notify_client(
entity,
ServerMsg::private(format!("Unable to teleport to player '{}'!", alias)),
),
},
None => {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("Player '{}' not found!", alias)),
);
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(
entity,
ServerMsg::private(String::from(action.help_string)),
);
}
},
None => {
server
.clients
.notify(entity, ServerMsg::private(format!("You have no position!")));
server.notify_client(entity, ServerMsg::private(format!("You have no position!")));
}
}
} else {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
@ -437,18 +430,19 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
let body = kind_to_body(id);
server
.state
.create_npc(pos, comp::Stats::new(get_npc_name(id), None), body)
.with(comp::Vel(vel))
.with(comp::MountState::Unmounted)
.with(agent)
.build();
}
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("Spawned {} entities", amount).to_owned()),
);
}
None => server.clients.notify(
None => server.notify_client(
entity,
ServerMsg::private("You have no position!".to_owned()),
),
@ -456,9 +450,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
}
}
_ => {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
}
@ -481,13 +473,9 @@ fn handle_players(server: &mut Server, entity: EcsEntity, _args: String, _action
s
});
server
.clients
.notify(entity, ServerMsg::private(header_message + &player_list));
server.notify_client(entity, ServerMsg::private(header_message + &player_list));
} else {
server
.clients
.notify(entity, ServerMsg::private(header_message));
server.notify_client(entity, ServerMsg::private(header_message));
}
}
@ -503,7 +491,7 @@ fn handle_build(server: &mut Server, entity: EcsEntity, _args: String, _action:
.ecs()
.write_storage::<comp::CanBuild>()
.remove(entity);
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("Toggled off build mode!")),
);
@ -513,7 +501,7 @@ fn handle_build(server: &mut Server, entity: EcsEntity, _args: String, _action:
.ecs()
.write_storage::<comp::CanBuild>()
.insert(entity, comp::CanBuild);
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("Toggled on build mode!")),
);
@ -523,9 +511,7 @@ fn handle_build(server: &mut Server, entity: EcsEntity, _args: String, _action:
fn handle_help(server: &mut Server, entity: EcsEntity, _args: String, _action: &ChatCommand) {
for cmd in CHAT_COMMANDS.iter() {
if !cmd.needs_admin || server.entity_is_admin(entity) {
server
.clients
.notify(entity, ServerMsg::private(String::from(cmd.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(cmd.help_string)));
}
}
}
@ -564,7 +550,7 @@ fn handle_killnpcs(server: &mut Server, entity: EcsEntity, _args: String, _actio
} else {
"No NPCs on server.".to_string()
};
server.clients.notify(entity, ServerMsg::private(text));
server.notify_client(entity, ServerMsg::private(text));
}
fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action: &ChatCommand) {
@ -636,7 +622,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
Ok("carpet_human_squircle") => comp::object::Body::CarpetHumanSquircle,
Ok("crafting_bench") => comp::object::Body::CraftingBench,
_ => {
return server.clients.notify(
return server.notify_client(
entity,
ServerMsg::private(String::from("Object not found!")),
);
@ -657,7 +643,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
.normalized(),
))
.build();
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!(
"Spawned: {}",
@ -665,9 +651,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
)),
);
} else {
server
.clients
.notify(entity, ServerMsg::private(format!("You have no position!")));
server.notify_client(entity, ServerMsg::private(format!("You have no position!")));
}
}
@ -704,13 +688,9 @@ fn handle_light(server: &mut Server, entity: EcsEntity, args: String, action: &C
.with(comp::ForceUpdate)
.with(light_emitter)
.build();
server
.clients
.notify(entity, ServerMsg::private(format!("Spawned object.")));
server.notify_client(entity, ServerMsg::private(format!("Spawned object.")));
} else {
server
.clients
.notify(entity, ServerMsg::private(format!("You have no position!")));
server.notify_client(entity, ServerMsg::private(format!("You have no position!")));
}
}
@ -731,7 +711,7 @@ fn handle_lantern(server: &mut Server, entity: EcsEntity, args: String, action:
.get_mut(entity)
{
light.strength = s.max(0.1).min(10.0);
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("You adjusted flame strength.")),
);
@ -742,7 +722,7 @@ fn handle_lantern(server: &mut Server, entity: EcsEntity, args: String, action:
.ecs()
.write_storage::<comp::LightEmitter>()
.remove(entity);
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("You put out the lantern.")),
);
@ -765,7 +745,7 @@ fn handle_lantern(server: &mut Server, entity: EcsEntity, args: String, action:
},
);
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("You lit your lantern.")),
);
@ -781,7 +761,7 @@ fn handle_explosion(server: &mut Server, entity: EcsEntity, args: String, action
.ecs()
.read_resource::<EventBus<ServerEvent>>()
.emit(ServerEvent::Explosion { pos: pos.0, radius }),
None => server.clients.notify(
None => server.notify_client(
entity,
ServerMsg::private(String::from("You have no position!")),
),
@ -796,11 +776,9 @@ fn handle_waypoint(server: &mut Server, entity: EcsEntity, _args: String, _actio
.ecs()
.write_storage::<comp::Waypoint>()
.insert(entity, comp::Waypoint::new(pos.0));
server
.clients
.notify(entity, ServerMsg::private(String::from("Waypoint set!")));
server.notify_client(entity, ServerMsg::private(String::from("Waypoint set!")));
}
None => server.clients.notify(
None => server.notify_client(
entity,
ServerMsg::private(String::from("You have no position!")),
),
@ -824,19 +802,15 @@ fn handle_adminify(server: &mut Server, entity: EcsEntity, args: String, action:
}
},
None => {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("Player '{}' not found!", alias)),
);
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
} else {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
@ -856,40 +830,40 @@ fn handle_tell(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
.get(entity)
.map(|s| s.alias.clone())
{
server
.clients
.notify(player, ServerMsg::tell(format!("[{}] tells:{}", name, msg)));
server
.clients
.notify(entity, ServerMsg::tell(format!("To [{}]:{}", alias, msg)));
server.notify_client(
player,
ServerMsg::tell(format!("[{}] tells:{}", name, msg)),
);
server.notify_client(
entity,
ServerMsg::tell(format!("To [{}]:{}", alias, msg)),
);
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("Failed to send message.")),
);
}
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("[{}] wants to talk to you.", alias)),
);
}
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("You can't /tell yourself.")),
);
}
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(format!("Player '{}' not found!", alias)),
);
}
} else {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
@ -949,17 +923,15 @@ spawn_rate {:?} "#,
))
};
if let Some(s) = foo() {
server.clients.notify(entity, ServerMsg::private(s));
server.notify_client(entity, ServerMsg::private(s));
} else {
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from("Not a pregenerated chunk.")),
);
}
} else {
server
.clients
.notify(entity, ServerMsg::private(String::from(action.help_string)));
server.notify_client(entity, ServerMsg::private(String::from(action.help_string)));
}
}
@ -972,24 +944,24 @@ fn handle_exp(server: &mut Server, entity: EcsEntity, args: String, action: &Cha
.find(|(_, player)| player.alias == alias)
.map(|(entity, _)| entity);
let mut error_msg = None;
match opt_player {
Some(_alias) => {
if let Some(stats) = ecs.write_storage::<comp::Stats>().get_mut(entity) {
stats.exp.change_by(exp);
} else {
server.clients.notify(
entity,
ServerMsg::private(String::from("Player has no stats!")),
);
error_msg = Some(ServerMsg::private(String::from("Player has no stats!")));
}
}
_ => {
server.clients.notify(
entity,
ServerMsg::private(format!("Player '{}' not found!", alias)),
);
error_msg = Some(ServerMsg::private(format!("Player '{}' not found!", alias)));
}
}
if let Some(msg) = error_msg {
server.notify_client(entity, msg);
}
}
}
@ -1022,7 +994,7 @@ fn handle_remove_lights(
}
}
}
None => server.clients.notify(
None => server.notify_client(
entity,
ServerMsg::private(String::from("You have no position.")),
),
@ -1034,7 +1006,7 @@ fn handle_remove_lights(
let _ = server.state.ecs_mut().delete_entity_synced(entity);
}
server.clients.notify(
server.notify_client(
entity,
ServerMsg::private(String::from(format!("Removed {} lights!", size))),
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,233 @@
use super::SysTimer;
use crate::{
client::{Client, RegionSubscription},
Tick,
};
use common::{
comp::{CharacterState, ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel},
msg::ServerMsg,
region::{Event as RegionEvent, RegionMap},
state::Uid,
};
use specs::{
Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage,
};
/// This system will send physics updates to the client
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, Tick>,
ReadExpect<'a, RegionMap>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Vel>,
ReadStorage<'a, Ori>,
ReadStorage<'a, CharacterState>,
ReadStorage<'a, Inventory>,
ReadStorage<'a, RegionSubscription>,
WriteStorage<'a, Last<Pos>>,
WriteStorage<'a, Last<Vel>>,
WriteStorage<'a, Last<Ori>>,
WriteStorage<'a, Last<CharacterState>>,
WriteStorage<'a, Client>,
WriteStorage<'a, ForceUpdate>,
WriteStorage<'a, InventoryUpdate>,
);
fn run(
&mut self,
(
entities,
tick,
region_map,
mut timer,
uids,
positions,
velocities,
orientations,
character_states,
inventories,
subscriptions,
mut last_pos,
mut last_vel,
mut last_ori,
mut last_character_state,
mut clients,
mut force_updates,
mut inventory_updates,
): Self::SystemData,
) {
timer.start();
let tick = tick.0;
// 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
// via iterating through regions
for (key, region) in region_map.iter() {
let mut subscribers = (&mut clients, &entities, &subscriptions, &positions)
.join()
.filter_map(|(client, entity, subscription, pos)| {
if client.is_ingame() && subscription.regions.contains(&key) {
Some((client, &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) = uids.get(entities.entity(*id)) {
for (client, regions, _, _) in &mut subscribers {
if !maybe_key
.as_ref()
.map(|key| regions.contains(key))
.unwrap_or(false)
{
client.notify(ServerMsg::DeleteEntity(uid.into()));
}
}
}
}
}
}
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);
// Throttle update rate based on distance to player
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());
}
}
}
}
};
for (_, entity, &uid, &pos, maybe_vel, maybe_ori, character_state, force_update) in (
region.entities(),
&entities,
&uids,
&positions,
velocities.maybe(),
orientations.maybe(),
character_states.maybe(),
force_updates.maybe(),
)
.join()
{
if last_pos.get(entity).map(|&l| l.0 != pos).unwrap_or(true) {
let _ = last_pos.insert(entity, Last(pos));
send_msg(
ServerMsg::EntityPos {
entity: uid.into(),
pos,
},
entity,
pos,
force_update,
true,
);
}
if let Some(&vel) = maybe_vel {
if last_vel.get(entity).map(|&l| l.0 != vel).unwrap_or(true) {
let _ = last_vel.insert(entity, Last(vel));
send_msg(
ServerMsg::EntityVel {
entity: uid.into(),
vel,
},
entity,
pos,
force_update,
true,
);
}
}
if let Some(&ori) = maybe_ori {
if last_ori.get(entity).map(|&l| l.0 != ori).unwrap_or(true) {
let _ = last_ori.insert(entity, Last(ori));
send_msg(
ServerMsg::EntityOri {
entity: uid.into(),
ori,
},
entity,
pos,
force_update,
true,
);
}
}
if let Some(&character_state) = character_state {
if last_character_state
.get(entity)
.map(|&l| !character_state.is_same_state(&l.0))
.unwrap_or(true)
{
let _ = last_character_state.insert(entity, Last(character_state));
send_msg(
ServerMsg::EntityCharacterState {
entity: uid.into(),
character_state,
},
entity,
pos,
force_update,
false,
);
}
}
}
}
// Sync inventories
for (client, inventory, _) in (&mut clients, &inventories, &inventory_updates).join() {
client.notify(ServerMsg::InventoryUpdate(inventory.clone()));
}
// Remove all force flags.
force_updates.clear();
inventory_updates.clear();
timer.end();
}
}

327
server/src/sys/message.rs Normal file
View File

@ -0,0 +1,327 @@
use super::SysTimer;
use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT};
use common::{
comp::{Admin, Body, CanBuild, Controller, Item, Ori, Player, Pos, Vel},
event::{EventBus, ServerEvent},
msg::{validate_chat_msg, ChatMsgValidationError, MAX_BYTES_CHAT_MSG},
msg::{ClientMsg, ClientState, RequestStateError, ServerMsg},
state::{BlockChange, Time},
terrain::{Block, TerrainGrid},
vol::Vox,
};
use specs::{
Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage,
};
/// This system will handle new messages from clients
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, EventBus<ServerEvent>>,
Read<'a, Time>,
ReadExpect<'a, TerrainGrid>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Body>,
ReadStorage<'a, CanBuild>,
ReadStorage<'a, Admin>,
WriteExpect<'a, AuthProvider>,
Write<'a, BlockChange>,
WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
WriteStorage<'a, Player>,
WriteStorage<'a, Client>,
WriteStorage<'a, Controller>,
);
fn run(
&mut self,
(
entities,
server_emitter,
time,
terrain,
mut timer,
bodies,
can_build,
admins,
mut accounts,
mut block_changes,
mut positions,
mut velocities,
mut orientations,
mut players,
mut clients,
mut controllers,
): Self::SystemData,
) {
timer.start();
let time = time.0;
let mut new_chat_msgs = Vec::new();
for (entity, client) in (&entities, &mut clients).join() {
let mut disconnect = false;
let new_msgs = client.postbox.new_messages();
// Update client ping.
if new_msgs.len() > 0 {
client.last_ping = time
} else if time - client.last_ping > CLIENT_TIMEOUT // Timeout
|| client.postbox.error().is_some()
// Postbox error
{
disconnect = true;
} else if time - client.last_ping > CLIENT_TIMEOUT * 0.5 {
// Try pinging the client if the timeout is nearing.
client.postbox.send_message(ServerMsg::Ping);
}
// Process incoming messages.
for msg in new_msgs {
match msg {
ClientMsg::RequestState(requested_state) => match requested_state {
ClientState::Connected => disconnect = true, // Default state
ClientState::Registered => match client.client_state {
// Use ClientMsg::Register instead.
ClientState::Connected => {
client.error_state(RequestStateError::WrongMessage)
}
ClientState::Registered => {
client.error_state(RequestStateError::Already)
}
ClientState::Spectator | ClientState::Character | ClientState::Dead => {
client.allow_state(ClientState::Registered)
}
ClientState::Pending => {}
},
ClientState::Spectator => match requested_state {
// Become Registered first.
ClientState::Connected => {
client.error_state(RequestStateError::Impossible)
}
ClientState::Spectator => {
client.error_state(RequestStateError::Already)
}
ClientState::Registered
| ClientState::Character
| ClientState::Dead => client.allow_state(ClientState::Spectator),
ClientState::Pending => {}
},
// Use ClientMsg::Character instead.
ClientState::Character => {
client.error_state(RequestStateError::WrongMessage)
}
ClientState::Dead => client.error_state(RequestStateError::Impossible),
ClientState::Pending => {}
},
// Valid player
ClientMsg::Register { player, password } if player.is_valid() => {
if !accounts.query(player.alias.clone(), password) {
client.error_state(RequestStateError::Denied);
break;
}
match client.client_state {
ClientState::Connected => {
let _ = players.insert(entity, player);
// Tell the client its request was successful.
client.allow_state(ClientState::Registered);
}
// Use RequestState instead (No need to send `player` again).
_ => client.error_state(RequestStateError::Impossible),
}
//client.allow_state(ClientState::Registered);
}
// Invalid player
ClientMsg::Register { .. } => client.error_state(RequestStateError::Impossible),
ClientMsg::SetViewDistance(view_distance) => match client.client_state {
ClientState::Character { .. } => {
players
.get_mut(entity)
.map(|player| player.view_distance = Some(view_distance));
}
_ => {}
},
ClientMsg::Character { name, body, main } => match client.client_state {
// Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator | ClientState::Dead => {
if let (Some(player), None) = (
players.get(entity),
// Only send login message if the player didn't have a body
// previously
bodies.get(entity),
) {
new_chat_msgs.push((
None,
ServerMsg::broadcast(format!(
"[{}] is now online.",
&player.alias
)),
));
}
server_emitter.emit(ServerEvent::CreatePlayer {
entity,
name,
body,
main: main.map(|t| Item::Tool {
kind: t,
power: 10,
stamina: 0,
strength: 0,
dexterity: 0,
intelligence: 0,
}),
});
}
ClientState::Character => client.error_state(RequestStateError::Already),
ClientState::Pending => {}
},
ClientMsg::ControllerInputs(inputs) => match client.client_state {
ClientState::Connected
| ClientState::Registered
| ClientState::Spectator => {
client.error_state(RequestStateError::Impossible)
}
ClientState::Dead | ClientState::Character => {
if let Some(controller) = controllers.get_mut(entity) {
controller.inputs = inputs;
}
}
ClientState::Pending => {}
},
ClientMsg::ControlEvent(event) => match client.client_state {
ClientState::Connected
| ClientState::Registered
| ClientState::Spectator => {
client.error_state(RequestStateError::Impossible)
}
ClientState::Dead | ClientState::Character => {
if let Some(controller) = controllers.get_mut(entity) {
controller.events.push(event);
}
}
ClientState::Pending => {}
},
ClientMsg::ChatMsg { chat_type, message } => match client.client_state {
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered
| ClientState::Spectator
| ClientState::Dead
| ClientState::Character => match validate_chat_msg(&message) {
Ok(()) => new_chat_msgs
.push((Some(entity), ServerMsg::ChatMsg { chat_type, message })),
Err(ChatMsgValidationError::TooLong) => log::warn!(
"Recieved a chat message that's too long (max:{} len:{})",
MAX_BYTES_CHAT_MSG,
message.len()
),
},
ClientState::Pending => {}
},
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);
}
// Only characters can send positions.
_ => client.error_state(RequestStateError::Impossible),
},
ClientMsg::BreakBlock(pos) => {
if can_build.get(entity).is_some() {
block_changes.set(pos, Block::empty());
}
}
ClientMsg::PlaceBlock(pos, block) => {
if can_build.get(entity).is_some() {
block_changes.try_set(pos, block);
}
}
ClientMsg::TerrainChunkRequest { key } => match client.client_state {
ClientState::Connected | ClientState::Registered | ClientState::Dead => {
client.error_state(RequestStateError::Impossible);
}
ClientState::Spectator | ClientState::Character => {
match terrain.get_key(key) {
Some(chunk) => {
client.postbox.send_message(ServerMsg::TerrainChunkUpdate {
key,
chunk: Ok(Box::new(chunk.clone())),
})
}
None => server_emitter.emit(ServerEvent::ChunkRequest(entity, key)),
}
}
ClientState::Pending => {}
},
// Always possible.
ClientMsg::Ping => client.postbox.send_message(ServerMsg::Pong),
ClientMsg::Pong => {}
ClientMsg::Disconnect => {
disconnect = true;
}
}
}
if disconnect {
if let (Some(player), Some(_)) = (
players.get(entity),
// It only shows a message if you had a body (not in char selection)
bodies.get(entity),
) {
new_chat_msgs.push((
None,
ServerMsg::broadcast(format!("{} went offline.", &player.alias)),
));
}
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
client.postbox.send_message(ServerMsg::Disconnect);
}
}
// Handle new chat messages.
for (entity, msg) in new_chat_msgs {
match msg {
ServerMsg::ChatMsg { chat_type, message } => {
if let Some(entity) = entity {
// Handle chat commands.
if message.starts_with("/") && message.len() > 1 {
let argv = String::from(&message[1..]);
server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
} else {
let message = match players.get(entity) {
Some(player) => {
if admins.get(entity).is_some() {
format!("[ADMIN][{}] {}", &player.alias, message)
} else {
format!("[{}] {}", &player.alias, message)
}
}
None => format!("[<Unknown>] {}", message),
};
let msg = ServerMsg::ChatMsg { chat_type, message };
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
}
} else {
let msg = ServerMsg::ChatMsg { chat_type, message };
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
}
}
_ => {
panic!("Invalid message type.");
}
}
}
timer.end()
}
}

61
server/src/sys/mod.rs Normal file
View File

@ -0,0 +1,61 @@
pub mod entity_sync;
pub mod message;
pub mod subscription;
pub mod terrain;
pub mod terrain_sync;
use specs::DispatcherBuilder;
use std::{marker::PhantomData, time::Instant};
pub type EntitySyncTimer = SysTimer<entity_sync::Sys>;
pub type MessageTimer = SysTimer<message::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 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) {
dispatch_builder.add(subscription::Sys, SUBSCRIPTION_SYS, &[]);
dispatch_builder.add(entity_sync::Sys, ENTITY_SYNC_SYS, &[SUBSCRIPTION_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
pub struct SysTimer<S> {
pub nanos: u64,
start: Option<Instant>,
_phantom: PhantomData<S>,
}
impl<S> SysTimer<S> {
pub fn start(&mut self) {
if self.start.is_some() {
panic!("Timer already started");
}
self.start = Some(Instant::now());
}
pub fn end(&mut self) {
self.nanos = self
.start
.take()
.expect("Timer ended without starting it")
.elapsed()
.as_nanos() as u64;
}
}
impl<S> Default for SysTimer<S> {
fn default() -> Self {
Self {
nanos: 0,
start: None,
_phantom: PhantomData,
}
}
}

View File

@ -0,0 +1,131 @@
use super::SysTimer;
use crate::client::{self, Client, RegionSubscription};
use common::{
comp::{Player, Pos},
msg::ServerMsg,
region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap},
state::Uid,
terrain::TerrainChunkSize,
vol::RectVolSize,
};
use specs::{Entities, Join, ReadExpect, ReadStorage, System, Write, WriteStorage};
use vek::*;
/// This system will update region subscriptions based on client positions
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
ReadExpect<'a, RegionMap>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Player>,
WriteStorage<'a, Client>,
WriteStorage<'a, RegionSubscription>,
);
fn run(
&mut self,
(entities, region_map, mut timer, uids, positions, players, mut clients, mut subscriptions): Self::SystemData,
) {
timer.start();
// 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 (client, subscription, pos, vd) in
(&mut clients, &mut subscriptions, &positions, &players)
.join()
.filter_map(|(c, s, pos, player)| player.view_distance.map(|v| (c, s, pos, v)))
{
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
// uses a fuzzy border to prevent rapid triggering when moving along chunk boundaries
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()
{
// Update current chunk
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;
// Iterate through currently subscribed regions
for key in &subscription.regions {
// Check if the region is not within range anymore
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(),
) {
// Add to the list of regions to remove
regions_to_remove.push(*key);
}
}
// Iterate through regions to remove
for key in regions_to_remove.drain(..) {
// Remove region from this clients 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
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) = uids.get(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 (&uids, 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
}
}
}
}
timer.end();
}
}

206
server/src/sys/terrain.rs Normal file
View File

@ -0,0 +1,206 @@
use super::SysTimer;
use crate::{chunk_generator::ChunkGenerator, client::Client, Tick};
use common::{
comp::{self, Player, Pos},
event::{EventBus, ServerEvent},
msg::ServerMsg,
state::TerrainChanges,
terrain::TerrainGrid,
};
use rand::Rng;
use specs::{Join, Read, ReadStorage, System, Write, WriteExpect, WriteStorage};
use std::sync::Arc;
use vek::*;
/// This system will handle loading generated chunks and unloading uneeded chunks.
/// 1. Inserts newly generated chunks into the TerrainGrid
/// 2. Sends new chunks to neaby clients
/// 3. Handles the chunk's supplement (e.g. npcs)
/// 4. Removes chunks outside the range of players
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Read<'a, EventBus<ServerEvent>>,
Read<'a, Tick>,
Write<'a, SysTimer<Self>>,
WriteExpect<'a, ChunkGenerator>,
WriteExpect<'a, TerrainGrid>,
Write<'a, TerrainChanges>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Player>,
WriteStorage<'a, Client>,
);
fn run(
&mut self,
(
server_emitter,
tick,
mut timer,
mut chunk_generator,
mut terrain,
mut terrain_changes,
positions,
players,
mut clients,
): Self::SystemData,
) {
timer.start();
// Fetch any generated `TerrainChunk`s and insert them into the terrain.
// Also, send the chunk data to anybody that is close by.
'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() {
let (chunk, supplement) = match res {
Ok((chunk, supplement)) => (chunk, supplement),
Err(entity) => {
if let Some(client) = clients.get_mut(entity) {
client.notify(ServerMsg::TerrainChunkUpdate {
key,
chunk: Err(()),
});
}
continue 'insert_terrain_chunks;
}
};
// Send the chunk to all nearby players.
for (view_distance, pos, client) in (&players, &positions, &mut clients)
.join()
.filter_map(|(player, pos, client)| {
player.view_distance.map(|vd| (vd, pos, client))
})
{
let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32));
let adjusted_dist_sqr = (Vec2::from(chunk_pos) - Vec2::from(key))
.map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0))
.magnitude_squared();
if adjusted_dist_sqr <= view_distance.pow(2) {
client.notify(ServerMsg::TerrainChunkUpdate {
key,
chunk: Ok(Box::new(chunk.clone())),
});
}
}
// TODO: code duplication for chunk insertion between here and state.rs
// Insert the chunk into terrain changes
if terrain.insert(key, Arc::new(chunk)).is_some() {
terrain_changes.modified_chunks.insert(key);
} else {
terrain_changes.new_chunks.insert(key);
}
// Handle chunk supplement
for npc in supplement.npcs {
let (mut stats, mut body) = if rand::random() {
let stats = comp::Stats::new(
"Humanoid".to_string(),
Some(comp::Item::Tool {
kind: comp::item::Tool::Sword,
power: 5,
stamina: 0,
strength: 0,
dexterity: 0,
intelligence: 0,
}),
);
let body = comp::Body::Humanoid(comp::humanoid::Body::random());
(stats, body)
} else {
let stats = comp::Stats::new("Wolf".to_string(), None);
let body = comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random());
(stats, body)
};
let mut scale = 1.0;
// TODO: Remove this and implement scaling or level depending on stuff like species instead
stats.level.set_level(rand::thread_rng().gen_range(1, 3));
if npc.boss {
if rand::random::<f32>() < 0.8 {
stats = comp::Stats::new(
"Humanoid".to_string(),
Some(comp::Item::Tool {
kind: comp::item::Tool::Sword,
power: 10,
stamina: 0,
strength: 0,
dexterity: 0,
intelligence: 0,
}),
);
body = comp::Body::Humanoid(comp::humanoid::Body::random());
}
stats.level.set_level(rand::thread_rng().gen_range(10, 50));
scale = 2.5 + rand::random::<f32>();
}
stats.update_max_hp();
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(npc.pos),
stats,
body,
agent: comp::Agent::enemy(),
scale: comp::Scale(scale),
})
}
}
// Remove chunks that are too far from players.
let mut chunks_to_remove = Vec::new();
terrain
.iter()
.map(|(k, _)| k)
// Don't every chunk every tick (spread over 16 ticks)
.filter(|k| k.x.abs() as u64 % 4 + k.y.abs() as u64 % 8 * 4 == tick.0 % 16)
// There shouldn't be to many pending chunks so we will just check them all
.chain(chunk_generator.pending_chunks())
.for_each(|chunk_key| {
let mut should_drop = true;
// For each player with a position, calculate the distance.
for (player, pos) in (&players, &positions).join() {
if player
.view_distance
.map(|vd| chunk_in_vd(pos.0, chunk_key, &terrain, vd))
.unwrap_or(false)
{
should_drop = false;
break;
}
}
if should_drop {
chunks_to_remove.push(chunk_key);
}
});
for key in chunks_to_remove {
// TODO: code duplication for chunk insertion between here and state.rs
if terrain.remove(key).is_some() {
terrain_changes.removed_chunks.insert(key);
}
chunk_generator.cancel_if_pending(key);
}
timer.end()
}
}
pub fn chunk_in_vd(
player_pos: Vec3<f32>,
chunk_pos: Vec2<i32>,
terrain: &TerrainGrid,
vd: u32,
) -> bool {
let player_chunk_pos = terrain.pos_key(player_pos.map(|e| e as i32));
let adjusted_dist_sqr = Vec2::from(player_chunk_pos - chunk_pos)
.map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0))
.magnitude_squared();
adjusted_dist_sqr <= vd.pow(2)
}

View File

@ -0,0 +1,63 @@
use super::SysTimer;
use crate::client::Client;
use common::{
comp::{Player, Pos},
msg::ServerMsg,
state::TerrainChanges,
terrain::TerrainGrid,
};
use specs::{Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage};
/// This system will handle loading generated chunks and unloading uneeded chunks.
/// 1. Inserts newly generated chunks into the TerrainGrid
/// 2. Sends new chunks to neaby clients
/// 3. Handles the chunk's supplement (e.g. npcs)
/// 4. Removes chunks outside the range of players
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
ReadExpect<'a, TerrainGrid>,
Read<'a, TerrainChanges>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Player>,
WriteStorage<'a, Client>,
);
fn run(
&mut self,
(terrain, terrain_changes, mut timer, positions, players, mut clients): Self::SystemData,
) {
timer.start();
// Sync changed chunks
'chunk: for chunk_key in &terrain_changes.modified_chunks {
for (player, pos, client) in (&players, &positions, &mut clients).join() {
if player
.view_distance
.map(|vd| super::terrain::chunk_in_vd(pos.0, *chunk_key, &terrain, vd))
.unwrap_or(false)
{
client.notify(ServerMsg::TerrainChunkUpdate {
key: *chunk_key,
chunk: Ok(Box::new(match terrain.get_key(*chunk_key) {
Some(chunk) => chunk.clone(),
None => break 'chunk,
})),
});
}
}
}
// TODO: Don't send all changed blocks to all clients
// Sync changed blocks
let msg = ServerMsg::TerrainBlockUpdates(terrain_changes.modified_blocks.clone());
for (player, client) in (&players, &mut clients).join() {
if player.view_distance.is_some() {
client.notify(msg.clone());
}
}
timer.end();
}
}

View File

@ -117,7 +117,7 @@ impl PlayState for CharSelectionState {
if let Err(err) = self
.client
.borrow_mut()
.tick(comp::Controller::default(), clock.get_last_delta())
.tick(comp::ControllerInputs::default(), clock.get_last_delta())
{
error!("Failed to tick the scene: {:?}", err);
return PlayStateResult::Pop;

View File

@ -4,7 +4,7 @@ use crate::{
PlayState, PlayStateResult,
};
use common::comp;
use log::warn;
use log::{info, warn};
use server::settings::ServerSettings;
pub struct StartSingleplayerState {
@ -58,6 +58,12 @@ impl PlayState for StartSingleplayerState {
}
};
// Print the metrics port
info!(
"Metrics port: {}",
self.server_settings.metrics_address.port()
);
PlayStateResult::Push(Box::new(CharSelectionState::new(
global_state,
std::rc::Rc::new(std::cell::RefCell::new(client)),

View File

@ -25,7 +25,7 @@ pub struct SessionState {
client: Rc<RefCell<Client>>,
hud: Hud,
key_state: KeyState,
controller: comp::Controller,
inputs: comp::ControllerInputs,
selected_block: Block,
}
@ -43,7 +43,7 @@ impl SessionState {
scene,
client,
key_state: KeyState::new(),
controller: comp::Controller::default(),
inputs: comp::ControllerInputs::default(),
hud,
selected_block: Block::new(BlockKind::Normal, Rgb::broadcast(255)),
}
@ -53,7 +53,7 @@ impl SessionState {
impl SessionState {
/// Tick the session (and the client attached to it).
fn tick(&mut self, dt: Duration) -> Result<(), Error> {
for event in self.client.borrow_mut().tick(self.controller.clone(), dt)? {
for event in self.client.borrow_mut().tick(self.inputs.clone(), dt)? {
match event {
client::Event::Chat {
chat_type: _,
@ -139,9 +139,6 @@ impl PlayState for SessionState {
};
self.scene.set_select_pos(select_pos);
// Reset controller events
self.controller.clear_events();
// Handle window events.
for event in global_state.window.fetch_events() {
// Pass all events to the ui first.
@ -168,12 +165,12 @@ impl PlayState for SessionState {
client.place_block(build_pos, self.selected_block);
}
} else {
self.controller.primary = state
self.inputs.primary = state
}
}
Event::InputUpdate(GameInput::Secondary, state) => {
self.controller.secondary = false; // To be changed later on
self.inputs.secondary = false; // To be changed later on
let mut client = self.client.borrow_mut();
@ -194,7 +191,7 @@ impl PlayState for SessionState {
.map(|cs| cs.action.is_wield())
.unwrap_or(false)
{
self.controller.secondary = state;
self.inputs.secondary = state;
} else {
if let Some(select_pos) = select_pos {
client.collect_block(select_pos);
@ -217,36 +214,34 @@ impl PlayState for SessionState {
}
}
} else {
self.controller.roll = state;
self.inputs.roll = state;
}
}
Event::InputUpdate(GameInput::Respawn, state) => {
self.controller.respawn = state;
self.inputs.respawn = state;
}
Event::InputUpdate(GameInput::Jump, state) => {
self.controller.jump = state;
self.inputs.jump = state;
}
Event::InputUpdate(GameInput::Sit, state) => {
self.controller.sit = state;
self.inputs.sit = state;
}
Event::InputUpdate(GameInput::MoveForward, state) => self.key_state.up = state,
Event::InputUpdate(GameInput::MoveBack, state) => self.key_state.down = state,
Event::InputUpdate(GameInput::MoveLeft, state) => self.key_state.left = state,
Event::InputUpdate(GameInput::MoveRight, state) => self.key_state.right = state,
Event::InputUpdate(GameInput::Glide, state) => {
self.controller.glide = state;
self.inputs.glide = state;
}
Event::InputUpdate(GameInput::Climb, state) => self.controller.climb = state,
Event::InputUpdate(GameInput::Climb, state) => self.inputs.climb = state,
Event::InputUpdate(GameInput::ClimbDown, state) => {
self.controller.climb_down = state
}
Event::InputUpdate(GameInput::WallLeap, state) => {
self.controller.wall_leap = state
self.inputs.climb_down = state
}
Event::InputUpdate(GameInput::WallLeap, state) => self.inputs.wall_leap = state,
Event::InputUpdate(GameInput::Mount, true) => {
let client = self.client.borrow();
let mut client = self.client.borrow_mut();
if client.is_mounted() {
self.controller.push_event(comp::ControlEvent::Unmount);
client.unmount();
} else {
let player_pos = client
.state()
@ -271,15 +266,10 @@ impl PlayState for SessionState {
.min_by_key(|(_, pos, _)| {
(player_pos.0.distance_squared(pos.0) * 1000.0) as i32
})
.map(|(entity, _, _)| entity);
.map(|(uid, _, _)| uid);
if let Some(mountee_uid) =
closest_mountable.and_then(|mountee_entity| {
client.state().ecs().uid_from_entity(mountee_entity)
})
{
self.controller
.push_event(comp::ControlEvent::Mount(mountee_uid.into()));
if let Some(mountee_entity) = closest_mountable {
client.mount(mountee_entity);
}
}
}
@ -329,9 +319,9 @@ impl PlayState for SessionState {
Vec2::new(ori[0].sin(), ori[0].cos()),
);
let dir_vec = self.key_state.dir_vec();
self.controller.move_dir = unit_vecs.0 * dir_vec[0] + unit_vecs.1 * dir_vec[1];
self.inputs.move_dir = unit_vecs.0 * dir_vec[0] + unit_vecs.1 * dir_vec[1];
self.controller.look_dir = cam_dir;
self.inputs.look_dir = cam_dir;
// Perform an in-game tick.
if let Err(err) = self.tick(clock.get_avg_delta()) {