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);
|
||||
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 achievement;
|
||||
mod admin;
|
||||
pub mod agent;
|
||||
mod body;
|
||||
@ -20,6 +21,7 @@ mod visual;
|
||||
|
||||
// Reexports
|
||||
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
|
||||
pub use achievement::{Achievement, AchievementItem, AchievementList, AchievementType};
|
||||
pub use admin::{Admin, AdminList};
|
||||
pub use agent::{Agent, Alignment};
|
||||
pub use body::{
|
||||
|
@ -83,6 +83,10 @@ pub enum ServerEvent {
|
||||
Chat(comp::ChatMsg),
|
||||
}
|
||||
|
||||
pub enum AchievementEvent {
|
||||
CollectedItem { entity: EcsEntity, item: comp::Item },
|
||||
}
|
||||
|
||||
pub struct EventBus<E> {
|
||||
queue: Mutex<VecDeque<E>>,
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
option_zip
|
||||
)]
|
||||
|
||||
pub mod achievement;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
pub mod assets;
|
||||
pub mod astar;
|
||||
pub mod character;
|
||||
|
@ -59,6 +59,11 @@ pub enum ServerMsg {
|
||||
time_of_day: state::TimeOfDay,
|
||||
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
|
||||
CharacterDataLoadError(String),
|
||||
/// 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},
|
||||
Pos, MAX_PICKUP_RANGE_SQR,
|
||||
},
|
||||
event::{AchievementEvent, EventBus},
|
||||
sync::{Uid, WorldSyncExt},
|
||||
terrain::block::Block,
|
||||
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
|
||||
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 {
|
||||
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()
|
||||
&& state.try_set_block(pos, Block::empty()).is_some()
|
||||
{
|
||||
comp::Item::try_reclaim_from_block(block)
|
||||
.map(|item| state.give_item(entity, item));
|
||||
comp::Item::try_reclaim_from_block(block).map(|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::{
|
||||
cmd::ChatCommand,
|
||||
comp::{self, ChatType},
|
||||
event::{EventBus, ServerEvent},
|
||||
event::{AchievementEvent, EventBus, ServerEvent},
|
||||
msg::{ClientState, ServerInfo, ServerMsg},
|
||||
state::{State, TimeOfDay},
|
||||
sync::WorldSyncExt,
|
||||
@ -42,7 +42,10 @@ use futures_timer::Delay;
|
||||
use futures_util::{select, FutureExt};
|
||||
use metrics::{ServerMetrics, TickMetrics};
|
||||
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 std::{
|
||||
i32,
|
||||
@ -95,13 +98,24 @@ impl Server {
|
||||
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
|
||||
let mut state = State::default();
|
||||
state.ecs_mut().insert(settings.clone());
|
||||
|
||||
// Event Emitters
|
||||
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(EventBus::<AchievementEvent>::default());
|
||||
|
||||
state.ecs_mut().insert(AuthProvider::new(
|
||||
settings.auth_server_address.clone(),
|
||||
settings.whitelist.clone(),
|
||||
));
|
||||
state.ecs_mut().insert(Tick(0));
|
||||
state.ecs_mut().insert(ChunkGenerator::new());
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(comp::AdminList(settings.admins.clone()));
|
||||
|
||||
// Persistence interaction
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(CharacterUpdater::new(settings.persistence_db_dir.clone()));
|
||||
@ -110,12 +124,7 @@ impl Server {
|
||||
.insert(CharacterLoader::new(settings.persistence_db_dir.clone()));
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(persistence::character::CharacterUpdater::new(
|
||||
settings.persistence_db_dir.clone(),
|
||||
));
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(comp::AdminList(settings.admins.clone()));
|
||||
.insert(AchievementLoader::new(settings.persistence_db_dir.clone()));
|
||||
|
||||
// System timers for performance monitoring
|
||||
state.ecs_mut().insert(sys::EntitySyncTimer::default());
|
||||
@ -243,6 +252,25 @@ impl Server {
|
||||
thread_pool.execute(f);
|
||||
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 {
|
||||
state,
|
||||
world: Arc::new(world),
|
||||
@ -256,22 +284,6 @@ impl Server {
|
||||
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");
|
||||
|
||||
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
|
||||
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();
|
||||
|
||||
// 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::{
|
||||
error::Error,
|
||||
establish_connection,
|
||||
models::{Achievement as AchievementModel, DataMigration, NewAchievement, NewDataMigration},
|
||||
models::{
|
||||
Achievement as AchievementModel, CharacterAchievement, DataMigration, NewAchievement,
|
||||
NewDataMigration,
|
||||
},
|
||||
schema,
|
||||
};
|
||||
use common::achievement::*;
|
||||
use common::comp::{self, AchievementItem};
|
||||
use crossbeam::{channel, channel::TryIter};
|
||||
use diesel::{
|
||||
prelude::*,
|
||||
result::{DatabaseErrorKind, Error as DieselError},
|
||||
@ -15,37 +19,144 @@ use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
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 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
|
||||
.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
|
||||
if result.is_empty() {
|
||||
let migration = NewDataMigration {
|
||||
title: "achievements",
|
||||
checksum: &hash(&achievements).to_string(),
|
||||
last_run: chrono::Utc::now().naive_utc(),
|
||||
};
|
||||
info!(?result, "result: ");
|
||||
|
||||
diesel::insert_into(schema::data_migration::table)
|
||||
.values(&migration)
|
||||
.execute(&connection)?;
|
||||
}
|
||||
let should_run = match result {
|
||||
Ok(migration_entry) => {
|
||||
let checksum = &migration_entry.checksum;
|
||||
info!(?checksum, "checksum: ");
|
||||
|
||||
// Also check checksum. Bail if same, continue if changed
|
||||
if result.is_empty() {
|
||||
info!("Achievements need updating...");
|
||||
migration_entry.checksum != hash(&achievements).to_string()
|
||||
}
|
||||
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
|
||||
let persisted_achievements =
|
||||
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
|
||||
diesel::insert_into(schema::data_migration::table)
|
||||
.values(&migration)
|
||||
.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
|
||||
// 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)),
|
||||
}
|
||||
}
|
||||
@ -96,21 +207,23 @@ pub fn sync(db_dir: &str) -> Result<(), Error> {
|
||||
info!("No achievement updates required");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?)
|
||||
}
|
||||
|
||||
fn load_data() -> Vec<AchievementItem> {
|
||||
if let Ok(path) = std::fs::canonicalize("../data/achievements.ron") {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
let manifest_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "data/achievements.ron");
|
||||
|
||||
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"),
|
||||
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);
|
||||
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::{
|
||||
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 chrono::NaiveDateTime;
|
||||
@ -377,7 +380,7 @@ pub struct NewDataMigration<'a> {
|
||||
/// the starter sword set as the main weapon
|
||||
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
|
||||
#[sql_type = "Text"]
|
||||
pub struct AchievementData(AchievementItem);
|
||||
pub struct AchievementData(comp::AchievementItem);
|
||||
|
||||
impl<DB> diesel::deserialize::FromSql<Text, DB> for AchievementData
|
||||
where
|
||||
@ -394,7 +397,7 @@ where
|
||||
Err(e) => {
|
||||
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,
|
||||
}
|
||||
|
||||
impl From<&AchievementData> for comp::AchievementItem {
|
||||
fn from(data: &AchievementData) -> comp::AchievementItem { data.0.clone() }
|
||||
}
|
||||
|
||||
#[derive(Insertable, PartialEq, Debug)]
|
||||
#[table_name = "achievement"]
|
||||
pub struct NewAchievement {
|
||||
@ -428,8 +435,8 @@ pub struct NewAchievement {
|
||||
pub details: AchievementData,
|
||||
}
|
||||
|
||||
impl From<&AchievementItem> for NewAchievement {
|
||||
fn from(item: &AchievementItem) -> Self {
|
||||
impl From<&comp::AchievementItem> for NewAchievement {
|
||||
fn from(item: &comp::AchievementItem) -> Self {
|
||||
Self {
|
||||
checksum: hash(item).to_string(),
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -30,6 +30,15 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
character_achievement (character_id) {
|
||||
character_id -> Integer,
|
||||
achievement_id -> Integer,
|
||||
completed -> Integer,
|
||||
progress -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
data_migration (id) {
|
||||
id -> Integer,
|
||||
@ -67,8 +76,18 @@ table! {
|
||||
}
|
||||
|
||||
joinable!(body -> character (character_id));
|
||||
joinable!(character_achievement -> achievement (achievement_id));
|
||||
joinable!(character_achievement -> character (character_id));
|
||||
joinable!(inventory -> character (character_id));
|
||||
joinable!(loadout -> 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,
|
||||
comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()),
|
||||
);
|
||||
self.write_component(entity, comp::AchievementList::default());
|
||||
|
||||
// Set the character id for the player
|
||||
// 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 crate::{
|
||||
auth_provider::AuthProvider, client::Client, persistence::character::CharacterLoader,
|
||||
auth_provider::AuthProvider,
|
||||
client::Client,
|
||||
persistence::{achievement::AchievementLoader, character::CharacterLoader},
|
||||
ServerSettings, CLIENT_TIMEOUT,
|
||||
};
|
||||
use common::{
|
||||
@ -395,6 +397,7 @@ impl<'a> System<'a> for Sys {
|
||||
Read<'a, EventBus<ServerEvent>>,
|
||||
Read<'a, Time>,
|
||||
ReadExpect<'a, CharacterLoader>,
|
||||
ReadExpect<'a, AchievementLoader>,
|
||||
ReadExpect<'a, TerrainGrid>,
|
||||
Write<'a, SysTimer<Self>>,
|
||||
ReadStorage<'a, Uid>,
|
||||
@ -425,6 +428,7 @@ impl<'a> System<'a> for Sys {
|
||||
server_event_bus,
|
||||
time,
|
||||
character_loader,
|
||||
achievement_loader,
|
||||
terrain,
|
||||
mut timer,
|
||||
uids,
|
||||
@ -520,16 +524,21 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new players.
|
||||
// Tell all clients to add them to the player list.
|
||||
// Handle new players: Tell all clients to add them to the player list, and load
|
||||
// non-critical data for their character
|
||||
for entity in new_players {
|
||||
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 {
|
||||
player_alias: player.alias.clone(),
|
||||
is_online: true,
|
||||
is_admin: admins.get(entity).is_some(),
|
||||
character: None, // new players will be on character select.
|
||||
}));
|
||||
|
||||
for client in (&mut clients).join().filter(|c| c.is_registered()) {
|
||||
client.notify(msg.clone())
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod achievement;
|
||||
pub mod entity_sync;
|
||||
pub mod message;
|
||||
pub mod object;
|
||||
@ -53,6 +54,9 @@ pub fn run_sync_systems(ecs: &mut specs::World) {
|
||||
// Sync
|
||||
terrain_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
|
||||
|
Loading…
Reference in New Issue
Block a user