Skip to content

Commit 6a5cd82

Browse files
committed
feat: enable uploading map images
1 parent 22277d9 commit 6a5cd82

File tree

24 files changed

+1179
-87
lines changed

24 files changed

+1179
-87
lines changed

Cargo.lock

Lines changed: 867 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,9 @@ enum_dispatch = "0.3.8"
3131
async-trait = "0.1.89"
3232
reqwest = { version = "0.11.17", features = ["json"] }
3333
rocket_cors = "0.6.0"
34+
image = "0.25.9"
35+
ravif = "0.12.0"
36+
rgb = "0.8.52"
37+
imgref = "1.12.0"
38+
bytes = "1.11.0"
39+
tokio-util = "0.7.17"

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
FROM rust:1.72
1+
FROM rust:latest
2+
RUN apt-get update && apt-get install -y nasm
23
WORKDIR /usr/src/mars-api
34
RUN mkdir /app
45
COPY . .

src/config.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ async fn deserialize_mars_options() -> Result<MarsConfigOptions, ConfigDeseriali
9898
"webhooks.reports" => { config.reports_webhook_url = v.to_string(); },
9999
"webhooks.notes" => { config.notes_webhook_url = v.to_string(); },
100100
"webhooks.debug" => { config.debug_log_webhook_url = v.to_string(); },
101-
"enable-exponential-exp" => { if let Ok(b) = v.to_string().parse::<bool>() { config.use_exponential_exp = b; } }
101+
"enable-exponential-exp" => { if let Ok(b) = v.to_string().parse::<bool>() { config.use_exponential_exp = b; } },
102+
"images-path" => { config.images_path = Some(v.to_string()); },
103+
"avif-transcode" => { config.avif_transcode = false; }
102104
_ => {}
103105
}
104106
});
@@ -175,7 +177,10 @@ pub struct MarsConfigOptions {
175177
pub reports_webhook_url: String,
176178
pub notes_webhook_url: String,
177179
pub debug_log_webhook_url: String,
178-
pub use_exponential_exp: bool
180+
pub use_exponential_exp: bool,
181+
pub images_path: Option<String>,
182+
// not supported yet
183+
pub avif_transcode: bool
179184
}
180185

181186
impl Default for MarsConfigOptions {
@@ -190,7 +195,9 @@ impl Default for MarsConfigOptions {
190195
reports_webhook_url: String::new(),
191196
notes_webhook_url: String::new(),
192197
debug_log_webhook_url: String::new(),
193-
use_exponential_exp: false
198+
use_exponential_exp: false,
199+
images_path: None,
200+
avif_transcode: false
194201
}
195202
}
196203
}

src/database/models/level.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl Default for LevelRecords {
7070
}
7171
}
7272

73-
#[derive(Serialize, Deserialize)]
73+
#[derive(Serialize, Deserialize, Debug)]
7474
#[serde(rename_all = "camelCase")]
7575
pub struct LevelContributor {
7676
uuid: String,

src/http/achievements/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async fn get_achievements(
9898
Json(state.database.get_all_documents::<Achievement>().await)
9999
}
100100

101-
pub fn mount(rocket_build: Rocket<Build>) -> Rocket<Build> {
101+
pub fn mount(rocket_build: Rocket<Build>, state: &MarsAPIState) -> Rocket<Build> {
102102
rocket_build.mount("/mc/achievements", routes![
103103
get_achievements,
104104
get_achievement_by_id,

src/http/broadcast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ pub fn broadcasts(state: &State<MarsAPIState>) -> Json<&Vec<Broadcast>> {
66
Json(&state.config.data.broadcasts)
77
}
88

9-
pub fn mount(rocket_build: Rocket<Build>) -> Rocket<Build> {
9+
pub fn mount(rocket_build: Rocket<Build>, state: &MarsAPIState) -> Rocket<Build> {
1010
rocket_build.mount("/mc/broadcasts", routes![broadcasts])
1111
}

src/http/leaderboard/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ async fn get_leaderboard_entries(
4747
Ok(Json(leaderboard))
4848
}
4949

50-
pub fn mount(rocket: Rocket<Build>) -> Rocket<Build> {
50+
pub fn mount(rocket: Rocket<Build>, state: &MarsAPIState) -> Rocket<Build> {
5151
rocket.mount("/mc/leaderboards", routes![get_leaderboard_entries])
5252
}

src/http/level/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ fn get_level_colors(state: &State<MarsAPIState>) -> Json<&Vec<LevelColor>> {
66
Json(&state.config.data.level_colors)
77
}
88

9-
pub fn mount(rocket_build: Rocket<Build>) -> Rocket<Build> {
9+
pub fn mount(rocket_build: Rocket<Build>, state: &MarsAPIState) -> Rocket<Build> {
1010
rocket_build.mount("/mc/levels", routes![get_level_colors])
1111
}

src/http/map/image.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use std::{io::Cursor, path::PathBuf, sync::Arc};
2+
3+
use bytes::Bytes;
4+
use image::{GenericImageView, ImageReader};
5+
use log::{error, warn};
6+
use rgb::FromSlice;
7+
use tokio::sync::{Semaphore, mpsc::{self, Sender}};
8+
9+
use crate::util::{r#macro::unwrap_helper, stream::{BoundedFrameDecoder, LengthPrefixedDataDecoder}};
10+
11+
pub struct ImageState {
12+
pub transmit: Arc<Sender<(String, Vec<u8>)>>
13+
}
14+
15+
pub fn image_queue_processor(
16+
base_path: Arc<PathBuf>, transcode: bool, image_guard: Arc<Semaphore>
17+
) -> mpsc::Sender<(String, Vec<u8>)> {
18+
let (tx, mut rx) = mpsc::channel::<(String, Vec<u8>)>(
19+
128
20+
);
21+
tokio::spawn(async move {
22+
while let Some(data) = rx.recv().await {
23+
submit_image(base_path.clone(), transcode, image_guard.clone(), data).await;
24+
}
25+
});
26+
tx
27+
}
28+
29+
async fn submit_image(base_path: Arc<PathBuf>, transcode: bool, image_guard: Arc<Semaphore>, data: (String, Vec<u8>)) {
30+
let permit = image_guard.clone().acquire_owned().await.unwrap();
31+
tokio::task::spawn_blocking(move || {
32+
let _permit_use = permit;
33+
store_map_image(base_path, transcode, data);
34+
});
35+
}
36+
37+
fn store_map_image(
38+
base_path: Arc<PathBuf>,
39+
transcode: bool,
40+
image_and_name: (String, Vec<u8>)
41+
) {
42+
let (file_name, image) = image_and_name;
43+
if !transcode {
44+
let destination = base_path.join(format!("{}.png", file_name));
45+
std::fs::write(destination, image);
46+
} else {
47+
let destination = base_path.join(format!("{}.avif", file_name));
48+
let data = image;
49+
50+
let mut image_reader = ImageReader::new(Cursor::new(data));
51+
image_reader.set_format(image::ImageFormat::Png);
52+
let image = match image_reader.decode() {
53+
Ok(image) => image,
54+
Err(e) => {
55+
error!("Could not save {} due to {:?}", file_name, e);
56+
return;
57+
},
58+
};
59+
let (width, height) = image.dimensions();
60+
let rgb_image = image.into_rgba8();
61+
let rgb_data = rgb_image.into_raw();
62+
let encoder = ravif::Encoder::new();
63+
let img = imgref::Img::new(rgb_data.as_rgba(), width as usize, height as usize);
64+
let avif_data = unwrap_helper::result_return_default!(encoder.encode_rgba(img), ());
65+
std::fs::write(destination, avif_data.avif_file);
66+
}
67+
}
68+
69+
pub fn create_image_decoder() -> BoundedFrameDecoder {
70+
BoundedFrameDecoder::new(
71+
1024,
72+
Box::new(
73+
LengthPrefixedDataDecoder::new()
74+
)
75+
)
76+
}
77+
78+
pub fn parse_image_data(mut bytes: Bytes) -> (String, Vec<u8>) {
79+
let image_name = read_length_prefixed_string(&mut bytes);
80+
(image_name, Vec::from(bytes))
81+
}
82+
83+
fn read_length_prefixed_string(bytes: &mut Bytes) -> String {
84+
String::from_utf8(Vec::from(read_length_prefixed_data(bytes))).unwrap()
85+
}
86+
87+
fn read_length_prefixed_data(bytes: &mut Bytes) -> Bytes {
88+
let arr = <[u8; 4]>::try_from(bytes.split_to(4).as_ref()).unwrap();
89+
let size = u32::from_be_bytes(arr);
90+
bytes.split_to(size as usize)
91+
}

0 commit comments

Comments
 (0)