diff --git a/.build.yml b/.build.yml index 1df2655..4d5e6b1 100644 --- a/.build.yml +++ b/.build.yml @@ -12,4 +12,4 @@ tasks: cargo install cargo-make - stable: | cd backend/ - cargo make test + cargo make build-config-and-test diff --git a/.gitignore b/.gitignore index 68c6f34..1b87342 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,8 @@ Cargo.lock hackagotchi.csv +/hcor +/config + /stead /slack diff --git a/Cargo.toml b/Cargo.toml index 5ebe61c..20180d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,6 @@ futures-channel = "0.3.5" [dependencies.hcor] git = "https://github.com/hackagotchi/hcor.git" -branch = "slim" +branch = "staging" # path = "../hcor" features = [ "message_derive" ] diff --git a/Makefile.toml b/Makefile.toml index c57b862..950ea3b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -23,6 +23,38 @@ args = ["test", "--all-features"] [tasks.test] clear = true -env = { "SERVER_URL" = "http://localhost:8000", "UPDATES_PER_SECOND" = 10000 } +env = { SERVER_URL = "http://localhost:8000", UPDATES_PER_SECOND = 1000, NO_CLIENT_TIMEOUT = 30 } dependencies = ["build-test-server"] run_task = { name = ["run-test-server", "test-against-server"], parallel = true } + +[tasks.build-config-and-test] +dependencies = [ "config" ] +env = { "CONFIG_PATH" = "./config" } +run_task = { name = ["test"], parallel = true } + +[tasks.clean-hcor] +ignore_errors = true +command = "rm" +args = [ "-rf", "./hcor" ] + +[tasks.clean-config] +ignore_errors = true +command = "rm" +args = [ "-rf", "./config" ] + +[tasks.build-config] +command = "cargo" +args = [ "r", "--bin", "verify", "--features=config_verify" ] + +[tasks.config] +script_runner = "@shell" +script = [ +''' +git clone https://github.com/hackagotchi/config.git --branch staging +git clone https://github.com/hackagotchi/hcor.git --branch staging +cd hcor +cargo r --bin verify --features=config_verify +export CONFIG_PATH=./config +''' +] +dependencies = [ "clean-config", "clean-hcor" ] diff --git a/src/from_csv.rs b/src/from_csv.rs index 9e2c17d..e14a64b 100644 --- a/src/from_csv.rs +++ b/src/from_csv.rs @@ -1,6 +1,6 @@ //! Dumps a CSV file of the old dynamodb format into the db. //! input CSVs are assumed to be generated with this tool: https://pypi.org/project/export-dynamodb/ -use hcor::{item, ItemId, TileId}; +use hcor::{item, Item, ItemId, TileId}; use serde::{Deserialize, Serialize}; /* To make the errors bearable. @@ -101,8 +101,9 @@ fn as_json(s: &str) -> Result { #[tokio::main] async fn main() -> Result<(), Box> { use chrono::{DateTime, Utc}; - use hcor::Hackstead; + use hcor::{item, plant, Hackstead}; use std::collections::HashMap; + use std::fs; pretty_env_logger::init(); @@ -111,6 +112,23 @@ async fn main() -> Result<(), Box> { .map_err(|e| format!("invalid csv: {}", e))?; let mut hacksteads: HashMap = HashMap::new(); + let item_rows_to_uuids: HashMap = serde_json::from_str( + &fs::read_to_string(&format!( + "{}/item_row_numbers_to_uuids.json", + &*hcor::config::CONFIG_PATH + )) + .unwrap(), + ) + .unwrap(); + let plant_rows_to_uuids: HashMap = serde_json::from_str( + &fs::read_to_string(&format!( + "{}/plant_row_numbers_to_uuids.json", + &*hcor::config::CONFIG_PATH + )) + .unwrap(), + ) + .unwrap(); + fn parse_date_time(dt: String) -> DateTime { DateTime::parse_from_str(&format!("{} +0000", dt), "%Y-%m-%dT%H:%M:%S%.fZ %z") .unwrap_or_else(|e| panic!("couldn't parse dt {}: {}", dt, e)) @@ -147,27 +165,18 @@ async fn main() -> Result<(), Box> { *found_profile = true; } 1 | 2 => { - let archetype_handle = r.archetype_handle.expect("item no archetype"); + let archetype_handle = r.archetype_handle.expect("item no archetype") as usize; let item_id = ItemId(uuid::Uuid::parse_str(&r.id).expect("item id not uuid")); - hs.inventory.push(item::Item { - item_id, - owner_id: hs.profile.steader_id, - archetype_handle: archetype_handle as usize, - gotchi: if let (Some(nickname), Some(_)) = (r.nickname, r.harvest_log) { - Some(item::Gotchi { nickname }) - } else { - None - }, - ownership_log: vec![item::LoggedOwner { - logged_owner_id: hs.profile.steader_id, - acquisition: item::Acquisition::Trade, - owner_index: 0, - }], - }) + let conf = item_rows_to_uuids[&archetype_handle]; + let mut item = + Item::from_conf(conf, hs.profile.steader_id, item::Acquisition::Trade); + item.item_id = item_id; + if let (Some(nickname), Ok(g)) = (r.nickname, item.gotchi_mut()) { + g.nickname = nickname; + } + hs.inventory.push(item); } 3 => { - use hcor::plant::{Craft, Effect}; - #[derive(serde::Serialize, serde::Deserialize)] struct OldPlant { pub xp: usize, @@ -191,16 +200,6 @@ async fn main() -> Result<(), Box> { pub item_archetype_handle: usize, pub effect_archetype_handle: usize, } - impl std::ops::Deref for OldPlant { - type Target = hcor::config::PlantArchetype; - - fn deref(&self) -> &Self::Target { - &hcor::config::CONFIG - .plant_archetypes - .get(self.archetype_handle as usize) - .expect("invalid archetype handle") - } - } let acquired = r.acquired.expect("tiles need acquired dates"); let tile_id = TileId(uuid::Uuid::parse_str(&r.id).expect("tile id not uuid")); @@ -213,25 +212,33 @@ async fn main() -> Result<(), Box> { acquired: parse_date_time(acquired), tile_id, owner_id: hs.profile.steader_id, - plant: p.map(|p| hcor::Plant { - xp: p.xp, - owner_id: hs.profile.steader_id, - archetype_handle: p.archetype_handle, - nickname: p.name.clone(), - tile_id, - lifetime_rubs: p.effects.len(), - effects: p - .effects - .into_iter() - .map(|e| Effect { - effect_id: hcor::plant::EffectId(uuid::Uuid::new_v4()), - effect_archetype_handle: e.effect_archetype_handle, - item_archetype_handle: e.item_archetype_handle, - }) - .collect(), - craft: p.craft.map(|c| Craft { - recipe_archetype_handle: c.recipe_archetype_handle, - }), + plant: p.map(|p| { + let conf = plant_rows_to_uuids[&p.archetype_handle]; + hcor::Plant { + owner_id: hs.profile.steader_id, + conf, + nickname: conf.name.clone(), + tile_id, + lifetime_rubs: p.effects.len(), + skills: { + let mut skills = plant::Skills::new(conf); + skills.xp = p.xp; + skills + }, + rub_effects: p + .effects + .into_iter() + .filter(|e| e.effect_archetype_handle == 0) + .flat_map(|e| { + plant::RubEffect::item_on_plant( + item_rows_to_uuids[&e.item_archetype_handle], + conf, + ) + .into_iter() + }) + .collect(), + craft: None, + } }), }) } diff --git a/src/hackstead/mod.rs b/src/hackstead/mod.rs index efb50e2..bd8a64f 100644 --- a/src/hackstead/mod.rs +++ b/src/hackstead/mod.rs @@ -18,22 +18,20 @@ fn user_path(iu: impl IdentifiesUser) -> String { } fn stead_path(is: impl IdentifiesSteader) -> String { - format!("stead/{}.json", is.steader_id()) + format!("stead/{}.bincode", is.steader_id()) } fn slack_path(slack: &str) -> String { - format!("slack/{}.json", slack) + format!("slack/{}.bincode", slack) } pub fn fs_get_stead(user_id: impl IdentifiesUser) -> Result { - Ok(serde_json::from_str(&fs::read_to_string(user_path( - user_id, - ))?)?) + Ok(bincode::deserialize(&fs::read(user_path(user_id))?)?) } pub fn fs_put_stead(hs: &Hackstead) -> Result<(), ServiceError> { let stead_path = stead_path(hs); - fs::write(&stead_path, serde_json::to_string(hs)?)?; + fs::write(&stead_path, bincode::serialize(hs)?)?; if let Some(s) = hs.profile.slack_id.as_ref() { fs::hard_link(&stead_path, &slack_path(s))?; diff --git a/src/lib.rs b/src/lib.rs index 2313c98..6be4537 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -194,51 +194,49 @@ //! # use uuid::Uuid; //! # use hcor::{ //! # id::{SteaderId, ItemId}, -//! # item::{Acquisition, Item, LoggedOwner}, -//! # wormhole::{AskedNote::ItemHatchResult, Note, AskMessage} +//! # item::{self, Item, LoggedOwner}, +//! # wormhole::{AskedNote::ItemHatchResult, Note, AskMessage}, +//! # config::evalput::Output, //! # }; //! # fn main() -> Result<(), Box> { -//! # let item_id = ItemId(Uuid::new_v4()); -//! # let owner_id = SteaderId(Uuid::new_v4()); //! assert_eq!( //! serde_json::from_value::(json!({ //! "Asked": { //! "ask_id": 1337, //! "note": { "ItemHatchResult": { -//! "Ok": [ -//! { -//! "item_id": item_id, -//! "owner_id": owner_id, -//! "archetype_handle": 5, +//! "Ok": { +//! "xp": 0, +//! "items": [{ +//! "item_id": "ff8e2ac7-7c8c-4cbc-973e-d1e278f1096a", +//! "owner_id": "1776527a-7d0c-4384-a427-df556d0e9294", +//! // Confs show where in the config to look for more info. +//! "conf": "9845a8fb-5697-45ca-b9de-c34a26d59bd2", //! "ownership_log": [ //! { -//! "logged_owner_id": owner_id, +//! "logged_owner_id": "1776527a-7d0c-4384-a427-df556d0e9294", //! "acquisition": "Farmed", //! "owner_index": 0, //! } //! ] -//! } -//! ] +//! }] +//! } //! }} //! } //! }))?, //! Note::Asked { //! ask_id: 1337, -//! note: ItemHatchResult(Ok(vec![ -//! Item { -//! item_id, -//! owner_id, -//! archetype_handle: 5, -//! gotchi: None, -//! ownership_log: vec![ -//! LoggedOwner { -//! logged_owner_id: owner_id, -//! owner_index: 0, -//! acquisition: Acquisition::Farmed -//! } -//! ] -//! } -//! ])) +//! note: ItemHatchResult(Ok(Output { +//! xp: 0, +//! items: vec![{ +//! let mut item = Item::from_conf( +//! hcor::CONFIG.item_named("Warp Powder").unwrap().conf, +//! SteaderId(Uuid::parse_str("1776527a-7d0c-4384-a427-df556d0e9294")?), +//! item::Acquisition::Farmed, +//! ); +//! item.item_id = ItemId(Uuid::parse_str("ff8e2ac7-7c8c-4cbc-973e-d1e278f1096a")?); +//! item +//! }] +//! })) //! } //! ); //! // or perhaps the Ask fails, @@ -339,27 +337,26 @@ //! ``` //! # use uuid::Uuid; //! # use serde_json::json; -//! # use hcor::{id::TileId, plant, wormhole::{Note, RudeNote::RubEffectFinish}}; +//! # use hcor::{id::TileId, plant::{self, RubEffectId}, wormhole::{Note, RudeNote::RubEffectFinish}}; //! # fn main() -> Result<(), Box> { -//! # let tile_id = TileId(Uuid::new_v4()); -//! # let effect_id = plant::EffectId(Uuid::new_v4()); //! assert_eq!( //! serde_json::from_value::(json!({ //! "Rude": { "RubEffectFinish": { -//! "tile_id": tile_id, +//! "tile_id": "fb5e9693-650f-432c-bc9a-4f4b980a80fe", //! "effect": { -//! "effect_id": effect_id, -//! "item_archetype_handle": 5, -//! "effect_archetype_handle": 0, +//! "effect_id": "bcf3d635-dae5-4785-9319-dd1739988320", +//! "item_conf": "9845a8fb-5697-45ca-b9de-c34a26d59bd2", +//! // It's possible to receive multiple effects from a single item; which is this? +//! "effect_index": 0, //! } //! }} //! }))?, //! Note::Rude(RubEffectFinish { -//! tile_id, -//! effect: plant::Effect { -//! effect_id, -//! item_archetype_handle: 5, -//! effect_archetype_handle: 0, +//! tile_id: TileId(Uuid::parse_str("fb5e9693-650f-432c-bc9a-4f4b980a80fe")?), +//! effect: plant::RubEffect { +//! effect_id: RubEffectId(Uuid::parse_str("bcf3d635-dae5-4785-9319-dd1739988320")?), +//! item_conf: hcor::CONFIG.item_named("Warp Powder").unwrap().conf, +//! effect_index: 0, //! } //! }) //! ); @@ -394,8 +391,6 @@ //! # use serde_json::json; //! # use hcor::{id::TileId, plant, wormhole::{Note, EditNote}}; //! # fn main() -> Result<(), Box> { -//! # let tile_id = TileId(Uuid::new_v4()); -//! # let effect_id = plant::EffectId(Uuid::new_v4()); //! assert_eq!( //! serde_json::from_value::(json!({ //! "Edit": { "Json": @@ -445,7 +440,7 @@ #![allow(clippy::many_single_char_names)] //#![forbid(missing_docs)] #![forbid(unsafe_code)] -#![forbid(intra_doc_link_resolution_failure)] +#![forbid(broken_intra_doc_links)] use actix_web::{error::ResponseError, HttpResponse}; use log::*; use std::fmt; @@ -547,6 +542,13 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(e: bincode::Error) -> ServiceError { + error!("bincode error: {}", e); + ServiceError::InternalServerError + } +} + impl From for ServiceError { fn from(e: std::io::Error) -> ServiceError { error!("io error: {}", e); @@ -563,9 +565,3 @@ impl From for ServiceError { ServiceError::InternalServerError } } - -impl From for ServiceError { - fn from(e: hcor::ConfigError) -> ServiceError { - ServiceError::bad_request(&e) - } -} diff --git a/src/wormhole/server/mod.rs b/src/wormhole/server/mod.rs index 145ad50..02ce6e6 100644 --- a/src/wormhole/server/mod.rs +++ b/src/wormhole/server/mod.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; -use actix::{dev::Envelope, Actor, Addr, AsyncContext, Context, Handler, Message}; +#[cfg(feature = "autoclose")] +use actix::AsyncContext; +use actix::{dev::Envelope, Actor, Addr, Context, Handler, Message}; use log::*; use super::session::{self, Session}; @@ -37,6 +39,7 @@ pub struct Disconnect(pub SteaderId); impl Handler for Server { type Result = (); + #[allow(unused_variables)] fn handle(&mut self, Disconnect(u): Disconnect, ctx: &mut Context) { self.sessions.remove(&u); @@ -44,13 +47,31 @@ impl Handler for Server { #[cfg(feature = "autoclose")] if self.sessions.len() == 0 { - warn!("ending process in 30 seconds if no more users log on"); - ctx.run_later(std::time::Duration::from_secs(30), |act, _| { - if act.sessions.len() == 0 { - warn!("ending process"); - std::process::exit(0) - } - }); + lazy_static::lazy_static! { + pub static ref NO_CLIENT_TIMEOUT: u64 = { + std::env::var("NO_CLIENT_TIMEOUT") + .map_err(|e| e.to_string()) + .and_then(|x| x.parse::().map_err(|e| e.to_string())) + .unwrap_or_else(|e| { + log::warn!("NO_CLIENT_TIMEOUT err, defaulting to 100. err: {}", e); + 100 + }) + }; + } + + warn!( + "ending process in {} seconds if no more users log on", + *NO_CLIENT_TIMEOUT + ); + ctx.run_later( + std::time::Duration::from_secs(*NO_CLIENT_TIMEOUT), + |act, _| { + if act.sessions.len() == 0 { + warn!("ending process"); + std::process::exit(0) + } + }, + ); } } } diff --git a/src/wormhole/server/throw.rs b/src/wormhole/server/throw.rs index 7c62b5a..ddd6a79 100644 --- a/src/wormhole/server/throw.rs +++ b/src/wormhole/server/throw.rs @@ -195,7 +195,7 @@ mod test { timeout(SERVER_WAIT, until).await.expect("timeout").unwrap(); // verify the items log eve then bob as the owners - bobstead = Hackstead::fetch(&bobstead).await.unwrap(); + bobstead.server_sync().await.unwrap(); let evestead = Hackstead::fetch(eve_steader_id).await.unwrap(); for item in &items { assert!( @@ -251,7 +251,7 @@ mod test { // eve let t2 = std::thread::spawn(move || { System::new("test").block_on(async move { - const ITEM_ARCHETYPE: hcor::config::ArchetypeHandle = 0; + let item_conf = *hcor::CONFIG.items.keys().next().unwrap(); const ITEM_SPAWN_COUNT: usize = 10; let evestead = Hackstead::register().await.unwrap(); @@ -259,7 +259,7 @@ mod test { // spawn eve items and verify that they log her as the owner let items = bobstead - .spawn_items(ITEM_ARCHETYPE, ITEM_SPAWN_COUNT) + .spawn_items(item_conf, ITEM_SPAWN_COUNT) .await .unwrap(); for item in &items { diff --git a/src/wormhole/session/hackstead_guard.rs b/src/wormhole/session/hackstead_guard.rs new file mode 100644 index 0000000..aa777dc --- /dev/null +++ b/src/wormhole/session/hackstead_guard.rs @@ -0,0 +1,158 @@ +use super::Orifice; +use hcor::serde_diff::Diff; +use hcor::{wormhole::EditNote, Hackstead}; +use std::fmt; + +/// We need all changes to a Hackstead to be also sent to the client; +/// to insure that we do not mutate the hackstead without also sending changes to the client, +/// we have this HacksteadGuard struct. +pub struct HacksteadGuard { + hackstead: Hackstead, +} + +#[derive(Debug)] +pub enum DiffError { + Json(serde_json::Error), + Bincode(bincode::Error), + Patching(std::io::Error), +} +type DiffResult = Result; +impl From for DiffError { + fn from(e: bincode::Error) -> DiffError { + DiffError::Bincode(e) + } +} +impl From for DiffError { + fn from(e: serde_json::Error) -> DiffError { + DiffError::Json(e) + } +} +impl From for DiffError { + fn from(e: std::io::Error) -> DiffError { + DiffError::Patching(e) + } +} +impl fmt::Display for DiffError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "couldn't create diff for EditNote: ")?; + + match self { + DiffError::Bincode(e) => { + write!(f, "couldn't serialize/deserialize hackstead bincode: {}", e) + } + DiffError::Json(e) => write!(f, "couldn't serialize/deserialize hackstead json: {}", e), + DiffError::Patching(e) => write!(f, "couldn't create bincode binary patch: {}", e), + } + } +} + +impl HacksteadGuard { + pub fn new(hs: Hackstead) -> Self { + HacksteadGuard { + hackstead: hs.clone(), + } + } + + fn json_diff(&mut self, new: &Hackstead) -> DiffResult { + Ok(serde_json::to_string(&Diff::serializable( + &self.hackstead, + &new, + ))?) + } + + fn bincode_diff(&mut self, new: &Hackstead) -> DiffResult> { + let old_bincode = bincode::serialize(&self.hackstead)?; + let new_bincode = bincode::serialize(&new)?; + + let mut diff_data = vec![]; + hcor::bidiff::simple_diff(&old_bincode, &new_bincode, &mut diff_data)?; + + Ok(diff_data) + } + + pub fn set(&mut self, new: Hackstead, orifice: Orifice) -> DiffResult { + let note = match orifice { + Orifice::Json => EditNote::Json(self.json_diff(&new)?), + Orifice::Bincode => EditNote::Bincode(self.bincode_diff(&new)?), + }; + + self.hackstead = new; + Ok(note) + } +} + +/* +#[cfg(test)] +fn hs_with_rubbed_plant() -> Hackstead { + use hcor::{plant, Plant}; + + let mut hs = Hackstead::new_user(Some("")); + let tile_id = hs.free_tile().unwrap().tile_id; + let conf = hcor::CONFIG.seeds().next().unwrap().0; + let mut plant = Plant::from_conf(hs.profile.steader_id, tile_id, conf); + plant.rub_effects.extend(plant::RubEffect::item_on_plant( + hcor::CONFIG.item_named("Warp Powder").unwrap().conf, + conf, + )); + hs.tile_mut(tile_id).unwrap().plant = Some(plant); + hs +} + +#[test] +fn rub_effect_diff() { + use bincode::Options; + use hcor::serde_diff::Apply; + + let mut old = hs_with_rubbed_plant(); + + let mut new = old.clone(); + new.plants_mut().next().unwrap().rub_effects.pop().unwrap(); + + let diff: Diff = Diff::serializable(&old, &new); + + let json = serde_json::to_string_pretty(&diff).unwrap(); + println!("{}", json); + + let bincode_data = bincode::serialize(&diff).unwrap(); + bincode::options() + .deserialize_seed(Apply::deserializable(&mut old), &bincode_data) + .unwrap(); +} + +#[test] +fn tile_diff() { + use bincode::Options; + use hcor::serde_diff::Apply; + + let mut old = Hackstead::new_user(Some("")); + + let mut new = old.clone(); + new.land.pop().unwrap(); + + let diff: Diff = Diff::serializable(&old, &new); + + { + let json = serde_json::to_string_pretty(&diff).unwrap(); + println!("{}", json); + } + + let bincode_data = bincode::serialize(&diff).unwrap(); + + { + let diff: Diff = bincode::deserialize(&bincode_data).unwrap(); + let json = serde_json::to_string_pretty(&diff).unwrap(); + println!("{}", json); + } + + bincode::options() + .deserialize_seed(Apply::deserializable(&mut old), &bincode_data) + .unwrap(); +}*/ + +impl std::ops::Deref for HacksteadGuard { + type Target = Hackstead; + + fn deref(&self) -> &Self::Target { + &self.hackstead + } +} diff --git a/src/wormhole/session/item/hatch.rs b/src/wormhole/session/item/hatch.rs index 38e22f0..c751cba 100644 --- a/src/wormhole/session/item/hatch.rs +++ b/src/wormhole/session/item/hatch.rs @@ -1,5 +1,5 @@ use super::SessSend; -use hcor::{id, item, Item, ItemId}; +use hcor::{config::evalput, id, item, Item, ItemId}; use std::fmt; #[derive(Debug)] @@ -23,34 +23,26 @@ impl fmt::Display for Error { NotConfigured(i) => write!( f, "provided item {}, which, as a {}[{}], is not configured to be hatched", - i.item_id, i.name, i.archetype_handle + i.item_id, i.name, i.conf ), } } } -pub fn hatch(ss: &mut SessSend, item_id: ItemId) -> Result, Error> { +pub fn hatch(ss: &mut SessSend, item_id: ItemId) -> Result, Error> { let item = ss.take_item(item_id)?; let hatch_table = item .hatch_table .as_ref() .ok_or_else(|| NotConfigured(item.clone()))?; - let items = hcor::config::spawn(&hatch_table, &mut rand::thread_rng()) - .map(|item_name| { - Item::from_archetype( - hcor::CONFIG.find_possession(&item_name)?, - item.owner_id, - item::Acquisition::Hatched, - ) - }) - .collect::, hcor::ConfigError>>() - .unwrap_or_else(|e| { - panic!("hatch table produced: {}", e); - }); - - ss.inventory.append(&mut items.clone()); - Ok(items) + let output = hatch_table + .evaluated(&mut rand::thread_rng()) + .spawned(item.owner_id, item::Acquisition::Hatched); + + output.copy_into(&mut *ss); + + Ok(output) } #[cfg(all(test, feature = "hcor_client"))] @@ -69,21 +61,21 @@ mod test { let mut bobstead = Hackstead::register().await?; debug!("finding prequisites in config..."); - let unhatchable_arch = hcor::CONFIG - .possession_archetypes - .iter() + let unhatchable_config = hcor::CONFIG + .items + .values() .find(|x| x.hatch_table.is_none()) .expect("no unhatchable items in config?"); - let hatchable_arch = hcor::CONFIG - .possession_archetypes - .iter() + let hatchable_config = hcor::CONFIG + .items + .values() .find(|x| x.hatch_table.is_some()) .expect("no unhatchable items in config?"); debug!("to prepare, we need to spawn bob hatchable and unhatchable items."); - let unhatchable_item = unhatchable_arch.spawn().await?; - let hatchable_item = hatchable_arch.spawn().await?; - bobstead = Hackstead::fetch(&bobstead).await?; + let unhatchable_item = unhatchable_config.spawn().await?; + let hatchable_item = hatchable_config.spawn().await?; + bobstead.server_sync().await?; debug!("let's start off by hatching the unhatchable and making sure that doesn't work."); match unhatchable_item.hatch().await { @@ -95,27 +87,36 @@ mod test { } assert_eq!( bobstead.inventory.len(), - Hackstead::fetch(&bobstead).await?.inventory.len(), + { + bobstead.server_sync().await?; + bobstead.inventory.len() + }, "failing to hatch modified inventory item count somehow", ); - debug!("great, now let's try actually hatching something hatchable!"); - let hatched_items = hatchable_item.hatch().await?; + debug!( + "great, now let's try actually hatching something hatchable ({})!", + hatchable_item.name + ); + let hatch_output = hatchable_item.hatch().await?; debug!( "let's make sure bob's inventory grew proportionally \ to the amount of items hatching produced" ); let starting_inventory = bobstead.inventory.clone(); - let new_inventory = Hackstead::fetch(&bobstead).await?.inventory; + + error!("hatched {} items", hatch_output.items.len()); + bobstead.server_sync().await?; + let new_inventory = bobstead.inventory.clone(); assert_eq!( - hatched_items.len(), + hatch_output.items.len(), (new_inventory.len() - (starting_inventory.len() - 1)), "the number of items in bob's inventory changed differently \ than the number of items produced by hatching this item, somehow. \ starting inventory: {:#?}\nitems hatched: {:#?}\nnew inventory: {:#?}", starting_inventory, - hatched_items, + hatch_output.items, new_inventory, ); @@ -134,7 +135,6 @@ mod test { debug!("kill bob so he's not left in the database"); bobstead.slaughter().await?; - Ok(()) } } diff --git a/src/wormhole/session/item/mod.rs b/src/wormhole/session/item/mod.rs index d7e36cd..a3bf7fb 100644 --- a/src/wormhole/session/item/mod.rs +++ b/src/wormhole/session/item/mod.rs @@ -13,10 +13,8 @@ use hatch::hatch; pub(super) fn handle_ask(ss: &mut SessSend, ask: ItemAsk) -> HandledAskKind { HandledAskKind::Direct(match ask { - Spawn { - item_archetype_handle: iah, - amount, - } => ItemSpawnResult(strerr(spawn(ss, iah, amount))), + Spawn { item_conf, amount } => ItemSpawnResult(strerr(spawn(ss, item_conf, amount))), + GotchiNickname { .. } => GotchiNicknameResult(strerr(Err("unimplemented route"))), Throw { receiver_id, item_ids, diff --git a/src/wormhole/session/item/spawn.rs b/src/wormhole/session/item/spawn.rs index 2d9ba17..9409cf3 100644 --- a/src/wormhole/session/item/spawn.rs +++ b/src/wormhole/session/item/spawn.rs @@ -1,19 +1,13 @@ use super::SessSend; -use hcor::{item, ConfigError, Item}; +use hcor::{item, Item}; use std::fmt; #[derive(Debug)] pub enum Error { - NoSuchItemConf(ConfigError), + NoSuchItemConf(item::Conf), } use Error::*; -impl From for Error { - fn from(e: hcor::ConfigError) -> Error { - Error::NoSuchItemConf(e) - } -} - impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "couldn't spawn items: ")?; @@ -23,16 +17,20 @@ impl fmt::Display for Error { } } -pub fn spawn(ss: &mut SessSend, item_conf: usize, amount: usize) -> Result, Error> { +pub fn spawn(ss: &mut SessSend, item_conf: item::Conf, amount: usize) -> Result, Error> { + if item_conf.try_lookup().is_none() { + return Err(NoSuchItemConf(item_conf)); + } + let items: Vec = (0..amount) .map(|_| { - Item::from_archetype_handle( + Item::from_conf( item_conf, ss.profile.steader_id, item::Acquisition::spawned(), ) }) - .collect::>()?; + .collect(); ss.inventory.append(&mut items.clone()); Ok(items) @@ -43,7 +41,8 @@ mod test { #[actix_rt::test] /// NOTE: requires that at least one item exists in the config! async fn spawn() -> hcor::ClientResult<()> { - use super::test::{ITEM_ARCHETYPE, ITEM_SPAWN_COUNT}; + const ITEM_SPAWN_COUNT: _ = 10; + let item_conf = *hcor::CONFIG.items().keys().next().unwrap(); use hcor::Hackstead; use log::*; @@ -59,16 +58,14 @@ mod test { hackstead .inventory .iter() - .filter(|i| i.archetype_handle == ITEM_ARCHETYPE) + .filter(|i| i.conf == item_conf) .count() } let starting_item_count = count_relevant_items(&bobstead); debug!("spawn bob some items and refresh his stead"); - let items = bobstead - .spawn_items(ITEM_ARCHETYPE, ITEM_SPAWN_COUNT) - .await?; - bobstead = Hackstead::fetch(&bobstead).await?; + let items = bobstead.spawn_items(ITEM_CONF, ITEM_SPAWN_COUNT).await?; + bobstead.server_sync().await?; debug!("make sure those new items are in there"); assert_eq!( diff --git a/src/wormhole/session/mod.rs b/src/wormhole/session/mod.rs index 687e209..c8d69da 100644 --- a/src/wormhole/session/mod.rs +++ b/src/wormhole/session/mod.rs @@ -9,13 +9,15 @@ use log::*; use super::server::{self, Server}; use hcor::{ - wormhole::{AskMessage, AskedNote, EditNote, CLIENT_TIMEOUT, HEARTBEAT_INTERVAL}, + wormhole::{AskMessage, AskedNote, CLIENT_TIMEOUT, HEARTBEAT_INTERVAL}, Hackstead, IdentifiesSteader, Note, UPDATE_INTERVAL, }; +mod hackstead_guard; mod item; mod ticker; mod tile; +use hackstead_guard::HacksteadGuard; use tile::plant; /// Which opening to the wormhole are they making use of? @@ -32,7 +34,7 @@ pub enum Orifice { /// that it can give it Notes to send down the Wormhole (or in this case, Websocket) to be displayed /// to the user. pub struct Session { - hackstead: Hackstead, + hackstead: HacksteadGuard, heartbeat: Instant, orifice: Orifice, server: Addr, @@ -43,13 +45,13 @@ type SessionContext = ws::WebsocketContext; impl Session { /// Constructs a new wormhole session from the uuid of the user who owns this session /// and an address which points to the Server. - pub fn new(mut hackstead: Hackstead, srv: &Addr, orifice: Orifice) -> Self { + pub fn new(mut hs: Hackstead, srv: &Addr, orifice: Orifice) -> Self { Self { heartbeat: Instant::now(), server: srv.clone(), - ticker: ticker::Ticker::new(&mut hackstead), + ticker: ticker::Ticker::new(&mut hs), orifice, - hackstead, + hackstead: HacksteadGuard::new(hs), } } @@ -114,10 +116,12 @@ impl Session { #[allow(clippy::unused_self)] fn tick(&self, ctx: &mut SessionContext) { - ctx.run_interval(*UPDATE_INTERVAL, |act, ctx| { - let mut ticker = std::mem::take(&mut act.ticker); - ticker.tick(act, ctx); - act.ticker = ticker; + ctx.run_interval(*UPDATE_INTERVAL, |ses, ctx| { + use actix::fut::WrapFuture; + let mut ss = SessSend::new(ses.hackstead.clone()); + let submit = ses.ticker.tick(&mut ss); + let fut = ses.apply_change(ctx, submit, ss); + ctx.wait(fut.into_actor(ses)) }); } } @@ -170,9 +174,7 @@ impl Handler for Session { type Result = Hackstead; fn handle(&mut self, GetStead: GetStead, _: &mut Self::Context) -> Self::Result { - let mut hs = self.hackstead.clone(); - hs.timers = self.ticker.timers.clone(); - hs + self.hackstead.clone() } } @@ -342,35 +344,27 @@ impl SessSend { /// Consumes a `SessSend`, sending all of the desired changes to the user's Session to be /// applied. pub fn submit(self, session: &mut Session, ctx: &mut SessionContext) { - use hcor::serde_diff::Diff; - - let Self { - hackstead: mut new, + let SessSend { + hackstead, mut pending_notes, pending_timers, .. } = self; - let old = session.hackstead.clone(); - assert_eq!(new.local_version, old.local_version); - pending_notes.push(Note::Edit(match session.orifice { - Orifice::Json => { - EditNote::Json(serde_json::to_string(&Diff::serializable(&old, &new)).unwrap()) + if *session.hackstead != hackstead { + match session.hackstead.set(hackstead, session.orifice) { + Err(e) => error!("couldn't make edit note: {}", e), + Ok(o) => pending_notes.push(Note::Edit(o)), } - Orifice::Bincode => { - EditNote::Bincode(bincode::serialize(&Diff::serializable(&old, &new)).unwrap()) - } - })); - new.local_version += 1; - session.hackstead = new; - - for n in pending_notes { - session.send_note(ctx, &n); } for t in pending_timers { session.ticker.start(t); } + + for n in pending_notes { + session.send_note(ctx, &n); + } } } impl std::ops::Deref for SessSend { diff --git a/src/wormhole/session/ticker/finish.rs b/src/wormhole/session/ticker/finish.rs index 9aa85cc..a71147b 100644 --- a/src/wormhole/session/ticker/finish.rs +++ b/src/wormhole/session/ticker/finish.rs @@ -1,9 +1,8 @@ -use super::SessionContext; +use super::SessSend; use hcor::{ id, plant::{Timer, TimerKind}, wormhole::RudeNote, - Hackstead, }; use std::fmt; @@ -28,26 +27,23 @@ impl fmt::Display for Error { } pub fn finish_timer( - hs: &mut Hackstead, - _: &mut SessionContext, + ss: &mut SessSend, Timer { tile_id, kind, .. }: Timer, ) -> Result { use TimerKind::*; - let plant = hs.plant_mut(tile_id)?; + let plant = ss.plant_mut(tile_id)?; Ok(match kind { Yield => RudeNote::YieldFinish { - items: vec![], - xp: 0, + output: Default::default(), tile_id, }, Craft { recipe_index } => RudeNote::CraftFinish { - items: vec![], - xp: 0, + output: Default::default(), tile_id, }, Rub { effect_id } => RudeNote::RubEffectFinish { - effect: plant.take_effect(effect_id)?, + effect: plant.take_rub_effect(effect_id)?, tile_id, }, }) diff --git a/src/wormhole/session/ticker/mod.rs b/src/wormhole/session/ticker/mod.rs index 140fafc..26e093a 100644 --- a/src/wormhole/session/ticker/mod.rs +++ b/src/wormhole/session/ticker/mod.rs @@ -1,4 +1,4 @@ -use super::{Session, SessionContext}; +use super::{SessSend, SessSendSubmit}; use hcor::{plant, Hackstead, Note}; mod finish; @@ -25,7 +25,7 @@ impl Ticker { self.timers.push(timer); } - pub fn tick(&mut self, ses: &mut Session, ctx: &mut SessionContext) { + pub fn tick(&mut self, ss: &mut SessSend) -> SessSendSubmit { for (i, t) in &mut self.timers.iter_mut().enumerate() { t.until_finish -= 1.0; @@ -46,10 +46,12 @@ impl Ticker { Lifecycle::Annual => self.timers.swap_remove(i), }; - match finish_timer(&mut ses.hackstead, ctx, timmy) { - Ok(n) => ses.send_note(ctx, &Note::Rude(n)), + match finish_timer(ss, timmy) { + Ok(n) => ss.send_note(Note::Rude(n)), Err(e) => log::error!("error finishing timer {:#?}: {}", timmy, e), } } + + SessSendSubmit::Submit } } diff --git a/src/wormhole/session/ticker/test/plant_yield.rs b/src/wormhole/session/ticker/test/plant_yield.rs index e02cd24..489ea50 100644 --- a/src/wormhole/session/ticker/test/plant_yield.rs +++ b/src/wormhole/session/ticker/test/plant_yield.rs @@ -1,23 +1,17 @@ #[actix_rt::test] async fn plant_yield() -> hcor::ClientResult<()> { use super::true_or_timeout; - use hcor::{wormhole::RudeNote::*, Hackstead, IdentifiesTile, CONFIG}; + use hcor::{wormhole::RudeNote::*, Hackstead, IdentifiesTile}; use log::*; // attempt to establish logging, do nothing if it fails // (it probably fails because it's already been established in another test) drop(pretty_env_logger::try_init()); - let (seed_arch, yield_duration) = hcor::CONFIG + let (seed_config, yield_duration) = hcor::CONFIG .seeds() - .filter_map(|(seed, seed_arch)| { - Some(( - seed_arch, - CONFIG - .find_plant(&seed.grows_into) - .ok()? - .base_yield_duration?, - )) + .filter_map(|(grows_into, seed_config)| { + Some((seed_config, grows_into.base_yield_duration?)) }) .min_by_key(|(_, yd)| *yd as usize) .expect("no seeds in config that yield?"); @@ -29,7 +23,7 @@ async fn plant_yield() -> hcor::ClientResult<()> { let plant = bobstead .free_tile() .unwrap() - .plant_seed(&seed_arch.spawn().await?) + .plant_seed(&seed_config.spawn().await?) .await?; let tid = plant.tile_id(); diff --git a/src/wormhole/session/ticker/test/rub_effect.rs b/src/wormhole/session/ticker/test/rub_effect.rs index b38ab7d..df455ef 100644 --- a/src/wormhole/session/ticker/test/rub_effect.rs +++ b/src/wormhole/session/ticker/test/rub_effect.rs @@ -3,20 +3,21 @@ async fn plant_rub_wear_off() -> hcor::ClientResult<()> { use super::true_or_timeout; use futures::{stream, StreamExt}; - use hcor::{wormhole::RudeNote::*, Hackstead}; + use hcor::{plant::RubEffect, wormhole::RudeNote::*, Hackstead}; use log::*; // attempt to establish logging, do nothing if it fails // (it probably fails because it's already been established in another test) drop(pretty_env_logger::try_init()); - let (seed_arch, rub_wear_off_arch) = hcor::CONFIG + let (seed_config, rub_wear_off_config) = hcor::CONFIG .seeds() - .find_map(|(seed, seed_arch)| { + .find_map(|(grows_into, seed_config)| { Some(( - seed_arch, - hcor::CONFIG.possession_archetypes.iter().find(|a| { - a.rub_effects_for_plant(&seed.grows_into) + seed_config, + hcor::CONFIG.items.keys().find(|c| { + RubEffect::item_on_plant(**c, grows_into) + .iter() .any(|e| e.duration.is_some()) })?, )) @@ -28,14 +29,14 @@ async fn plant_rub_wear_off() -> hcor::ClientResult<()> { // make plant info!("spawning first item"); - let seed_item = seed_arch.spawn().await?; + let seed_item = seed_config.spawn().await?; info!("seed item spawn"); let tile = bobstead.free_tile().expect("new hackstead no open tiles"); let plant = tile.plant_seed(&seed_item).await?; // rub 2 items, simultaneously - for i in 0..2 { - let rub_item = rub_wear_off_arch.spawn().await?; + for i in 0..2_usize { + let rub_item = rub_wear_off_config.spawn().await?; let effects = plant.rub_with(&rub_item).await?; stream::iter( diff --git a/src/wormhole/session/tile/mod.rs b/src/wormhole/session/tile/mod.rs index c01d20b..fac5c16 100644 --- a/src/wormhole/session/tile/mod.rs +++ b/src/wormhole/session/tile/mod.rs @@ -26,7 +26,7 @@ impl fmt::Display for Error { NotConfigured(item) => write!( f, "item {}[{}] is not configured to unlock land", - item.name, item.archetype_handle, + item.name, item.conf, ), Ineligible => write!(f, "you aren't eligible to unlock more land."), } @@ -66,19 +66,19 @@ mod test { // (it probably fails because it's already been established in another test) drop(pretty_env_logger::try_init()); - let requires_xp_arch = hcor::CONFIG + let requires_xp_config = hcor::CONFIG .land_unlockers() .find(|(ul, _)| ul.requires_xp) .expect("no items in config that unlock land and require xp to do so?") .1; - let no_requires_xp_arch = hcor::CONFIG + let no_requires_xp_config = hcor::CONFIG .land_unlockers() .find(|(ul, _)| !ul.requires_xp) .expect("no items in config that unlock land and don't require xp to do so?") .1; - let non_land_redeemable_arch = hcor::CONFIG - .possession_archetypes - .iter() + let non_land_redeemable_config = hcor::CONFIG + .items + .values() .find(|x| x.unlocks_land.is_none()) .expect("no items in config that don't unlock land?"); @@ -110,7 +110,7 @@ mod test { (false, Ok(tile)) => panic!("/tile/new unexpectedly returned tile: {:#?}", tile), }; - *bobstead = Hackstead::fetch(&*bobstead).await?; + bobstead.server_sync().await?; assert_eq!( bobstead.land.len(), @@ -133,7 +133,7 @@ mod test { } debug!("spawn bob an item he can redeem for a tile if he has enough xp"); - let requires_xp_item = requires_xp_arch.spawn().await?; + let requires_xp_item = requires_xp_config.spawn().await?; debug!("try and redeem this item bob doesn't have enough xp to redeem for land"); new_tile_assuming( @@ -148,7 +148,7 @@ mod test { .await?; debug!("spawn an item bob can redeem for land without having enough xp"); - let no_requires_xp_item = no_requires_xp_arch.spawn().await?; + let no_requires_xp_item = no_requires_xp_config.spawn().await?; debug!("try and redeem that item, this should actually work"); new_tile_assuming( @@ -201,7 +201,7 @@ mod test { .await?; debug!("try to redeem the non-land-redeemable item for land"); - let non_land_redeemable_item = non_land_redeemable_arch.spawn().await?; + let non_land_redeemable_item = non_land_redeemable_config.spawn().await?; new_tile_assuming( &mut bobstead, &non_land_redeemable_item, diff --git a/src/wormhole/session/tile/plant/mod.rs b/src/wormhole/session/tile/plant/mod.rs index de013bb..2753680 100644 --- a/src/wormhole/session/tile/plant/mod.rs +++ b/src/wormhole/session/tile/plant/mod.rs @@ -10,6 +10,9 @@ use summon::summon; mod rub; use rub::rub; +mod skill_unlock; +use skill_unlock::skill_unlock; + mod slaughter; pub fn handle_ask(ss: &mut SessSend, ask: PlantAsk) -> AskedNote { @@ -19,14 +22,28 @@ pub fn handle_ask(ss: &mut SessSend, ask: PlantAsk) -> AskedNote { seed_item_id, } => PlantSummonResult(strerr(summon(ss, tile_id, seed_item_id))), Slaughter { tile_id } => PlantSlaughterResult(strerr(ss.take_plant(tile_id))), - Craft { - tile_id, - recipe_index, - } => PlantSlaughterResult(strerr(Err("unimplemented route"))), + KnowledgeSnort { tile_id, xp } => { + PlantKnowledgeSnortResult(strerr(ss.plant_mut(tile_id).map(|p| { + p.skills.xp += xp; + p.skills.xp + }))) + } Rub { tile_id, rub_item_id, } => PlantRubStartResult(strerr(rub(ss, tile_id, rub_item_id))), + Craft { .. } => PlantCraftStartResult(strerr(Err("unimplemented route"))), + Nickname { .. } => PlantNicknameResult(strerr(Err("unimplemented route"))), + SkillUnlock { + tile_id, + source_skill_conf, + unlock_index, + } => PlantSkillUnlockResult(strerr(skill_unlock( + ss, + tile_id, + source_skill_conf, + unlock_index, + ))), } } /* @@ -43,15 +60,15 @@ async fn plant_craft() -> hcor::ClientResult<()> { // (it probably fails because it's already been established in another test) drop(pretty_env_logger::try_init()); - let seed_arch = hcor::CONFIG - .possession_archetypes + let seed_config = hcor::CONFIG + .possession_configetypes .iter() .find(|a| a.seed.is_some()) .expect("no items in config that are seeds?"); // create bob's stead! let mut bobstead = Hackstead::register().await?; - let seed_item = seed_arch.spawn().await?; + let seed_item = seed_config.spawn().await?; let tile = bobstead .free_tile() .expect("new hackstead no open tiles"); diff --git a/src/wormhole/session/tile/plant/rub.rs b/src/wormhole/session/tile/plant/rub.rs index 492da1c..fa34e6e 100644 --- a/src/wormhole/session/tile/plant/rub.rs +++ b/src/wormhole/session/tile/plant/rub.rs @@ -1,5 +1,9 @@ use super::SessSend; -use hcor::{id, plant, Item, ItemId, Plant, TileId}; +use hcor::{ + id, + plant::{self, RubEffect}, + Item, ItemId, Plant, TileId, +}; use std::fmt; #[derive(Debug)] @@ -23,59 +27,48 @@ impl fmt::Display for Error { NoEffect(None, item) => write!( f, "item {}[{}] isn't configured to impart any effects when rubbed on plants", - item.name, item.archetype_handle + item.name, item.conf ), NoEffect(Some(plant), item) => write!( f, "rubbing item {}[{}] on plant {}[{}] would have no effects", - item.name, item.archetype_handle, plant.name, plant.archetype_handle + item.name, item.conf, plant.name, plant.conf ), } } } -pub fn rub( - ss: &mut SessSend, - tile_id: TileId, - item_id: ItemId, -) -> Result, Error> { +pub fn rub(ss: &mut SessSend, tile_id: TileId, item_id: ItemId) -> Result, Error> { let item = ss.take_item(item_id)?; - if item.plant_rub_effects.is_empty() { + if item.conf.plant_rub_effects.is_empty() { return Err(NoEffect(None, item)); } let plant = ss.plant(tile_id)?; - let plant_name = plant.name.clone(); - let mut effect_confs = item.rub_effects_for_plant_indexed(&plant_name).peekable(); - if effect_confs.peek().is_none() { + let effects = RubEffect::item_on_plant(item.conf, plant.conf); + if effects.is_empty() { return Err(NoEffect(Some(plant.clone()), item.clone())); } - let effects: Vec = effect_confs - .into_iter() - .map(|(i, a)| { - let effect_id = plant::EffectId(uuid::Uuid::new_v4()); - - // register any timers we'll need for the effects that'll wear off - if let Some(until_finish) = a.duration { - ss.set_timer(plant::Timer { - until_finish, - tile_id, - lifecycle: plant::timer::Lifecycle::Annual, - kind: plant::TimerKind::Rub { effect_id }, - }) - } - - plant::Effect { - effect_id, - item_archetype_handle: item.archetype_handle, - effect_archetype_handle: i, - } + for (until_finish, effect_id) in effects + .iter() + .filter_map(|e| Some((e.duration?, e.effect_id))) + { + // register any timers we'll need for the effects that'll wear off + ss.set_timer(plant::Timer { + until_finish, + tile_id, + lifecycle: plant::timer::Lifecycle::Annual, + kind: plant::TimerKind::Rub { + effect_id: effect_id, + }, }) - .collect(); + } - ss.plant_mut(tile_id)?.effects.append(&mut effects.clone()); + ss.plant_mut(tile_id)? + .rub_effects + .append(&mut effects.clone()); Ok(effects) } @@ -84,21 +77,21 @@ mod test { #[actix_rt::test] /// NOTE: relies on plant/new, item/spawn! async fn rub() -> hcor::ClientResult<()> { - use hcor::Hackstead; + use hcor::{plant::RubEffect, Hackstead}; // attempt to establish logging, do nothing if it fails // (it probably fails because it's already been established in another test) drop(pretty_env_logger::try_init()); - let (seed_arch, rub_arch) = hcor::CONFIG + let (seed_config, rub_config) = hcor::CONFIG .seeds() - .find_map(|(seed, seed_arch)| { + .find_map(|(grows_into, seed_config)| { Some(( - seed_arch, + seed_config, hcor::CONFIG - .possession_archetypes - .iter() - .find(|a| a.rub_effects_for_plant(&seed.grows_into).count() > 0)?, + .items + .keys() + .find(|c| !RubEffect::item_on_plant(**c, grows_into).is_empty())?, )) }) .expect("no seeds in config that grow into plants we can rub with effects?"); @@ -107,29 +100,26 @@ mod test { let mut bobstead = Hackstead::register().await?; // make plant - let seed_item = seed_arch.spawn().await?; + let seed_item = seed_config.spawn().await?; let tile = bobstead.free_tile().expect("new hackstead no open tiles"); let mut plant = tile.plant_seed(&seed_item).await?; // rub item - let rub_item = rub_arch.spawn().await?; + let rub_item = rub_config.spawn().await?; let effects = plant.rub_with(&rub_item).await?; - bobstead = Hackstead::fetch(&bobstead).await?; + bobstead.server_sync().await?; plant = bobstead.plant(&plant).unwrap().clone(); assert_eq!( - plant.effects, effects, + plant.rub_effects, effects, "brand new plant has more effects than those from the item that was just rubbed on", ); assert!( - rub_arch - .rub_effects_for_plant(&plant.name) - .enumerate() - .all(|(i, _)| { - effects - .iter() - .any(|e| e.effect_archetype_handle == i as hcor::config::ArchetypeHandle) - }), + RubEffect::item_on_plant(rub_item.conf, plant.conf) + .iter() + .all(|a| effects + .iter() + .any(|b| { a.item_conf == b.item_conf && a.effect_index == b.effect_index })), "the effects of this item we just rubbed on can't be found on this plant" ); diff --git a/src/wormhole/session/tile/plant/skill_unlock.rs b/src/wormhole/session/tile/plant/skill_unlock.rs new file mode 100644 index 0000000..ef261e1 --- /dev/null +++ b/src/wormhole/session/tile/plant/skill_unlock.rs @@ -0,0 +1,135 @@ +use super::SessSend; +use hcor::{ + id, + plant::{self, skill}, + TileId, +}; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + NoSuch(id::NoSuch), + NoSkill(plant::Conf, skill::Conf), + NoUnlock(plant::Conf, skill::Conf, usize), + NoAfford(usize), +} +use Error::*; + +impl From for Error { + fn from(ns: id::NoSuch) -> Error { + Error::NoSuch(ns) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "couldn't unlock skill for plant: ")?; + match self { + NoSuch(ns) => write!(f, "{}", ns), + NoSkill(plant_conf, skill_conf) => write!( + f, + "there's no valid skill with the conf {} on this {}[{}] plant.", + skill_conf, plant_conf.name, plant_conf + ), + NoUnlock(plant_conf, skill_conf, unlock_index) => write!( + f, + "there's no valid unlock at the index {} on the skill conf {} on this {}[{}] plant.", + unlock_index, skill_conf, plant_conf.name, plant_conf + ), + NoAfford(0) => write!(f, "You can't afford to unlock this skill."), + NoAfford(n) => write!(f, "You need {} more skillpoints to unlock this skill", n), + } + } +} + +pub fn skill_unlock( + ss: &mut SessSend, + tile_id: TileId, + source_skill: skill::Conf, + unlock_index: usize, +) -> Result { + let (plant_conf, plant_id) = { + let plant = ss.plant(tile_id)?; + (plant.conf, plant.tile_id) + }; + + if source_skill.try_lookup().is_none() { + return Err(NoSkill(plant_conf, source_skill)); + } + + let unlock = source_skill + .unlocks + .get(unlock_index) + .ok_or_else(|| NoUnlock(plant_conf, source_skill, unlock_index))?; + + unlock + .costs + .charge(&mut *ss, plant_id)? + .map_err(|e| NoAfford(e))?; + + let plant = ss.plant_mut(tile_id)?; + plant.skills.unlocked.push(unlock.skill); + Ok(plant.skills.available_points()) +} + +#[cfg(all(feature = "hcor_client", test))] +mod test { + #[actix_rt::test] + /// NOTE: relies on plant/new, item/spawn! + async fn skill_unlock() -> hcor::ClientResult<()> { + use hcor::{plant, Hackstead}; + use log::*; + + // attempt to establish logging, do nothing if it fails + // (it probably fails because it's already been established in another test) + drop(pretty_env_logger::try_init()); + + let (_, seed_config) = hcor::CONFIG + .seeds() + .next() + .expect("no seeds in config that grow into plants we can rub with effects?"); + + // create bob's stead! + let bobstead = Hackstead::register().await?; + + // make plant + let seed_item = seed_config.spawn().await?; + let tile = bobstead.free_tile().expect("new hackstead no open tiles"); + let plant = tile.plant_seed(&seed_item).await?; + assert!(plant.skills.available_points() == 0); + let next_skill_unlock = plant + .skills + .unlocked + .iter() + .flat_map(|s| s.unlocks.iter()) + .find(|s| s.costs == plant::skill::Cost::points(1)) + .expect("no skills I can unlock for just 1 point? D:"); + + match next_skill_unlock.unlock_for(&plant).await { + Ok(uh) => error!( + "next skill unlock unexpectedly succeeded, skillpoints left: {}", + uh + ), + Err(e) => info!("next skill unlock failed as expected, err: {}", e), + } + + plant + .knowledge_snort( + *plant + .skillpoint_unlock_xps + .get(1) + .expect("no more skillpoint unlock xps?"), + ) + .await?; + + assert_eq!( + 0, + next_skill_unlock.unlock_for(&plant).await?, + "got skill but more than 0 points left", + ); + + bobstead.slaughter().await?; + + Ok(()) + } +} diff --git a/src/wormhole/session/tile/plant/slaughter.rs b/src/wormhole/session/tile/plant/slaughter.rs index fad3257..eaff4c5 100644 --- a/src/wormhole/session/tile/plant/slaughter.rs +++ b/src/wormhole/session/tile/plant/slaughter.rs @@ -58,7 +58,7 @@ async fn slaughter() -> hcor::ClientResult<()> { let mut doomed_plant: Plant = tile.plant_seed(&seed_item).await?; // make sure that tile is no longer open - bobstead = Hackstead::fetch(&bobstead).await?; + bobstead.server_sync().await?; assert!( !bobstead.free_tiles().any(|t| t.tile_id == tile.tile_id), "bob's plant is still not open even though we just killed its plant!" @@ -68,7 +68,7 @@ async fn slaughter() -> hcor::ClientResult<()> { doomed_plant = doomed_plant.slaughter().await?; // make sure there's no plant now - bobstead = Hackstead::fetch(&bobstead).await?; + bobstead.server_sync().await?; assert!( bobstead.free_tiles().any(|t| t.tile_id == tile.tile_id), "bob's plant is still not open even though we just killed its plant!" diff --git a/src/wormhole/session/tile/plant/summon.rs b/src/wormhole/session/tile/plant/summon.rs index a72ec01..b19138f 100644 --- a/src/wormhole/session/tile/plant/summon.rs +++ b/src/wormhole/session/tile/plant/summon.rs @@ -25,12 +25,12 @@ impl fmt::Display for Error { NotConfigured(item) => write!( f, "item {}[{}] is not configured to be used as a seed", - item.name, item.archetype_handle, + item.name, item.conf, ), AlreadyOccupied(tile_id, plant) => write!( f, "tile {} is already occupied by a {}[{}] plant.", - tile_id, plant.name, plant.archetype_handle + tile_id, plant.name, plant.conf ), } } @@ -38,12 +38,9 @@ impl fmt::Display for Error { pub fn summon(ss: &mut SessSend, tile_id: TileId, item_id: ItemId) -> Result { let item = ss.take_item(item_id)?; - let seed = item - .seed - .as_ref() - .ok_or_else(|| NotConfigured(item.clone()))?; + let seed = item.grows_into.ok_or_else(|| NotConfigured(item.clone()))?; - let plant = Plant::from_seed(item.owner_id, tile_id, seed).unwrap(); + let plant = Plant::from_conf(item.owner_id, tile_id, seed); if let Some(until_finish) = plant.base_yield_duration { trace!("adding yield timer"); ss.set_timer(plant::Timer { @@ -77,21 +74,21 @@ mod test { // (it probably fails because it's already been established in another test) drop(pretty_env_logger::try_init()); - let (_, seed_arch) = hcor::CONFIG + let (_, seed_config) = hcor::CONFIG .seeds() .next() .expect("no items in config that are seeds?"); - let not_seed_arch = hcor::CONFIG - .possession_archetypes - .iter() - .find(|a| a.seed.is_none()) + let not_seed_config = hcor::CONFIG + .items + .values() + .find(|a| a.grows_into.is_none()) .expect("no items in config that aren't seeds?"); // create bob's stead! let mut bobstead = Hackstead::register().await?; - let seed_item = seed_arch.spawn().await?; - let not_seed_item = not_seed_arch.spawn().await?; + let seed_item = seed_config.spawn().await?; + let not_seed_item = not_seed_config.spawn().await?; let open_tile = bobstead.free_tile().expect("fresh hackstead no open land?"); struct NewPlantAssumptions { @@ -115,8 +112,8 @@ mod test { plant ); assert_eq!( - seed_item.seed.as_ref().unwrap().grows_into, - plant.name, + seed_item.grows_into.unwrap(), + plant.conf, "seed grew into unexpected type of plant" ); } @@ -125,7 +122,7 @@ mod test { (false, Ok(tile)) => panic!("/plant/new unexpectedly returned plant: {:#?}", tile), }; - *bobstead = Hackstead::fetch(&*bobstead).await?; + bobstead.server_sync().await?; assert_eq!( assumptions.item_consumed,