mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Add mechanisms for loading achievement data, more work on a
migration/sync strategy for dealing with updates to achievements
This commit is contained in:
parent
707412edc0
commit
3ff5e105dd
@ -1,10 +0,0 @@
|
|||||||
[
|
|
||||||
(
|
|
||||||
title: "Collect 10 Apples",
|
|
||||||
achievement_type: Collect("common.items.apple", 10)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
title: "Collect 50 Apples",
|
|
||||||
achievement_type: Collect("common.items.apple", 50)
|
|
||||||
)
|
|
||||||
]
|
|
@ -992,6 +992,14 @@ impl Client {
|
|||||||
self.view_distance = Some(vd);
|
self.view_distance = Some(vd);
|
||||||
frontend_events.push(Event::SetViewDistance(vd));
|
frontend_events.push(Event::SetViewDistance(vd));
|
||||||
},
|
},
|
||||||
|
ServerMsg::AchievementDataUpdate(achievements) => {
|
||||||
|
// TODO should this happen? Can't you just save it
|
||||||
|
// against the entity on the server and it will et
|
||||||
|
// synced?
|
||||||
|
},
|
||||||
|
ServerMsg::AchievementDataError(error) => {
|
||||||
|
// TODO handle somehow
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
use crate::comp::item::{Item, ItemKind};
|
|
||||||
|
|
||||||
// TODO: Kill(Race, amount)
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
pub enum AchievementType {
|
|
||||||
Collect(String, i32),
|
|
||||||
ReachLevel(i32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
pub struct AchievementItem {
|
|
||||||
pub title: String,
|
|
||||||
pub achievement_type: AchievementType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AchievementItem {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
title: String::new(),
|
|
||||||
achievement_type: AchievementType::ReachLevel(9999),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
99
common/src/comp/achievement.rs
Normal file
99
common/src/comp/achievement.rs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use crate::{
|
||||||
|
comp::{self, inventory::item::Consumable, InventoryUpdateEvent},
|
||||||
|
event::ServerEvent,
|
||||||
|
};
|
||||||
|
use specs::{Component, FlaggedStorage};
|
||||||
|
use specs_idvs::IDVStorage;
|
||||||
|
|
||||||
|
pub enum AchievementCategory {
|
||||||
|
CollectConsumable,
|
||||||
|
ReachLevel,
|
||||||
|
KillHumanoidSpecies,
|
||||||
|
KillBodyType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potential additions
|
||||||
|
// - ReachCoordinate
|
||||||
|
// - CollectCurrency(amount)
|
||||||
|
// - KillPlayers(amount)
|
||||||
|
// - OpenChests
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum AchievementType {
|
||||||
|
CollectConsumable(Consumable, i32),
|
||||||
|
ReachLevel(i32),
|
||||||
|
KillHumanoidSpecies(comp::body::humanoid::Species, i32),
|
||||||
|
KillBodyType(comp::Body, i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AchievementType> for AchievementCategory {
|
||||||
|
fn from(achievement_type: AchievementType) -> AchievementCategory {
|
||||||
|
match achievement_type {
|
||||||
|
AchievementType::CollectConsumable(_, _) => AchievementCategory::CollectConsumable,
|
||||||
|
AchievementType::ReachLevel(_) => AchievementCategory::ReachLevel,
|
||||||
|
AchievementType::KillHumanoidSpecies(_, _) => AchievementCategory::KillHumanoidSpecies,
|
||||||
|
AchievementType::KillBodyType(_, _) => AchievementCategory::KillBodyType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The representation of an achievement that is declared in .ron config.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct AchievementItem {
|
||||||
|
pub title: String,
|
||||||
|
pub achievement_type: AchievementType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO remove this, it's a confusing state
|
||||||
|
impl Default for AchievementItem {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
title: String::new(),
|
||||||
|
achievement_type: AchievementType::ReachLevel(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AchievementItem {
|
||||||
|
pub fn matches_event(&self, event: InventoryUpdateEvent) -> bool {
|
||||||
|
match event {
|
||||||
|
InventoryUpdateEvent::Collected(_item) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The complete representation of an achievement that has been
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Achievement {
|
||||||
|
pub id: i32,
|
||||||
|
pub item: AchievementItem,
|
||||||
|
pub completed: bool,
|
||||||
|
pub progress: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl Achievement {
|
||||||
|
// pub fn incr(&self, event: InventoryUpdateEvent) -> Option<bool> {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct AchievementList(Vec<Achievement>);
|
||||||
|
|
||||||
|
impl Default for AchievementList {
|
||||||
|
fn default() -> AchievementList { AchievementList(Vec::new()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AchievementList {
|
||||||
|
/// Process a single achievement item, inrementing or doing whataver it does
|
||||||
|
/// to indicate it's one step closer to cmpletion
|
||||||
|
pub fn process(&self, item: &AchievementItem) -> Option<bool> {
|
||||||
|
// if self.0.iter().
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for AchievementList {
|
||||||
|
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
mod ability;
|
mod ability;
|
||||||
|
mod achievement;
|
||||||
mod admin;
|
mod admin;
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
mod body;
|
mod body;
|
||||||
@ -20,6 +21,7 @@ mod visual;
|
|||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
|
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
|
||||||
|
pub use achievement::{Achievement, AchievementItem, AchievementList, AchievementType};
|
||||||
pub use admin::{Admin, AdminList};
|
pub use admin::{Admin, AdminList};
|
||||||
pub use agent::{Agent, Alignment};
|
pub use agent::{Agent, Alignment};
|
||||||
pub use body::{
|
pub use body::{
|
||||||
|
@ -83,6 +83,10 @@ pub enum ServerEvent {
|
|||||||
Chat(comp::ChatMsg),
|
Chat(comp::ChatMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum AchievementEvent {
|
||||||
|
CollectedItem { entity: EcsEntity, item: comp::Item },
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EventBus<E> {
|
pub struct EventBus<E> {
|
||||||
queue: Mutex<VecDeque<E>>,
|
queue: Mutex<VecDeque<E>>,
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
option_zip
|
option_zip
|
||||||
)]
|
)]
|
||||||
|
|
||||||
pub mod achievement;
|
#[macro_use] extern crate serde_derive;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod astar;
|
pub mod astar;
|
||||||
pub mod character;
|
pub mod character;
|
||||||
|
@ -59,6 +59,11 @@ pub enum ServerMsg {
|
|||||||
time_of_day: state::TimeOfDay,
|
time_of_day: state::TimeOfDay,
|
||||||
world_map: (Vec2<u32>, Vec<u32>),
|
world_map: (Vec2<u32>, Vec<u32>),
|
||||||
},
|
},
|
||||||
|
/// A list of achievements which the character has fully or partially
|
||||||
|
/// completed
|
||||||
|
AchievementDataUpdate(Vec<comp::Achievement>),
|
||||||
|
/// An error occurred while loading character achievements
|
||||||
|
AchievementDataError(String),
|
||||||
/// An error occurred while loading character data
|
/// An error occurred while loading character data
|
||||||
CharacterDataLoadError(String),
|
CharacterDataLoadError(String),
|
||||||
/// A list of characters belonging to the a authenticated player was sent
|
/// A list of characters belonging to the a authenticated player was sent
|
||||||
|
10
server/data/achievements.ron
Normal file
10
server/data/achievements.ron
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
(
|
||||||
|
title: "Collect 10 Apples",
|
||||||
|
achievement_type: CollectConsumable(Apple, 10)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
title: "Collect 50 Apples",
|
||||||
|
achievement_type: CollectConsumable(Apple, 50)
|
||||||
|
),
|
||||||
|
]
|
@ -1,10 +0,0 @@
|
|||||||
[
|
|
||||||
(
|
|
||||||
title: "Collect 10 Apples",
|
|
||||||
achievement_type: Collect("common.items.apple", 10)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
title: "Collect 50 Apples",
|
|
||||||
achievement_type: Collect("common.items.apple", 50)
|
|
||||||
)
|
|
||||||
]
|
|
@ -5,6 +5,7 @@ use common::{
|
|||||||
slot::{self, Slot},
|
slot::{self, Slot},
|
||||||
Pos, MAX_PICKUP_RANGE_SQR,
|
Pos, MAX_PICKUP_RANGE_SQR,
|
||||||
},
|
},
|
||||||
|
event::{AchievementEvent, EventBus},
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::{Uid, WorldSyncExt},
|
||||||
terrain::block::Block,
|
terrain::block::Block,
|
||||||
vol::{ReadVol, Vox},
|
vol::{ReadVol, Vox},
|
||||||
@ -82,9 +83,18 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
// player's inventory but also left on the ground
|
// player's inventory but also left on the ground
|
||||||
panic!("Failed to delete picked up item entity: {:?}", err);
|
panic!("Failed to delete picked up item entity: {:?}", err);
|
||||||
}
|
}
|
||||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(
|
|
||||||
picked_up_item.unwrap(),
|
let item = picked_up_item.unwrap();
|
||||||
))
|
|
||||||
|
state
|
||||||
|
.ecs()
|
||||||
|
.read_resource::<EventBus<AchievementEvent>>()
|
||||||
|
.emit_now(AchievementEvent::CollectedItem {
|
||||||
|
entity,
|
||||||
|
item: item.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(item))
|
||||||
} else {
|
} else {
|
||||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::CollectFailed)
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::CollectFailed)
|
||||||
};
|
};
|
||||||
@ -111,8 +121,17 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
} else if block.is_collectible()
|
} else if block.is_collectible()
|
||||||
&& state.try_set_block(pos, Block::empty()).is_some()
|
&& state.try_set_block(pos, Block::empty()).is_some()
|
||||||
{
|
{
|
||||||
comp::Item::try_reclaim_from_block(block)
|
comp::Item::try_reclaim_from_block(block).map(|item| {
|
||||||
.map(|item| state.give_item(entity, item));
|
state
|
||||||
|
.ecs()
|
||||||
|
.read_resource::<EventBus<AchievementEvent>>()
|
||||||
|
.emit_now(AchievementEvent::CollectedItem {
|
||||||
|
entity,
|
||||||
|
item: item.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
state.give_item(entity, item);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -30,7 +30,7 @@ use crate::{
|
|||||||
use common::{
|
use common::{
|
||||||
cmd::ChatCommand,
|
cmd::ChatCommand,
|
||||||
comp::{self, ChatType},
|
comp::{self, ChatType},
|
||||||
event::{EventBus, ServerEvent},
|
event::{AchievementEvent, EventBus, ServerEvent},
|
||||||
msg::{ClientState, ServerInfo, ServerMsg},
|
msg::{ClientState, ServerInfo, ServerMsg},
|
||||||
state::{State, TimeOfDay},
|
state::{State, TimeOfDay},
|
||||||
sync::WorldSyncExt,
|
sync::WorldSyncExt,
|
||||||
@ -42,7 +42,10 @@ use futures_timer::Delay;
|
|||||||
use futures_util::{select, FutureExt};
|
use futures_util::{select, FutureExt};
|
||||||
use metrics::{ServerMetrics, TickMetrics};
|
use metrics::{ServerMetrics, TickMetrics};
|
||||||
use network::{Address, Network, Pid};
|
use network::{Address, Network, Pid};
|
||||||
use persistence::character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater};
|
use persistence::{
|
||||||
|
achievement::{AchievementLoader, AchievementLoaderResponse, AvailableAchievements},
|
||||||
|
character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater},
|
||||||
|
};
|
||||||
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
|
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
|
||||||
use std::{
|
use std::{
|
||||||
i32,
|
i32,
|
||||||
@ -95,13 +98,24 @@ impl Server {
|
|||||||
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
|
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
state.ecs_mut().insert(settings.clone());
|
state.ecs_mut().insert(settings.clone());
|
||||||
|
|
||||||
|
// Event Emitters
|
||||||
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
|
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
|
||||||
|
state
|
||||||
|
.ecs_mut()
|
||||||
|
.insert(EventBus::<AchievementEvent>::default());
|
||||||
|
|
||||||
state.ecs_mut().insert(AuthProvider::new(
|
state.ecs_mut().insert(AuthProvider::new(
|
||||||
settings.auth_server_address.clone(),
|
settings.auth_server_address.clone(),
|
||||||
settings.whitelist.clone(),
|
settings.whitelist.clone(),
|
||||||
));
|
));
|
||||||
state.ecs_mut().insert(Tick(0));
|
state.ecs_mut().insert(Tick(0));
|
||||||
state.ecs_mut().insert(ChunkGenerator::new());
|
state.ecs_mut().insert(ChunkGenerator::new());
|
||||||
|
state
|
||||||
|
.ecs_mut()
|
||||||
|
.insert(comp::AdminList(settings.admins.clone()));
|
||||||
|
|
||||||
|
// Persistence interaction
|
||||||
state
|
state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
.insert(CharacterUpdater::new(settings.persistence_db_dir.clone()));
|
.insert(CharacterUpdater::new(settings.persistence_db_dir.clone()));
|
||||||
@ -110,12 +124,7 @@ impl Server {
|
|||||||
.insert(CharacterLoader::new(settings.persistence_db_dir.clone()));
|
.insert(CharacterLoader::new(settings.persistence_db_dir.clone()));
|
||||||
state
|
state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
.insert(persistence::character::CharacterUpdater::new(
|
.insert(AchievementLoader::new(settings.persistence_db_dir.clone()));
|
||||||
settings.persistence_db_dir.clone(),
|
|
||||||
));
|
|
||||||
state
|
|
||||||
.ecs_mut()
|
|
||||||
.insert(comp::AdminList(settings.admins.clone()));
|
|
||||||
|
|
||||||
// System timers for performance monitoring
|
// System timers for performance monitoring
|
||||||
state.ecs_mut().insert(sys::EntitySyncTimer::default());
|
state.ecs_mut().insert(sys::EntitySyncTimer::default());
|
||||||
@ -243,6 +252,25 @@ impl Server {
|
|||||||
thread_pool.execute(f);
|
thread_pool.execute(f);
|
||||||
block_on(network.listen(Address::Tcp(settings.gameserver_address)))?;
|
block_on(network.listen(Address::Tcp(settings.gameserver_address)))?;
|
||||||
|
|
||||||
|
// Run pending DB migrations (if any)
|
||||||
|
debug!("Running DB migrations...");
|
||||||
|
|
||||||
|
if let Some(e) = persistence::run_migrations(&settings.persistence_db_dir).err() {
|
||||||
|
info!(?e, "Migration error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync and Load Achievement Data
|
||||||
|
debug!("Syncing Achievement data...");
|
||||||
|
|
||||||
|
match persistence::achievement::sync(&settings.persistence_db_dir) {
|
||||||
|
Ok(achievements) => {
|
||||||
|
info!("Achievement data loaded...");
|
||||||
|
info!(?achievements, "data:");
|
||||||
|
state.ecs_mut().insert(AvailableAchievements(achievements));
|
||||||
|
},
|
||||||
|
Err(e) => error!(?e, "Achievement data migration error"),
|
||||||
|
}
|
||||||
|
|
||||||
let this = Self {
|
let this = Self {
|
||||||
state,
|
state,
|
||||||
world: Arc::new(world),
|
world: Arc::new(world),
|
||||||
@ -256,22 +284,6 @@ impl Server {
|
|||||||
tick_metrics,
|
tick_metrics,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run pending DB migrations (if any)
|
|
||||||
debug!("Running DB migrations...");
|
|
||||||
|
|
||||||
if let Some(e) = persistence::run_migrations(&settings.persistence_db_dir).err() {
|
|
||||||
info!(?e, "Migration error");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync Achievement Data
|
|
||||||
debug!("Syncing Achievement data...");
|
|
||||||
|
|
||||||
if let Some(e) =
|
|
||||||
persistence::achievement::sync(&this.server_settings.persistence_db_dir).err()
|
|
||||||
{
|
|
||||||
info!(?e, "Achievement data migration error");
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(?settings, "created veloren server with");
|
debug!(?settings, "created veloren server with");
|
||||||
|
|
||||||
let git_hash = *common::util::GIT_HASH;
|
let git_hash = *common::util::GIT_HASH;
|
||||||
@ -419,6 +431,20 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let achievement_events = self
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.read_resource::<EventBus<AchievementEvent>>()
|
||||||
|
.recv_all();
|
||||||
|
|
||||||
|
for event in achievement_events {
|
||||||
|
match event {
|
||||||
|
AchievementEvent::CollectedItem { entity, item } => {
|
||||||
|
info!(?item, "Achievement event: item");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 7 Persistence updates
|
// 7 Persistence updates
|
||||||
let before_persistence_updates = Instant::now();
|
let before_persistence_updates = Instant::now();
|
||||||
|
|
||||||
@ -467,6 +493,22 @@ impl Server {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.state
|
||||||
|
.ecs()
|
||||||
|
.read_resource::<persistence::achievement::AchievementLoader>()
|
||||||
|
.messages()
|
||||||
|
.for_each(|query_result| match query_result {
|
||||||
|
AchievementLoaderResponse::LoadCharacterAchievementListResponse((
|
||||||
|
entity,
|
||||||
|
result,
|
||||||
|
)) => match result {
|
||||||
|
Ok(achievement_data) => self
|
||||||
|
.notify_client(entity, ServerMsg::AchievementDataUpdate(achievement_data)),
|
||||||
|
Err(error) => self
|
||||||
|
.notify_client(entity, ServerMsg::AchievementDataError(error.to_string())),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let end_of_server_tick = Instant::now();
|
let end_of_server_tick = Instant::now();
|
||||||
|
|
||||||
// 8) Update Metrics
|
// 8) Update Metrics
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS "character_achievement";
|
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "character_achievement" (
|
||||||
|
character_id INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
achievement_id INTEGER NOT NULL,
|
||||||
|
completed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
progress INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(achievement_id) REFERENCES "achievement"(id) ON DELETE CASCADE
|
||||||
|
);
|
@ -3,10 +3,14 @@ extern crate diesel;
|
|||||||
use super::{
|
use super::{
|
||||||
error::Error,
|
error::Error,
|
||||||
establish_connection,
|
establish_connection,
|
||||||
models::{Achievement as AchievementModel, DataMigration, NewAchievement, NewDataMigration},
|
models::{
|
||||||
|
Achievement as AchievementModel, CharacterAchievement, DataMigration, NewAchievement,
|
||||||
|
NewDataMigration,
|
||||||
|
},
|
||||||
schema,
|
schema,
|
||||||
};
|
};
|
||||||
use common::achievement::*;
|
use common::comp::{self, AchievementItem};
|
||||||
|
use crossbeam::{channel, channel::TryIter};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
result::{DatabaseErrorKind, Error as DieselError},
|
result::{DatabaseErrorKind, Error as DieselError},
|
||||||
@ -15,37 +19,144 @@ use std::{
|
|||||||
collections::hash_map::DefaultHasher,
|
collections::hash_map::DefaultHasher,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
};
|
};
|
||||||
use tracing::{info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
pub fn sync(db_dir: &str) -> Result<(), Error> {
|
/// Available database operations when modifying a player's characetr list
|
||||||
|
enum AchievementLoaderRequestKind {
|
||||||
|
LoadCharacterAchievementList {
|
||||||
|
entity: specs::Entity,
|
||||||
|
character_id: i32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadCharacterAchievementsResult = (specs::Entity, Result<Vec<comp::Achievement>, Error>);
|
||||||
|
|
||||||
|
/// Wrapper for results
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AchievementLoaderResponse {
|
||||||
|
LoadCharacterAchievementListResponse(LoadCharacterAchievementsResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AchievementLoader {
|
||||||
|
update_rx: Option<channel::Receiver<AchievementLoaderResponse>>,
|
||||||
|
update_tx: Option<channel::Sender<AchievementLoaderRequestKind>>,
|
||||||
|
handle: Option<std::thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AchievementLoader {
|
||||||
|
pub fn new(db_dir: String) -> Self {
|
||||||
|
let (update_tx, internal_rx) = channel::unbounded::<AchievementLoaderRequestKind>();
|
||||||
|
let (internal_tx, update_rx) = channel::unbounded::<AchievementLoaderResponse>();
|
||||||
|
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
while let Ok(request) = internal_rx.recv() {
|
||||||
|
if let Err(e) = internal_tx.send(match request {
|
||||||
|
AchievementLoaderRequestKind::LoadCharacterAchievementList {
|
||||||
|
entity,
|
||||||
|
character_id,
|
||||||
|
} => AchievementLoaderResponse::LoadCharacterAchievementListResponse((
|
||||||
|
entity,
|
||||||
|
load_character_achievement_list(character_id, &db_dir),
|
||||||
|
)),
|
||||||
|
}) {
|
||||||
|
error!(?e, "Could not send send persistence request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
update_tx: Some(update_tx),
|
||||||
|
update_rx: Some(update_rx),
|
||||||
|
handle: Some(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_character_achievement_list(&self, entity: specs::Entity, character_id: i32) {
|
||||||
|
if let Err(e) = self.update_tx.as_ref().unwrap().send(
|
||||||
|
AchievementLoaderRequestKind::LoadCharacterAchievementList {
|
||||||
|
entity,
|
||||||
|
character_id,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
error!(?e, "Could not send character achievement load request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a non-blocking iterator over AchievementLoaderResponse messages
|
||||||
|
pub fn messages(&self) -> TryIter<AchievementLoaderResponse> {
|
||||||
|
self.update_rx.as_ref().unwrap().try_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AchievementLoader {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
drop(self.update_tx.take());
|
||||||
|
if let Err(e) = self.handle.take().unwrap().join() {
|
||||||
|
error!(?e, "Error from joining character loader thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_character_achievement_list(
|
||||||
|
character_id: i32,
|
||||||
|
db_dir: &str,
|
||||||
|
) -> Result<Vec<comp::Achievement>, Error> {
|
||||||
|
let character_achievements = schema::character_achievement::dsl::character_achievement
|
||||||
|
.filter(schema::character_achievement::character_id.eq(character_id))
|
||||||
|
.load::<CharacterAchievement>(&establish_connection(db_dir))?;
|
||||||
|
|
||||||
|
Ok(character_achievements
|
||||||
|
.iter()
|
||||||
|
.map(comp::Achievement::from)
|
||||||
|
.collect::<Vec<comp::Achievement>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
|
||||||
let achievements = load_data();
|
let achievements = load_data();
|
||||||
let connection = establish_connection(db_dir);
|
let connection = establish_connection(db_dir);
|
||||||
|
|
||||||
// Get a hash of the Vec<Achievement> and compare to the migration table
|
// Use the full dataset for checks
|
||||||
|
let persisted_achievements =
|
||||||
|
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
|
||||||
|
|
||||||
|
// Get a hash of the Vec<Achievement> to compare to the migration table
|
||||||
let result = schema::data_migration::dsl::data_migration
|
let result = schema::data_migration::dsl::data_migration
|
||||||
.filter(schema::data_migration::title.eq(String::from("achievements")))
|
.filter(schema::data_migration::title.eq(String::from("achievements")))
|
||||||
.load::<DataMigration>(&connection)?;
|
.first::<DataMigration>(&connection);
|
||||||
|
|
||||||
// First check whether the table has an entry for this data type
|
info!(?result, "result: ");
|
||||||
if result.is_empty() {
|
|
||||||
let migration = NewDataMigration {
|
|
||||||
title: "achievements",
|
|
||||||
checksum: &hash(&achievements).to_string(),
|
|
||||||
last_run: chrono::Utc::now().naive_utc(),
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(schema::data_migration::table)
|
let should_run = match result {
|
||||||
.values(&migration)
|
Ok(migration_entry) => {
|
||||||
.execute(&connection)?;
|
let checksum = &migration_entry.checksum;
|
||||||
}
|
info!(?checksum, "checksum: ");
|
||||||
|
|
||||||
// Also check checksum. Bail if same, continue if changed
|
migration_entry.checksum != hash(&achievements).to_string()
|
||||||
if result.is_empty() {
|
}
|
||||||
info!("Achievements need updating...");
|
Err(diesel::result::Error::NotFound) => {
|
||||||
|
let migration = NewDataMigration {
|
||||||
|
title: "achievements",
|
||||||
|
checksum: &hash(&achievements).to_string(),
|
||||||
|
last_run: chrono::Utc::now().naive_utc(),
|
||||||
|
};
|
||||||
|
|
||||||
// Use the full dataset for checks
|
diesel::insert_into(schema::data_migration::table)
|
||||||
let persisted_achievements =
|
.values(&migration)
|
||||||
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
|
.execute(&connection)?;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
error!("Failed to run migrations"); // TODO better error messaging
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (should_run || persisted_achievements.is_empty()) && !achievements.is_empty() {
|
||||||
|
let items = &achievements;
|
||||||
|
|
||||||
|
info!(?items, "Achievements need updating...");
|
||||||
|
|
||||||
// Make use of the unique constraint in the DB, attempt to insert, on unique
|
// Make use of the unique constraint in the DB, attempt to insert, on unique
|
||||||
// failure check if it needs updating and do so if necessary
|
// failure check if it needs updating and do so if necessary
|
||||||
@ -78,7 +189,7 @@ pub fn sync(db_dir: &str) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => return Err(Error::DatabaseError(error)),
|
_ => return Err(Error::DatabaseError(error)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,21 +207,23 @@ pub fn sync(db_dir: &str) -> Result<(), Error> {
|
|||||||
info!("No achievement updates required");
|
info!("No achievement updates required");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_data() -> Vec<AchievementItem> {
|
fn load_data() -> Vec<AchievementItem> {
|
||||||
if let Ok(path) = std::fs::canonicalize("../data/achievements.ron") {
|
let manifest_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "data/achievements.ron");
|
||||||
let path = std::path::PathBuf::from(path);
|
|
||||||
|
|
||||||
info!(?path, "Path: ");
|
info!(?manifest_dir, "manifest_dir:");
|
||||||
|
|
||||||
match std::fs::File::open(path) {
|
match std::fs::canonicalize(manifest_dir) {
|
||||||
|
Ok(path) => match std::fs::File::open(path) {
|
||||||
Ok(file) => ron::de::from_reader(file).expect("Error parsing achievement data"),
|
Ok(file) => ron::de::from_reader(file).expect("Error parsing achievement data"),
|
||||||
Err(error) => panic!(error.to_string()),
|
Err(error) => panic!(error.to_string()),
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
warn!(?error, "Unable to find achievement data file");
|
||||||
|
Vec::new()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,3 +232,10 @@ pub fn hash<T: Hash>(t: &T) -> u64 {
|
|||||||
t.hash(&mut s);
|
t.hash(&mut s);
|
||||||
s.finish()
|
s.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds a list of achievements available to players.
|
||||||
|
///
|
||||||
|
/// This acts as the reference for checks on achievements, and holds id's as
|
||||||
|
/// well as details of achievement
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AvailableAchievements(pub Vec<AchievementModel>);
|
||||||
|
@ -2,7 +2,10 @@ extern crate serde_json;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
achievement::hash,
|
achievement::hash,
|
||||||
schema::{achievement, body, character, data_migration, inventory, loadout, stats},
|
schema::{
|
||||||
|
achievement, body, character, character_achievement, data_migration, inventory, loadout,
|
||||||
|
stats,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use crate::comp;
|
use crate::comp;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
@ -377,7 +380,7 @@ pub struct NewDataMigration<'a> {
|
|||||||
/// the starter sword set as the main weapon
|
/// the starter sword set as the main weapon
|
||||||
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
|
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
|
||||||
#[sql_type = "Text"]
|
#[sql_type = "Text"]
|
||||||
pub struct AchievementData(AchievementItem);
|
pub struct AchievementData(comp::AchievementItem);
|
||||||
|
|
||||||
impl<DB> diesel::deserialize::FromSql<Text, DB> for AchievementData
|
impl<DB> diesel::deserialize::FromSql<Text, DB> for AchievementData
|
||||||
where
|
where
|
||||||
@ -394,7 +397,7 @@ where
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(?e, "Failed to deserialise achevement data");
|
warn!(?e, "Failed to deserialise achevement data");
|
||||||
|
|
||||||
Ok(Self(AchievementItem::default()))
|
Ok(Self(comp::AchievementItem::default()))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,6 +424,10 @@ pub struct Achievement {
|
|||||||
pub details: AchievementData,
|
pub details: AchievementData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&AchievementData> for comp::AchievementItem {
|
||||||
|
fn from(data: &AchievementData) -> comp::AchievementItem { data.0.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Insertable, PartialEq, Debug)]
|
#[derive(Insertable, PartialEq, Debug)]
|
||||||
#[table_name = "achievement"]
|
#[table_name = "achievement"]
|
||||||
pub struct NewAchievement {
|
pub struct NewAchievement {
|
||||||
@ -428,8 +435,8 @@ pub struct NewAchievement {
|
|||||||
pub details: AchievementData,
|
pub details: AchievementData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&AchievementItem> for NewAchievement {
|
impl From<&comp::AchievementItem> for NewAchievement {
|
||||||
fn from(item: &AchievementItem) -> Self {
|
fn from(item: &comp::AchievementItem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
checksum: hash(item).to_string(),
|
checksum: hash(item).to_string(),
|
||||||
details: AchievementData(item.clone()),
|
details: AchievementData(item.clone()),
|
||||||
@ -437,6 +444,34 @@ impl From<&AchievementItem> for NewAchievement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Character Achievements belong to characters
|
||||||
|
#[derive(Queryable, Debug, Identifiable)]
|
||||||
|
#[primary_key(character_id)]
|
||||||
|
#[table_name = "character_achievement"]
|
||||||
|
pub struct CharacterAchievement {
|
||||||
|
pub character_id: i32,
|
||||||
|
pub achievement_id: i32,
|
||||||
|
pub completed: i32,
|
||||||
|
pub progress: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&CharacterAchievement> for comp::Achievement {
|
||||||
|
fn from(_achievement: &CharacterAchievement) -> comp::Achievement {
|
||||||
|
comp::Achievement::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Achievement> for comp::Achievement {
|
||||||
|
fn from(achievement: Achievement) -> comp::Achievement {
|
||||||
|
comp::Achievement {
|
||||||
|
id: achievement.id,
|
||||||
|
item: comp::AchievementItem::from(&achievement.details),
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -30,6 +30,15 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
character_achievement (character_id) {
|
||||||
|
character_id -> Integer,
|
||||||
|
achievement_id -> Integer,
|
||||||
|
completed -> Integer,
|
||||||
|
progress -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
data_migration (id) {
|
data_migration (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
@ -67,8 +76,18 @@ table! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
joinable!(body -> character (character_id));
|
joinable!(body -> character (character_id));
|
||||||
|
joinable!(character_achievement -> achievement (achievement_id));
|
||||||
|
joinable!(character_achievement -> character (character_id));
|
||||||
joinable!(inventory -> character (character_id));
|
joinable!(inventory -> character (character_id));
|
||||||
joinable!(loadout -> character (character_id));
|
joinable!(loadout -> character (character_id));
|
||||||
joinable!(stats -> character (character_id));
|
joinable!(stats -> character (character_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(achievement, body, character, inventory, loadout, stats);
|
allow_tables_to_appear_in_same_query!(
|
||||||
|
achievement,
|
||||||
|
body,
|
||||||
|
character,
|
||||||
|
character_achievement,
|
||||||
|
inventory,
|
||||||
|
loadout,
|
||||||
|
stats,
|
||||||
|
);
|
||||||
|
@ -175,6 +175,7 @@ impl StateExt for State {
|
|||||||
entity,
|
entity,
|
||||||
comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()),
|
comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()),
|
||||||
);
|
);
|
||||||
|
self.write_component(entity, comp::AchievementList::default());
|
||||||
|
|
||||||
// Set the character id for the player
|
// Set the character id for the player
|
||||||
// TODO this results in a warning in the console: "Error modifying synced
|
// TODO this results in a warning in the console: "Error modifying synced
|
||||||
|
56
server/src/sys/achievement.rs
Normal file
56
server/src/sys/achievement.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use crate::persistence::achievement::AvailableAchievements;
|
||||||
|
|
||||||
|
use common::comp::{AchievementItem, AchievementList, InventoryUpdate, Player};
|
||||||
|
use specs::{Entities, Join, ReadExpect, ReadStorage, System};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub struct Sys;
|
||||||
|
|
||||||
|
impl<'a> System<'a> for Sys {
|
||||||
|
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
|
||||||
|
type SystemData = (
|
||||||
|
Entities<'a>,
|
||||||
|
ReadStorage<'a, Player>,
|
||||||
|
ReadStorage<'a, AchievementList>,
|
||||||
|
ReadStorage<'a, InventoryUpdate>,
|
||||||
|
ReadExpect<'a, AvailableAchievements>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&mut self,
|
||||||
|
(entities, players, achievement_lists, inventory_updates, available_achievements): Self::SystemData,
|
||||||
|
) {
|
||||||
|
for (_entity, _player, ach_list, inv_event) in
|
||||||
|
(&entities, &players, &achievement_lists, &inventory_updates).join()
|
||||||
|
{
|
||||||
|
(available_achievements.0)
|
||||||
|
.iter()
|
||||||
|
.for_each(|achievement_item| {
|
||||||
|
let ach_item = AchievementItem::from(&achievement_item.details);
|
||||||
|
// pass the event to each achievement
|
||||||
|
// achievement checks if the event matches what it is looking for
|
||||||
|
if ach_item.matches_event(inv_event.event()) {
|
||||||
|
if let Some(event) = ach_list.process(&ach_item) {
|
||||||
|
info!(?event, "Achievement event");
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's a match, pass it to the characters
|
||||||
|
// achievement list
|
||||||
|
// get a result from the call to the players achievement
|
||||||
|
// list
|
||||||
|
// - It checks for an entry No entry = append and
|
||||||
|
// increment Entry = Increment
|
||||||
|
// inrement(achievement_id, ?amount)
|
||||||
|
// if let Some(results) =
|
||||||
|
// _player_character_list.process(achievement_item) {
|
||||||
|
//
|
||||||
|
// - if its a completion result, dispatch an event which
|
||||||
|
// notifies the client
|
||||||
|
// server_events.dispatch(ServerEvent::
|
||||||
|
// AchievementUpdate(TheAchievementInfo))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
use super::SysTimer;
|
use super::SysTimer;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth_provider::AuthProvider, client::Client, persistence::character::CharacterLoader,
|
auth_provider::AuthProvider,
|
||||||
|
client::Client,
|
||||||
|
persistence::{achievement::AchievementLoader, character::CharacterLoader},
|
||||||
ServerSettings, CLIENT_TIMEOUT,
|
ServerSettings, CLIENT_TIMEOUT,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
@ -395,6 +397,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
Read<'a, EventBus<ServerEvent>>,
|
Read<'a, EventBus<ServerEvent>>,
|
||||||
Read<'a, Time>,
|
Read<'a, Time>,
|
||||||
ReadExpect<'a, CharacterLoader>,
|
ReadExpect<'a, CharacterLoader>,
|
||||||
|
ReadExpect<'a, AchievementLoader>,
|
||||||
ReadExpect<'a, TerrainGrid>,
|
ReadExpect<'a, TerrainGrid>,
|
||||||
Write<'a, SysTimer<Self>>,
|
Write<'a, SysTimer<Self>>,
|
||||||
ReadStorage<'a, Uid>,
|
ReadStorage<'a, Uid>,
|
||||||
@ -425,6 +428,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
server_event_bus,
|
server_event_bus,
|
||||||
time,
|
time,
|
||||||
character_loader,
|
character_loader,
|
||||||
|
achievement_loader,
|
||||||
terrain,
|
terrain,
|
||||||
mut timer,
|
mut timer,
|
||||||
uids,
|
uids,
|
||||||
@ -520,16 +524,21 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new players.
|
// Handle new players: Tell all clients to add them to the player list, and load
|
||||||
// Tell all clients to add them to the player list.
|
// non-critical data for their character
|
||||||
for entity in new_players {
|
for entity in new_players {
|
||||||
if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) {
|
if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) {
|
||||||
|
if let Some(character_id) = player.character_id {
|
||||||
|
achievement_loader.load_character_achievement_list(entity, character_id);
|
||||||
|
}
|
||||||
|
|
||||||
let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo {
|
let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo {
|
||||||
player_alias: player.alias.clone(),
|
player_alias: player.alias.clone(),
|
||||||
is_online: true,
|
is_online: true,
|
||||||
is_admin: admins.get(entity).is_some(),
|
is_admin: admins.get(entity).is_some(),
|
||||||
character: None, // new players will be on character select.
|
character: None, // new players will be on character select.
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for client in (&mut clients).join().filter(|c| c.is_registered()) {
|
for client in (&mut clients).join().filter(|c| c.is_registered()) {
|
||||||
client.notify(msg.clone())
|
client.notify(msg.clone())
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod achievement;
|
||||||
pub mod entity_sync;
|
pub mod entity_sync;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
@ -53,6 +54,9 @@ pub fn run_sync_systems(ecs: &mut specs::World) {
|
|||||||
// Sync
|
// Sync
|
||||||
terrain_sync::Sys.run_now(ecs);
|
terrain_sync::Sys.run_now(ecs);
|
||||||
entity_sync::Sys.run_now(ecs);
|
entity_sync::Sys.run_now(ecs);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
achievement::Sys.run_now(ecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to schedule systems to run at an interval
|
/// Used to schedule systems to run at an interval
|
||||||
|
Loading…
Reference in New Issue
Block a user