blastmud/blastmud_game/src/message_handler/user_commands/delete.rs

212 lines
7.3 KiB
Rust

use std::collections::BTreeMap;
use super::{
follow::cancel_follow_by_leader, get_player_item_or_fail, get_user_or_fail, look,
rent::recursively_destroy_or_move_item, user_error, UResult, UserError, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{
models::task::{Task, TaskDetails, TaskMeta},
regular_tasks::{TaskHandler, TaskRunContext},
services::{
combat::{corpsify_item, handle_death},
destroy_container,
skills::calculate_total_stats_skills_for_user,
},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
use rand::{distributions::Alphanumeric, Rng};
use std::time;
async fn verify_code(
ctx: &mut VerbContext<'_>,
input: &str,
base_command: &str,
action_text: &str,
) -> UResult<bool> {
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
let code = match &user_dat.danger_code {
None => {
let new_code = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect::<String>();
user_dat.danger_code = Some(new_code.clone());
new_code
}
Some(code) => code.clone(),
};
let input_tr = input.trim();
if input_tr == "" || !input_tr.starts_with("code ") {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("To verify you want to {}, type <bold>delete {} code {}<reset>\n"),
action_text, base_command, code
)),
)
.await?;
ctx.trans.save_user_model(&user_dat).await?;
return Ok(false);
}
if input_tr["code ".len()..].trim() != code {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!(
"Your confirmation code didn't match! \
To verify you want to {}, type <bold>delete {} code {}<reset>\n"
),
action_text, base_command, code
)),
)
.await?;
ctx.trans.save_user_model(&user_dat).await?;
return Ok(false);
}
Ok(true)
}
async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> {
let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
handle_death(&ctx.trans, &mut player_item).await?;
corpsify_item(&ctx.trans, &player_item).await?;
player_item.death_data = None;
player_item.location = "room/repro_xv_chargen".to_owned();
player_item.total_xp = ((player_item.total_xp as i64)
- user_dat.experience.xp_change_for_this_reroll)
.max(0) as u64;
player_item.urges = Some(Default::default());
user_dat.experience.xp_change_for_this_reroll = 0;
user_dat.raw_stats = BTreeMap::new();
user_dat.raw_skills = BTreeMap::new();
user_dat.wristpad_hacks = vec![];
user_dat.scan_codes = vec![];
calculate_total_stats_skills_for_user(&mut player_item, &user_dat);
ctx.trans.save_user_model(&user_dat).await?;
ctx.trans.save_item_model(&player_item).await?;
look::VERB.handle(ctx, "look", "").await?;
Ok(())
}
#[derive(Clone)]
pub struct DestroyUserHandler;
pub static DESTROY_USER_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DestroyUserHandler;
#[async_trait]
impl TaskHandler for DestroyUserHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let username = match &ctx.task.details {
TaskDetails::DestroyUser { username } => username.clone(),
_ => {
return Ok(None);
}
};
let _user = match ctx.trans.find_by_username(&username).await? {
None => return Ok(None),
Some(u) => u,
};
let player_item = match ctx
.trans
.find_item_by_type_code("player", &username.to_lowercase())
.await?
{
None => return Ok(None),
Some(p) => p,
};
cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?;
destroy_container(&ctx.trans, &player_item).await?;
for dynzone in ctx
.trans
.find_dynzone_for_owner(&format!("player/{}", &username.to_lowercase()))
.await?
{
recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
}
ctx.trans.delete_user(&username).await?;
Ok(None)
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let rtrim = remaining.trim();
let username = get_user_or_fail(ctx)?.username.clone();
if rtrim.starts_with("character forever") {
if !verify_code(ctx, &rtrim["character forever".len()..], "character forever",
"permanently destroy your character (after a one week reflection period), making the name available for other players").await? {
return Ok(());
}
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: username.clone(),
next_scheduled: Utc::now() + chrono::Duration::days(7),
..Default::default()
},
details: TaskDetails::DestroyUser { username },
})
.await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(
"Puny human, your permanent destruction has been scheduled \
for one week from now! If you change your mind, just log \
in again before the week is up. After one week, your username \
will be available for others to take, and you will need to start \
again with a new character. This character will count towards the \
character limit for the week, but will not once it is deleted. \
Goodbye forever!\n",
),
)
.await?;
ctx.trans.queue_for_session(ctx.session, None).await?;
} else if rtrim.starts_with("stats") {
if !verify_code(ctx, &rtrim["stats".len()..], "stats",
"kill your character, reset your stats and non-journal XP, and pick new stats to reclone with").await? {
return Ok(());
}
reset_stats(ctx).await?;
} else {
user_error(
ansi!("Try <bold>delete character forever<reset> or <bold>delete stats<reset>")
.to_owned(),
)?
}
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
user_dat.danger_code = None;
ctx.trans.save_user_model(&user_dat).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;