smol bot
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | src/bot/colors | 1 | ||||
| -rw-r--r-- | src/bot/map.rs | 49 | ||||
| -rw-r--r-- | src/bot/mod.rs | 8 | ||||
| -rw-r--r-- | src/bot/repos.rs | 5 | ||||
| -rw-r--r-- | src/bot/sorter.rs | 183 |
6 files changed, 148 insertions, 103 deletions
@@ -23,7 +23,9 @@ poise = { git = "https://github.com/fgardt/poise", branch = "feat/user_apps" } anyhow = "1.0.75" regex = { version = "1.8.4", features = ["std"], default-features = false } mindus = { version = "5.0.7", features = [], default-features = false } -lemu = { features = ["diagnose"], default-features = false, version = "0.2.0" } +lemu = { features = [ + "diagnose", +], default-features = false, git = "https://github.com/bend-n/mindus" } dashmap = "5.5.3" oxipng = { version = "9.0.0", default-features = false } fimg = { version = "0.4.26", features = ["save"] } @@ -58,6 +60,7 @@ image = { version = "0.25.5", features = [ car = "0.1.1" kv = "0.24.0" sled = { version = "0.34.7", features = ["compression"] } +remapper = { version = "0.1.0", path = "../remapper" } [features] server = ["axum"] diff --git a/src/bot/colors b/src/bot/colors new file mode 100644 index 0000000..27e0795 --- /dev/null +++ b/src/bot/colors @@ -0,0 +1 @@ +[0.6627451181411743,0.2078431397676468,0.15294118225574493,0.7686274647712708,0.26274511218070984,0.16470588743686676,0.239215686917305,0.2862745225429535,0.501960813999176,0.27843138575553894,0.3294117748737335,0.5607843399047852,0.29019609093666077,0.2235294133424759,0.4470588266849518,0.2078431397676468,0.16078431904315948,0.32549020648002625,0.2235294133424759,0.19607843458652496,0.29019609093666077,0.45490196347236633,0.40784314274787903,0.47843137383461,0.2078431397676468,0.21568627655506134,0.3176470696926117,0.08627451211214066,0.0941176488995552,0.09019608050584793,0.3294117748737335,0.615686297416687,0.7098039388656616,0.7686274647712708,0.38823530077934265,0.19607843458652496,0.019607843831181526,0.0,0.07058823853731155,0.25882354378700256,0.25882354378700256,0.27843138575553894,0.26274511218070984,0.26274511218070984,0.27843138575553894,0.23529411852359772,0.23529411852359772,0.25882354378700256,0.1921568661928177,0.18431372940540314,0.18431372940540314,0.26274511218070984,0.21960784494876862,0.1921568661928177,0.40784314274787903,0.29019609093666077,0.2078431397676468,0.6549019813537598,0.5372549295425415,0.43529412150382996,0.1882352977991104,0.18039216101169586,0.18039216101169586,0.30980393290519714,0.1921568661928177,0.16470588743686676,0.16078431904315948,0.0941176488995552,0.08235294371843338,0.47058823704719543,0.4745098054409027,0.5254902243614197,0.35686275362968445,0.2235294133424759,0.18039216101169586,0.3490196168422699,0.21568627655506134,0.1725490242242813,0.3176470696926117,0.19607843458652496,0.16862745583057404,0.4470588266849518,0.29411765933036804,0.21176470816135406,0.45490196347236633,0.3019607961177826,0.21176470816135406,0.13333334028720856,0.14901961386203766,0.16862745583057404,0.21960784494876862,0.19607843458652496,0.1921568661928177,0.23529411852359772,0.2078431397676468,0.20000000298023224,0.16078431904315948,0.1882352977991104,0.16862745583057404,0.1764705926179886,0.15294118225574493,0.18431372940540314,0.22745098173618317,0.18431372940540314,0.26274511218070984,0.5058823823928833,0.364705890417099,0.21960784494876862,0.545098066329956,0.239215686917305,0.2549019753932953,0.572549045085907,0.27843138575553894,0.2666666805744171,0.7568627595901489,0.6823529601097107,0.658823549747467,0.3333333432674408,0.43921568989753723,0.23137255012989044,0.21960784494876862,0.2862745225429535,0.19607843458652496,0.25882354378700256,0.13725490868091583,0.11764705926179886,0.09019608050584793,0.09803921729326248,0.12156862765550613,0.1568627506494522,0.21176470816135406,0.16078431904315948,0.3607843220233917,0.21960784494876862,0.14901961386203766,0.42352941632270813,0.18039216101169586,0.21568627655506134,0.12156862765550613,0.10980392247438431,0.13333334028720856,0.20000000298023224,0.1921568661928177,0.21960784494876862,0.14901961386203766,0.13725490868091583,0.1411764770746231,0.2705882489681244,0.16862745583057404,0.16470588743686676,0.1764705926179886,0.15294118225574493,0.250980406999588,0.3294117748737335,0.5176470875740051,0.2862745225429535,0.7372549176216125,0.7411764860153198,0.7490196228027344,0.7019608020782471,0.7176470756530762,0.7333333492279053,0.6352941393852234,0.6352941393852234,0.7607843279838562,0.6117647290229797,0.6196078658103943,0.7333333492279053,0.29019609093666077,0.27450981736183167,0.3960784375667572,0.33725491166114807,0.21176470816135406,0.3607843220233917,0.41960784792900085,0.35686275362968445,0.29411765933036804,0.33725491166114807,0.21960784494876862,0.40784314274787903,0.3333333432674408,0.34117648005485535,0.3764705955982208,0.32549020648002625,0.3294117748737335,0.3686274588108063,0.3137255012989044,0.3176470696926117,0.3529411852359772,0.3529411852359772,0.35686275362968445,0.3960784375667572,0.364705890417099,0.37254902720451355,0.4156862795352936,0.42352941632270813,0.3450980484485626,0.2980392277240753,0.23529411852359772,0.24313725531101227,0.27843138575553894,0.2549019753932953,0.24313725531101227,0.2705882489681244,0.23137255012989044,0.23529411852359772,0.2705882489681244,0.26274511218070984,0.2705882489681244,0.30980393290519714,0.27450981736183167,0.26274511218070984,0.29411765933036804,0.30588236451148987,0.2705882489681244,0.2980392277240753,0.2823529541492462,0.27843138575553894,0.29411765933036804,0.32156863808631897,0.20392157137393951,0.3490196168422699,0.8509804010391235,0.615686297416687,0.45098039507865906,0.5490196347236633,0.49803921580314636,0.6627451181411743,0.46666666865348816,0.46666666865348816,0.46666666865348816,0.15294118225574493,0.15294118225574493,0.15294118225574493,0.5529412031173706,0.6313725709915161,0.8901960849761963,0.9764705896377563,0.6392157077789307,0.7803921699523926,0.22745098173618317,0.5607843399047852,0.3921568691730499,0.4627451002597809,0.5411764979362488,0.6039215922355652,0.9764705896377563,0.6392157077789307,0.7803921699523926,0.9764705896377563,0.6392157077789307,0.7803921699523926,0.22745098173618317,0.5607843399047852,0.3921568691730499,0.4627451002597809,0.5411764979362488,0.6039215922355652]
\ No newline at end of file diff --git a/src/bot/map.rs b/src/bot/map.rs index 0c6e3aa..6642860 100644 --- a/src/bot/map.rs +++ b/src/bot/map.rs @@ -55,17 +55,10 @@ pub async fn reply(a: &Attachment) -> Result<ControlFlow<CreateReply, String>> { (Err(e), _) => return Ok(ControlFlow::Continue(string(e))), (Ok(m), deser_took) => (m, deser_took), }; - let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap()); - let ( - Timings { - deser_took, - render_took, - compression_took, - total, - }, - png, - ) = render(m, deser_took).await; - Ok(ControlFlow::Break(CreateReply::default().attachment(CreateAttachment::bytes(png,"map.png")).embed(CreateEmbed::new().title(&name).footer(CreateEmbedFooter::new(format!("render of {name} took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", total.as_secs_f32(), deser_took.as_millis(), render_took.as_secs_f32(), compression_took.as_secs_f32()))).attachment("map.png").color(SUCCESS)))) + let (a, e) = embed(m, deser_took).await; + Ok(ControlFlow::Break( + CreateReply::default().attachment(a).embed(e), + )) } struct Timings { @@ -116,11 +109,11 @@ pub async fn find( } pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { - let Some((auth, m, deser_took)) = find(msg, c).await? else { + let Some((_auth, m, deser_took)) = find(msg, c).await? else { return Ok(()); }; let t = msg.channel_id.start_typing(&c.http); - let (png, embed) = embed(m, &auth, deser_took).await; + let (png, embed) = embed(m, deser_took).await; t.stop(); msg.channel_id .send_message(c, CreateMessage::new().add_file(png).embed(embed)) @@ -128,31 +121,29 @@ pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { Ok(()) } -async fn embed(m: Map, auth: &str, deser_took: Duration) -> (CreateAttachment, CreateEmbed) { +async fn embed(m: Map, deser_took: Duration) -> (CreateAttachment, CreateEmbed) { 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) = render(m, deser_took).await; ( CreateAttachment::bytes(png, "map.png"), CreateEmbed::new() .title(&name) - .footer(footer((&name, auth), timings)) + .description(d) + .footer(CreateEmbedFooter::new(format!( + "render of {name} ({f}) took: {:.3}s", + timings.total.as_secs_f64() + ))) .attachment("map.png") .color(SUCCESS), ) } -fn footer( - (name, auth): (&str, &str), - Timings { - deser_took, - render_took, - compression_took, - total, - }: Timings, -) -> CreateEmbedFooter { - CreateEmbedFooter::new(format!("render of {name} (requested by {auth}) took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", total.as_secs_f32(), deser_took.as_millis(), render_took.as_secs_f32(), compression_took.as_secs_f32())) -} - #[poise::command( context_menu_command = "Render map", install_context = "User", @@ -160,11 +151,11 @@ fn footer( )] /// Renders map inside a message. pub async fn render_message(c: super::Context<'_>, m: Message) -> Result<()> { - let Some((auth, m, deser_took)) = find(&m, c.serenity_context()).await? else { + let Some((_auth, m, deser_took)) = find(&m, c.serenity_context()).await? else { poise::say_reply(c, "no map").await?; return Ok(()); }; - let (png, embed) = embed(m, &auth, deser_took).await; + let (png, embed) = embed(m, deser_took).await; poise::send_reply(c, CreateReply::default().attachment(png).embed(embed)).await?; Ok(()) } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 5428969..4a05ee0 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -305,7 +305,7 @@ impl Bot { std::env::var("TOKEN").unwrap_or_else(|_| read_to_string("token").expect("wher token")); let f = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![logic::run(), lb(), logic::run_file(), sorter::sorter(), schembrowser_instructions(), lb_no_vds(), ping(), help(), scour(), search::search(), search::file(), rename(), rename_file(), render(), render_file(), render_message(), map::render_message()], + commands: vec![logic::run(), lb(), logic::run_file(), sorter::sorter(), sorter::mapper(), schembrowser_instructions(), lb_no_vds(), ping(), help(), scour(), search::search(), search::file(), rename(), rename_file(), render(), render_file(), render_message(), map::render_message()], event_handler: |c, e, _, d| { Box::pin(async move { match e { @@ -490,11 +490,10 @@ impl Bot { map::render_message(), logic::run_file(), sorter::sorter(), + sorter::mapper(), ], ) .await?; - poise::builtins::register_in_guild(ctx, &[scour()], 1110086242177142854.into()) - .await?; poise::builtins::register_in_guild( ctx, &[search::search(), lb(), lb_no_vds(), search::file()], @@ -593,6 +592,7 @@ impl Bot { const VDS: &[u64] = &[ 1222024015015706668, + 742034952077705317, 126381304857100288, 175218107084832768, 221780012372721664, @@ -870,7 +870,7 @@ pub async fn ping(c: Context<'_>) -> Result<()> { // let m = (m / 0.1) + 0.5; // let m = m.floor() * 0.1; c.reply(format!( - "pong!\n{DISCORD}{RIGHT}: {} — {HOST} mem used: {m:.1}MiB - <:stopwatch:1283755550726684723> cpu utilization {util:.2}% — <:time:1244901561688260742> uptime: {}", + "pong!\n{DISCORD}{RIGHT}: {} — {HOST}: {m:.1}MiB - <:stopwatch:1361892467510870167><:world_processor:1307657404128690268> {util:.0}% — <:up:1307658579251167302><:time:1361892343199957022> {}", humantime::format_duration(Duration::from_millis( Timestamp::now() .signed_duration_since(*c.created_at()) diff --git a/src/bot/repos.rs b/src/bot/repos.rs index d59c2aa..4e27c24 100644 --- a/src/bot/repos.rs +++ b/src/bot/repos.rs @@ -251,7 +251,8 @@ decl! { 1222270513045438464u64 => "bore": [PRODUCTION], 1226407271978766356u64 => "pulveriser": [PULVERIZER, SAND], 1277138620863742003u64 => "melter": [MELTER, SLAG], -1277138532355543070u64 => "separator": [SEPARATOR, SCRAP] +1277138532355543070u64 => "separator": [SEPARATOR, SCRAP], +1365819562259386533u64 => "launch-pad": [ADVANCED_LAUNCH_PAD] ]; MISC => [ forum 1297452357549821972u64 => "s-defensive-outpost", @@ -275,7 +276,7 @@ ACP => [ macro_rules! chief { ($c:ident) => {{ let repo = repos::SPECIAL - .get(&$c.id()) + .get(&$c.channel_id().get()) .ok_or(anyhow::anyhow!("not repo"))? .repo; if repo.chief != $c.author().id.get() { diff --git a/src/bot/sorter.rs b/src/bot/sorter.rs index c35f1a3..b0a68f2 100644 --- a/src/bot/sorter.rs +++ b/src/bot/sorter.rs @@ -1,13 +1,10 @@ use anyhow::Result; use atools::prelude::*; use block::SORTER; -use exoquant::{ - ditherer::{self, Ditherer}, - Color, Remapper, SimpleColorSpace, -}; -use fimg::Image; +use fimg::{indexed::IndexedImage, Image}; use mindus::*; use poise::{serenity_prelude::*, ChoiceParameter}; +use remapper::pal; #[derive(ChoiceParameter)] enum Scaling { @@ -30,69 +27,80 @@ enum Scaling { } #[derive(ChoiceParameter)] +/// seeks to reduce banding enum Dithering { - #[name = "floyd steinberg"] - FloydSteinberg, - #[name = "ordered"] - /// A 2x2 ordered dithering. - Ordered, + #[name = "atkinsons"] + /// error diffusion based dithering. + Atkinsons, + #[name = "bayer4x4"] + /// bayer matrix. + Bayer4x4, + #[name = "bayer8x8"] + /// bayer matrix. + Bayer8x8, + #[name = "bayer16x16"] + /// bayer matrix. + Bayer16x16, +} + +fn d<'a>( + x: Image<&[u8], 4>, + d: Option<Dithering>, + p: pal<'a, 4>, +) -> IndexedImage<Box<[u32]>, pal<'a, 4>> { + let x = Image::<Box<[f32]>, 4>::from(x); + match d { + None => remapper::ordered::remap(x.as_ref(), p), + Some(Dithering::Atkinsons) => remapper::diffusion::atkinson(x, p), + Some(Dithering::Bayer4x4) => remapper::ordered::bayer4x4(x.as_ref(), p), + Some(Dithering::Bayer8x8) => remapper::ordered::bayer8x8(x.as_ref(), p), + Some(Dithering::Bayer16x16) => remapper::ordered::bayer16x16(x.as_ref(), p), + } +} + +fn s(mut x: Image<Box<[u8]>, 4>, f: f32, a: Option<Scaling>) -> Image<Box<[u8]>, 4> { + let f = f.min(1.0); + let width = (x.width() as f32 * f).round() as u32; + let height = (x.height() as f32 * f).round() as u32; + match a.unwrap_or(Scaling::Nearest) { + Scaling::Nearest => x.scale::<fimg::scale::Nearest>(width, height), + Scaling::Lanczos3 => x.scale::<fimg::scale::Lanczos3>(width, height), + Scaling::Box => x.scale::<fimg::scale::Box>(width, height), + Scaling::Bilinear => x.scale::<fimg::scale::Bilinear>(width, height), + Scaling::Hamming => x.scale::<fimg::scale::Hamming>(width, height), + Scaling::CatmullRom => x.scale::<fimg::scale::CatmullRom>(width, height), + Scaling::Mitchell => x.scale::<fimg::scale::Mitchell>(width, height), + } } fn sort( mut x: Image<Box<[u8]>, 4>, - height: Option<u8>, - width: Option<u8>, + scale_factor: Option<f32>, algorithm: Option<Scaling>, dithered: Option<Dithering>, ) -> (Image<Box<[u8]>, 4>, Schematic) { - const PAL: [Color; 23] = car::map!( - [0, 0, 0, 0].join(car::map!( - car::map!(mindus::item::Type::ALL, |i| i.color()), - |(r, g, b)| [r, g, b, 255] - )), - |[r, g, b, a]| Color { r, g, b, a } - ); + const PAL: [[f32; 4]; 23] = [0.; 4].join(car::map!( + car::map!(mindus::item::Type::ALL, |i| i.color()), + |(r, g, b)| [r as f32 / 256.0, g as f32 / 256.0, b as f32 / 256.0, 1.0] + )); - if width.is_some() || height.is_some() { - let width = width.map(|x| x as u32).unwrap_or(x.width()); - let height = height.map(|x| x as u32).unwrap_or(x.height()); - x = match algorithm.unwrap_or(Scaling::Nearest) { - Scaling::Nearest => x.scale::<fimg::scale::Nearest>(width, height), - Scaling::Lanczos3 => x.scale::<fimg::scale::Lanczos3>(width, height), - Scaling::Box => x.scale::<fimg::scale::Box>(width, height), - Scaling::Bilinear => x.scale::<fimg::scale::Bilinear>(width, height), - Scaling::Hamming => x.scale::<fimg::scale::Hamming>(width, height), - Scaling::CatmullRom => x.scale::<fimg::scale::CatmullRom>(width, height), - Scaling::Mitchell => x.scale::<fimg::scale::Mitchell>(width, height), - }; + if let Some(f) = scale_factor { + x = s(x, f, algorithm); }; - fn quant(x: Image<&[u8], 4>, d: impl Ditherer) -> Vec<u8> { - Remapper::new(&PAL, &SimpleColorSpace::default(), &d).remap( - &x.chunked() - .map(|&[r, g, b, a]| Color::new(r, g, b, a)) - .collect::<Vec<_>>(), - x.width() as usize, - ) - } - - let quant = match dithered { - Some(Dithering::FloydSteinberg) => quant(x.as_ref(), ditherer::FloydSteinberg::vanilla()), - Some(Dithering::Ordered) => quant(x.as_ref(), ditherer::Ordered), - None => quant(x.as_ref(), ditherer::None), - }; + let mut quant = d(x.as_ref(), dithered, pal::new(&PAL)); let (width, height) = (x.width() as usize, x.height() as usize); let mut s = Schematic::new(width, height); let pixels = (0..width) .flat_map(|x_| (0..height).map(move |y| (x_, y))) - .filter_map( - move |(x_, y_)| match quant[(height - y_ - 1) * width + x_] { + .filter_map(|(x_, y_)| { + match unsafe { quant.raw().buffer() }[(height - y_ - 1) * width + x_] { 0 => None, x => Some(((x_, y_), x - 1)), - }, - ); - for ((x, y), i) in pixels.clone() { + } + }); + for ((x, y), i) in pixels { s.set( x, y, @@ -102,32 +110,35 @@ fn sort( ) .unwrap(); } - let mut preview = Image::build(x.width(), x.height()).alloc(); - for ((x, y), i) in pixels { - unsafe { - preview.set_pixel( - x as _, - (height - y - 1) as _, - mindus::item::Type::ALL[i as usize] - .color() - .array() - .join(255), - ) - }; - } + let mut preview = quant.to().to_u8(); ( preview.scale::<fimg::scale::Nearest>(preview.width() * 4, preview.height() * 4), s, ) } +fn map( + mut x: Image<Box<[u8]>, 4>, + scale_factor: Option<f32>, + algorithm: Option<Scaling>, + dithered: Option<Dithering>, +) -> Image<Box<[u8]>, 4> { + const PAL: &[[f32; 3]] = unsafe { include!("colors").as_chunks_unchecked::<3>() }; + + if let Some(f) = scale_factor { + x = s(x, f, algorithm); + }; + + let pal = PAL.iter().map(|&x| x.join(1.0)).collect::<Vec<_>>(); + d(x.as_ref(), dithered, pal::new(&pal)).to().to_u8() +} + #[poise::command(slash_command)] /// Create sorter representations of images. pub async fn sorter( c: super::Context<'_>, #[description = "image: png, webp, jpg"] i: Attachment, - #[description = "height in blocks"] height: Option<u8>, - #[description = "height in blocks"] width: Option<u8>, + #[description = "scaling factor"] factor: Option<f32>, #[description = "scaling algorithm, defaults to nearest"] algorithm: Option<Scaling>, #[description = "dithering algorithm, defaults to none"] dithered: Option<Dithering>, ) -> Result<()> { @@ -140,8 +151,7 @@ pub async fn sorter( Image::<_, 4>::build(x.width(), x.height()) .buf(x.into_vec()) .boxed(), - width, - height, + factor, algorithm, dithered, ); @@ -174,3 +184,42 @@ pub async fn sorter( } Ok(()) } + +#[poise::command(slash_command)] +/// Create map representations of images. +pub async fn mapper( + c: super::Context<'_>, + #[description = "image: png, webp, jpg"] i: Attachment, + #[description = "scaling factor"] factor: Option<f32>, + #[description = "scaling algorithm, defaults to nearest"] algorithm: Option<Scaling>, + #[description = "dithering algorithm, defaults to none (if you want the map to be playable, go with ordered)"] + dithered: Option<Dithering>, +) -> Result<()> { + c.defer().await?; + let image = i.download().await?; + match image::load_from_memory(&image) { + Ok(x) => { + let x = x.to_rgba8(); + let preview = map( + Image::<_, 4>::build(x.width(), x.height()) + .buf(x.into_vec()) + .boxed(), + factor, + algorithm, + dithered, + ); + let mut preview_png = Vec::with_capacity(1 << 11); + fimg::WritePng::write(&preview, &mut preview_png).unwrap(); + poise::send_reply( + c, + poise::CreateReply::default() + .attachment(CreateAttachment::bytes(preview_png, "preview.png")), + ) + .await?; + } + Err(e) => { + c.reply(e.to_string()).await?; + } + } + Ok(()) +} |