diff --git a/Cargo.lock b/Cargo.lock index 0c465b9..8695d3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,7 @@ dependencies = [ "deadpool", "deadpool-postgres", "futures", + "itertools", "log", "nix", "nom", @@ -392,6 +393,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "enum-ordinalize" version = "3.1.12" @@ -751,6 +758,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.4" diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index c06afb5..431dd21 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -32,3 +32,4 @@ ouroboros = "0.15.5" chrono = { version = "0.4.23", features = ["serde"] } bcrypt = "0.13.0" validator = "0.16.0" +itertools = "0.10.5" diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 28338f5..3fa317b 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -10,6 +10,7 @@ use crate::message_handler::ListenerSession; use crate::DResult; use crate::models::{session::Session, user::User, item::Item}; use tokio_postgres::types::ToSql; +use std::collections::BTreeSet; use serde_json; use futures::FutureExt; @@ -132,6 +133,24 @@ impl DBPool { conn.execute("DELETE FROM sendqueue WHERE item=$1", &[&item.item]).await?; Ok(()) } + + pub async fn find_static_item_types(self: &Self) -> DResult>> { + Ok(Box::new( + self + .get_conn().await? + .query("SELECT DISTINCT details->>'item_type' AS item_type \ + FROM items WHERE details->>'is_static' = 'true'", &[]).await? + .iter() + .map(|r| r.get("item_type")) + .collect())) + } + + pub async fn delete_static_items_by_type(self: &Self, item_type: &str) -> DResult<()> { + self.get_conn().await?.query( + "DELETE FROM items WHERE details->>'is_static' = 'true' AND details->>'item_type' = {}", + &[&item_type]).await?; + Ok(()) + } pub async fn get_conn(self: &DBPool) -> DResult { @@ -243,6 +262,29 @@ impl DBTrans { Ok(()) } + pub async fn find_static_items_by_type(self: &Self, item_type: &str) -> + DResult>> { + Ok(Box::new( + self.pg_trans()? + .query("SELECT DISTINCT details->>'item_code' AS item_code FROM items WHERE \ + details->>'is_static' = 'true' AND \ + details->>'item_type' = $1", &[&item_type]) + .await? + .into_iter() + .map(|v| v.get("item_code")) + .collect())) + } + + pub async fn delete_static_items_by_code(self: &Self, item_type: &str, + item_code: &str) -> DResult<()> { + self.pg_trans()?.query( + "DELETE FROM items WHERE details->>'is_static' = 'true' AND \ + details->>'item_type' = {} AND \ + details->>'item_code' = {}", + &[&item_type, &item_code]).await?; + Ok(()) + } + pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); if let Some(trans) = trans_opt { diff --git a/blastmud_game/src/main.rs b/blastmud_game/src/main.rs index 78e4e1b..81c15d8 100644 --- a/blastmud_game/src/main.rs +++ b/blastmud_game/src/main.rs @@ -13,6 +13,7 @@ mod version_cutover; mod av; mod regular_tasks; mod models; +mod static_content; pub type DResult = Result>; @@ -53,6 +54,8 @@ async fn main() -> DResult<()> { } ).await?; + static_content::refresh_static_content(&pool).await?; + version_cutover::replace_old_gameserver(&config.pidfile)?; regular_tasks::start_regular_tasks(&pool, listener_map)?; diff --git a/blastmud_game/src/static_content.rs b/blastmud_game/src/static_content.rs new file mode 100644 index 0000000..256d538 --- /dev/null +++ b/blastmud_game/src/static_content.rs @@ -0,0 +1,87 @@ +use crate::DResult; +use crate::db::DBPool; +use crate::models::item::Item; +use log::error; +use itertools::Itertools; +use std::collections::{BTreeSet, BTreeMap}; + +mod room; + +pub struct StaticItem { + pub item_code: &'static str, + pub initial_item: fn () -> Item +} + +struct StaticItemTypeGroup { + item_type: &'static str, + items: fn () -> Box> +} + +fn static_item_registry() -> Vec { + vec!( + // Must have no duplicates. + StaticItemTypeGroup { + item_type: "npc", + items: || Box::new(vec!().into_iter()) + }, + StaticItemTypeGroup { + item_type: "room", + items: || room::static_items() + }, + ) +} + + +async fn refresh_static_items(pool: &DBPool) -> DResult<()> { + let mut registry = static_item_registry(); + registry.sort_unstable_by(|x, y| x.item_type.cmp(y.item_type)); + + let duplicates: Vec<&'static str> = + registry.iter() + .group_by(|x| x.item_type).into_iter() + .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) + .collect(); + if duplicates.len() > 0 { + error!("static_item_registry has duplicate item_types: {:}", duplicates.join(", ")); + Err("Duplicate item_types in static_item_registry")?; + } + let expected_type: BTreeSet = + registry.iter().map(|x| x.item_type.to_owned()).collect(); + let cur_types: Box> = pool.find_static_item_types().await?; + for item_type in cur_types.difference(&expected_type) { + pool.delete_static_items_by_type(item_type).await?; + } + + for type_group in registry.iter() { + let iterator : Box> = (type_group.items)(); + let duplicates: Vec<&'static str> = iterator + .group_by(|x| x.item_code) + .into_iter() + .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) + .collect(); + if duplicates.len() > 0 { + error!("static_item_registry has duplicate item_codes for {}: {:}", + type_group.item_type, + duplicates.join(", ")); + Err("Duplicate item_types in static_item_registry")?; + } + let tx = pool.start_transaction().await?; + let existing_items = tx.find_static_items_by_type(type_group.item_type).await?; + let expected_items: BTreeMap = + (type_group.items)().map(|x| (x.item_code.to_owned(), x)).collect(); + let expected_set: BTreeSet = expected_items.keys().map(|x|x.to_owned()).collect(); + for unwanted_item in existing_items.difference(&expected_set) { + tx.delete_static_items_by_code(type_group.item_type, unwanted_item).await?; + } + for new_item_code in expected_set.difference(&existing_items) { + tx.create_item(&(expected_items.get(new_item_code) + .unwrap().initial_item)()).await?; + } + } + Ok(()) +} + +pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> { + refresh_static_items(pool).await?; + Ok(()) +} diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs new file mode 100644 index 0000000..fde23f8 --- /dev/null +++ b/blastmud_game/src/static_content/room.rs @@ -0,0 +1,5 @@ +use super::StaticItem; + +pub fn static_items() -> Box> { + Box::new(vec!().into_iter()) +} diff --git a/schema/schema.sql b/schema/schema.sql index e124655..59842d1 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -21,7 +21,7 @@ CREATE TABLE items ( item_id BIGSERIAL NOT NULL PRIMARY KEY, details JSONB NOT NULL ); -CREATE UNIQUE INDEX item_index ON items ((details->>'item_code'), (details->>'item_type')); +CREATE UNIQUE INDEX item_index ON items ((details->>'item_type'), (details->>'item_code')); CREATE INDEX item_by_loc ON items ((details->>'location')); CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));