Merge branch 'xvar/gotta-catch-em-all' into 'master'

Pet persistence

See merge request veloren/veloren!2668
This commit is contained in:
Ben Wallis 2021-07-28 22:36:42 +00:00
commit be0a2c0105
33 changed files with 808 additions and 157 deletions

View File

@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- HUD debug info now displays current biome and site
- Quotes and escape codes can be used in command arguments
- Toggle chat with a shortcut (default is F5)
- Pets are now saved on logout 🐕 🦎 🐼
### Changed
@ -34,15 +35,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Harvester boss now has new abilities and AI
- Death particles and SFX
- Default keybindings were made more consistent
- Adjust Yeti difficulty
- Adjusted Yeti difficulty
- Now most of the food gives Saturation in the process of eating
- Mushroom Curry gives long-lasting Regeneration buff
- Trades now consider if items can stack in full inventories.
- The types of animals that can be tamed as pets are now limited to certain species, pending further balancing of pets
### Removed
- Enemies no more spawn in dungeon boss room
- Melee critical hit no more applies after reduction by armour
- Enemies no longer spawn in dungeon boss room
- Melee critical hit no longer applies after reduction by armour
### Fixed

20
common/src/comp/anchor.rs Normal file
View File

@ -0,0 +1,20 @@
use specs::{Component, Entity};
use specs_idvs::IdvStorage;
use vek::Vec2;
/// This component exists in order to fix a bug that caused entities
/// such as campfires to duplicate because the chunk was double-loaded.
/// See https://gitlab.com/veloren/veloren/-/merge_requests/1543
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Anchor {
/// An entity with an Entity Anchor will be destroyed when its anchor Entity
/// no longer exists
Entity(Entity),
/// An entity with Chunk Anchor will be destroyed when both the chunk it's
/// currently positioned within and its anchor chunk are unloaded
Chunk(Vec2<i32>),
}
impl Component for Anchor {
type Storage = IdvStorage<Self>;
}

View File

