mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
commit
4b1fb112de
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
},
|
||||
|
@ -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
360
common/src/region.rs
Normal 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)
|
||||
}*/
|
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
70
server/src/chunk_generator.rs
Normal file
70
server/src/chunk_generator.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>>;
|
||||
}
|
||||
|
@ -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))),
|
||||
);
|
||||
|
1406
server/src/lib.rs
1406
server/src/lib.rs
File diff suppressed because it is too large
Load Diff
233
server/src/sys/entity_sync.rs
Normal file
233
server/src/sys/entity_sync.rs
Normal 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
327
server/src/sys/message.rs
Normal 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
61
server/src/sys/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
131
server/src/sys/subscription.rs
Normal file
131
server/src/sys/subscription.rs
Normal 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
206
server/src/sys/terrain.rs
Normal 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)
|
||||
}
|
63
server/src/sys/terrain_sync.rs
Normal file
63
server/src/sys/terrain_sync.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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)),
|
||||
|
@ -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()) {
|
||||
|
Loading…
Reference in New Issue
Block a user