Create 60 NPCs and let them spontaneously talk.

This commit is contained in:
Condorra 2023-01-07 23:06:02 +11:00
parent d93a1fc3ca
commit 4207ab8f4d
10 changed files with 922 additions and 44 deletions

1
Cargo.lock generated
View File

@ -126,6 +126,7 @@ dependencies = [
"once_cell",
"ouroboros",
"phf",
"rand",
"ring",
"serde",
"serde_json",

View File

@ -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"

View File

@ -151,6 +151,17 @@ impl DBPool {
.collect()))
}
pub async fn find_static_task_types(self: &Self) -> DResult<Box<BTreeSet<String>>> {
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<Object> {
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"),
&params).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<Box<BTreeSet<String>>> {
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<Option<Arc<Item>>> {
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"))?)

View File

@ -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> {

View File

@ -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!("<yellow>{} says: <reset><bold>\"{}\"<reset>\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;

View File

@ -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<SkillType, u16>,
pub temporary_buffs: Vec<Buff>,
pub pronouns: Pronouns,
pub flags: Vec<ItemFlag>,
pub sex: Option<Sex>,
}
@ -162,6 +169,7 @@ impl Default for Item {
total_skills: BTreeMap::new(),
temporary_buffs: Vec::new(),
pronouns: Pronouns::default_inanimate(),
flags: vec!(),
sex: None
}
}

View File

@ -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",
}
}
}

View File

@ -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()
)
}

View File

@ -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<dyn Fn() -> Item>
}
struct StaticItemTypeGroup {
item_type: &'static str,
items: fn () -> Box<dyn Iterator<Item = StaticItem>>
pub struct StaticTask {
pub task_code: String,
pub initial_task: Box<dyn Fn() -> Task>
}
fn static_item_registry() -> Vec<StaticItemTypeGroup> {
struct StaticThingTypeGroup<Thing> {
thing_type: &'static str,
things: fn () -> Box<dyn Iterator<Item = Thing>>
}
fn static_item_registry() -> Vec<StaticThingTypeGroup<StaticItem>> {
vec!(
// Must have no duplicates.
StaticItemTypeGroup {
item_type: "npc",
items: || npc::npc_static_items()
StaticThingTypeGroup::<StaticItem> {
thing_type: "npc",
things: || npc::npc_static_items()
},
StaticItemTypeGroup {
item_type: "room",
items: || room::room_static_items()
StaticThingTypeGroup::<StaticItem> {
thing_type: "room",
things: || room::room_static_items()
},
StaticItemTypeGroup {
item_type: "fixed_item",
items: || fixed_item::static_items()
StaticThingTypeGroup::<StaticItem> {
thing_type: "fixed_item",
things: || fixed_item::static_items()
},
)
}
fn static_task_registry() -> Vec<StaticThingTypeGroup<StaticTask>> {
vec!(
// Must have no duplicates.
StaticThingTypeGroup::<StaticTask> {
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<String> =
registry.iter().map(|x| x.item_type.to_owned()).collect();
registry.iter().map(|x| x.thing_type.to_owned()).collect();
let cur_types: Box<BTreeSet<String>> = 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<String, StaticItem> =
(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<String> = 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<String> =
registry.iter().map(|x| x.thing_type.to_owned()).collect();
let cur_types: Box<BTreeSet<String>> = 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<String, StaticTask> =
(type_group.things)().map(|x| (x.task_code.to_owned(), x)).collect();
let expected_set: BTreeSet<String> = 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<dyn Iterator<Item = StaticItem>> = (type_group.items)();
let iterator : Box<dyn Iterator<Item = StaticItem>> = (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<dyn Iterator<Item = StaticTask>> = (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(", "));
}
}

View File

@ -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<NPCSayInfo>
}
pub fn npc_list() -> &'static Vec<NPC> {
use NPCSayType::FromFixedList;
static NPC_LIST: OnceCell<Vec<NPC>> = 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<BTreeMap<(&'static str, &'static str),
&'static NPCSayInfo>> = 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<dyn Iterator<Item = StaticItem>> {
Box::new(npc_list().iter().map(|c| StaticItem {
item_code: c.code,
@ -62,3 +644,78 @@ pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
})
}))
}
pub fn npc_say_tasks() -> Box<dyn Iterator<Item = StaticTask>> {
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<Option<time::Duration>> {
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;