@ -1,6 +1,7 @@
use crate::{make_case_elim, make_proj_elim};
use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
make_proj_elim!(
body,
@ -29,9 +30,13 @@ impl From<Body> for super::Body {
fn from(body: Body) -> Self { super::Body::QuadrupedLow(body) }
}
// Renaming any enum entries here (re-ordering is fine) will require a
// database migration to ensure pets correctly de-serialize on player login.
make_case_elim!(
species,
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Copy, Clone, Debug, Display, EnumString, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[repr(u32)]
pub enum Species {
Crocodile = 0,
@ -120,9 +125,13 @@ impl<'a, SpeciesMeta: 'a> IntoIterator for &'a AllSpecies<SpeciesMeta> {
fn into_iter(self) -> Self::IntoIter { ALL_SPECIES.iter().copied() }
}
// Renaming any enum entries here (re-ordering is fine) will require a
// database migration to ensure pets correctly de-serialize on player login.
make_case_elim!(
body_type,
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Copy, Clone, Debug, Display, EnumString, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[repr(u32)]
pub enum BodyType {
Female = 0,

View File

@ -1,6 +1,7 @@
use crate::{make_case_elim, make_proj_elim};
use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
make_proj_elim!(
body,
@ -29,7 +30,9 @@ impl From<Body> for super::Body {
fn from(body: Body) -> Self { super::Body::QuadrupedMedium(body) }
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
// Renaming any enum entries here (re-ordering is fine) will require a
// database migration to ensure pets correctly de-serialize on player login.
#[derive(Copy, Clone, Debug, Display, EnumString, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Species {
Grolgar = 0,
@ -197,9 +200,13 @@ impl<'a, SpeciesMeta: 'a> IntoIterator for &'a AllSpecies<SpeciesMeta> {
fn into_iter(self) -> Self::IntoIter { ALL_SPECIES.iter().copied() }
}
// Renaming any enum entries here (re-ordering is fine) will require a
// database migration to ensure pets correctly de-serialize on player login.
make_case_elim!(
body_type,
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Copy, Clone, Debug, Display, EnumString, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[repr(u32)]
pub enum BodyType {
Female = 0,

View File

@ -1,6 +1,7 @@
use crate::{make_case_elim, make_proj_elim};
use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
make_proj_elim!(
body,
@ -29,7 +30,9 @@ impl From<Body> for super::Body {
fn from(body: Body) -> Self { super::Body::QuadrupedSmall(body) }
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
// Renaming any enum entries here (re-ordering is fine) will require a
// database migration to ensure pets correctly de-serialize on player login.
#[derive(Copy, Clone, Debug, Display, EnumString, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Species {
Pig = 0,
@ -169,9 +172,13 @@ impl<'a, SpeciesMeta: 'a> IntoIterator for &'a AllSpecies<SpeciesMeta> {
fn into_iter(self) -> Self::IntoIter { ALL_SPECIES.iter().copied() }
}
// Renaming any enum entries here (re-ordering is fine) will require a
// database migration to ensure pets correctly de-serialize on player login.
make_case_elim!(
body_type,
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Copy, Clone, Debug, Display, EnumString, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[repr(u32)]
pub enum BodyType {
Female = 0,

View File

@ -1,13 +0,0 @@
use specs::Component;
use specs_idvs::IdvStorage;
use vek::Vec2;
/// This component exists in order to fix a bug that caused entitites
/// such as campfires to duplicate because the chunk was double-loaded.
/// See https://gitlab.com/veloren/veloren/-/merge_requests/1543
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct HomeChunk(pub Vec2<i32>);
impl Component for HomeChunk {
type Storage = IdvStorage<Self>;
}

View File

@ -1,6 +1,8 @@
#[cfg(not(target_arch = "wasm32"))] mod ability;
#[cfg(not(target_arch = "wasm32"))] mod admin;
#[cfg(not(target_arch = "wasm32"))] pub mod agent;
#[cfg(not(target_arch = "wasm32"))]
pub mod anchor;
#[cfg(not(target_arch = "wasm32"))] pub mod aura;
#[cfg(not(target_arch = "wasm32"))] pub mod beam;
#[cfg(not(target_arch = "wasm32"))] pub mod body;
@ -19,8 +21,6 @@ pub mod dialogue;
pub mod fluid_dynamics;
#[cfg(not(target_arch = "wasm32"))] pub mod group;
mod health;
#[cfg(not(target_arch = "wasm32"))]
pub mod home_chunk;
#[cfg(not(target_arch = "wasm32"))] mod inputs;
#[cfg(not(target_arch = "wasm32"))]
pub mod inventory;
@ -30,6 +30,7 @@ pub mod invite;
#[cfg(not(target_arch = "wasm32"))] mod location;
#[cfg(not(target_arch = "wasm32"))] mod misc;
#[cfg(not(target_arch = "wasm32"))] pub mod ori;
#[cfg(not(target_arch = "wasm32"))] pub mod pet;
#[cfg(not(target_arch = "wasm32"))] mod phys;
#[cfg(not(target_arch = "wasm32"))] mod player;
#[cfg(not(target_arch = "wasm32"))] pub mod poise;
@ -49,6 +50,7 @@ pub use self::{
ability::{CharacterAbility, CharacterAbilityType},
admin::{Admin, AdminRole},
agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController},
anchor::Anchor,
aura::{Aura, AuraChange, AuraKind, Auras},
beam::{Beam, BeamSegment},
body::{
@ -73,7 +75,6 @@ pub use self::{
energy::{Energy, EnergyChange, EnergySource},
fluid_dynamics::Fluid,
group::Group,
home_chunk::HomeChunk,
inputs::CanBuild,
inventory::{
item::{self, tool, Item, ItemConfig, ItemDrop},
@ -83,6 +84,7 @@ pub use self::{
location::{Waypoint, WaypointArea},
misc::Object,
ori::Ori,
pet::Pet,
phys::{
Collider, Density, ForceUpdate, Mass, PhysicsState, Pos, PosVelOriDefer, PreviousPhysCache,
Scale, Sticky, Vel,

51
common/src/comp/pet.rs Normal file
View File

@ -0,0 +1,51 @@
use crate::comp::body::Body;
use crossbeam_utils::atomic::AtomicCell;
use serde::{Deserialize, Serialize};
use specs::Component;
use specs_idvs::IdvStorage;
use std::{num::NonZeroU64, sync::Arc};
pub type PetId = AtomicCell<Option<NonZeroU64>>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Pet {
#[serde(skip)]
database_id: Arc<PetId>,
}
impl Pet {
/// Not to be used outside of persistence - provides mutable access to the
/// pet component's database ID which is required to save the pet's data
/// from the persistence thread.
#[doc(hidden)]
pub fn get_database_id(&self) -> Arc<PetId> { Arc::clone(&self.database_id) }
pub fn new_from_database(database_id: NonZeroU64) -> Self {
Self {
database_id: Arc::new(AtomicCell::new(Some(database_id))),
}
}
}
impl Default for Pet {
fn default() -> Self {
Self {
database_id: Arc::new(AtomicCell::new(None)),
}
}
}
/// Determines whether an entity of a particular body variant is tameable.
pub fn is_tameable(body: &Body) -> bool {
// Currently only Quadruped animals can be tamed pending further work
// on the pets feature (allowing larger animals to be tamed will
// require balance issues to be addressed).
matches!(
body,
Body::QuadrupedLow(_) | Body::QuadrupedMedium(_) | Body::QuadrupedSmall(_)
)
}
impl Component for Pet {
type Storage = IdvStorage<Self>;
}

View File

@ -102,12 +102,14 @@ pub enum ServerEvent {
},
UpdateCharacterData {
entity: EcsEntity,
#[allow(clippy::type_complexity)]
components: (
comp::Body,
comp::Stats,
comp::SkillSet,
comp::Inventory,
Option<comp::Waypoint>,
Vec<(comp::Pet, comp::Body, comp::Stats)>,
),
},
ExitIngame {
@ -125,7 +127,7 @@ pub enum ServerEvent {
agent: Option<comp::Agent>,
alignment: comp::Alignment,
scale: comp::Scale,
home_chunk: Option<comp::HomeChunk>,
anchor: Option<comp::Anchor>,
drop_item: Option<Item>,
rtsim_entity: Option<RtSimEntity>,
projectile: Option<comp::Projectile>,
@ -186,6 +188,10 @@ pub enum ServerEvent {
pos: Vec3<i32>,
sprite: SpriteKind,
},
TamePet {
pet_entity: EcsEntity,
owner_entity: EcsEntity,
},
}
pub struct EventBus<E> {

View File

@ -186,7 +186,7 @@ impl CharacterBehavior for Data {
.summon_info
.scale
.unwrap_or(comp::Scale(1.0)),
home_chunk: None,
anchor: None,
drop_item: None,
rtsim_entity: None,
projectile,

View File

@ -59,6 +59,6 @@ pub fn create_character(
entity,
player_uuid,
character_alias,
(body, stats, skill_set, inventory, waypoint),
(body, stats, skill_set, inventory, waypoint, Vec::new()),
);
}

View File

@ -1045,31 +1045,12 @@ fn handle_spawn(
// Add to group system if a pet
if matches!(alignment, comp::Alignment::Owned { .. }) {
let state = server.state();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager =
state.ecs().write_resource::<comp::group::GroupManager>();
group_manager.new_pet(
new_entity,
target,
&mut state.ecs().write_storage(),
&state.ecs().entities(),
&state.ecs().read_storage(),
&uids,
&mut |entity, group_change| {
clients
.get(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| {
c.send_fallible(ServerGeneral::GroupUpdate(g));
});
},
);
let server_eventbus =
server.state.ecs().read_resource::<EventBus<ServerEvent>>();
server_eventbus.emit_now(ServerEvent::TamePet {
owner_entity: target,
pet_entity: new_entity,
});
} else if let Some(group) = match alignment {
comp::Alignment::Wild => None,
comp::Alignment::Passive => None,

View File

@ -8,9 +8,9 @@ use common::{
beam,
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
inventory::loadout::Loadout,
shockwave, Agent, Alignment, Body, Health, HomeChunk, Inventory, Item, ItemDrop,
LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats,
Vel, WaypointArea,
shockwave, Agent, Alignment, Anchor, Body, Health, Inventory, Item, ItemDrop, LightEmitter,
Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, Vel,
WaypointArea,
},
outcome::Outcome,
rtsim::RtSimEntity,
@ -30,6 +30,7 @@ pub fn handle_initialize_character(
server.state.initialize_character_data(entity, character_id);
}
#[allow(clippy::type_complexity)]
pub fn handle_loaded_character_data(
server: &mut Server,
entity: EcsEntity,
@ -39,6 +40,7 @@ pub fn handle_loaded_character_data(
comp::SkillSet,
comp::Inventory,
Option<comp::Waypoint>,
Vec<(comp::Pet, comp::Body, comp::Stats)>,
),
) {
server
@ -61,7 +63,7 @@ pub fn handle_create_npc(
alignment: Alignment,
scale: Scale,
drop_item: Option<Item>,
home_chunk: Option<HomeChunk>,
home_chunk: Option<Anchor>,
rtsim_entity: Option<RtSimEntity>,
projectile: Option<Projectile>,
) {

View File

@ -29,6 +29,7 @@ use crate::{
Server,
};
use crate::pet::tame_pet;
use hashbrown::{HashMap, HashSet};
use lazy_static::lazy_static;
use serde::Deserialize;
@ -442,3 +443,9 @@ pub fn handle_create_sprite(server: &mut Server, pos: Vec3<i32>, sprite: SpriteK
}
}
}
pub fn handle_tame_pet(server: &mut Server, pet_entity: EcsEntity, owner_entity: EcsEntity) {
// TODO: Raise outcome to send to clients to play sound/render an indicator
// showing taming success?
tame_pet(server.state.ecs(), pet_entity, owner_entity);
}

View File

@ -16,11 +16,15 @@ use common::{
util::find_dist::{self, FindDist},
vol::ReadVol,
};
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use common_net::sync::WorldSyncExt;
use common_state::State;
use comp::LightEmitter;
use crate::{client::Client, Server, StateExt};
use crate::{Server, StateExt};
use common::{
comp::pet::is_tameable,
event::{EventBus, ServerEvent},
};
pub fn swap_lantern(
storage: &mut WriteStorage<comp::LightEmitter>,
@ -310,6 +314,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
kind: comp::item::Utility::Collar,
..
} => {
const MAX_PETS: usize = 3;
let reinsert = if let Some(pos) =
state.read_storage::<comp::Pos>().get(entity)
{
@ -322,65 +327,36 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
alignment == &&comp::Alignment::Owned(uid)
})
.count()
>= 3
>= MAX_PETS
{
true
} else if let Some(tameable_entity) = {
let nearest_tameable = (
&state.ecs().entities(),
&state.ecs().read_storage::<comp::Body>(),
&state.ecs().read_storage::<comp::Pos>(),
&state.ecs().read_storage::<comp::Alignment>(),
)
.join()
.filter(|(_, wild_pos, _)| {
.filter(|(_, _, wild_pos, _)| {
wild_pos.0.distance_squared(pos.0) < 5.0f32.powi(2)
})
.filter(|(_, _, alignment)| {
.filter(|(_, body, _, alignment)| {
alignment == &&comp::Alignment::Wild
&& is_tameable(body)
})
.min_by_key(|(_, wild_pos, _)| {
.min_by_key(|(_, _, wild_pos, _)| {
(wild_pos.0.distance_squared(pos.0) * 100.0) as i32
})
.map(|(entity, _, _)| entity);
.map(|(entity, _, _, _)| entity);
nearest_tameable
} {
let _ = state
.ecs()
.write_storage()
.insert(tameable_entity, comp::Alignment::Owned(uid));
// Add to group system
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager = state
.ecs()
.write_resource::<comp::group::GroupManager>(
);
group_manager.new_pet(
tameable_entity,
entity,
&mut state.ecs().write_storage(),
&state.ecs().entities(),
&state.ecs().read_storage(),
&uids,
&mut |entity, group_change| {
clients
.get(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| {
c.send(ServerGeneral::GroupUpdate(g))
});
},
);
let _ = state
.ecs()
.write_storage()
.insert(tameable_entity, comp::Agent::default());
let server_eventbus =
state.ecs().read_resource::<EventBus<ServerEvent>>();
server_eventbus.emit_now(ServerEvent::TamePet {
owner_entity: entity,
pet_entity: tameable_entity,
});
false
} else {
true

View File

@ -1,4 +1,4 @@
use crate::{state_ext::StateExt, Server};
use crate::{events::interaction::handle_tame_pet, state_ext::StateExt, Server};
use common::event::{EventBus, ServerEvent};
use common_base::span;
use entity_creation::{
@ -144,7 +144,7 @@ impl Server {
agent,
alignment,
scale,
home_chunk,
anchor: home_chunk,
drop_item,
rtsim_entity,
projectile,
@ -224,6 +224,10 @@ impl Server {
ServerEvent::CreateSprite { pos, sprite } => {
handle_create_sprite(self, pos, sprite)
},
ServerEvent::TamePet {
pet_entity,
owner_entity,
} => handle_tame_pet(self, pet_entity, owner_entity),
}
}

View File

@ -5,13 +5,13 @@ use crate::{
};
use common::{
comp,
comp::group,
comp::{group, pet::is_tameable},
uid::{Uid, UidAllocator},
};
use common_base::span;
use common_net::msg::{PlayerListUpdate, PresenceKind, ServerGeneral};
use common_state::State;
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, WorldExt};
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt};
use tracing::{debug, error, trace, warn, Instrument};
pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
@ -195,10 +195,17 @@ pub fn handle_client_disconnect(
// the race condition of their login fetching their old data
// and overwriting the data saved here.
fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
if let (Some(presence), Some(skill_set), Some(inventory), mut character_updater) = (
if let (
Some(presence),
Some(skill_set),
Some(inventory),
Some(player_uid),
mut character_updater,
) = (
state.read_storage::<Presence>().get(entity),
state.read_storage::<comp::SkillSet>().get(entity),
state.read_storage::<comp::Inventory>().get(entity),
state.read_storage::<Uid>().get(entity),
state.ecs().fetch_mut::<CharacterUpdater>(),
) {
match presence.kind {
@ -209,9 +216,29 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
.get(entity)
.cloned();
// Get player's pets
let alignments = state.ecs().read_storage::<comp::Alignment>();
let bodies = state.ecs().read_storage::<comp::Body>();
let stats = state.ecs().read_storage::<comp::Stats>();
let pets = state.ecs().read_storage::<comp::Pet>();
let pets = (&alignments, &bodies, &stats, &pets)
.join()
.filter_map(|(alignment, body, stats, pet)| match alignment {
// Don't try to persist non-tameable pets (likely spawned
// using /spawn) since there isn't any code to handle
// persisting them
common::comp::Alignment::Owned(ref pet_owner)
if pet_owner == player_uid && is_tameable(body) =>
{
Some(((*pet).clone(), *body, stats.clone()))
},
_ => None,
})
.collect();
character_updater.add_pending_logout_update(
char_id,
(skill_set.clone(), inventory.clone(), waypoint),
(skill_set.clone(), inventory.clone(), pets, waypoint),
);
},
PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ },

View File

@ -25,6 +25,7 @@ pub mod input;
pub mod login_provider;
pub mod metrics;
pub mod persistence;
mod pet;
pub mod presence;
pub mod rtsim;
pub mod settings;
@ -87,7 +88,7 @@ use persistence::{
};
use prometheus::Registry;
use prometheus_hyper::Server as PrometheusServer;
use specs::{join::Join, Builder, Entity as EcsEntity, SystemData, WorldExt};
use specs::{join::Join, Builder, Entity as EcsEntity, Entity, SystemData, WorldExt};
use std::{
i32,
ops::{Deref, DerefMut},
@ -112,6 +113,7 @@ use {
common_state::plugin::{memory_manager::EcsWorld, PluginMgr},
};
use common::comp::Anchor;
#[cfg(feature = "worldgen")]
use world::{
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
@ -249,7 +251,8 @@ impl Server {
state.ecs_mut().register::<Presence>();
state.ecs_mut().register::<wiring::WiringElement>();
state.ecs_mut().register::<wiring::Circuit>();
state.ecs_mut().register::<comp::HomeChunk>();
state.ecs_mut().register::<comp::Anchor>();
state.ecs_mut().register::<comp::Pet>();
state.ecs_mut().register::<login_provider::PendingLogin>();
state.ecs_mut().register::<RepositionOnChunkLoad>();
@ -617,6 +620,32 @@ impl Server {
run_now::<terrain::Sys>(self.state.ecs_mut());
}
// Prevent anchor entity chains which are not currently supported
let anchors = self.state.ecs().read_storage::<comp::Anchor>();
let anchored_anchor_entities: Vec<Entity> = (
&self.state.ecs().entities(),
&self.state.ecs().read_storage::<comp::Anchor>(),
)
.join()
.filter_map(|(_, anchor)| match anchor {
Anchor::Entity(anchor_entity) => Some(*anchor_entity),
_ => None,
})
.filter(|anchor_entity| anchors.get(*anchor_entity).is_some())
.collect();
drop(anchors);
for entity in anchored_anchor_entities {
if cfg!(debug_assertions) {
panic!("Entity anchor chain detected");
}
error!(
"Detected an anchor entity that itself has an anchor entity - anchor chains are \
not currently supported. The entity's Anchor component has been deleted"
);
self.state.delete_component::<Anchor>(entity);
}
// Remove NPCs that are outside the view distances of all players
// This is done by removing NPCs in unloaded chunks
let to_delete = {
@ -625,16 +654,22 @@ impl Server {
&self.state.ecs().entities(),
&self.state.ecs().read_storage::<comp::Pos>(),
!&self.state.ecs().read_storage::<Presence>(),
self.state.ecs().read_storage::<comp::HomeChunk>().maybe(),
self.state.ecs().read_storage::<comp::Anchor>().maybe(),
)
.join()
.filter(|(_, pos, _, home_chunk)| {
.filter(|(_, pos, _, anchor)| {
let chunk_key = terrain.pos_key(pos.0.map(|e| e.floor() as i32));
// Check if both this chunk and the NPCs `home_chunk` is unloaded. If so,
// we delete them. We check for `home_chunk` in order to avoid duplicating
// the entity under some circumstances.
terrain.get_key(chunk_key).is_none()
&& home_chunk.map_or(true, |hc| terrain.get_key(hc.0).is_none())
match anchor {
Some(Anchor::Chunk(hc)) => {
// Check if both this chunk and the NPCs `home_chunk` is unloaded. If
// so, we delete them. We check for
// `home_chunk` in order to avoid duplicating
// the entity under some circumstances.
terrain.get_key(chunk_key).is_none() && terrain.get_key(*hc).is_none()
},
Some(Anchor::Entity(entity)) => !self.state.ecs().is_alive(*entity),
None => terrain.get_key(chunk_key).is_none(),
}
})
.map(|(entity, _, _, _)| entity)
.collect::<Vec<_>>()

View File

@ -0,0 +1,10 @@
-- Creates new pet table
CREATE TABLE "pet" (
"pet_id" INT NOT NULL,
"character_id" INT NOT NULL,
"name" TEXT NOT NULL,
PRIMARY KEY("pet_id"),
FOREIGN KEY("pet_id") REFERENCES entity(entity_id),
FOREIGN KEY("character_id") REFERENCES "character"("character_id")
);

View File

@ -20,6 +20,7 @@ use crate::{
convert_waypoint_from_database_json, convert_waypoint_to_database_json,
},
character_loader::{CharacterCreationResult, CharacterDataResult, CharacterListResult},
character_updater::PetPersistenceData,
error::PersistenceError::DatabaseError,
PersistedComponents,
},
@ -27,8 +28,8 @@ use crate::{
use common::character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
use core::ops::Range;
use rusqlite::{types::Value, Connection, ToSql, Transaction, NO_PARAMS};
use std::rc::Rc;
use tracing::{error, trace, warn};
use std::{num::NonZeroU64, rc::Rc};
use tracing::{debug, error, trace, warn};
/// Private module for very tightly coupled database conversion methods. In
/// general, these have many invariants that need to be maintained when they're
@ -203,8 +204,55 @@ pub fn load_character_data(
.filter_map(Result::ok)
.collect::<Vec<SkillGroup>>();
#[rustfmt::skip]
let mut stmt = connection.prepare_cached("
SELECT p.pet_id,
p.name,
b.variant,
b.body_data
FROM pet p
JOIN body b ON (p.pet_id = b.body_id)
WHERE p.character_id = ?1",
)?;
let db_pets = stmt
.query_map(&[char_id], |row| {
Ok(Pet {
database_id: row.get(0)?,
name: row.get(1)?,
body_variant: row.get(2)?,
body_data: row.get(3)?,
})
})?
.filter_map(Result::ok)
.collect::<Vec<Pet>>();
// Re-construct the pet components for the player's pets, including
// de-serializing the pets' bodies and creating their Pet and Stats
// components
let pets = db_pets
.iter()
.filter_map(|db_pet| {
if let Ok(pet_body) =
convert_body_from_database(&db_pet.body_variant, &db_pet.body_data)
{
let pet = comp::Pet::new_from_database(
NonZeroU64::new(db_pet.database_id as u64).unwrap(),
);
let pet_stats = comp::Stats::new(db_pet.name.to_owned());
Some((pet, pet_body, pet_stats))
} else {
warn!(
"Failed to deserialize pet_id: {} for character_id {}",
db_pet.database_id, char_id
);
None
}
})
.collect::<Vec<(comp::Pet, comp::Body, comp::Stats)>>();
Ok((
convert_body_from_database(&body_data)?,
convert_body_from_database(&body_data.variant, &body_data.body_data)?,
convert_stats_from_database(character_data.alias),
convert_skill_set_from_database(&skill_data, &skill_group_data),
convert_inventory_from_database_items(
@ -214,6 +262,7 @@ pub fn load_character_data(
&loadout_items,
)?,
char_waypoint,
pets,
))
}
@ -269,7 +318,7 @@ pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> Chara
})?;
drop(stmt);
let char_body = convert_body_from_database(&db_body)?;
let char_body = convert_body_from_database(&db_body.variant, &db_body.body_data)?;
let loadout_container_id = get_pseudo_container_id(
connection,
@ -299,7 +348,7 @@ pub fn create_character(
) -> CharacterCreationResult {
check_character_limit(uuid, transactionn)?;
let (body, _stats, skill_set, inventory, waypoint) = persisted_components;
let (body, _stats, skill_set, inventory, waypoint, _) = persisted_components;
// Fetch new entity IDs for character, inventory and loadout
let mut new_entity_ids = get_new_entity_ids(transactionn, |next_id| next_id + 3)?;
@ -362,10 +411,11 @@ pub fn create_character(
VALUES (?1, ?2, ?3)",
)?;
let (body_variant, body_json) = convert_body_to_database_json(&body)?;
stmt.execute(&[
&character_id as &dyn ToSql,
&"humanoid".to_string(),
&convert_body_to_database_json(&body)?,
&body_variant.to_string(),
&body_json,
])?;
drop(stmt);
@ -548,6 +598,14 @@ pub fn delete_character(
)));
}
let pet_ids = get_pet_ids(char_id, transaction)?
.iter()
.map(|x| Value::from(*x))
.collect::<Vec<Value>>();
if !pet_ids.is_empty() {
delete_pets(transaction, char_id, Rc::new(pet_ids))?;
}
load_character_list(requesting_player_uuid, transaction)
}
@ -678,17 +736,142 @@ fn get_pseudo_container_id(
}
}
/// Stores new pets in the database, and removes pets from the database that the
/// player no longer has. Currently there are no actual updates to pet data
/// since we don't store any updatable data about pets in the database.
fn update_pets(
char_id: CharacterId,
pets: Vec<PetPersistenceData>,
transaction: &mut Transaction,
) -> Result<(), PersistenceError> {
debug!("Updating {} pets for character {}", pets.len(), char_id);
let db_pets = get_pet_ids(char_id, transaction)?;
if !db_pets.is_empty() {
let dead_pet_ids = Rc::new(
db_pets
.iter()
.filter(|pet_id| {
!pets.iter().any(|(pet, _, _)| {
pet.get_database_id()
.load()
.map_or(false, |x| x.get() == **pet_id as u64)
})
})
.map(|x| Value::from(*x))
.collect::<Vec<Value>>(),
);
if !dead_pet_ids.is_empty() {
delete_pets(transaction, char_id, dead_pet_ids)?;
}
}
for (pet, body, stats) in pets
.iter()
.filter(|(pet, _, _)| pet.get_database_id().load().is_none())
{
let pet_entity_id = get_new_entity_ids(transaction, |next_id| next_id + 1)?.start;
let (body_variant, body_json) = convert_body_to_database_json(body)?;
#[rustfmt::skip]
let mut stmt = transaction.prepare_cached("
INSERT
INTO body (
body_id,
variant,
body_data)
VALUES (?1, ?2, ?3)"
)?;
stmt.execute(&[
&pet_entity_id as &dyn ToSql,
&body_variant.to_string(),
&body_json,
])?;
#[rustfmt::skip]
let mut stmt = transaction.prepare_cached("
INSERT
INTO pet (
pet_id,
character_id,
name)
VALUES (?1, ?2, ?3)",
)?;
stmt.execute(&[&pet_entity_id as &dyn ToSql, &char_id, &stats.name])?;
drop(stmt);
pet.get_database_id()
.store(NonZeroU64::new(pet_entity_id as u64));
}
Ok(())
}
fn get_pet_ids(char_id: i64, transaction: &mut Transaction) -> Result<Vec<i64>, PersistenceError> {
#[rustfmt::skip]
let mut stmt = transaction.prepare_cached("
SELECT pet_id
FROM pet
WHERE character_id = ?1
")?;
#[allow(clippy::needless_question_mark)]
let db_pets = stmt
.query_map(&[&char_id], |row| Ok(row.get(0)?))?
.map(|x| x.unwrap())
.collect::<Vec<i64>>();
drop(stmt);
Ok(db_pets)
}
fn delete_pets(
transaction: &mut Transaction,
char_id: CharacterId,
pet_ids: Rc<Vec<Value>>,
) -> Result<(), PersistenceError> {
#[rustfmt::skip]
let mut stmt = transaction.prepare_cached("
DELETE
FROM pet
WHERE pet_id IN rarray(?1)"
)?;
let delete_count = stmt.execute(&[&pet_ids])?;
drop(stmt);
debug!("Deleted {} pets for character id {}", delete_count, char_id);
#[rustfmt::skip]
let mut stmt = transaction.prepare_cached("
DELETE
FROM body
WHERE body_id IN rarray(?1)"
)?;
let delete_count = stmt.execute(&[&pet_ids])?;
debug!(
"Deleted {} pet bodies for character id {}",
delete_count, char_id
);
Ok(())
}
pub fn update(
char_id: CharacterId,
char_skill_set: comp::SkillSet,
inventory: comp::Inventory,
pets: Vec<PetPersistenceData>,
char_waypoint: Option<comp::Waypoint>,
transaction: &mut Transaction,
) -> Result<(), PersistenceError> {
// Run pet persistence
update_pets(char_id, pets, transaction)?;
let pseudo_containers = get_pseudo_containers(transaction, char_id)?;
let mut upserts = Vec::new();
// First, get all the entity IDs for any new items, and identify which
// slots to upsert and which ones to delete.
get_new_entity_ids(transaction, |mut next_id| {

View File

@ -1,11 +1,11 @@
use crate::persistence::{
character::EntityId,
models::{Body, Character, Item, Skill, SkillGroup},
models::{Character, Item, Skill, SkillGroup},
};
use crate::persistence::{
error::PersistenceError,
json_models::{self, CharacterPosition, HumanoidBody},
json_models::{self, CharacterPosition, GenericBody, HumanoidBody},
};
use common::{
character::CharacterId,
@ -23,7 +23,7 @@ use common::{
use core::{convert::TryFrom, num::NonZeroU64};
use hashbrown::HashMap;
use lazy_static::lazy_static;
use std::{collections::VecDeque, sync::Arc};
use std::{collections::VecDeque, str::FromStr, sync::Arc};
use tracing::trace;
#[derive(Debug)]
@ -188,13 +188,33 @@ pub fn convert_items_to_database_items(
upserts
}
pub fn convert_body_to_database_json(body: &CompBody) -> Result<String, PersistenceError> {
let json_model = match body {
common::comp::Body::Humanoid(humanoid_body) => HumanoidBody::from(humanoid_body),
_ => unimplemented!("Only humanoid bodies are currently supported for persistence"),
};
serde_json::to_string(&json_model).map_err(PersistenceError::SerializationError)
pub fn convert_body_to_database_json(
comp_body: &CompBody,
) -> Result<(&str, String), PersistenceError> {
Ok(match comp_body {
common::comp::Body::Humanoid(body) => (
"humanoid",
serde_json::to_string(&HumanoidBody::from(body))?,
),
common::comp::Body::QuadrupedLow(body) => (
"quadruped_low",
serde_json::to_string(&GenericBody::from(body))?,
),
common::comp::Body::QuadrupedMedium(body) => (
"quadruped_medium",
serde_json::to_string(&GenericBody::from(body))?,
),
common::comp::Body::QuadrupedSmall(body) => (
"quadruped_small",
serde_json::to_string(&GenericBody::from(body))?,
),
_ => {
return Err(PersistenceError::ConversionError(format!(
"Unsupported body type for persistence: {:?}",
comp_body
)));
},
})
}
pub fn convert_waypoint_to_database_json(waypoint: Option<Waypoint>) -> Option<String> {
@ -388,10 +408,39 @@ pub fn convert_loadout_from_database_items(
Ok(loadout)
}
pub fn convert_body_from_database(body: &Body) -> Result<CompBody, PersistenceError> {
Ok(match body.variant.as_str() {
/// Generates the code to deserialize a specific body variant from JSON
macro_rules! deserialize_body {
($body_data:expr, $body_variant:tt, $body_type:tt) => {{
let json_model = serde_json::de::from_str::<GenericBody>($body_data)?;
CompBody::$body_variant(common::comp::$body_type::Body {
species: common::comp::$body_type::Species::from_str(&json_model.species)
.map_err(|_| {
PersistenceError::ConversionError(format!(
"Missing species: {}",
json_model.species
))
})?
.to_owned(),
body_type: common::comp::$body_type::BodyType::from_str(&json_model.body_type)
.map_err(|_| {
PersistenceError::ConversionError(format!(
"Missing body type: {}",
json_model.species
))
})?
.to_owned(),
})
}};
}
pub fn convert_body_from_database(
variant: &str,
body_data: &str,
) -> Result<CompBody, PersistenceError> {
Ok(match variant {
// The humanoid variant doesn't use the body_variant! macro as it is unique in having
// extra fields on its body struct
"humanoid" => {
let json_model = serde_json::de::from_str::<HumanoidBody>(&body.body_data)?;
let json_model = serde_json::de::from_str::<HumanoidBody>(body_data)?;
CompBody::Humanoid(common::comp::humanoid::Body {
species: common::comp::humanoid::ALL_SPECIES
.get(json_model.species as usize)
@ -420,10 +469,20 @@ pub fn convert_body_from_database(body: &Body) -> Result<CompBody, PersistenceEr
eye_color: json_model.eye_color,
})
},
"quadruped_low" => {
deserialize_body!(body_data, QuadrupedLow, quadruped_low)
},
"quadruped_medium" => {
deserialize_body!(body_data, QuadrupedMedium, quadruped_medium)
},
"quadruped_small" => {
deserialize_body!(body_data, QuadrupedSmall, quadruped_small)
},
_ => {
return Err(PersistenceError::ConversionError(
"Only humanoid bodies are supported for characters".to_string(),
));
return Err(PersistenceError::ConversionError(format!(
"{} is not a supported body type for deserialization",
variant.to_string()
)));
},
})
}

View File

@ -18,7 +18,14 @@ use std::{
};
use tracing::{debug, error, info, trace, warn};
pub type CharacterUpdateData = (comp::SkillSet, comp::Inventory, Option<comp::Waypoint>);
pub type CharacterUpdateData = (
comp::SkillSet,
comp::Inventory,
Vec<PetPersistenceData>,
Option<comp::Waypoint>,
);
pub type PetPersistenceData = (comp::Pet, comp::Body, comp::Stats);
#[allow(clippy::large_enum_variant)]
pub enum CharacterUpdaterEvent {
@ -258,15 +265,21 @@ impl CharacterUpdater {
CharacterId,
&'a comp::SkillSet,
&'a comp::Inventory,
Vec<PetPersistenceData>,
Option<&'a comp::Waypoint>,
),
>,
) {
let updates = updates
.map(|(character_id, skill_set, inventory, waypoint)| {
.map(|(character_id, skill_set, inventory, pets, waypoint)| {
(
character_id,
(skill_set.clone(), inventory.clone(), waypoint.cloned()),
(
skill_set.clone(),
inventory.clone(),
pets,
waypoint.cloned(),
),
)
})
.chain(self.pending_logout_updates.drain())
@ -308,8 +321,15 @@ fn execute_batch_update(
trace!("Transaction started for character batch update");
updates
.into_iter()
.try_for_each(|(character_id, (stats, inventory, waypoint))| {
super::character::update(character_id, stats, inventory, waypoint, &mut transaction)
.try_for_each(|(character_id, (stats, inventory, pets, waypoint))| {
super::character::update(
character_id,
stats,
inventory,
pets,
waypoint,
&mut transaction,
)
})?;
transaction.commit()?;

View File

@ -1,5 +1,6 @@
use common::comp;
use serde::{Deserialize, Serialize};
use std::string::ToString;
use vek::Vec3;
#[derive(Serialize, Deserialize)]
@ -31,6 +32,32 @@ impl From<&comp::humanoid::Body> for HumanoidBody {
}
}
/// A serializable model used to represent a generic Body. Since all variants
/// of Body except Humanoid (currently) have the same struct layout, a single
/// struct is used for persistence conversions.
#[derive(Serialize, Deserialize)]
pub struct GenericBody {
pub species: String,
pub body_type: String,
}
macro_rules! generic_body_from_impl {
($body_type:ty) => {
impl From<&$body_type> for GenericBody {
fn from(body: &$body_type) -> Self {
GenericBody {
species: body.species.to_string(),
body_type: body.body_type.to_string(),
}
}
}
};
}
generic_body_from_impl!(comp::quadruped_low::Body);
generic_body_from_impl!(comp::quadruped_medium::Body);
generic_body_from_impl!(comp::quadruped_small::Body);
#[derive(Serialize, Deserialize)]
pub struct CharacterPosition {
pub waypoint: Vec3<f32>,

View File

@ -8,6 +8,7 @@ pub mod error;
mod json_models;
mod models;
use crate::persistence::character_updater::PetPersistenceData;
use common::comp;
use refinery::Report;
use rusqlite::{Connection, OpenFlags};
@ -27,6 +28,7 @@ pub type PersistedComponents = (
comp::SkillSet,
comp::Inventory,
Option<comp::Waypoint>,
Vec<PetPersistenceData>,
);
// See: https://docs.rs/refinery/0.5.0/refinery/macro.embed_migrations.html

View File

@ -33,3 +33,10 @@ pub struct SkillGroup {
pub available_sp: i32,
pub earned_sp: i32,
}
pub struct Pet {
pub database_id: i64,
pub name: String,
pub body_variant: String,
pub body_data: String,
}

77
server/src/pet.rs Normal file
View File

@ -0,0 +1,77 @@
use crate::client::Client;
use common::{
comp::{anchor::Anchor, group::GroupManager, Agent, Alignment, Pet},
uid::Uid,
};
use common_net::msg::ServerGeneral;
use specs::{Entity, WorldExt};
use tracing::warn;
/// Restores a pet retrieved from the database on login, assigning it to its
/// owner
pub fn restore_pet(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet: Pet) {
tame_pet_internal(ecs, pet_entity, owner, Some(pet));
}
/// Tames a pet, adding to the owner's group and setting its alignment
pub fn tame_pet(ecs: &specs::World, pet_entity: Entity, owner: Entity) {
tame_pet_internal(ecs, pet_entity, owner, None);
}
fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet: Option<Pet>) {
let uids = ecs.read_storage::<Uid>();
let owner_uid = match uids.get(owner) {
Some(uid) => *uid,
None => return,
};
if let Some(Alignment::Owned(existing_owner_uid)) =
ecs.read_storage::<Alignment>().get(pet_entity)
{
if *existing_owner_uid != owner_uid {
warn!("Disallowing taming of pet already owned by another entity");
return;
}
}
let _ = ecs
.write_storage()
.insert(pet_entity, common::comp::Alignment::Owned(owner_uid));
// Anchor the pet to the player to prevent it de-spawning
// when its chunk is unloaded if its owner is still logged
// in
let _ = ecs
.write_storage()
.insert(pet_entity, Anchor::Entity(owner));
let _ = ecs
.write_storage()
.insert(pet_entity, pet.unwrap_or_default());
// TODO: Review whether we should be doing this or not, should the Agent always
// be overwritten when taming a pet?
let _ = ecs.write_storage().insert(pet_entity, Agent::default());
// Add to group system
let clients = ecs.read_storage::<Client>();
let mut group_manager = ecs.write_resource::<GroupManager>();
group_manager.new_pet(
pet_entity,
owner,
&mut ecs.write_storage(),
&ecs.entities(),
&ecs.read_storage(),
&uids,
&mut |entity, group_change| {
clients
.get(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.send_fallible(ServerGeneral::GroupUpdate(g)));
},
);
}

View File

@ -153,7 +153,7 @@ impl<'a> System<'a> for Sys {
_ => comp::Scale(1.0),
},
drop_item: None,
home_chunk: None,
anchor: None,
rtsim_entity,
projectile: None,
},

View File

@ -1,6 +1,7 @@
use crate::{
client::Client,
persistence::PersistedComponents,
pet::restore_pet,
presence::{Presence, RepositionOnChunkLoad},
settings::Settings,
sys::sentinel::DeletedEntities,
@ -12,7 +13,7 @@ use common::{
comp::{
self,
skills::{GeneralSkill, Skill},
Group, Inventory,
Group, Inventory, Poise,
},
effect::Effect,
resources::TimeOfDay,
@ -30,7 +31,7 @@ use specs::{
Join, WorldExt,
};
use std::time::Duration;
use tracing::warn;
use tracing::{trace, warn};
use vek::*;
pub trait StateExt {
@ -485,7 +486,7 @@ impl StateExt for State {
}
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) {
let (body, stats, skill_set, inventory, waypoint) = components;
let (body, stats, skill_set, inventory, waypoint, pets) = components;
if let Some(player_uid) = self.read_component_copied::<Uid>(entity) {
// Notify clients of a player list update
@ -535,6 +536,38 @@ impl StateExt for State {
self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero()));
self.write_component_ignore_entity_dead(entity, comp::ForceUpdate);
}
let player_pos = self.ecs().read_storage::<comp::Pos>().get(entity).copied();
if let Some(player_pos) = player_pos {
trace!(
"Loading {} pets for player at pos {:?}",
pets.len(),
player_pos
);
// This is the same as wild creatures naturally spawned in the world
const DEFAULT_PET_HEALTH_LEVEL: u16 = 0;
for (pet, body, stats) in pets {
let pet_entity = self
.create_npc(
player_pos,
stats,
comp::SkillSet::default(),
Some(comp::Health::new(body, DEFAULT_PET_HEALTH_LEVEL)),
Poise::new(body),
Inventory::new_empty(),
body,
)
.with(comp::Scale(1.0))
.with(comp::Vel(Vec3::new(0.0, 0.0, 0.0)))
.with(comp::MountState::Unmounted)
.build();
restore_pet(self.ecs(), pet_entity, entity, pet);
}
} else {
warn!("Player has no pos, cannot load {} pets", pets.len());
}
}
}

View File

@ -5,6 +5,7 @@ pub mod metrics;
pub mod msg;
pub mod object;
pub mod persistence;
pub mod pets;
pub mod sentinel;
pub mod subscription;
pub mod terrain;

View File

@ -5,7 +5,7 @@ pub mod ping;
pub mod register;
pub mod terrain;
use crate::client::Client;
use crate::{client::Client, sys::pets};
use common_ecs::{dispatch, System};
use serde::de::DeserializeOwned;
use specs::DispatcherBuilder;
@ -19,6 +19,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch::<ping::Sys>(dispatch_builder, &[&general::Sys::sys_name()]);
dispatch::<register::Sys>(dispatch_builder, &[]);
dispatch::<terrain::Sys>(dispatch_builder, &[]);
dispatch::<pets::Sys>(dispatch_builder, &[]);
}
/// handles all send msg and calls a handle fn

View File

@ -1,5 +1,11 @@
use crate::{persistence::character_updater, presence::Presence, sys::SysScheduler};
use common::comp::{Inventory, SkillSet, Waypoint};
use common::{
comp::{
pet::{is_tameable, Pet},
Alignment, Body, Inventory, SkillSet, Stats, Waypoint,
},
uid::Uid,
};
use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::PresenceKind;
use specs::{Join, ReadStorage, Write, WriteExpect};
@ -10,10 +16,15 @@ pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)]
type SystemData = (
ReadStorage<'a, Alignment>,
ReadStorage<'a, Body>,
ReadStorage<'a, Presence>,
ReadStorage<'a, SkillSet>,
ReadStorage<'a, Inventory>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Waypoint>,
ReadStorage<'a, Pet>,
ReadStorage<'a, Stats>,
WriteExpect<'a, character_updater::CharacterUpdater>,
Write<'a, SysScheduler<Self>>,
);
@ -25,10 +36,15 @@ impl<'a> System<'a> for Sys {
fn run(
_job: &mut Job<Self>,
(
alignments,
bodies,
presences,
player_skill_set,
player_inventories,
player_waypoint,
uids,
player_waypoints,
pets,
stats,
mut updater,
mut scheduler,
): Self::SystemData,
@ -39,13 +55,30 @@ impl<'a> System<'a> for Sys {
&presences,
&player_skill_set,
&player_inventories,
player_waypoint.maybe(),
&uids,
player_waypoints.maybe(),
)
.join()
.filter_map(
|(presence, skill_set, inventory, waypoint)| match presence.kind {
|(presence, skill_set, inventory, player_uid, waypoint)| match presence.kind
{
PresenceKind::Character(id) => {
Some((id, skill_set, inventory, waypoint))
let pets = (&alignments, &bodies, &stats, &pets)
.join()
.filter_map(|(alignment, body, stats, pet)| match alignment {
// Don't try to persist non-tameable pets (likely spawned
// using /spawn) since there isn't any code to handle
// persisting them
Alignment::Owned(ref pet_owner)
if pet_owner == player_uid && is_tameable(body) =>
{
Some(((*pet).clone(), *body, stats.clone()))
},
_ => None,
})
.collect();
Some((id, skill_set, inventory, pets, waypoint))
},
PresenceKind::Spectator => None,
},

75
server/src/sys/pets.rs Normal file
View File

@ -0,0 +1,75 @@
use common::{
comp::{Alignment, Pet, PhysicsState, Pos},
terrain::TerrainGrid,
uid::UidAllocator,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
saveload::MarkerAllocator, Entities, Entity, Join, Read, ReadExpect, ReadStorage, WriteStorage,
};
/// This system is responsible for handling pets
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
ReadExpect<'a, TerrainGrid>,
WriteStorage<'a, Pos>,
ReadStorage<'a, Alignment>,
ReadStorage<'a, Pet>,
ReadStorage<'a, PhysicsState>,
Read<'a, UidAllocator>,
);
const NAME: &'static str = "pets";
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
fn run(
_job: &mut Job<Self>,
(entities, terrain, mut positions, alignments, pets, physics, uid_allocator): Self::SystemData,
) {
const LOST_PET_DISTANCE_THRESHOLD: f32 = 200.0;
// Find pets that are too far away from their owner
let lost_pets: Vec<(Entity, Pos)> = (&entities, &positions, &alignments, &pets)
.join()
.filter_map(|(entity, pos, alignment, _)| match alignment {
Alignment::Owned(owner_uid) => Some((entity, pos, owner_uid)),
_ => None,
})
.filter_map(|(pet_entity, pet_pos, owner_uid)| {
uid_allocator
.retrieve_entity_internal(owner_uid.0)
.and_then(|owner_entity| {
match (positions.get(owner_entity), physics.get(owner_entity)) {
(Some(position), Some(physics)) => {
Some((pet_entity, position, physics, pet_pos))
},
_ => None,
}
})
})
.filter(|(_, owner_pos, owner_physics, pet_pos)| {
// Don't teleport pets to the player if they're in the air, nobody wants
// pets to go splat :(
owner_physics.on_ground.is_some()
&& owner_pos.0.distance_squared(pet_pos.0) > LOST_PET_DISTANCE_THRESHOLD.powi(2)
})
.map(|(entity, owner_pos, _, _)| (entity, *owner_pos))
.collect();
for (pet_entity, owner_pos) in lost_pets.iter() {
if let Some(mut pet_pos) = positions.get_mut(*pet_entity) {
// Move the pets to their owner's position
// TODO: Create a teleportation event to handle this instead of
// processing the entity position move here
pet_pos.0 = terrain
.find_space(owner_pos.0.map(|e| e.floor() as i32))
.map(|e| e as f32);
}
}
}
}

View File

@ -312,7 +312,7 @@ impl<'a> System<'a> for Sys {
body,
alignment,
scale: comp::Scale(scale),
home_chunk: Some(comp::HomeChunk(key)),
anchor: Some(comp::Anchor::Chunk(key)),
drop_item: entity.loot_drop,
rtsim_entity: None,
projectile: None,