diff --git a/Cargo.lock b/Cargo.lock index 1c1ab90..e84eeb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,7 @@ dependencies = [ "once_cell", "ouroboros", "phf", + "rand", "ring", "serde", "serde_json", diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index a3853f1..e6bd780 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -34,3 +34,4 @@ bcrypt = "0.13.0" validator = "0.16.0" itertools = "0.10.5" once_cell = "1.16.0" +rand = "0.8.5" diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index b89e5f4..e3edc63 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -151,6 +151,17 @@ impl DBPool { .collect())) } + pub async fn find_static_task_types(self: &Self) -> DResult>> { + Ok(Box::new( + self + .get_conn().await? + .query("SELECT DISTINCT details->>'task_type' AS task_type \ + FROM tasks WHERE details->>'is_static' = 'true'", &[]).await? + .iter() + .map(|r| r.get("task_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' = {}", @@ -158,6 +169,13 @@ impl DBPool { Ok(()) } + pub async fn delete_static_tasks_by_type(self: &Self, task_type: &str) -> DResult<()> { + self.get_conn().await?.query( + "DELETE FROM tasks WHERE details->>'is_static' = 'true' AND details->>'task_type' = {}", + &[&task_type]).await?; + Ok(()) + } + pub async fn get_conn(self: &DBPool) -> DResult { let conn = self.pool.get().await?; @@ -279,6 +297,29 @@ impl DBTrans { Ok(()) } + pub async fn limited_update_static_task(self: &Self, task: &Task) -> DResult<()> { + let value = serde_json::to_value(task)?; + let obj_map = value.as_object() + .expect("Static task to be object in JSON"); + let task_name: &(dyn ToSql + Sync) = &task.details.name(); + let mut params: Vec<&(dyn ToSql + Sync)> = vec!(task_name, &task.meta.task_code); + let mut det_ex: String = "details".to_owned(); + let mut var_id = 3; + // Only copy more permanent fields, others are supposed to change over time and shouldn't + // be reset on restart. We do reset failure count since the problem may be fixed. + for to_copy in ["recurrence", "consecutive_failure_count", "task_details"] { + det_ex = format!("jsonb_set({}, '{{{}}}', ${})", det_ex, to_copy, var_id); + params.push(obj_map.get(to_copy).unwrap_or(&Value::Null)); + var_id += 1; + } + self.pg_trans()?.execute( + &("UPDATE tasks SET details = ".to_owned() + &det_ex + + " WHERE details->>'task_type' = $1 AND details->>'task_code' = $2"), + ¶ms).await?; + Ok(()) + } + + pub async fn create_user(self: &Self, session: &ListenerSession, user_dat: &User) -> DResult<()> { self.pg_trans()?.execute("INSERT INTO users (\ username, current_session, current_listener, details\ @@ -332,6 +373,19 @@ impl DBTrans { .collect())) } + pub async fn find_static_tasks_by_type(self: &Self, task_type: &str) -> + DResult>> { + Ok(Box::new( + self.pg_trans()? + .query("SELECT DISTINCT details->>'task_code' AS task_code FROM tasks WHERE \ + details->>'is_static' = 'true' AND \ + details->>'task_type' = $1", &[&task_type]) + .await? + .into_iter() + .map(|v| v.get("task_code")) + .collect())) + } + pub async fn delete_static_items_by_code(self: &Self, item_type: &str, item_code: &str) -> DResult<()> { self.pg_trans()?.query( @@ -342,6 +396,16 @@ impl DBTrans { Ok(()) } + pub async fn delete_static_tasks_by_code(self: &Self, task_type: &str, + task_code: &str) -> DResult<()> { + self.pg_trans()?.query( + "DELETE FROM task WHERE details->>'is_static' = 'true' AND \ + details->>'task_type' = $1 AND \ + details->>'task_code' = $2", + &[&task_type, &task_code]).await?; + Ok(()) + } + pub async fn find_item_by_type_code(self: &Self, item_type: &str, item_code: &str) -> DResult>> { if let Some(item) = self.pg_trans()?.query_opt( @@ -464,7 +528,7 @@ impl DBTrans { match self.pg_trans()?.query_opt( "SELECT details FROM tasks WHERE \ CAST(details->>'next_scheduled' AS TIMESTAMPTZ) <= now() \ - ORDER BY details->>'next_scheduled'", &[] + ORDER BY details->>'next_scheduled' ASC LIMIT 1", &[] ).await? { None => Ok(None), Some(row) => Ok(serde_json::from_value(row.get("details"))?) diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index cf6a9d3..fe5c9f8 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -21,6 +21,7 @@ pub mod movement; pub mod parsing; mod quit; mod register; +pub mod say; mod whisper; pub struct VerbContext<'l> { @@ -100,8 +101,11 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "look" => look::VERB, "read" => look::VERB, "lmap" => map::VERB, + "\'" => say::VERB, + "say" => say::VERB, "-" => whisper::VERB, "whisper" => whisper::VERB, + "tell" => whisper::VERB, }; fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> { diff --git a/blastmud_game/src/message_handler/user_commands/say.rs b/blastmud_game/src/message_handler/user_commands/say.rs new file mode 100644 index 0000000..cc0b7ad --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/say.rs @@ -0,0 +1,56 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, + user_error, + get_player_item_or_fail, is_likely_explicit}; +use crate::models::item::{Item, ItemFlag}; +use crate::db::DBTrans; +use async_trait::async_trait; +use ansi::{ignore_special_characters, ansi}; + +pub async fn say_to_room<'l>( + trans: &DBTrans, + from_item: &Item, + location: &str, + say_what: &str, + is_explicit: bool +) -> UResult<()> { + let (loc_type, loc_code) = location.split_once("/") + .ok_or_else(|| UserError("Invalid location".to_owned()))?; + let room_item = trans.find_item_by_type_code(loc_type, loc_code).await? + .ok_or_else(|| UserError("Room missing".to_owned()))?; + if room_item.flags.contains(&ItemFlag::NoSay) { + user_error("Your wristpad vibrates and flashes up an error - apparently it has \ + been programmed to block your voice from working here.".to_owned())? + } + for item in trans.find_items_by_location(location).await? { + if item.item_type != "player" { + continue; + } + if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? { + if session_dat.less_explicit_mode && is_explicit && from_item.item_code != item.item_code { + continue; + } + trans.queue_for_session(&session, Some(&format!( + ansi!("{} says: \"{}\"\n"), + from_item.display_for_session(&session_dat), + say_what + ))).await?; + } + } + Ok(()) +} + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + let say_what = ignore_special_characters(remaining); + if say_what == "" { + user_error("You need to provide a message to send.".to_owned())?; + } + let player_item = get_player_item_or_fail(ctx).await?; + say_to_room(ctx.trans, &player_item, &player_item.location, + &say_what, is_likely_explicit(&say_what)).await + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index a5492cb..b7fc02f 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -107,6 +107,12 @@ pub enum Sex { } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum ItemFlag { + NoSay +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[serde(default)] pub struct Item { pub item_code: String, pub item_type: String, @@ -124,6 +130,7 @@ pub struct Item { pub total_skills: BTreeMap, pub temporary_buffs: Vec, pub pronouns: Pronouns, + pub flags: Vec, pub sex: Option, } @@ -162,6 +169,7 @@ impl Default for Item { total_skills: BTreeMap::new(), temporary_buffs: Vec::new(), pronouns: Pronouns::default_inanimate(), + flags: vec!(), sex: None } } diff --git a/blastmud_game/src/models/task.rs b/blastmud_game/src/models/task.rs index 85d694a..5912776 100644 --- a/blastmud_game/src/models/task.rs +++ b/blastmud_game/src/models/task.rs @@ -2,7 +2,6 @@ use serde::{Serialize, Deserialize}; use serde_json::Value; use chrono::{DateTime, Utc}; - #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum TaskRecurrence { FixedDuration { seconds: u32 } @@ -11,13 +10,18 @@ pub enum TaskRecurrence { #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[serde(tag="task_type", content="task_details")] pub enum TaskDetails { - RunQueuedCommand + RunQueuedCommand, + NPCSay { + npc_code: String, + say_code: String + } } impl TaskDetails { pub fn name(self: &Self) -> &'static str { use TaskDetails::*; match self { - RunQueuedCommand => "RunQueuedCommand" + RunQueuedCommand => "RunQueuedCommand", + NPCSay { .. } => "NPCSay", } } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index d4b956a..0581536 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -8,6 +8,7 @@ use once_cell::sync::OnceCell; use std::ops::AddAssign; use std::collections::BTreeMap; use chrono::Utc; +use crate::static_content::npc; pub mod queued_command; @@ -26,7 +27,8 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task OnceCell::new(); TASK_HANDLER_REGISTRY.get_or_init( || vec!( - ("RunQueuedCommand", queued_command::HANDLER.clone()) + ("RunQueuedCommand", queued_command::HANDLER.clone()), + ("NPCSay", npc::SAY_HANDLER.clone()), ).into_iter().collect() ) } diff --git a/blastmud_game/src/static_content.rs b/blastmud_game/src/static_content.rs index d6c2b8f..8338e09 100644 --- a/blastmud_game/src/static_content.rs +++ b/blastmud_game/src/static_content.rs @@ -1,6 +1,6 @@ use crate::DResult; use crate::db::DBPool; -use crate::models::item::Item; +use crate::models::{item::Item, task::Task}; use std::collections::{BTreeSet, BTreeMap}; use log::info; @@ -13,50 +13,64 @@ pub struct StaticItem { pub initial_item: Box Item> } -struct StaticItemTypeGroup { - item_type: &'static str, - items: fn () -> Box> +pub struct StaticTask { + pub task_code: String, + pub initial_task: Box Task> } -fn static_item_registry() -> Vec { +struct StaticThingTypeGroup { + thing_type: &'static str, + things: fn () -> Box> +} + +fn static_item_registry() -> Vec> { vec!( // Must have no duplicates. - StaticItemTypeGroup { - item_type: "npc", - items: || npc::npc_static_items() + StaticThingTypeGroup:: { + thing_type: "npc", + things: || npc::npc_static_items() }, - StaticItemTypeGroup { - item_type: "room", - items: || room::room_static_items() + StaticThingTypeGroup:: { + thing_type: "room", + things: || room::room_static_items() }, - StaticItemTypeGroup { - item_type: "fixed_item", - items: || fixed_item::static_items() + StaticThingTypeGroup:: { + thing_type: "fixed_item", + things: || fixed_item::static_items() + }, + ) +} + +fn static_task_registry() -> Vec> { + vec!( + // Must have no duplicates. + StaticThingTypeGroup:: { + thing_type: "NPCSay", + things: || npc::npc_say_tasks() }, ) } - async fn refresh_static_items(pool: &DBPool) -> DResult<()> { let registry = static_item_registry(); let expected_type: BTreeSet = - registry.iter().map(|x| x.item_type.to_owned()).collect(); + registry.iter().map(|x| x.thing_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() { - info!("Checking static_content of item_type {}", type_group.item_type); + info!("Checking static_content of item_type {}", type_group.thing_type); let tx = pool.start_transaction().await?; - let existing_items = tx.find_static_items_by_type(type_group.item_type).await?; + let existing_items = tx.find_static_items_by_type(type_group.thing_type).await?; let expected_items: BTreeMap = - (type_group.items)().map(|x| (x.item_code.to_owned(), x)).collect(); + (type_group.things)().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) { info!("Deleting item {:?}", unwanted_item); - tx.delete_static_items_by_code(type_group.item_type, unwanted_item).await?; + tx.delete_static_items_by_code(type_group.thing_type, unwanted_item).await?; } for new_item_code in expected_set.difference(&existing_items) { info!("Creating item {:?}", new_item_code); @@ -69,13 +83,51 @@ async fn refresh_static_items(pool: &DBPool) -> DResult<()> { .unwrap().initial_item)()).await?; } tx.commit().await?; - info!("Committed any changes for static_content of item_type {}", type_group.item_type); + info!("Committed any changes for static_content of item_type {}", type_group.thing_type); + } + Ok(()) +} + +async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> { + let registry = static_task_registry(); + + let expected_type: BTreeSet = + registry.iter().map(|x| x.thing_type.to_owned()).collect(); + let cur_types: Box> = pool.find_static_task_types().await?; + for task_type in cur_types.difference(&expected_type) { + pool.delete_static_tasks_by_type(task_type).await?; + } + + for type_group in registry.iter() { + info!("Checking static_content of task_type {}", type_group.thing_type); + let tx = pool.start_transaction().await?; + let existing_tasks = tx.find_static_tasks_by_type(type_group.thing_type).await?; + let expected_tasks: BTreeMap = + (type_group.things)().map(|x| (x.task_code.to_owned(), x)).collect(); + let expected_set: BTreeSet = expected_tasks.keys().map(|x|x.to_owned()).collect(); + for unwanted_task in existing_tasks.difference(&expected_set) { + info!("Deleting task {:?}", unwanted_task); + tx.delete_static_tasks_by_code(type_group.thing_type, unwanted_task).await?; + } + for new_task_code in expected_set.difference(&existing_tasks) { + info!("Creating task {:?}", new_task_code); + tx.upsert_task(&(expected_tasks.get(new_task_code) + .unwrap().initial_task)()).await?; + } + for existing_task_code in expected_set.intersection(&existing_tasks) { + tx.limited_update_static_task( + &(expected_tasks.get(existing_task_code) + .unwrap().initial_task)()).await?; + } + tx.commit().await?; + info!("Committed any changes for static_content of task_type {}", type_group.thing_type); } Ok(()) } pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> { refresh_static_items(pool).await?; + refresh_static_tasks(pool).await?; Ok(()) } @@ -85,13 +137,13 @@ mod test { use super::*; #[test] - fn no_duplicate_static_content() { + fn no_duplicate_static_items() { let mut registry = static_item_registry(); - registry.sort_unstable_by(|x, y| x.item_type.cmp(y.item_type)); + registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type)); let duplicates: Vec<&'static str> = registry.iter() - .group_by(|x| x.item_type).into_iter() + .group_by(|x| x.thing_type).into_iter() .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) .collect(); if duplicates.len() > 0 { @@ -99,7 +151,7 @@ mod test { } for type_group in registry.iter() { - let iterator : Box> = (type_group.items)(); + let iterator : Box> = (type_group.things)(); let duplicates: Vec<&'static str> = iterator .group_by(|x| x.item_code) .into_iter() @@ -107,7 +159,36 @@ mod test { .collect(); if duplicates.len() > 0 { panic!("static_item_registry has duplicate item_codes for {}: {:}", - type_group.item_type, + type_group.thing_type, + duplicates.join(", ")); + } + } + } + + #[test] + fn no_duplicate_static_tasks() { + let mut registry = static_task_registry(); + registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type)); + + let duplicates: Vec<&'static str> = + registry.iter() + .group_by(|x| x.thing_type).into_iter() + .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) + .collect(); + if duplicates.len() > 0 { + panic!("static_task_registry has duplicate task_types: {:}", duplicates.join(", ")); + } + + for type_group in registry.iter() { + let iterator : Box> = (type_group.things)(); + let duplicates: Vec<&'static str> = iterator + .group_by(|x| x.task_code) + .into_iter() + .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) + .collect(); + if duplicates.len() > 0 { + panic!("static_task_registry has duplicate task_codes for {}: {:}", + type_group.thing_type, duplicates.join(", ")); } } diff --git a/blastmud_game/src/static_content/npc.rs b/blastmud_game/src/static_content/npc.rs index 6190697..85d5a7d 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -1,9 +1,21 @@ -use super::StaticItem; -use crate::models::item::Item; +use super::{StaticItem, StaticTask}; +use crate::models::{ + item::Item, + task::{Task, TaskMeta, TaskRecurrence, TaskDetails} +}; use once_cell::sync::OnceCell; use std::collections::BTreeMap; -use crate::message_handler::user_commands::{VerbContext, UResult}; +use crate::message_handler::user_commands::{ + VerbContext, UResult, CommandHandlingError, + say::say_to_room +}; +use crate::DResult; use async_trait::async_trait; +use chrono::Utc; +use rand::{thread_rng, Rng, prelude::*}; +use crate::regular_tasks::{TaskHandler, TaskRunContext}; +use log::info; +use std::time; pub mod statbot; @@ -18,26 +30,584 @@ pub trait NPCMessageHandler { ) -> UResult<()>; } +#[derive(Clone, Debug)] +pub enum NPCSayType { + // Bool is true if it should be filtered for less-explicit. + FromFixedList(Vec<(bool, &'static str)>) +} + +#[derive(Clone, Debug)] +pub struct NPCSayInfo { + pub say_code: &'static str, + pub frequency_secs: u64, + pub talk_type: NPCSayType +} pub struct NPC { pub code: &'static str, pub name: &'static str, pub description: &'static str, pub spawn_location: &'static str, - pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)> + pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>, + pub says: Vec } pub fn npc_list() -> &'static Vec { + use NPCSayType::FromFixedList; static NPC_LIST: OnceCell> = OnceCell::new(); - NPC_LIST.get_or_init(|| vec!( - NPC { - code: "repro_xv_chargen_statbot", - name: "Statbot", - description: "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.", - spawn_location: "room/repro_xv_chargen", - message_handler: Some(&statbot::StatbotMessageHandler) - } - )) + NPC_LIST.get_or_init( + || { + let melbs_citizen_stdsay = NPCSayInfo { say_code: "babble", frequency_secs: 60, talk_type: FromFixedList(vec!((false, "I'm so sick of being cloned")))}; + vec!( + NPC { + code: "repro_xv_chargen_statbot", + name: "Statbot", + description: "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.", + spawn_location: "room/repro_xv_chargen", + message_handler: Some(&statbot::StatbotMessageHandler), + says: vec!() + }, + NPC { + code: "melbs_citizen_1", + name: "Matthew Thomas", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_latrobest", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_2", + name: "Matthew Perez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_20", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_3", + name: "Kimberly Jackson", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_40", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_4", + name: "Michael Sanchez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_50", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_5", + name: "Jessica Davis", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_bourkest", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_6", + name: "Robert Davis", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_70", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_7", + name: "Paul Lewis", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_90", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_8", + name: "Andrew Moore", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_collinsst", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_9", + name: "Betty Thomas", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_100", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_10", + name: "Mary Robinson", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_110", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_11", + name: "Lisa Lopez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_flinderst", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_12", + name: "Kimberly Martinez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_200", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_13", + name: "Anthony Nguyen", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_190", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_14", + name: "Joshua Green", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_180", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_15", + name: "Emily Wright", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_170", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_16", + name: "Ashley Thomas", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_lonsdalest_130", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_17", + name: "Jessica Miller", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_80", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_18", + name: "Anthony Lopez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_lonsdalest_140", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_19", + name: "John Lopez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_elizabethst_lonsdalest", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_20", + name: "Thomas Garcia", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_williamsst_120", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_21", + name: "Donna Thompson", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_elizabethst_60", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_22", + name: "Matthew Davis", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_williamsst_100", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_23", + name: "Steven Jones", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_120", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_24", + name: "Linda Smith", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_lonsdalest", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_25", + name: "Karen Rodriguez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_bourkest_180", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_26", + name: "Paul Scott", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_70", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_27", + name: "Ashley Thomas", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_lonsdalest_130", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_28", + name: "Sandra Scott", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_elizabethst_30", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_29", + name: "Michael Rodriguez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_70", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_30", + name: "Donald Miller", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_elizabethst_30", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_31", + name: "Charles Moore", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_lonsdalest_160", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_32", + name: "Ashley Sanchez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_100", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_33", + name: "Margaret Lewis", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_180", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_34", + name: "Sandra Thompson", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_80", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_35", + name: "Sandra King", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_lonsdalest_150", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_36", + name: "Lisa Anderson", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_lonsdalest_210", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_37", + name: "Kimberly Martin", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_80", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_38", + name: "Susan Smith", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_latrobest_190", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_39", + name: "Susan Martin", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_collinsst_150", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_40", + name: "Linda Scott", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_williamsst_30", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_41", + name: "Donald Miller", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_elizabethst_80", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_42", + name: "Mark Hill", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_collinsst_120", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_43", + name: "William Perez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_queenst_90", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_44", + name: "Donald Perez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_queenst_lonsdalest", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_45", + name: "Lisa Rodriguez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_collinsst_100", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_46", + name: "James Adams", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_latrobest_150", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_47", + name: "James Moore", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_latrobest_130", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_48", + name: "Joseph Martin", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_bourkest_150", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_49", + name: "Matthew Jones", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_60", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_50", + name: "Michael Sanchez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_queenst_100", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_51", + name: "Donna Torres", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_150", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_52", + name: "Barbara Garcia", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_50", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_53", + name: "Daniel Miller", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_bourkest_110", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_54", + name: "Robert Young", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_kingst_collinsst", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_55", + name: "Donald Flores", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_40", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_56", + name: "Charles Thomas", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_flindersst_110", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_57", + name: "William Torres", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_swanstonst_60", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_58", + name: "Barbara Gonzalez", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_collinsst_190", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_59", + name: "Mary Smith", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_bourkest_180", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + }, + + NPC { + code: "melbs_citizen_60", + name: "Michael Jackson", + description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harst reality of post-apocalyptic life", + spawn_location: "room/melbs_williamsst_110", + message_handler: None, + says: vec!(melbs_citizen_stdsay.clone()) + } + ) + }) } pub fn npc_by_code() -> &'static BTreeMap<&'static str, &'static NPC> { @@ -48,6 +618,18 @@ pub fn npc_by_code() -> &'static BTreeMap<&'static str, &'static NPC> { .collect()) } +pub fn npc_say_info_by_npc_code_say_code() -> &'static BTreeMap<(&'static str, &'static str), + &'static NPCSayInfo> { + static NPC_SAYINFO_MAP: OnceCell> = OnceCell::new(); + NPC_SAYINFO_MAP.get_or_init( + || npc_list().iter().flat_map( + |npc| npc.says.iter().map( + |says| ((npc.code, says.say_code), says) + ) + ).collect()) +} + pub fn npc_static_items() -> Box> { Box::new(npc_list().iter().map(|c| StaticItem { item_code: c.code, @@ -62,3 +644,78 @@ pub fn npc_static_items() -> Box> { }) })) } + +pub fn npc_say_tasks() -> Box> { + Box::new(npc_list().iter().flat_map(|c| c.says.iter().map(|say| StaticTask { + task_code: c.code.to_owned() + "_" + say.say_code, + initial_task: Box::new( + || { + let mut rng = thread_rng(); + Task { + meta: TaskMeta { + task_code: c.code.to_owned() + "_" + say.say_code, + is_static: true, + recurrence: Some(TaskRecurrence::FixedDuration { seconds: say.frequency_secs as u32 }), + next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..say.frequency_secs) as i64), + ..TaskMeta::default() + }, + details: TaskDetails::NPCSay { + npc_code: c.code.to_owned(), + say_code: say.say_code.to_owned() + }, + } + }) + }))) +} + +pub struct NPCSayTaskHandler; +#[async_trait] +impl TaskHandler for NPCSayTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + let (npc_code, say_code) = match &ctx.task.details { + TaskDetails::NPCSay { npc_code, say_code } => (npc_code.clone(), say_code.clone()), + _ => Err("Expected NPC say task to be NPCSay type")? + }; + + let say_info = match npc_say_info_by_npc_code_say_code().get(&(&npc_code, &say_code)) { + None => { + info!("NPCSayTaskHandler can't find NPCSayInfo for npc {} say_code {}", + npc_code, say_code); + return Ok(None); + } + Some(r) => r + }; + let npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { + None => { + info!("NPCSayTaskHandler can't find NPC {}", npc_code); + return Ok(None); + } + Some(r) => r + }; + + let (is_explicit, say_what) = match &say_info.talk_type { + NPCSayType::FromFixedList(l) => { + let mut rng = thread_rng(); + match l[..].choose(&mut rng) { + None => { + info!("NPCSayTaskHandler NPCSayInfo for npc {} say_code {} has no choices", + npc_code, say_code); + return Ok(None); + } + Some(r) => r.clone() + } + } + }; + + match say_to_room(ctx.trans, &npc_item, &npc_item.location, say_what, is_explicit).await { + Ok(()) => {} + Err(CommandHandlingError::UserError(e)) => { + info!("NPCSayHandler couldn't send for npc {} say_code {}: {}", + npc_code, say_code, e); + } + Err(CommandHandlingError::SystemError(e)) => Err(e)? + } + Ok(None) + } +} +pub static SAY_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCSayTaskHandler;