use anyhow::Result; use mindus::{data::map::ReadError, *}; use poise::{CreateReply, serenity_prelude::*}; use std::{ ops::ControlFlow, time::{Duration, Instant}, }; use tokio::task::JoinError; const BENDN: ChannelId = ChannelId::new(1149866218057117747); use super::{SUCCESS, strip_colors}; fn string((x, f): (ReadError, &str)) -> String { match x { ReadError::Decompress(_) | ReadError::Header(_) => { format!("not a map.") } ReadError::NoBlockFound(b) => { format!("couldnt find block `{b}`. mods are not supported") } ReadError::NoSuchBlock(b) => { format!("couldnt find block at index `{b}`. mods are not supported") } ReadError::Version(v) => { format!("unsupported version: `{v}`. supported versions: `7-11`.",) } ReadError::Read(r) => { format!("failed to read map. error: `{r}`. originates from `{f}`") } ReadError::ReadState(r) => { format!("failed to read dyn data in map. error: `{r}`. originates from `{f}`") } ReadError::NoBlockWithData => { format!("no block with data") } } } pub async fn download( a: &Attachment, ) -> Result<(Result<(Map, Box<[u8]>), (ReadError, &str)>, Duration)> { let s = a.download().await?.into_boxed_slice(); let then = Instant::now(); // could ignore, but i think if you have a msav, you dont want to ignore failures. Ok(( Map::deserialize(&mut mindus::data::DataRead::new(&s)) .map_err(|x| (x, &*a.filename)) .map(|x| (x, s)), then.elapsed(), )) } pub async fn scour( m: &Message, ) -> Result), (ReadError, &str)>, Duration)>> { for a in &m.attachments { if a.filename.ends_with("msav") { return Ok(Some(download(a).await?)); } } Ok(None) } pub async fn reply( c: super::Context<'_>, a: &Attachment, ) -> Result> { let ((m, b), deser_took) = match download(a).await? { (Err(e), _) => return Ok(ControlFlow::Continue(string(e))), (Ok(m), deser_took) => (m, deser_took), }; let (a, e) = match embed(m, deser_took).await { Ok(x) => x, Err(e) => { BENDN .send_files( &c, [CreateAttachment::bytes(b, "map.msav")], CreateMessage::new().content(format!("<@696196765564534825> failure: {e}")), ) .await?; return Ok(ControlFlow::Break(CreateReply::default().content( "there was a problem. i have notified bendn about this issue.", ))); } }; Ok(ControlFlow::Break( CreateReply::default().attachment(a).embed(e), )) } struct Timings { deser_took: Duration, render_took: Duration, compression_took: Duration, total: Duration, } fn render(m: Map, deser_took: Duration) -> (Timings, Vec) { let render_took = Instant::now(); let i = m.render(); let render_took = render_took.elapsed(); let compression_took = Instant::now(); let png = super::png(i); let compression_took = compression_took.elapsed(); let total = deser_took + render_took + compression_took; ( Timings { deser_took, render_took, compression_took, total, }, png, ) } pub async fn find( msg: &Message, c: &serenity::client::Context, ) -> Result), Duration)>> { match scour(msg).await? { None => Ok(None), Some((Err(e), _)) => { msg.reply(c, string(e)).await?; Ok(None) } Some((Ok(m), deser_took)) => Ok(Some(( msg.author_nick(c).await.unwrap_or(msg.author.name.clone()), m, deser_took, ))), } } #[implicit_fn::implicit_fn] pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { let Some((_auth, (m, b), deser_took)) = find(msg, c).await? else { return Ok(()); }; let t = msg.channel_id.start_typing(&c.http); let (png, embed) = match embed(m, deser_took).await { Ok(x) => x, Err(e) => { use crate::emoji::named::*; BENDN .send_files( &c, [CreateAttachment::bytes(b, "file.msch")], CreateMessage::new().content(format!( "<@696196765564534825> panic `{e}` in {}\n", msg.guild(&c.cache).map_or( msg.guild_id .map(_.get().to_string()) .unwrap_or(format!("dms with {}", msg.author.name)), _.name.clone(), ) )), ) .await?; msg.reply(c, format!("{CANCEL} there was an error while rendering this map.\nthis issue has been reported to bendn, who will hopefully take a look eventually.")) .await?; return Ok(()); } }; t.stop(); super::data::push_j(serde_json::json! {{ "locale": msg.author.locale.as_deref().unwrap_or("no locale"), "name": msg.author.name, "id": msg.author.id, "cname": "map message input", "guild": msg.guild_id.map_or(0,|x|x.get()), "channel": msg.channel_id.get(), }}); msg.channel_id .send_message(c, CreateMessage::new().add_file(png).embed(embed)) .await?; Ok(()) } async fn embed(m: Map, deser_took: Duration) -> Result<(CreateAttachment, CreateEmbed), JoinError> { let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap()); let d = strip_colors(m.tags.get("description").map(|x| &**x).unwrap_or("?")); let f = if m.width == m.height { format!("{}²", m.width) } else { format!("{}×{}", m.height, m.width) }; let (timings, png) = tokio::task::spawn_blocking(move || render(m, deser_took)).await?; Ok(( CreateAttachment::bytes(png, "map.png"), CreateEmbed::new() .title(&name) .description(d) .footer(CreateEmbedFooter::new(format!( "render of {name} ({f}) took: {:.3}s", timings.total.as_secs_f64() ))) .attachment("map.png") .color(SUCCESS), )) } #[poise::command( context_menu_command = "Render map", install_context = "User", interaction_context = "Guild|PrivateChannel" )] /// Renders map inside a message. pub async fn render_message(c: super::Context<'_>, m: Message) -> Result<()> { super::log(&c); let Some((_auth, (m, b), deser_took)) = find(&m, c.serenity_context()).await? else { poise::say_reply(c, "no map").await?; return Ok(()); }; let (png, embed) = match embed(m, deser_took).await { Ok(x) => x, Err(e) => { BENDN .send_files( &c, [CreateAttachment::bytes(b, "map.msav")], CreateMessage::new().content(format!("<@696196765564534825> failure: {e}")), ) .await?; c.say("there was a problem. i have notified bendn about this issue.") .await?; return Ok(()); } }; poise::send_reply(c, CreateReply::default().attachment(png).embed(embed)).await?; Ok(()) }