mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Rework some data structures and persistence. Abandoned the approach of
generating row ids for achievements via checksums in favour of static uuids for each item.
This commit is contained in:
parent
efe7a62c92
commit
c056b2b079
@ -1,4 +1,5 @@
|
|||||||
use crate::comp::item::{Consumable, Item, ItemKind};
|
use crate::comp::item::{Consumable, Item, ItemKind};
|
||||||
|
use hashbrown::HashMap;
|
||||||
use specs::{Component, Entity, FlaggedStorage};
|
use specs::{Component, Entity, FlaggedStorage};
|
||||||
use specs_idvs::IDVStorage;
|
use specs_idvs::IDVStorage;
|
||||||
|
|
||||||
@ -11,6 +12,12 @@ pub struct AchievementTrigger {
|
|||||||
pub event: AchievementEvent,
|
pub event: AchievementEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used to indicate an in-game event that can contribute towards the completion
|
||||||
|
/// of an achievement.
|
||||||
|
///
|
||||||
|
/// These are paired with `AchievementAction` items - for
|
||||||
|
/// example an event of type `LevelUp` will trigger a check for `ReachLevel`
|
||||||
|
/// achievements.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum AchievementEvent {
|
pub enum AchievementEvent {
|
||||||
None,
|
None,
|
||||||
@ -30,17 +37,15 @@ pub enum AchievementAction {
|
|||||||
KillNpcs,
|
KillNpcs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about an achievement. This differs from a complete
|
|
||||||
/// [`Achievement`](struct.Achievement.html) in that it describes the
|
|
||||||
/// achievement without any information about progress or completion status
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct AchievementItem {
|
pub struct Achievement {
|
||||||
|
pub uuid: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub action: AchievementAction,
|
pub action: AchievementAction,
|
||||||
pub target: usize,
|
pub target: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AchievementItem {
|
impl Achievement {
|
||||||
pub fn matches_event(&self, event: &AchievementEvent) -> bool {
|
pub fn matches_event(&self, event: &AchievementEvent) -> bool {
|
||||||
match event {
|
match event {
|
||||||
AchievementEvent::KilledNpc => self.action == AchievementAction::KillNpcs,
|
AchievementEvent::KilledNpc => self.action == AchievementAction::KillNpcs,
|
||||||
@ -63,21 +68,24 @@ impl AchievementItem {
|
|||||||
|
|
||||||
/// The complete representation of an achievement that has been
|
/// The complete representation of an achievement that has been
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct Achievement {
|
pub struct CharacterAchievement {
|
||||||
pub id: i32,
|
pub achievement: Achievement,
|
||||||
pub item: AchievementItem,
|
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
pub progress: usize,
|
pub progress: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Achievement {
|
impl CharacterAchievement {
|
||||||
/// Increment the progress of this Achievement based on its type
|
/// Increment the progress of this Achievement based on its type
|
||||||
///
|
///
|
||||||
/// By default, when an achievement is incremented, its `progress` value is
|
/// By default, when an achievement is incremented, its `progress` value is
|
||||||
/// incremented by 1. This covers many cases, but using this method allows
|
/// incremented by 1. This covers many cases, but using this method allows
|
||||||
/// handling of unique types of achievements which are not simple
|
/// handling of unique types of achievements which are not simple
|
||||||
/// counters for events
|
/// counters for events
|
||||||
pub fn increment_progress(&mut self, event: &AchievementEvent) -> bool {
|
pub fn increment_progress(&mut self, event: &AchievementEvent) -> Option<&mut Self> {
|
||||||
|
if self.completed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
AchievementEvent::LevelUp(level) => {
|
AchievementEvent::LevelUp(level) => {
|
||||||
self.progress = *level as usize;
|
self.progress = *level as usize;
|
||||||
@ -85,61 +93,67 @@ impl Achievement {
|
|||||||
_ => self.progress += 1,
|
_ => self.progress += 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.completed = self.progress >= self.item.target;
|
self.completed = self.progress >= self.achievement.target;
|
||||||
self.completed
|
|
||||||
|
if self.completed == true {
|
||||||
|
Some(self)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For initialisation of a new CharacterAchievement item when a new achievement
|
||||||
|
/// is progressed
|
||||||
|
impl From<Achievement> for CharacterAchievement {
|
||||||
|
fn from(achievement: Achievement) -> Self {
|
||||||
|
Self {
|
||||||
|
achievement,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Each character is assigned an achievement list, which holds information
|
/// Each character is assigned an achievement list, which holds information
|
||||||
/// about which achievements that the player has made some progress on, or
|
/// about which achievements that the player has made some progress on, or
|
||||||
/// completed.
|
/// completed.
|
||||||
///
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
/// This minimises storage of data per-character, and can be merged with a full
|
pub struct AchievementList(HashMap<String, CharacterAchievement>);
|
||||||
/// achievement list
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
pub struct AchievementList(Vec<Achievement>);
|
|
||||||
|
|
||||||
impl Default for AchievementList {
|
impl Default for AchievementList {
|
||||||
fn default() -> AchievementList { AchievementList(Vec::new()) }
|
fn default() -> AchievementList { AchievementList(HashMap::new()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AchievementList {
|
impl AchievementList {
|
||||||
pub fn from(data: Vec<Achievement>) -> Self { Self(data) }
|
pub fn from(data: HashMap<String, CharacterAchievement>) -> Self { Self(data) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for AchievementList {
|
impl Component for AchievementList {
|
||||||
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
type Storage = FlaggedStorage<Self, IDVStorage<Self>>; // TODO check
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AchievementList {
|
impl AchievementList {
|
||||||
pub fn item_by_id(&mut self, id: i32) -> Option<&mut Achievement> {
|
/// Process a single CharacterAchievement item based on the occurance of an
|
||||||
self.0.iter_mut().find(|a| a.id == id)
|
/// `AchievementEvent`.
|
||||||
}
|
///
|
||||||
|
/// When the character has existing progress on the achievement it is
|
||||||
/// Process a single achievement item, inrementing the progress of the
|
/// updated, otherwise an insert-then-update is performed.
|
||||||
/// achievement. This is called as part of server/sys/Achievements.
|
///
|
||||||
|
/// Returns the `CharacterAchievement` item that was processed in the event
|
||||||
|
/// that prcessing resulted in its completion.
|
||||||
pub fn process_achievement(
|
pub fn process_achievement(
|
||||||
&mut self,
|
&mut self,
|
||||||
achievement: Achievement,
|
achievement: &Achievement,
|
||||||
event: &AchievementEvent,
|
event: &AchievementEvent,
|
||||||
) -> bool {
|
) -> Option<CharacterAchievement> {
|
||||||
let id = achievement.id;
|
let uuid = achievement.uuid.clone();
|
||||||
|
|
||||||
if !self.0.contains(&achievement) {
|
self.0
|
||||||
self.0.push(achievement);
|
.entry(uuid)
|
||||||
}
|
.or_insert(CharacterAchievement::from(achievement.clone()))
|
||||||
|
.increment_progress(event)
|
||||||
if let Some(char_achievement) = self.item_by_id(id) {
|
.cloned()
|
||||||
if char_achievement.completed {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
char_achievement.increment_progress(event)
|
|
||||||
} else {
|
|
||||||
tracing::warn!("Failed to find achievement after inserting");
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,8 +164,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inv_collect_event_matches_consumable_achievement_item() {
|
fn inv_collect_event_matches_consumable_achievement_item() {
|
||||||
let item = AchievementItem {
|
let item = Achievement {
|
||||||
title: String::from("Test"),
|
uuid: String::new(),
|
||||||
|
title: String::new(),
|
||||||
action: AchievementAction::CollectConsumable(Consumable::Apple),
|
action: AchievementAction::CollectConsumable(Consumable::Apple),
|
||||||
target: 10,
|
target: 10,
|
||||||
};
|
};
|
||||||
@ -164,8 +179,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inv_collect_event_not_matches_consumable_achievement_item() {
|
fn inv_collect_event_not_matches_consumable_achievement_item() {
|
||||||
let item = AchievementItem {
|
let item = Achievement {
|
||||||
title: String::from("Test"),
|
uuid: String::new(),
|
||||||
|
title: String::new(),
|
||||||
action: AchievementAction::CollectConsumable(Consumable::Cheese),
|
action: AchievementAction::CollectConsumable(Consumable::Cheese),
|
||||||
target: 10,
|
target: 10,
|
||||||
};
|
};
|
||||||
@ -178,8 +194,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn levelup_event_matches_reach_level_achievement_item() {
|
fn levelup_event_matches_reach_level_achievement_item() {
|
||||||
let item = AchievementItem {
|
let item = Achievement {
|
||||||
title: String::from("Test"),
|
uuid: String::new(),
|
||||||
|
title: String::new(),
|
||||||
action: AchievementAction::ReachLevel,
|
action: AchievementAction::ReachLevel,
|
||||||
target: 100,
|
target: 100,
|
||||||
};
|
};
|
||||||
@ -190,20 +207,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_collect_achievement_increments_progress() {
|
fn process_achievement_increments_progress() {
|
||||||
let item = AchievementItem {
|
let uuid = String::from("2ef30659-5884-40aa-ba4d-8f5af32ff9ac");
|
||||||
|
|
||||||
|
let achievement = Achievement {
|
||||||
|
uuid: uuid.clone(),
|
||||||
title: String::from("Collect 3 Mushrooms"),
|
title: String::from("Collect 3 Mushrooms"),
|
||||||
action: AchievementAction::CollectConsumable(Consumable::Mushroom),
|
action: AchievementAction::CollectConsumable(Consumable::Mushroom),
|
||||||
target: 3,
|
target: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
let achievement = Achievement {
|
|
||||||
id: 1,
|
|
||||||
item,
|
|
||||||
completed: false,
|
|
||||||
progress: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let event =
|
let event =
|
||||||
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
|
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
|
||||||
|
|
||||||
@ -211,92 +224,75 @@ mod tests {
|
|||||||
|
|
||||||
// The first two increments should not indicate that it is complete
|
// The first two increments should not indicate that it is complete
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
achievement_list.process_achievement(achievement.clone(), &event),
|
achievement_list.process_achievement(&achievement.clone(), &event),
|
||||||
false
|
None
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
achievement_list.process_achievement(achievement.clone(), &event),
|
achievement_list.process_achievement(&achievement.clone(), &event),
|
||||||
false
|
None
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().progress, 2);
|
assert_eq!(achievement_list.0.get(&uuid).unwrap().progress, 2);
|
||||||
|
|
||||||
// It should return true when completed
|
|
||||||
assert_eq!(
|
|
||||||
achievement_list.process_achievement(achievement, &event),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().progress, 3);
|
|
||||||
|
|
||||||
// The achievement `completed` field should be true
|
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().completed, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_levelup_achievement_increments_progress() {
|
fn process_achievement_returns_achievement_when_complete() {
|
||||||
let item = AchievementItem {
|
let uuid = String::from("Test");
|
||||||
|
|
||||||
|
let achievement = Achievement {
|
||||||
|
uuid: uuid.clone(),
|
||||||
title: String::from("Reach Level 10"),
|
title: String::from("Reach Level 10"),
|
||||||
action: AchievementAction::ReachLevel,
|
action: AchievementAction::ReachLevel,
|
||||||
target: 10,
|
target: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
let achievement = Achievement {
|
|
||||||
id: 1,
|
|
||||||
item,
|
|
||||||
completed: false,
|
|
||||||
progress: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut achievement_list = AchievementList::default();
|
let mut achievement_list = AchievementList::default();
|
||||||
|
|
||||||
assert_eq!(
|
achievement_list.process_achievement(&achievement, &AchievementEvent::LevelUp(6));
|
||||||
achievement_list
|
|
||||||
.process_achievement(achievement.clone(), &AchievementEvent::LevelUp(6)),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// The achievement progress should be the new level value, and be incomplete
|
// The achievement progress should be the new level value, but be incomplete
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().progress, 6);
|
let incomplete_result = achievement_list.0.get(&uuid).unwrap();
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().completed, false);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(incomplete_result.progress, 6);
|
||||||
achievement_list.process_achievement(achievement, &AchievementEvent::LevelUp(10)),
|
assert_eq!(incomplete_result.completed, false);
|
||||||
true
|
|
||||||
);
|
achievement_list.process_achievement(&achievement, &AchievementEvent::LevelUp(10));
|
||||||
|
|
||||||
// The achievement progress should be the new level value, and be completed
|
// The achievement progress should be the new level value, and be completed
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().progress, 10);
|
let complete_result = achievement_list.0.get(&uuid).unwrap();
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().completed, true);
|
|
||||||
|
assert_eq!(complete_result.progress, 10);
|
||||||
|
assert_eq!(complete_result.completed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_completed_achievement_doesnt_increment_progress() {
|
fn process_completed_achievement_doesnt_increment_progress() {
|
||||||
let item = AchievementItem {
|
let uuid = String::from("Test");
|
||||||
|
|
||||||
|
let achievement = Achievement {
|
||||||
|
uuid: uuid.clone(),
|
||||||
title: String::from("Collect 3 Mushrooms"),
|
title: String::from("Collect 3 Mushrooms"),
|
||||||
action: AchievementAction::CollectConsumable(Consumable::Mushroom),
|
action: AchievementAction::KillNpcs,
|
||||||
target: 3,
|
target: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
let achievement = Achievement {
|
// Initialise with the already completed event
|
||||||
id: 1,
|
let mut achievement_list = AchievementList::default();
|
||||||
item,
|
|
||||||
completed: true,
|
|
||||||
progress: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut achievement_list = AchievementList(vec![achievement.clone()]);
|
achievement_list
|
||||||
|
.0
|
||||||
|
.insert(uuid.clone(), CharacterAchievement {
|
||||||
|
achievement: achievement.clone(),
|
||||||
|
completed: true,
|
||||||
|
progress: 3,
|
||||||
|
});
|
||||||
|
|
||||||
let event =
|
achievement_list.process_achievement(&achievement, &AchievementEvent::KilledNpc);
|
||||||
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
|
|
||||||
|
|
||||||
assert_eq!(
|
let result = achievement_list.0.get(&uuid).unwrap();
|
||||||
achievement_list.process_achievement(achievement, &event),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// The achievement progress should not have incremented
|
// The achievement progress should not have incremented
|
||||||
assert_eq!(achievement_list.0.get(0).unwrap().progress, 3);
|
assert_eq!(result.progress, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ mod visual;
|
|||||||
// Reexports
|
// Reexports
|
||||||
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
|
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
|
||||||
pub use achievement::{
|
pub use achievement::{
|
||||||
Achievement, AchievementAction, AchievementEvent, AchievementItem, AchievementList,
|
Achievement, AchievementAction, AchievementEvent, AchievementList, AchievementTrigger,
|
||||||
AchievementTrigger,
|
CharacterAchievement,
|
||||||
};
|
};
|
||||||
pub use admin::{Admin, AdminList};
|
pub use admin::{Admin, AdminList};
|
||||||
pub use agent::{Agent, Alignment};
|
pub use agent::{Agent, Alignment};
|
||||||
|
@ -65,7 +65,7 @@ pub enum ServerMsg {
|
|||||||
/// An error occurred while loading character achievements
|
/// An error occurred while loading character achievements
|
||||||
CharacterAchievementDataError(String),
|
CharacterAchievementDataError(String),
|
||||||
/// The client has completed an achievement
|
/// The client has completed an achievement
|
||||||
AchievementCompletion(comp::AchievementItem),
|
AchievementCompletion(comp::CharacterAchievement),
|
||||||
/// 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
|
||||||
|
@ -1,50 +1,60 @@
|
|||||||
[
|
[
|
||||||
(
|
(
|
||||||
|
id: "9d48b606-f70a-4e7f-a3b7-6630e75ba8fe",
|
||||||
title: "Collect 10 apples",
|
title: "Collect 10 apples",
|
||||||
action: CollectConsumable(Apple),
|
action: CollectConsumable(Apple),
|
||||||
target: 10
|
target: 10
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "ffccb88c-2bb0-41bd-942b-3cbdf9882295",
|
||||||
title: "Collect 50 apples",
|
title: "Collect 50 apples",
|
||||||
action: CollectConsumable(Apple),
|
action: CollectConsumable(Apple),
|
||||||
target: 50
|
target: 50
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "290c0bc1-ab0a-450e-82d7-6917d9ea7497",
|
||||||
title: "Pick a mushroom",
|
title: "Pick a mushroom",
|
||||||
action: CollectConsumable(Mushroom),
|
action: CollectConsumable(Mushroom),
|
||||||
target: 1
|
target: 1
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "598aff28-01f6-46a9-a4c1-d75aead98794",
|
||||||
title: "Kill an NPC",
|
title: "Kill an NPC",
|
||||||
action: KillNpcs,
|
action: KillNpcs,
|
||||||
target: 1
|
target: 1
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "046e08c5-a512-4ee1-b6e4-76e4e11dd502",
|
||||||
title: "Kill 10 NPCs",
|
title: "Kill 10 NPCs",
|
||||||
action: KillNpcs,
|
action: KillNpcs,
|
||||||
target: 10
|
target: 10
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "9c2a326b-c4fa-45bf-9dd6-d4bf9dd85bb5",
|
||||||
title: "Kill 100 NPCs",
|
title: "Kill 100 NPCs",
|
||||||
action: KillNpcs,
|
action: KillNpcs,
|
||||||
target: 100
|
target: 100
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "de782069-4366-41dc-9645-1c02c64b8037",
|
||||||
title: "Kill another player",
|
title: "Kill another player",
|
||||||
action: KillPlayers,
|
action: KillPlayers,
|
||||||
target: 1
|
target: 1
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "df6bf984-efdf-40f7-8bac-e09e9927acba",
|
||||||
title: "Kill 10 players",
|
title: "Kill 10 players",
|
||||||
action: KillPlayers,
|
action: KillPlayers,
|
||||||
target: 10
|
target: 10
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "d733c6cc-57c9-4a22-aea6-13f4850d750f",
|
||||||
title: "Find a Velorite fragment",
|
title: "Find a Velorite fragment",
|
||||||
action: CollectConsumable(VeloriteFrag),
|
action: CollectConsumable(VeloriteFrag),
|
||||||
target: 1
|
target: 1
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
id: "50140fe9-1811-40c4-bb3b-89503ac60ccf",
|
||||||
title: "Find Velorite",
|
title: "Find Velorite",
|
||||||
action: CollectConsumable(Velorite),
|
action: CollectConsumable(Velorite),
|
||||||
target: 1
|
target: 1
|
||||||
|
@ -43,7 +43,7 @@ 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::{
|
use persistence::{
|
||||||
achievement::{Achievement, AchievementLoader, AchievementLoaderResponse},
|
achievement::{AchievementLoader, AchievementLoaderResponse},
|
||||||
character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater},
|
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};
|
||||||
@ -90,7 +90,8 @@ pub struct Server {
|
|||||||
metrics: ServerMetrics,
|
metrics: ServerMetrics,
|
||||||
tick_metrics: TickMetrics,
|
tick_metrics: TickMetrics,
|
||||||
|
|
||||||
achievement_data: Vec<Achievement>,
|
/// Holds a list of all available achievements
|
||||||
|
achievement_data: Vec<comp::Achievement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
@ -447,9 +448,7 @@ impl Server {
|
|||||||
for trigger in achievement_events {
|
for trigger in achievement_events {
|
||||||
// Get the achievement that matches this event
|
// Get the achievement that matches this event
|
||||||
for achievement in &self.achievement_data {
|
for achievement in &self.achievement_data {
|
||||||
let achievement_item = comp::AchievementItem::from(&achievement.details);
|
if achievement.matches_event(&trigger.event) {
|
||||||
|
|
||||||
if achievement_item.matches_event(&trigger.event) {
|
|
||||||
// Calls to `process_achievement` return true to indicate that the
|
// Calls to `process_achievement` return true to indicate that the
|
||||||
// achievement is complete. In this case, we notify the client to notify them of
|
// achievement is complete. In this case, we notify the client to notify them of
|
||||||
// completing the achievement
|
// completing the achievement
|
||||||
@ -459,14 +458,12 @@ impl Server {
|
|||||||
.write_storage::<comp::AchievementList>()
|
.write_storage::<comp::AchievementList>()
|
||||||
.get_mut(trigger.entity)
|
.get_mut(trigger.entity)
|
||||||
{
|
{
|
||||||
if achievement_list.process_achievement(
|
if let Some(character_achievement) =
|
||||||
comp::Achievement::from(achievement),
|
achievement_list.process_achievement(achievement, &trigger.event)
|
||||||
&trigger.event,
|
|
||||||
) == true
|
|
||||||
{
|
{
|
||||||
self.notify_client(
|
self.notify_client(
|
||||||
trigger.entity,
|
trigger.entity,
|
||||||
ServerMsg::AchievementCompletion(achievement_item),
|
ServerMsg::AchievementCompletion(character_achievement),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
CREATE TABLE IF NOT EXISTS "achievement" (
|
CREATE TABLE IF NOT EXISTS "achievements" (
|
||||||
id INTEGER PRIMARY KEY NOT NULL,
|
"uuid" TEXT PRIMARY KEY NOT NULL,
|
||||||
checksum VARCHAR(64) NOT NULL UNIQUE,
|
"checksum" TEXT NOT NULL UNIQUE,
|
||||||
details TEXT NOT NULL
|
"title" TEXT NOT NULL,
|
||||||
|
"action" INT NOT NULL,
|
||||||
|
"target" INT NOT NULL
|
||||||
);
|
);
|
@ -1,8 +1,8 @@
|
|||||||
CREATE TABLE IF NOT EXISTS "character_achievement" (
|
CREATE TABLE IF NOT EXISTS "character_achievement" (
|
||||||
character_id INTEGER PRIMARY KEY NOT NULL,
|
character_id INTEGER PRIMARY KEY NOT NULL,
|
||||||
achievement_id INTEGER NOT NULL,
|
achievement_uuid TEXT NOT NULL,
|
||||||
completed INTEGER NOT NULL DEFAULT 0,
|
completed INTEGER NOT NULL DEFAULT 0,
|
||||||
progress INTEGER NOT NULL DEFAULT 0,
|
progress INTEGER NOT NULL DEFAULT 0,
|
||||||
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE,
|
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY(achievement_id) REFERENCES "achievement"(id) ON DELETE CASCADE
|
FOREIGN KEY(achievement_uuid) REFERENCES "achievement"(uuid) ON DELETE CASCADE
|
||||||
);
|
);
|
@ -4,12 +4,12 @@ use super::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
establish_connection,
|
establish_connection,
|
||||||
models::{
|
models::{
|
||||||
Achievement as AchievementModel, CharacterAchievement, DataMigration, NewAchievement,
|
Achievement as AchievementModel, CharacterAchievement, CharacterAchievementJoinData,
|
||||||
NewDataMigration,
|
DataMigration, NewDataMigration,
|
||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
};
|
};
|
||||||
use common::comp::{self, AchievementItem};
|
use common::comp;
|
||||||
use crossbeam::{channel, channel::TryIter};
|
use crossbeam::{channel, channel::TryIter};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@ -21,8 +21,6 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
pub type Achievement = AchievementModel;
|
|
||||||
|
|
||||||
/// Available database operations when modifying a player's characetr list
|
/// Available database operations when modifying a player's characetr list
|
||||||
enum AchievementLoaderRequestKind {
|
enum AchievementLoaderRequestKind {
|
||||||
LoadCharacterAchievementList {
|
LoadCharacterAchievementList {
|
||||||
@ -105,25 +103,34 @@ fn load_character_achievement_list(
|
|||||||
) -> Result<comp::AchievementList, Error> {
|
) -> Result<comp::AchievementList, Error> {
|
||||||
let character_achievements = schema::character_achievement::dsl::character_achievement
|
let character_achievements = schema::character_achievement::dsl::character_achievement
|
||||||
.filter(schema::character_achievement::character_id.eq(character_id))
|
.filter(schema::character_achievement::character_id.eq(character_id))
|
||||||
.load::<CharacterAchievement>(&establish_connection(db_dir))?;
|
.inner_join(schema::achievements::table)
|
||||||
|
.load::<(CharacterAchievement, AchievementModel)>(&establish_connection(db_dir))?;
|
||||||
|
|
||||||
Ok(comp::AchievementList::from(
|
Ok(comp::AchievementList::from(
|
||||||
character_achievements
|
character_achievements
|
||||||
.iter()
|
.iter()
|
||||||
.map(comp::Achievement::from)
|
.map(|(character_achievement, achievement)| {
|
||||||
.collect::<Vec<comp::Achievement>>(),
|
(
|
||||||
|
achievement.uuid.clone(),
|
||||||
|
comp::CharacterAchievement::from(CharacterAchievementJoinData {
|
||||||
|
character_achievement,
|
||||||
|
achievement,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
|
pub fn sync(db_dir: &str) -> Result<Vec<comp::Achievement>, Error> {
|
||||||
let achievements = load_data();
|
let achievements = load_data();
|
||||||
let connection = establish_connection(db_dir);
|
let connection = establish_connection(db_dir);
|
||||||
|
|
||||||
// Use the full dataset for checks
|
// Use the full dataset for checksums
|
||||||
let persisted_achievements =
|
let persisted_achievements =
|
||||||
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
|
schema::achievements::dsl::achievements.load::<AchievementModel>(&connection)?;
|
||||||
|
|
||||||
// Get a hash of the Vec<Achievement> to compare to the migration table
|
// Get a hash of the Vec<Achievement> we have in config to compare
|
||||||
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")))
|
||||||
.first::<DataMigration>(&connection);
|
.first::<DataMigration>(&connection);
|
||||||
@ -132,12 +139,11 @@ pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
|
|||||||
|
|
||||||
let should_run = match result {
|
let should_run = match result {
|
||||||
Ok(migration_entry) => {
|
Ok(migration_entry) => {
|
||||||
let checksum = &migration_entry.checksum;
|
// If these don't match, we need to sync data
|
||||||
info!(?checksum, "checksum: ");
|
|
||||||
|
|
||||||
migration_entry.checksum != hash(&achievements).to_string()
|
migration_entry.checksum != hash(&achievements).to_string()
|
||||||
},
|
},
|
||||||
Err(diesel::result::Error::NotFound) => {
|
Err(diesel::result::Error::NotFound) => {
|
||||||
|
// If there was no migration entry (first run on this server) we need to run
|
||||||
let migration = NewDataMigration {
|
let migration = NewDataMigration {
|
||||||
title: "achievements",
|
title: "achievements",
|
||||||
checksum: &hash(&achievements).to_string(),
|
checksum: &hash(&achievements).to_string(),
|
||||||
@ -158,39 +164,29 @@ pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (should_run || persisted_achievements.is_empty()) && !achievements.is_empty() {
|
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
|
||||||
for item in &achievements {
|
for item in &achievements {
|
||||||
let new_item = NewAchievement::from(item);
|
if let Err(error) = diesel::insert_into(schema::achievements::table)
|
||||||
|
.values(item)
|
||||||
if let Err(error) = diesel::insert_into(schema::achievement::table)
|
|
||||||
.values(&new_item)
|
|
||||||
.execute(&connection)
|
.execute(&connection)
|
||||||
{
|
{
|
||||||
match error {
|
match error {
|
||||||
DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _) => {
|
DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _) => {
|
||||||
let entry = persisted_achievements
|
// This uuid already exists, so overwrite the data
|
||||||
|
if let Some(existing_item) = persisted_achievements
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&a| &a.checksum == &new_item.checksum);
|
.find(|&a| &a.uuid == &item.uuid)
|
||||||
|
{
|
||||||
if let Some(existing_item) = entry {
|
match diesel::update(
|
||||||
if existing_item.details != new_item.details {
|
schema::achievements::dsl::achievements
|
||||||
match diesel::update(
|
.filter(schema::achievements::uuid.eq(&existing_item.uuid)),
|
||||||
schema::achievement::dsl::achievement.filter(
|
)
|
||||||
schema::achievement::checksum
|
.set(item)
|
||||||
.eq(String::from(&existing_item.checksum)),
|
.execute(&connection)
|
||||||
),
|
{
|
||||||
)
|
Ok(_) => warn!(?existing_item.checksum, "Updated achievement"),
|
||||||
.set(schema::achievement::details.eq(new_item.details))
|
Err(err) => return Err(Error::DatabaseError(err)),
|
||||||
.execute(&connection)
|
|
||||||
{
|
|
||||||
Ok(_) => warn!(?existing_item.checksum, "Updated achievement"),
|
|
||||||
Err(err) => return Err(Error::DatabaseError(err)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -211,17 +207,16 @@ pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
|
|||||||
info!("No achievement updates required");
|
info!("No achievement updates required");
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
|
let data = schema::achievements::dsl::achievements.load::<AchievementModel>(&connection)?;
|
||||||
|
|
||||||
Ok(data)
|
// Ok(data)
|
||||||
// Ok(data.iter().map(comp::Achievement::from).collect::<_>())
|
Ok(data.iter().map(comp::Achievement::from).collect::<_>())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_data() -> Vec<AchievementItem> {
|
fn load_data() -> Vec<AchievementModel> {
|
||||||
|
// TODO this better
|
||||||
let manifest_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "data/achievements.ron");
|
let manifest_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "data/achievements.ron");
|
||||||
|
|
||||||
info!(?manifest_dir, "manifest_dir:");
|
|
||||||
|
|
||||||
match std::fs::canonicalize(manifest_dir) {
|
match std::fs::canonicalize(manifest_dir) {
|
||||||
Ok(path) => match std::fs::File::open(path) {
|
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"),
|
||||||
@ -234,7 +229,7 @@ fn load_data() -> Vec<AchievementItem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash<T: Hash>(t: &T) -> u64 {
|
fn hash<T: Hash>(t: &T) -> u64 {
|
||||||
let mut s = DefaultHasher::new();
|
let mut s = DefaultHasher::new();
|
||||||
t.hash(&mut s);
|
t.hash(&mut s);
|
||||||
s.finish()
|
s.finish()
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
use super::{
|
use super::schema::{
|
||||||
achievement::hash,
|
achievements, body, character, character_achievement, 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;
|
||||||
@ -373,114 +369,58 @@ pub struct NewDataMigration<'a> {
|
|||||||
pub last_run: NaiveDateTime,
|
pub last_run: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Achievements hold the data related to achievements available in-game. They
|
/// The achievements table holds data related to all achievements available to
|
||||||
/// are the data referenced for characters
|
/// players in-game. They serve as a strong reference for character achievements
|
||||||
/// A wrapper type for Loadout components used to serialise to and from JSON
|
/// (see below) and are populated form configuration files holding the data
|
||||||
/// If the column contains malformed JSON, a default loadout is returned, with
|
#[derive(Queryable, Debug, Identifiable, AsChangeset, Hash, Insertable, Deserialize)]
|
||||||
/// the starter sword set as the main weapon
|
#[primary_key(uuid)]
|
||||||
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
|
|
||||||
#[sql_type = "Text"]
|
|
||||||
pub struct AchievementData(comp::AchievementItem);
|
|
||||||
|
|
||||||
impl<DB> diesel::deserialize::FromSql<Text, DB> for AchievementData
|
|
||||||
where
|
|
||||||
DB: diesel::backend::Backend,
|
|
||||||
String: diesel::deserialize::FromSql<Text, DB>,
|
|
||||||
{
|
|
||||||
fn from_sql(
|
|
||||||
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
|
|
||||||
) -> diesel::deserialize::Result<Self> {
|
|
||||||
let t = String::from_sql(bytes)?;
|
|
||||||
|
|
||||||
match serde_json::from_str(&t) {
|
|
||||||
Ok(data) => Ok(Self(data)),
|
|
||||||
Err(e) => {
|
|
||||||
warn!(?e, "Failed to deserialise achevement data");
|
|
||||||
|
|
||||||
Ok(Self(comp::AchievementItem {
|
|
||||||
title: String::new(),
|
|
||||||
action: comp::AchievementAction::None,
|
|
||||||
target: 0,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<DB> diesel::serialize::ToSql<Text, DB> for AchievementData
|
|
||||||
where
|
|
||||||
DB: diesel::backend::Backend,
|
|
||||||
{
|
|
||||||
fn to_sql<W: std::io::Write>(
|
|
||||||
&self,
|
|
||||||
out: &mut diesel::serialize::Output<W, DB>,
|
|
||||||
) -> diesel::serialize::Result {
|
|
||||||
let s = serde_json::to_string(&self.0)?;
|
|
||||||
<String as diesel::serialize::ToSql<Text, DB>>::to_sql(&s, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Queryable, Debug, Identifiable)]
|
|
||||||
#[table_name = "achievement"]
|
|
||||||
pub struct Achievement {
|
pub struct Achievement {
|
||||||
pub id: i32,
|
pub uuid: String,
|
||||||
pub checksum: String,
|
pub checksum: String,
|
||||||
pub details: AchievementData,
|
pub title: String,
|
||||||
}
|
pub action: i32,
|
||||||
|
pub target: i32,
|
||||||
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 {
|
|
||||||
pub checksum: String,
|
|
||||||
pub details: AchievementData,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&comp::AchievementItem> for NewAchievement {
|
|
||||||
fn from(item: &comp::AchievementItem) -> Self {
|
|
||||||
Self {
|
|
||||||
checksum: hash(item).to_string(),
|
|
||||||
details: AchievementData(item.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
id: achievement.achievement_id,
|
|
||||||
item: comp::AchievementItem {
|
|
||||||
title: String::from("TODO"),
|
|
||||||
action: comp::AchievementAction::None,
|
|
||||||
target: 0,
|
|
||||||
},
|
|
||||||
completed: achievement.completed != 0,
|
|
||||||
progress: achievement.progress as usize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Achievement> for comp::Achievement {
|
impl From<&Achievement> for comp::Achievement {
|
||||||
fn from(achievement: &Achievement) -> comp::Achievement {
|
fn from(achievement: &Achievement) -> comp::Achievement {
|
||||||
comp::Achievement {
|
comp::Achievement {
|
||||||
id: achievement.id,
|
uuid: achievement.uuid.clone(),
|
||||||
item: comp::AchievementItem::from(&achievement.details),
|
title: achievement.title.clone(),
|
||||||
completed: false,
|
action: comp::AchievementAction::None, // TODO find a way to store this data
|
||||||
progress: 0,
|
target: achievement.target as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Character Achievements belong to characters, and contain information about
|
||||||
|
/// achievement progress.
|
||||||
|
///
|
||||||
|
/// In the interest of storing as little achievement data per-character as
|
||||||
|
/// pssible, we only store a 'character_achievement' entry when a character has
|
||||||
|
/// made some progress towards the completion of an achievement.
|
||||||
|
#[derive(Queryable, Debug, Identifiable)]
|
||||||
|
#[primary_key(character_id)]
|
||||||
|
#[table_name = "character_achievement"]
|
||||||
|
pub struct CharacterAchievement {
|
||||||
|
pub character_id: i32,
|
||||||
|
pub achievement_uuid: String,
|
||||||
|
pub completed: i32,
|
||||||
|
pub progress: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The required elements to build comp::CharacterAchievement from database data
|
||||||
|
pub struct CharacterAchievementJoinData<'a> {
|
||||||
|
pub character_achievement: &'a CharacterAchievement,
|
||||||
|
pub achievement: &'a Achievement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CharacterAchievementJoinData<'_>> for comp::CharacterAchievement {
|
||||||
|
fn from(data: CharacterAchievementJoinData) -> comp::CharacterAchievement {
|
||||||
|
comp::CharacterAchievement {
|
||||||
|
achievement: comp::Achievement::from(data.achievement),
|
||||||
|
completed: data.character_achievement.completed == 1,
|
||||||
|
progress: data.character_achievement.progress as usize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
table! {
|
table! {
|
||||||
achievement (id) {
|
achievements (uuid) {
|
||||||
id -> Integer,
|
uuid -> Text,
|
||||||
checksum -> Text,
|
checksum -> Text,
|
||||||
details -> Text,
|
title -> Text,
|
||||||
|
action -> Integer,
|
||||||
|
target -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ table! {
|
|||||||
table! {
|
table! {
|
||||||
character_achievement (character_id) {
|
character_achievement (character_id) {
|
||||||
character_id -> Integer,
|
character_id -> Integer,
|
||||||
achievement_id -> Integer,
|
achievement_uuid -> Text,
|
||||||
completed -> Integer,
|
completed -> Integer,
|
||||||
progress -> Integer,
|
progress -> Integer,
|
||||||
}
|
}
|
||||||
@ -76,14 +78,14 @@ table! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
joinable!(body -> character (character_id));
|
joinable!(body -> character (character_id));
|
||||||
joinable!(character_achievement -> achievement (achievement_id));
|
|
||||||
joinable!(character_achievement -> character (character_id));
|
joinable!(character_achievement -> character (character_id));
|
||||||
|
joinable!(character_achievement -> achievements (achievement_uuid));
|
||||||
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!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
achievement,
|
achievements,
|
||||||
body,
|
body,
|
||||||
character,
|
character,
|
||||||
character_achievement,
|
character_achievement,
|
||||||
|
Loading…
Reference in New Issue
Block a user