smol bot
changes
bendn 8 months ago
parent 9f42d52 · commit 456949a
-rw-r--r--Cargo.toml5
-rw-r--r--src/bot/colors1
-rw-r--r--src/bot/map.rs49
-rw-r--r--src/bot/mod.rs8
-rw-r--r--src/bot/repos.rs5
-rw-r--r--src/bot/sorter.rs183
6 files changed, 148 insertions, 103 deletions
diff --git a/Cargo.toml b/Cargo.toml
index bdedade..e4d4f50 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(())
+}