smol bot
-rw-r--r--Cargo.toml5
-rw-r--r--src/bot/map.rs81
-rw-r--r--src/bot/mod.rs5
-rw-r--r--src/bot/sorter.rs176
-rw-r--r--src/bot/usage.md4
-rw-r--r--src/main.rs3
6 files changed, 255 insertions, 19 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 8fae5a1..ec1a236 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,7 +29,7 @@ flate2 = { version = "1.0", features = [
lemu = { features = ["diagnose"], default-features = false, version = "0.2.0" }
dashmap = "5.5.3"
oxipng = { version = "9.0.0", default-features = false }
-fimg = "0.4.26"
+fimg = { version = "0.4.26", features = ["save"] }
phf = { version = "0.11.2", features = ["macros"] }
emojib = { git = "https://github.com/Apricot-Conservation-Project/emoji", package = "emoji" }
rust-fuzzy-search = "0.1.1"
@@ -52,6 +52,9 @@ httpdate = "1.0.3"
pollster = "0.3.0"
btparse-stable = "0.1.2"
cpu-monitor = "0.1.1"
+exoquant = "0.2.0"
+image = { version = "0.25.5", features = ["bmp", "jpeg", "png", "webp"], default-features = false }
+car = "0.1.1"
[build-dependencies]
emojib = { git = "https://github.com/Apricot-Conservation-Project/emoji", features = [
diff --git a/src/bot/map.rs b/src/bot/map.rs
index 4188fc9..0c6e3aa 100644
--- a/src/bot/map.rs
+++ b/src/bot/map.rs
@@ -97,25 +97,74 @@ async fn render(m: Map, deser_took: Duration) -> (Timings, Vec<u8>) {
.unwrap()
}
+pub async fn find(
+ msg: &Message,
+ c: &serenity::client::Context,
+) -> Result<Option<(String, Map, 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,
+ ))),
+ }
+}
+
pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> {
- let auth = msg.author_nick(c).await.unwrap_or(msg.author.name.clone());
- let (m, deser_took) = match scour(msg).await? {
- None => return Ok(()),
- Some((Err(e), _)) => return Ok(drop(msg.reply(c, string(e)).await?)),
- Some((Ok(m), deser_took)) => (m, deser_took),
+ let Some((auth, m, deser_took)) = find(msg, c).await? else {
+ return Ok(());
};
let t = msg.channel_id.start_typing(&c.http);
- 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;
+ let (png, embed) = embed(m, &auth, deser_took).await;
t.stop();
- msg.channel_id.send_message(c,CreateMessage::new().add_file(CreateAttachment::bytes(png,"map.png")).embed(CreateEmbed::new().title(&name).footer(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()))).attachment("map.png").color(SUCCESS))).await?;
+ msg.channel_id
+ .send_message(c, CreateMessage::new().add_file(png).embed(embed))
+ .await?;
+ Ok(())
+}
+
+async fn embed(m: Map, auth: &str, deser_took: Duration) -> (CreateAttachment, CreateEmbed) {
+ let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap());
+ let (timings, png) = render(m, deser_took).await;
+ (
+ CreateAttachment::bytes(png, "map.png"),
+ CreateEmbed::new()
+ .title(&name)
+ .footer(footer((&name, auth), timings))
+ .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",
+ interaction_context = "Guild|PrivateChannel"
+)]
+/// 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 {
+ poise::say_reply(c, "no map").await?;
+ return Ok(());
+ };
+ let (png, embed) = embed(m, &auth, 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 ff02b4c..f288db1 100644
--- a/src/bot/mod.rs
+++ b/src/bot/mod.rs
@@ -4,6 +4,7 @@ pub mod ownership;
mod repos;
mod schematic;
pub mod search;
+mod sorter;
use crate::emoji;
use anyhow::Result;
@@ -263,7 +264,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(), schembrowser_instructions(), lb_no_vds(), ping(), help(), scour(), search::search(), search::file(), render(), render_file(), render_message()],
+ commands: vec![logic::run(), lb(), logic::run_file(), sorter::sorter(), schembrowser_instructions(), lb_no_vds(), ping(), help(), scour(), search::search(), search::file(), render(), render_file(), render_message(), map::render_message()],
event_handler: |c, e, _, d| {
Box::pin(async move {
match e {
@@ -428,7 +429,9 @@ impl Bot {
schembrowser_instructions(),
render_file(),
render_message(),
+ map::render_message(),
logic::run_file(),
+ sorter::sorter(),
],
)
.await?;
diff --git a/src/bot/sorter.rs b/src/bot/sorter.rs
new file mode 100644
index 0000000..c35f1a3
--- /dev/null
+++ b/src/bot/sorter.rs
@@ -0,0 +1,176 @@
+use anyhow::Result;
+use atools::prelude::*;
+use block::SORTER;
+use exoquant::{
+ ditherer::{self, Ditherer},
+ Color, Remapper, SimpleColorSpace,
+};
+use fimg::Image;
+use mindus::*;
+use poise::{serenity_prelude::*, ChoiceParameter};
+
+#[derive(ChoiceParameter)]
+enum Scaling {
+ /// dumbest, jaggedest, scaling algorithm
+ #[name = "nearest"]
+ Nearest,
+ /// prettiest scaling algorithm
+ #[name = "lanczos 3"]
+ Lanczos3,
+ #[name = "box"]
+ Box,
+ #[name = "bilinear"]
+ Bilinear,
+ #[name = "hamming"]
+ Hamming,
+ #[name = "catmull rom"]
+ CatmullRom,
+ #[name = "mitchell"]
+ Mitchell,
+}
+
+#[derive(ChoiceParameter)]
+enum Dithering {
+ #[name = "floyd steinberg"]
+ FloydSteinberg,
+ #[name = "ordered"]
+ /// A 2x2 ordered dithering.
+ Ordered,
+}
+
+fn sort(
+ mut x: Image<Box<[u8]>, 4>,
+ height: Option<u8>,
+ width: Option<u8>,
+ 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 }
+ );
+
+ 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),
+ };
+ };
+
+ 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 (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_] {
+ 0 => None,
+ x => Some(((x_, y_), x - 1)),
+ },
+ );
+ for ((x, y), i) in pixels.clone() {
+ s.set(
+ x,
+ y,
+ &SORTER,
+ data::dynamic::DynData::Content(mindus::content::Type::Item, i as _),
+ block::Rotation::Up,
+ )
+ .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),
+ )
+ };
+ }
+ (
+ preview.scale::<fimg::scale::Nearest>(preview.width() * 4, preview.height() * 4),
+ s,
+ )
+}
+
+#[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 algorithm, defaults to nearest"] algorithm: Option<Scaling>,
+ #[description = "dithering algorithm, defaults to none"] 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, mut schem) = sort(
+ Image::<_, 4>::build(x.width(), x.height())
+ .buf(x.into_vec())
+ .boxed(),
+ width,
+ height,
+ algorithm,
+ dithered,
+ );
+ use crate::emoji::to_mindustry::named::*;
+ schem
+ .tags
+ .insert("labels".to_string(), format!(r#"["{SORTER}"]"#));
+ let mut h = std::hash::DefaultHasher::default();
+ std::hash::Hasher::write(&mut h, preview.bytes());
+ let h = std::hash::Hasher::finish(&h) as u32;
+ schem
+ .tags
+ .insert("name".to_string(), format!("{SORTER} #{h:x}"));
+ let mut buff = data::DataWrite::default();
+ schem.serialize(&mut buff)?;
+ let buff = buff.consume();
+ 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"))
+ .attachment(CreateAttachment::bytes(buff, format!("sorter{h:x}.msch"))),
+ )
+ .await?;
+ }
+ Err(e) => {
+ c.reply(e.to_string()).await?;
+ }
+ }
+ Ok(())
+}
diff --git a/src/bot/usage.md b/src/bot/usage.md
index edf534e..f091955 100644
--- a/src/bot/usage.md
+++ b/src/bot/usage.md
@@ -6,6 +6,8 @@ you may instead upload a message containing a base64 encoded schematic.
you can also upload maps, eg `salt_flats.msav`.
commands:
+
- `eval`: executes mlog. see `/help eval` for more info.
+- `sorter`: creates sorter representations of images.
-bugs to be reported [here](<https://github.com/bend-n/mindus/issues/new>), or ping <@696196765564534825>. \ No newline at end of file
+bugs to be reported [here](https://github.com/bend-n/mindus/issues/new), or ping <@696196765564534825>.
diff --git a/src/main.rs b/src/main.rs
index b5999a4..4963eae 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,8 @@
+#![allow(incomplete_features)]
#![feature(
let_chains,
+ generic_const_exprs,
+ effects,
lazy_cell_consume,
iter_intersperse,
if_let_guard,