mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'xvar/gotta-catch-em-all' into 'master'
Pet persistence See merge request veloren/veloren!2668
This commit is contained in:
commit
be0a2c0105
@ -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
20
common/src/comp/anchor.rs
Normal 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>;
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
}
|
@ -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
51
common/src/comp/pet.rs
Normal 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>;
|
||||
}
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 */ },
|
||||
|
@ -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<_>>()
|
||||
|
10
server/src/migrations/V43__pets.sql
Normal file
10
server/src/migrations/V43__pets.sql
Normal 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")
|
||||
);
|
||||
|
@ -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| {
|
||||
|
@ -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()
|
||||
)));
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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>,
|
||||
|
@ -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
|
||||
|
@ -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
77
server/src/pet.rs
Normal 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)));
|
||||
},
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
75
server/src/sys/pets.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user