Support for serializing and deserializing a Pokemon
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Deukhoofd 2023-07-22 12:23:33 +02:00
parent f23fc43405
commit 12802ce542
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
15 changed files with 473 additions and 12 deletions

View File

@ -14,7 +14,7 @@ path = "src/lib.rs"
[features]
ffi = []
serde = ["dep:serde"]
serde = ["dep:serde", "dep:serde-xml-rs", "atomig/serde"]
wasm = ["dep:wasmer"]
default = ["serde", "wasm", "ffi"]
@ -53,6 +53,8 @@ hashbrown = "0.13.1"
indexmap = "1.8.2"
parking_lot = "0.12.1"
serde = { version = "1.0.137", optional = true, features = ["derive"] }
serde_repr = "0.1.12"
serde-xml-rs = { version = "0.6.0", optional = true }
wasmer = { version = "3.3.0", optional = true, default-features = false, features = ["sys", "wat", "llvm"] }
uuid = "1.2.2"
paste = { version = "1.0.8" }

View File

@ -1,3 +1,5 @@
use crate::dynamic_data::DynamicLibrary;
use crate::PkmnError;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
@ -10,7 +12,7 @@ pub struct LearnedMove {
/// The immutable move information of the move.
move_data: Arc<dyn MoveData>,
/// The maximal power points for this move.
max_pp: u8,
max_pp_modification: u8,
/// The amount of remaining power points. If this is 0, we can not use the move anymore.
remaining_pp: AtomicU8,
/// The way the move was learned.
@ -20,6 +22,7 @@ pub struct LearnedMove {
/// The different ways a move can be learned.
#[derive(Copy, Clone, Debug, Default)]
#[repr(u8)]
#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))]
pub enum MoveLearnMethod {
/// We do not know the learn method.
#[default]
@ -31,11 +34,11 @@ pub enum MoveLearnMethod {
impl LearnedMove {
/// Instantiate a new learned move.
pub fn new(move_data: Arc<dyn MoveData>, learn_method: MoveLearnMethod) -> Self {
let max_pp = move_data.base_usages();
let base_usages = move_data.base_usages();
Self {
move_data,
max_pp,
remaining_pp: AtomicU8::new(max_pp),
max_pp_modification: 0,
remaining_pp: AtomicU8::new(base_usages),
learn_method,
}
}
@ -46,8 +49,15 @@ impl LearnedMove {
}
/// The maximal power points for this move.
pub fn max_pp(&self) -> u8 {
self.max_pp
self.move_data.base_usages() + self.max_pp_modification
}
/// The amount by which the maximal power points have been modified for this move.
/// This could for example be due to PP Ups.
pub fn max_pp_modification(&self) -> u8 {
self.max_pp_modification
}
/// The amount of remaining power points. If this is 0, we can not use the move anymore.
pub fn remaining_pp(&self) -> u8 {
self.remaining_pp.load(Ordering::Relaxed)
@ -72,20 +82,44 @@ impl LearnedMove {
/// Set the remaining PP to the max amount of PP.
pub fn restore_all_uses(&self) {
self.remaining_pp.store(self.max_pp, Ordering::SeqCst);
self.remaining_pp.store(self.max_pp(), Ordering::SeqCst);
}
/// Restore the remaining PP by a certain amount. Will prevent it from going above max PP.
pub fn restore_uses(&self, mut uses: u8) {
self.remaining_pp
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
if x + uses > self.max_pp {
uses = self.max_pp - x;
let max = self.max_pp();
if x + uses > max {
uses = max - x;
}
Some(x + uses)
})
.ok();
}
/// Deserialize a learned move from a serialized learned move.
pub fn deserialize(
library: &Arc<dyn DynamicLibrary>,
value: &super::serialization::SerializedLearnedMove,
) -> anyhow_ext::Result<Self> {
Ok(Self {
move_data: {
let move_data =
library
.static_data()
.moves()
.get(&value.move_data)
.ok_or_else(|| PkmnError::InvalidMoveName {
move_name: value.move_data.clone(),
})?;
Arc::clone(&move_data)
},
max_pp_modification: value.max_pp_modification,
remaining_pp: AtomicU8::new(value.remaining_pp),
learn_method: value.learn_method,
})
}
}
#[cfg(test)]

View File

@ -35,3 +35,6 @@ mod pokemon;
mod pokemon_builder;
/// Data for a group of Pokemon belonging to a trainer.
mod pokemon_party;
#[cfg(feature = "serde")]
/// Data for serialization of models
pub(crate) mod serialization;

View File

@ -188,7 +188,7 @@ impl Pokemon {
ability_index: ability,
override_ability: None,
battle_data: RwLock::new(None),
moves: RwLock::new([None, None, None, None]),
moves: RwLock::new(Default::default()),
allowed_experience: false,
types: RwLock::new(form.types().to_vec()),
is_egg: false,
@ -222,6 +222,10 @@ impl Pokemon {
pub fn form(&self) -> Arc<dyn Form> {
self.data.form.read().clone()
}
/// Whether or not the Pokemon is showing as a different species than it actually is.
pub fn has_different_display_species(&self) -> bool {
self.data.display_species.is_some()
}
/// The species that should be displayed to the user. This handles stuff like the Illusion ability.
pub fn display_species(&self) -> Arc<dyn Species> {
if let Some(v) = &self.data.display_species {
@ -230,6 +234,10 @@ impl Pokemon {
self.species()
}
}
/// Whether or not the Pokemon is showing as a different form than it actually is.
pub fn has_different_display_form(&self) -> bool {
self.data.display_form.is_some()
}
/// The form that should be displayed to the user. This handles stuff like the Illusion ability.
pub fn display_form(&self) -> Arc<dyn Form> {
if let Some(v) = &self.data.display_form {
@ -351,6 +359,11 @@ impl Pokemon {
&self.data.stat_boost
}
/// Whether or not this Pokemon is still an egg, and therefore cannot battle.
pub fn is_egg(&self) -> bool {
self.data.is_egg
}
/// The stats of the Pokemon including the stat boosts
pub fn boosted_stats(&self) -> &Arc<StatisticSet<u32>> {
&self.data.boosted_stats
@ -833,6 +846,136 @@ impl Pokemon {
self.recalculate_flat_stats()
}
/// Converts the Pokemon into a serializable form.
#[cfg(feature = "serde")]
pub fn serialize(&self) -> Result<super::serialization::SerializedPokemon> {
self.into()
}
/// Deserializes a Pokemon from a serializable form.
#[cfg(feature = "serde")]
pub fn deserialize(
library: &Arc<dyn DynamicLibrary>,
value: &super::serialization::SerializedPokemon,
) -> Result<Self> {
let species = library
.static_data()
.species()
.get(&value.species)
.ok_or(PkmnError::InvalidSpeciesName {
species: value.species.clone(),
})?;
let form = species.get_form(&value.form).ok_or(PkmnError::InvalidFormName {
species: value.species.clone(),
form: value.form.clone(),
})?;
let display_species = if let Some(v) = &value.display_species {
Some(
library
.static_data()
.species()
.get(v)
.ok_or(PkmnError::InvalidSpeciesName { species: v.clone() })?,
)
} else {
None
};
let display_form = if let Some(v) = &value.display_form {
if let Some(display_species) = &display_species {
Some(display_species.get_form(v).ok_or(PkmnError::InvalidFormName {
species: display_species.name().clone(),
form: v.clone(),
})?)
} else {
None
}
} else {
None
};
Ok(Self {
data: Arc::new(PokemonData {
library: library.clone(),
species: RwLock::new(species),
form: RwLock::new(form),
display_species,
display_form,
level: Atomic::new(value.level),
experience: AtomicU32::new(value.experience),
personality_value: value.personality_value,
gender: RwLock::new(value.gender),
coloring: value.coloring,
held_item: RwLock::new({
if let Some(v) = &value.held_item {
Some(
library
.static_data()
.items()
.get(v)
.ok_or(PkmnError::InvalidItemName { item: v.clone() })?,
)
} else {
None
}
}),
current_health: AtomicU32::new(value.current_health),
weight: Atomic::new(value.weight),
height: Atomic::new(value.height),
flat_stats: Arc::new(Default::default()),
stat_boost: Arc::new(value.stat_boosts.clone()),
boosted_stats: Arc::new(Default::default()),
individual_values: Arc::new(value.individual_values.clone()),
effort_values: Arc::new(value.effort_values.clone()),
nature: {
library
.static_data()
.natures()
.get_nature(&value.nature)
.ok_or(PkmnError::InvalidNatureName {
nature: value.nature.clone(),
})?
},
nickname: value.nickname.clone(),
ability_index: value.ability_index,
override_ability: {
if let Some(v) = &value.override_ability {
Some(
library
.static_data()
.abilities()
.get(v)
.ok_or(PkmnError::InvalidAbilityName { ability: v.clone() })?,
)
} else {
None
}
},
battle_data: Default::default(),
moves: {
let mut moves: [Option<Arc<LearnedMove>>; MAX_MOVES] = Default::default();
for (i, v) in value.moves.iter().enumerate() {
if i >= MAX_MOVES {
break;
}
moves
.get_mut(i)
.replace(&mut Some(Arc::new(LearnedMove::deserialize(library, v)?)));
}
RwLock::new(moves)
},
allowed_experience: value.allowed_experience,
types: RwLock::new(value.types.clone()),
is_egg: value.is_egg,
is_caught: false,
held_item_trigger_script: Default::default(),
ability_script: Default::default(),
status_script: Default::default(),
volatile: Arc::new(Default::default()),
script_source_data: Default::default(),
}),
})
}
/// Take a weak reference to the Pokemon.
pub fn weak(&self) -> WeakPokemonReference {
WeakPokemonReference {

View File

@ -0,0 +1,4 @@
/// Serialization format for Pokemon data.
mod serialized_pokemon;
pub use serialized_pokemon::*;

View File

@ -0,0 +1,135 @@
use crate::defines::LevelInt;
use crate::dynamic_data::{MoveLearnMethod, Pokemon, VolatileScriptsOwner};
use crate::static_data::{AbilityIndex, ClampedStatisticSet, Gender, TypeIdentifier};
use crate::StringKey;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
/// The serializable version of a [Pokemon](Pokemon).
#[derive(Serialize, Deserialize)]
pub struct SerializedPokemon {
/// The name of the species of the Pokemon.
pub species: StringKey,
/// The name of the form of the Pokemon.
pub form: StringKey,
/// The name of the species of the Pokemon that is displayed, if overridden.
pub display_species: Option<StringKey>,
/// The name of the form of the Pokemon that is displayed, if overridden.
pub display_form: Option<StringKey>,
/// The level of the Pokemon.
pub level: LevelInt,
/// The experience of the Pokemon.
pub experience: u32,
/// The personality value of the Pokemon.
pub personality_value: u32,
/// The gender of the Pokemon.
pub gender: Gender,
/// The coloring of the Pokemon.
pub coloring: u8,
/// The held item of the Pokemon.
pub held_item: Option<StringKey>,
/// The current health of the Pokemon.
pub current_health: u32,
/// The weight of the Pokemon.
pub weight: f32,
/// The height of the Pokemon.
pub height: f32,
/// The stat boosts of the Pokemon.
pub stat_boosts: ClampedStatisticSet<i8, -6, 6>,
/// The individual values of the Pokemon.
pub individual_values: ClampedStatisticSet<u8, 0, 31>,
/// The effort values of the Pokemon.
pub effort_values: ClampedStatisticSet<u8, 0, 252>,
/// The nature of the Pokemon.
pub nature: StringKey,
/// The nickname of the Pokemon.
pub nickname: Option<String>,
/// The ability index of the Pokemon.
pub ability_index: AbilityIndex,
/// The ability that is currently active on the Pokemon, if overridden.
pub override_ability: Option<StringKey>,
/// The moves of the Pokemon.
pub moves: Vec<SerializedLearnedMove>,
/// Whether the Pokemon is allowed to gain experience.
pub allowed_experience: bool,
/// The types of the Pokemon.
pub types: Vec<TypeIdentifier>,
/// Whether the Pokemon is an egg.
pub is_egg: bool,
/// The volatile scripts of the Pokemon.
pub volatile_scripts: Vec<StringKey>,
}
/// The serializable version of a [LearnedMove](LearnedMove).
#[derive(Serialize, Deserialize)]
pub struct SerializedLearnedMove {
/// The name of the move.
pub move_data: StringKey,
/// A potential offset from the normal maximal power points.
pub max_pp_modification: u8,
/// The amount of remaining power points. If this is 0, we can not use the move anymore.
pub remaining_pp: u8,
/// The way the move was learned.
pub learn_method: MoveLearnMethod,
}
#[allow(clippy::from_over_into)] // From doesn't allow for Result.
impl Into<anyhow_ext::Result<SerializedPokemon>> for &Pokemon {
fn into(self) -> anyhow::Result<SerializedPokemon> {
Ok(SerializedPokemon {
species: self.species().name().clone(),
form: self.form().name().clone(),
display_species: if self.has_different_display_species() {
Some(self.display_species().name().clone())
} else {
None
},
display_form: if self.has_different_display_form() {
Some(self.display_form().name().clone())
} else {
None
},
level: self.level(),
experience: self.experience(),
personality_value: self.personality_value(),
gender: self.gender(),
coloring: self.coloring(),
held_item: {
let held_item = self.held_item().read();
held_item.as_ref().map(|held_item| held_item.name().clone())
},
current_health: self.current_health(),
weight: self.weight(),
height: self.height(),
stat_boosts: self.stat_boosts().deref().deref().clone(),
individual_values: self.individual_values().deref().deref().clone(),
effort_values: self.effort_values().deref().deref().clone(),
nature: self.library().static_data().natures().get_nature_name(self.nature())?,
nickname: self.nickname().clone(),
ability_index: *self.real_ability(),
override_ability: {
if self.is_ability_overriden() {
Some(self.active_ability()?.name().clone())
} else {
None
}
},
moves: self
.learned_moves()
.read()
.iter()
.flatten()
.map(|lm| SerializedLearnedMove {
move_data: lm.move_data().name().clone(),
max_pp_modification: lm.max_pp_modification(),
remaining_pp: lm.remaining_pp(),
learn_method: lm.learn_method(),
})
.collect(),
allowed_experience: self.allowed_experience_gain(),
types: self.types().to_vec(),
is_egg: self.is_egg(),
volatile_scripts: self.volatile_scripts().get_script_names(),
})
}
}

View File

@ -154,4 +154,14 @@ impl ScriptSet {
}
v
}
/// Gets the names of all scripts in the set.
pub fn get_script_names(&self) -> Vec<StringKey> {
let s = self.scripts.read();
let mut v = Vec::with_capacity(s.deref().len());
for script in s.deref().keys() {
v.push(script.clone());
}
v
}
}

View File

@ -450,3 +450,51 @@ extern "C" fn pokemon_learn_move(
extern "C" fn pokemon_clear_status(handle: FFIHandle<Pokemon>) {
handle.from_ffi_handle().clear_status()
}
/// Returns a serialized version of the Pokemon as xml.
#[cfg(feature = "serde")]
#[no_mangle]
extern "C" fn pokemon_serialize_as_xml(handle: FFIHandle<Pokemon>) -> FFIResult<OwnedPtrString> {
let serialized = match handle.from_ffi_handle().serialize() {
Ok(v) => v,
Err(err) => return FFIResult::err(err),
};
let serialized =
match serde_xml_rs::to_string(&serialized).map_err(|err| anyhow!("Could not serialize Pokemon: {}", err)) {
Ok(v) => v,
Err(err) => return FFIResult::err(err),
};
let cstring = match CString::new(serialized) {
Ok(v) => v,
Err(err) => return FFIResult::err(anyhow!("Could not convert serialized Pokemon to CString: {}", err)),
};
FFIResult::ok(OwnedPtrString(cstring.into_raw()))
}
/// Converts an XML representation of a Pokemon to a Pokemon.
#[cfg(feature = "serde")]
#[no_mangle]
extern "C" fn pokemon_deserialize_from_xml(
library: FFIHandle<Arc<dyn DynamicLibrary>>,
str: OwnedPtrString,
) -> FFIResult<FFIHandle<Pokemon>> {
let str = unsafe {
match CString::from_raw(str.0).to_str() {
Ok(v) => v.to_string(),
Err(err) => return FFIResult::err(anyhow!("Could not read CString: {}", err)),
}
};
let deserialized = match serde_xml_rs::from_str::<crate::dynamic_data::serialization::SerializedPokemon>(&str)
.map_err(|err| anyhow!("Could not deserialize Pokemon: {}", err))
{
Ok(v) => v,
Err(err) => return FFIResult::err(err),
};
let library = library.from_ffi_handle();
let pokemon = match Pokemon::deserialize(&library, &deserialized) {
Ok(v) => v,
Err(err) => return FFIResult::err(err),
};
FFIResult::ok(FFIHandle::get_handle(pokemon.into()))
}

View File

@ -8,7 +8,7 @@ mod static_data;
pub(self) use ffi_handle::*;
/// Helper type for clearer functions.
#[repr(C)]
#[repr(transparent)]
#[derive(Debug, Copy, Clone)]
struct OwnedPtrString(*mut c_char);

View File

@ -97,6 +97,12 @@ pub enum PkmnError {
/// The move that was requested
move_name: StringKey,
},
/// Unable to get an item
#[error("Unable to get item {item}")]
InvalidItemName {
/// The item that was requested
item: StringKey,
},
/// Unable to get nature
#[error("Unable to get nature {nature}")]
InvalidNatureName {
@ -109,6 +115,14 @@ pub enum PkmnError {
/// The species that was requested
species: StringKey,
},
/// Unable to get form
#[error("Unable to get form {form} for species {species}")]
InvalidFormName {
/// The species for which the form was requested
species: StringKey,
/// The form that was requested
form: StringKey,
},
/// Unable to get type
#[error("Unable to get type {type_id}")]
InvalidTypeIdentifier {

View File

@ -9,7 +9,8 @@ use crate::{PkmnError, StringKey};
/// A unique key that can be used to store a reference to a type. Opaque reference to a byte
/// internally.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, Atom)]
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct TypeIdentifier {
/// The unique internal value.
val: u8,

View File

@ -53,6 +53,7 @@ impl Ability for AbilityImpl {
/// An ability index allows us to find an ability on a form. It combines a bool for whether the
/// ability is hidden or not, and then an index of the ability.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct AbilityIndex {
/// Whether or not the ability we're referring to is a hidden ability.

View File

@ -3,6 +3,7 @@
/// Required for standard pokemon functions, but somewhat controversial nowadays. Consider adding a feature
/// that allows for a more progressive gender system for those that want it?
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))]
#[repr(u8)]
pub enum Gender {
/// The Pokemon has no gender.

View File

@ -205,6 +205,7 @@ where
/// A clamped statistic set holds the 6 normal stats for a Pokemon, but ensures it always remains
/// between two values (inclusive on the two values).
#[derive(Default, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClampedStatisticSet<T, const MIN: i64, const MAX: i64>
where
T: PrimitiveAtom,
@ -376,6 +377,28 @@ where
}
}
impl<T, const MIN: i64, const MAX: i64> Clone for ClampedStatisticSet<T, MIN, MAX>
where
T: PrimitiveAtom,
T: Atom,
T: PrimitiveAtomInteger,
<T as Atom>::Repr: PrimitiveAtomInteger,
T: AtomInteger,
T: NumCast,
T: PrimInt,
{
fn clone(&self) -> Self {
Self {
hp: Atomic::<T>::new(self.hp()),
attack: Atomic::<T>::new(self.attack()),
defense: Atomic::<T>::new(self.defense()),
special_attack: Atomic::<T>::new(self.special_attack()),
special_defense: Atomic::<T>::new(self.special_defense()),
speed: Atomic::<T>::new(self.speed()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,6 +1,7 @@
use arcstr::ArcStr;
use hashbrown::HashMap;
use parking_lot::RwLock;
use serde::{Deserializer, Serializer};
use std::borrow::Borrow;
use std::ffi::CStr;
use std::fmt::{Display, Formatter};
@ -170,6 +171,47 @@ const CRC_TABLE: &[u32] = &[
0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
];
#[cfg(feature = "serde")]
impl serde::Serialize for StringKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.str())
}
}
/// A visitor for deserializing a StringKey.
#[cfg(feature = "serde")]
struct StringKeyVisitor;
#[cfg(feature = "serde")]
impl<'de> serde::de::Visitor<'de> for StringKeyVisitor {
type Value = String;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(String::from(v))
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for StringKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = deserializer.deserialize_string(StringKeyVisitor)?;
Ok(StringKey::new(&str))
}
}
#[cfg(test)]
mod tests {
extern crate test;