blastmud/blastmud_game/src/message_handler/user_commands/rent.rs
2024-03-11 20:42:52 +11:00

545 lines
22 KiB
Rust

use super::{
corp::check_corp_perm, get_player_item_or_fail, get_user_or_fail, user_error, UResult,
UserError, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::{
dynzone::dynzone_by_type,
npc::npc_by_code,
room::{room_map_by_code, Direction, RentSuiteType},
},
DResult,
};
use ansi::ansi;
use async_recursion::async_recursion;
use async_trait::async_trait;
use chrono::{DateTime, TimeDelta, Utc};
use itertools::Itertools;
use log::info;
use mockall_double::double;
use std::time;
#[async_recursion]
pub async fn recursively_destroy_or_move_item(trans: &DBTrans, item: &Item) -> DResult<()> {
let mut item_mut = item.clone();
match item.item_type.as_str() {
"npc" => {
let npc = match npc_by_code().get(item.item_code.as_str()) {
None => return Ok(()),
Some(r) => r,
};
item_mut.location = npc.spawn_location.to_owned();
trans.save_item_model(&item_mut).await?;
return Ok(());
}
"player" => {
let session = trans.find_session_for_player(&item.item_code).await?;
match session.as_ref() {
Some((listener_sess, _)) => {
trans.queue_for_session(
&listener_sess,
Some(ansi!("<red>The landlord barges in with a bunch of very big hired goons, who stuff you in a sack, while the landlord mumbles something about vacant possession.<reset> After what seems like an eternity being jostled along while stuffed in the sack, they dump you out into a room that seems to be some kind of homeless shelter, and beat a hasty retreat.\n"))
).await?;
}
None => {}
}
item_mut.location = "room/melbs_homelessshelter".to_owned();
trans.save_item_model(&item_mut).await?;
return Ok(());
}
_ => {}
}
trans.delete_item(&item.item_type, &item.item_code).await?;
let loc = format!("{}/{}", &item.item_type, &item.item_code);
// It's paginated so we loop to get everything...
loop {
let result = trans.find_items_by_location(&loc).await?;
if result.is_empty() {
return Ok(());
}
for sub_item in result {
recursively_destroy_or_move_item(trans, &sub_item).await?;
}
}
}
static EVICTION_NOTICE: &'static str = ansi!(". Nailed to the door is a notice: <red>Listen here you lazy bum - you didn't pay your rent on time, and so unless you come down in the next 24 hours and re-rent the place (and pay the setup fee again for wasting my time), I'm having you, and any other deadbeats who might be in there with you, evicted, and I'm just gonna sell anything I find in there<reset>");
async fn bill_residential_room(
ctx: &mut TaskRunContext<'_>,
bill_player_code: &str,
daily_price: u64,
zone_item: &Item,
vacate_after: Option<DateTime<Utc>>,
) -> DResult<Option<time::Duration>> {
let mut bill_user = match ctx.trans.find_by_username(bill_player_code).await? {
None => return Ok(None),
Some(user) => user,
};
let session = ctx.trans.find_session_for_player(bill_player_code).await?;
// Check if they have enough money.
if bill_user.credits < daily_price {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
let mut zone_item_mut = (*zone_item).clone();
zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData {
vacate_after: Some(Utc::now() + TimeDelta::try_days(1).unwrap()),
zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => zone_exit.clone(),
_ => None,
},
});
ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx
.trans
.find_item_by_type_code("dynroom", &(zone_item.item_code.clone() + "/doorstep"))
.await?
{
None => {}
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details =
Some(doorstep_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE);
ctx.trans.save_item_model(&doorstep_mut).await?;
}
}
match session.as_ref() {
None => {},
Some((listener, _)) => ctx.trans.queue_for_session(
listener, Some(&format!(
ansi!("<red>Your wristpad beeps a sad sounding tone as your landlord at {} \
tries and fails to take rent.<reset> You'd better earn some more money fast \
and hurry to reception (in the next 24 hours) and sign a new rental \
agreement for the premises, or all your stuff will be gone forever!\n"),
&zone_item.display
)),
).await?
}
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
bill_user.credits -= daily_price;
ctx.trans.save_user_model(&bill_user).await?;
match session.as_ref() {
None => {},
Some((listener, _)) => ctx.trans.queue_for_session(
listener, Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${} for rent for {}.<reset>\n"),
daily_price, &zone_item.display
))
).await?
}
Ok(Some(time::Duration::from_secs(3600 * 24)))
}
async fn bill_commercial_room(
ctx: &mut TaskRunContext<'_>,
bill_corp: &str,
daily_price: u64,
zone_item: &Item,
vacate_after: Option<DateTime<Utc>>,
) -> DResult<Option<time::Duration>> {
let mut bill_corp = match ctx.trans.find_corp_by_name(bill_corp).await? {
None => return Ok(None),
Some(c) => c,
};
// Check if they have enough money.
if bill_corp.1.credits < daily_price {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
let mut zone_item_mut = (*zone_item).clone();
zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData {
vacate_after: Some(Utc::now() + TimeDelta::try_days(1).unwrap()),
zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => zone_exit.clone(),
_ => None,
},
});
ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx
.trans
.find_item_by_type_code("dynroom", &(zone_item.item_code.clone() + "/reception"))
.await?
{
None => {}
Some(reception_room) => {
let mut reception_mut = (*reception_room).clone();
reception_mut.details =
Some(reception_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE);
ctx.trans.save_item_model(&reception_mut).await?;
}
}
ctx.trans.broadcast_to_corp(
&bill_corp.0,
&CorpCommType::Notice,
None,
&format!(
ansi!("<red>All wristpads of members of {} beep a sad sounding tone as the corp's landlord at {} \
tries and fails to take rent.<reset> You'd better earn some more money for the corp \
fast and have your holder hurry to reception (in the next 24 hours) and sign a new \
lease agreement for the premises, or all your corp's stuff will be gone forever!\n"),
&bill_corp.1.name,
&zone_item.display
),
).await?;
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
bill_corp.1.credits -= daily_price;
ctx.trans
.update_corp_details(&bill_corp.0, &bill_corp.1)
.await?;
ctx.trans.broadcast_to_corp(
&bill_corp.0,
&CorpCommType::Notice,
None,
&format!(
ansi!("<yellow>Your wristpad beeps as a deduction is made from {}'s account of ${} for rent for {}.<reset>\n"),
&bill_corp.1.name,
daily_price, &zone_item.display
)
).await?;
Ok(Some(time::Duration::from_secs(3600 * 24)))
}
pub struct ChargeRoomTaskHandler;
#[async_trait]
impl TaskHandler for ChargeRoomTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (zone_item_ref, daily_price) = match ctx.task.details {
TaskDetails::ChargeRoom {
ref zone_item,
ref daily_price,
} => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?,
};
let zone_item_code = match zone_item_ref.split_once("/") {
Some(("dynzone", c)) => c,
_ => Err("Invalid zone item ref when charging room")?,
};
let zone_item = match ctx
.trans
.find_item_by_type_code("dynzone", zone_item_code)
.await?
{
None => {
info!(
"Can't charge rent for dynzone {}, it's gone",
zone_item_code
);
return Ok(None);
}
Some(it) => it,
};
let vacate_after = match zone_item.special_data {
Some(ItemSpecialData::DynzoneData { vacate_after, .. }) => vacate_after,
_ => Err("Expected ChargeRoom dynzone to have DynzoneData")?,
};
match vacate_after {
Some(t) if t < Utc::now() => {
recursively_destroy_or_move_item(&ctx.trans, &zone_item).await?;
return Ok(None);
}
_ => (),
}
match zone_item.owner.as_ref().and_then(|s| s.split_once("/")) {
Some((player_item_type, player_item_code)) if player_item_type == "player" => {
bill_residential_room(
ctx,
player_item_code,
daily_price.clone(),
&zone_item,
vacate_after,
)
.await
}
Some((item_type, corpname)) if item_type == "corp" => {
bill_commercial_room(ctx, corpname, daily_price.clone(), &zone_item, vacate_after)
.await
}
_ => {
info!(
"Can't charge rent for dynzone {}, owner {:?} isn't chargeable",
zone_item_code, &zone_item.owner
);
return Ok(None);
}
}
}
}
pub static CHARGE_ROOM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ChargeRoomTaskHandler;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let remaining = remaining.trim();
let (item_name, corp_name) = match remaining.split_once(" for ") {
None => (remaining, None),
Some((i, c)) => (i.trim(), Some(c.trim())),
};
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
if loc_type != "room" {
user_error("You can't rent anything from here.".to_owned())?;
}
let room = room_map_by_code()
.get(loc_code)
.ok_or_else(|| UserError("Can't find your room".to_owned()))?;
if room.rentable_dynzone.is_empty() {
user_error("You can't rent anything from here.".to_owned())?;
}
let rentinfo = match room
.rentable_dynzone
.iter()
.find(|ri| ri.rent_what == item_name)
{
None => user_error(format!(
"Rent must be followed by the specific thing you want to rent: {}",
room.rentable_dynzone
.iter()
.map(|ri| ri.rent_what.as_str())
.join(", ")
))?,
Some(v) => v,
};
let user = get_user_or_fail(ctx)?;
let mut corp = match (&rentinfo.suite_type, corp_name) {
(RentSuiteType::Commercial, None) =>
user_error(format!(
ansi!("This is a commercial suite, you need to rent it in the name of a corp. Try <bold>rent {} for corpname<reset>"),
item_name
))?,
(RentSuiteType::Residential, Some(_)) =>
user_error("This is a residential suite, you can't rent it for a corp. Try finding a commercial suite for your corp, or rent it personally.".to_owned())?,
(RentSuiteType::Residential, None) => None,
(RentSuiteType::Commercial, Some(n)) => match ctx.trans.match_user_corp_by_name(&n, &user.username).await? {
None => user_error("I can't find that corp in your list of corps!".to_owned())?,
Some((_, _, mem)) if !check_corp_perm(&CorpPermission::Holder, &mem) => user_error("You don't have holder permissions in that corp.".to_owned())?,
Some((corp_id, corp, _)) => Some((corp_id, corp))
},
};
match corp.as_ref() {
None if user.credits < rentinfo.setup_fee =>
user_error("The robot rolls its eyes at you derisively. \"I don't think so - you couldn't even afford the setup fee!\"".to_owned())?,
Some((_, c)) if c.credits < rentinfo.setup_fee =>
user_error("The robot rolls its eyes at you derisively. \"I don't think so - your corp couldn't even afford the setup fee!\"".to_owned())?,
_ => {}
}
let zone = dynzone_by_type().get(&rentinfo.dynzone).ok_or_else(|| {
UserError("That seems to no longer exist, so you can't rent it.".to_owned())
})?;
let exit = Direction::IN {
item: corp
.as_ref()
.map(|c| c.1.name.clone())
.unwrap_or_else(|| player_item.display.clone()),
};
match ctx
.trans
.find_exact_dyn_exit(&player_item.location, &exit)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
{
None => {}
Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) = ctx
.trans
.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData {
vacate_after: None, ..
}) => user_error(
"You can only rent one apartment here, and you already have one!"
.to_owned(),
)?,
Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_),
zone_exit: ref ex,
}) => {
match corp.as_mut() {
None => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee
)),
)
.await?;
}
Some(corptup) => {
corptup.1.credits -= rentinfo.setup_fee;
ctx.trans
.update_corp_details(&corptup.0, &corptup.1)
.await?;
ctx.trans
.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
None,
&format!(
"[{}] {} just cancelled plans to vacate a {} for the corp for a setup fee of ${}.\n",
&corptup.1.name, &user.username, item_name, rentinfo.setup_fee
),
)
.await?;
}
}
ctx.trans
.save_item_model(&Item {
special_data: Some(ItemSpecialData::DynzoneData {
vacate_after: None,
zone_exit: ex.clone(),
}),
..(*ex_zone).clone()
})
.await?;
match ctx
.trans
.find_item_by_type_code(
"dynroom",
&(ex_zone.item_code.clone() + "/" + zone.entrypoint_subcode),
)
.await?
{
None => {}
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details = Some(
doorstep_mut
.details
.clone()
.unwrap_or("".to_owned())
.replace(EVICTION_NOTICE, ""),
);
ctx.trans.save_item_model(&doorstep_mut).await?;
}
}
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(ansi!(
"\"Okay - let's forget this ever happened - and apart from me having a few extra credits for my trouble, your lease will continue as before!\"\n")
))).await?;
return Ok(());
}
_ => {}
}
}
}
}
let owner = corp
.as_ref()
.map(|c| format!("corp/{}", c.1.name))
.unwrap_or_else(|| player_item.refstr());
let zonecode = zone
.create_instance(
ctx.trans,
&player_item.location,
"You can only rent one apartment here, and you already have one!",
&owner,
&exit,
)
.await?;
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("charge_rent/{}", &zonecode),
is_static: false,
recurrence: None, // Managed by the handler.
next_scheduled: Utc::now() + TimeDelta::try_days(1).unwrap(),
..Default::default()
},
details: TaskDetails::ChargeRoom {
zone_item: zonecode.clone(),
daily_price: rentinfo.daily_price,
},
})
.await?;
match corp.as_mut() {
None => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee
)),
)
.await?;
}
Some(corptup) => {
corptup.1.credits -= rentinfo.setup_fee;
ctx.trans
.update_corp_details(&corptup.0, &corptup.1)
.await?;
ctx.trans
.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
None,
&format!(
"[{}] {} just rented a {} for the corp for a setup fee of ${}.\n",
&corptup.1.name, &user.username, item_name, rentinfo.setup_fee
),
)
.await?;
}
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;