Progress on freetype stuff
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
3dc320d524
commit
dfa99d9c5d
BIN
data/assets/placeholder-jacket.jpg
Normal file
BIN
data/assets/placeholder-jacket.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 246 KiB |
33
src/assets.rs
Normal file
33
src/assets.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock};
|
||||||
|
|
||||||
|
use freetype::{Face, Library};
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_data_dir() -> PathBuf {
|
||||||
|
PathBuf::from_str(&var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var"))
|
||||||
|
.expect("`SHIMMERING_DATA_DIR` is not a valid path")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_font(name: &str, assets_dir: &PathBuf) -> RefCell<Face> {
|
||||||
|
let face = FREETYPE_LIB.with(|lib| {
|
||||||
|
lib.new_face(assets_dir.join(format!("{}-variable.ttf", name)), 0)
|
||||||
|
.expect(&format!("Could not load {} font", name))
|
||||||
|
});
|
||||||
|
RefCell::new(face)
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
pub static DATA_DIR: PathBuf = get_data_dir();
|
||||||
|
pub static ASSETS_DIR: PathBuf = DATA_DIR.with(|p| p.join("assets"));
|
||||||
|
pub static FREETYPE_LIB: Library = Library::init().unwrap();
|
||||||
|
pub static SAIRA_FONT: RefCell<Face> = ASSETS_DIR.with(|assets_dir| get_font("saira", assets_dir));
|
||||||
|
pub static EXO_FONT: RefCell<Face> = ASSETS_DIR.with(|assets_dir| get_font("exo", assets_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn should_skip_jacket_art() -> bool {
|
||||||
|
static CELL: OnceLock<bool> = OnceLock::new();
|
||||||
|
*CELL.get_or_init(|| var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1")
|
||||||
|
}
|
151
src/bitmap.rs
151
src/bitmap.rs
|
@ -1,12 +1,32 @@
|
||||||
use freetype::{
|
use freetype::{
|
||||||
|
bitmap::PixelMode,
|
||||||
face::{KerningMode, LoadFlag},
|
face::{KerningMode, LoadFlag},
|
||||||
ffi::FT_GLYPH_BBOX_PIXELS,
|
ffi::{FT_Err_Ok, FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS},
|
||||||
Face,
|
Face, FtResult, Stroker, StrokerLineCap, StrokerLineJoin,
|
||||||
};
|
};
|
||||||
use num::traits::Euclid;
|
use num::traits::Euclid;
|
||||||
|
|
||||||
use crate::context::Error;
|
use crate::{assets::FREETYPE_LIB, context::Error};
|
||||||
|
|
||||||
|
// {{{ Config types
|
||||||
|
pub type Color = (u8, u8, u8, u8);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Align {
|
||||||
|
Start,
|
||||||
|
Center,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct TextStyle {
|
||||||
|
pub size: u32,
|
||||||
|
pub weight: u32,
|
||||||
|
pub color: Color,
|
||||||
|
pub h_align: Align,
|
||||||
|
pub v_align: Align,
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
// {{{ BitmapCanvas
|
// {{{ BitmapCanvas
|
||||||
pub struct BitmapCanvas {
|
pub struct BitmapCanvas {
|
||||||
pub buffer: Box<[u8]>,
|
pub buffer: Box<[u8]>,
|
||||||
|
@ -15,7 +35,7 @@ pub struct BitmapCanvas {
|
||||||
|
|
||||||
impl BitmapCanvas {
|
impl BitmapCanvas {
|
||||||
// {{{ Draw pixel
|
// {{{ Draw pixel
|
||||||
pub fn set_pixel(&mut self, pos: (u32, u32), color: (u8, u8, u8, u8)) {
|
pub fn set_pixel(&mut self, pos: (u32, u32), color: Color) {
|
||||||
let index = 3 * (pos.1 * self.width + pos.0) as usize;
|
let index = 3 * (pos.1 * self.width + pos.0) as usize;
|
||||||
let alpha = color.3 as u32;
|
let alpha = color.3 as u32;
|
||||||
self.buffer[index + 0] =
|
self.buffer[index + 0] =
|
||||||
|
@ -71,7 +91,7 @@ impl BitmapCanvas {
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Fill
|
// {{{ Fill
|
||||||
/// Fill with solid color
|
/// Fill with solid color
|
||||||
pub fn fill(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), color: (u8, u8, u8, u8)) {
|
pub fn fill(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), color: Color) {
|
||||||
let height = self.buffer.len() as u32 / 3 / self.width;
|
let height = self.buffer.len() as u32 / 3 / self.width;
|
||||||
for dx in 0..iw {
|
for dx in 0..iw {
|
||||||
for dy in 0..ih {
|
for dy in 0..ih {
|
||||||
|
@ -85,17 +105,39 @@ impl BitmapCanvas {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Draw text
|
// {{{ Draw text
|
||||||
|
// TODO: perform gamma correction on the color interpolation.
|
||||||
/// Render text
|
/// Render text
|
||||||
pub fn text(
|
pub fn text(
|
||||||
&mut self,
|
&mut self,
|
||||||
pos: (i32, i32),
|
pos: (i32, i32),
|
||||||
face: Face,
|
face: &mut Face,
|
||||||
size: u32,
|
style: TextStyle,
|
||||||
text: &str,
|
text: &str,
|
||||||
color: (u8, u8, u8, u8),
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
face.set_char_size(0, (size as isize) << 6, 300, 300)?;
|
// {{{ Control weight
|
||||||
|
unsafe {
|
||||||
|
let raw = face.raw_mut() as *mut _;
|
||||||
|
let slice = [(style.weight as i64) << 16];
|
||||||
|
|
||||||
|
// {{{ Debug logging
|
||||||
|
// let mut amaster = 0 as *mut FT_MM_Var;
|
||||||
|
// FT_Get_MM_Var(raw, &mut amaster as *mut _);
|
||||||
|
// println!("{:?}", *amaster);
|
||||||
|
// println!("{:?}", *(*amaster).axis);
|
||||||
|
// println!("{:?}", *(*amaster).namedstyle);
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
// Set variable weight
|
||||||
|
let err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr());
|
||||||
|
if err != FT_Err_Ok {
|
||||||
|
let err: FtResult<_> = Err(err.into());
|
||||||
|
err?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
face.set_char_size((style.size << 6) as isize, 0, 0, 0)?;
|
||||||
|
|
||||||
|
// {{{ Compute layout
|
||||||
let mut pen_x = 0;
|
let mut pen_x = 0;
|
||||||
let kerning = face.has_kerning();
|
let kerning = face.has_kerning();
|
||||||
let mut previous = None;
|
let mut previous = None;
|
||||||
|
@ -120,6 +162,8 @@ impl BitmapCanvas {
|
||||||
previous = Some(glyph_index);
|
previous = Some(glyph_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
// {{{ Find bounding box
|
||||||
let mut x_min = 32000;
|
let mut x_min = 32000;
|
||||||
let mut y_min = 32000;
|
let mut y_min = 32000;
|
||||||
let mut x_max = -32000;
|
let mut x_max = -32000;
|
||||||
|
@ -135,7 +179,7 @@ impl BitmapCanvas {
|
||||||
x_min = bbox.xMin
|
x_min = bbox.xMin
|
||||||
}
|
}
|
||||||
|
|
||||||
if bbox.xMax < x_max {
|
if bbox.xMax > x_max {
|
||||||
x_max = bbox.xMax
|
x_max = bbox.xMax
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +187,7 @@ impl BitmapCanvas {
|
||||||
y_min = bbox.yMin
|
y_min = bbox.yMin
|
||||||
}
|
}
|
||||||
|
|
||||||
if bbox.yMax < y_max {
|
if bbox.yMax > y_max {
|
||||||
y_max = bbox.yMax
|
y_max = bbox.yMax
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,19 +200,77 @@ impl BitmapCanvas {
|
||||||
y_max = 0;
|
y_max = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// println!("{}, {} - {}, {}", x_min, y_min, x_max, y_max);
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
// {{{ Render glyphs
|
||||||
for (pos_x, glyph) in &data {
|
for (pos_x, glyph) in &data {
|
||||||
let b_glyph = glyph.to_bitmap(freetype::RenderMode::Normal, None)?;
|
let b_glyph = glyph.to_bitmap(freetype::RenderMode::Normal, None)?;
|
||||||
let bitmap = b_glyph.bitmap();
|
let bitmap = b_glyph.bitmap();
|
||||||
let pixel_mode = bitmap.pixel_mode()?;
|
let pixel_mode = bitmap.pixel_mode()?;
|
||||||
println!(
|
assert_eq!(pixel_mode, PixelMode::Gray);
|
||||||
"Pixel mode: {:?}, width {:?}, height {:?}, len {:?}, pen x {:?}",
|
println!("starting to stroke");
|
||||||
pixel_mode,
|
|
||||||
bitmap.width(),
|
// {{{ Blit border
|
||||||
bitmap.rows(),
|
let stroker = FREETYPE_LIB.with(|lib| lib.new_stroker())?;
|
||||||
bitmap.buffer().len(),
|
stroker.set(1 << 6, StrokerLineCap::Round, StrokerLineJoin::Round, 0);
|
||||||
pos_x
|
let sglyph = glyph.stroke(&stroker)?;
|
||||||
);
|
let sb_glyph = sglyph.to_bitmap(freetype::RenderMode::Normal, None)?;
|
||||||
|
let sbitmap = sb_glyph.bitmap();
|
||||||
|
let spixel_mode = sbitmap.pixel_mode()?;
|
||||||
|
assert_eq!(spixel_mode, PixelMode::Gray);
|
||||||
|
|
||||||
|
let iw = sbitmap.width();
|
||||||
|
let ih = sbitmap.rows();
|
||||||
|
println!("pitch {}, width {}, height {}", sbitmap.pitch(), iw, ih);
|
||||||
|
let height = self.buffer.len() as u32 / 3 / self.width;
|
||||||
|
let src = sbitmap.buffer();
|
||||||
|
for dx in 0..iw {
|
||||||
|
for dy in 0..ih {
|
||||||
|
let x = pos.0 + *pos_x as i32 + dx as i32 + sb_glyph.left();
|
||||||
|
let y = pos.1 + dy as i32 - sb_glyph.top();
|
||||||
|
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
||||||
|
let gray = src[(dx + dy * iw) as usize];
|
||||||
|
|
||||||
|
let r = 255 - style.color.0;
|
||||||
|
let g = 255 - style.color.1;
|
||||||
|
let b = 255 - style.color.2;
|
||||||
|
let a = gray;
|
||||||
|
|
||||||
|
let color = (r, g, b, a);
|
||||||
|
|
||||||
|
self.set_pixel((x as u32, y as u32), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Blit
|
||||||
|
let iw = bitmap.width();
|
||||||
|
let ih = bitmap.rows();
|
||||||
|
let height = self.buffer.len() as u32 / 3 / self.width;
|
||||||
|
let src = bitmap.buffer();
|
||||||
|
|
||||||
|
for dx in 0..iw {
|
||||||
|
for dy in 0..ih {
|
||||||
|
let x = pos.0 + *pos_x as i32 + dx as i32 + b_glyph.left();
|
||||||
|
let y = pos.1 + dy as i32 - b_glyph.top();
|
||||||
|
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
||||||
|
let gray = src[(dx + dy * iw) as usize];
|
||||||
|
|
||||||
|
let r = style.color.0;
|
||||||
|
let g = style.color.1;
|
||||||
|
let b = style.color.2;
|
||||||
|
let a = gray;
|
||||||
|
|
||||||
|
let color = (r, g, b, a);
|
||||||
|
|
||||||
|
self.set_pixel((x as u32, y as u32), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -362,7 +464,7 @@ impl LayoutDrawer {
|
||||||
|
|
||||||
// {{{ Drawing
|
// {{{ Drawing
|
||||||
// {{{ Draw pixel
|
// {{{ Draw pixel
|
||||||
pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: (u8, u8, u8, u8)) {
|
pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: Color) {
|
||||||
let pos = self
|
let pos = self
|
||||||
.layout
|
.layout
|
||||||
.position_relative_to(id, (pos.0 as i32, pos.1 as i32));
|
.position_relative_to(id, (pos.0 as i32, pos.1 as i32));
|
||||||
|
@ -385,7 +487,7 @@ impl LayoutDrawer {
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Fill
|
// {{{ Fill
|
||||||
/// Fills with solid color
|
/// Fills with solid color
|
||||||
pub fn fill(&mut self, id: LayoutBoxId, color: (u8, u8, u8, u8)) {
|
pub fn fill(&mut self, id: LayoutBoxId, color: Color) {
|
||||||
let current = self.layout.lookup(id);
|
let current = self.layout.lookup(id);
|
||||||
self.canvas
|
self.canvas
|
||||||
.fill((current.0, current.1), (current.2, current.3), color);
|
.fill((current.0, current.1), (current.2, current.3), color);
|
||||||
|
@ -397,13 +499,12 @@ impl LayoutDrawer {
|
||||||
&mut self,
|
&mut self,
|
||||||
id: LayoutBoxId,
|
id: LayoutBoxId,
|
||||||
pos: (i32, i32),
|
pos: (i32, i32),
|
||||||
face: Face,
|
face: &mut Face,
|
||||||
size: u32,
|
style: TextStyle,
|
||||||
text: &str,
|
text: &str,
|
||||||
color: (u8, u8, u8, u8),
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let pos = self.layout.position_relative_to(id, pos);
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
self.canvas.text(pos, face, size, text, color)
|
self.canvas.text(pos, face, style, text)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
|
@ -17,7 +17,8 @@ use poise::{
|
||||||
use sqlx::query_as;
|
use sqlx::query_as;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bitmap::{BitmapCanvas, LayoutDrawer, LayoutManager},
|
assets::EXO_FONT,
|
||||||
|
bitmap::{Align, BitmapCanvas, LayoutDrawer, LayoutManager},
|
||||||
chart::{Chart, Song},
|
chart::{Chart, Song},
|
||||||
context::{Context, Error},
|
context::{Context, Error},
|
||||||
jacket::BITMAP_IMAGE_SIZE,
|
jacket::BITMAP_IMAGE_SIZE,
|
||||||
|
@ -354,18 +355,22 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
// jacket_area.draw_text(
|
|
||||||
// &chart.level,
|
// EXO_FONT.with_borrow_mut(|font| {
|
||||||
// &TextStyle::from(("Exo", 30).into_font())
|
// drawer.text(
|
||||||
// .color(&WHITE)
|
// jacket_area,
|
||||||
// .with_anchor::<RGBAColor>(Pos {
|
// (BITMAP_IMAGE_SIZE as i32 + x_offset - 30, 2),
|
||||||
// h_pos: HPos::Center,
|
// font,
|
||||||
// v_pos: VPos::Center,
|
// crate::bitmap::TextStyle {
|
||||||
// })
|
// size: 40,
|
||||||
// .into_text_style(&jacket_area),
|
// weight: 250,
|
||||||
// (BITMAP_IMAGE_SIZE as i32 + x_offset, 2),
|
// color: (0xff, 0xff, 0xff, 0xff),
|
||||||
// )?;
|
// h_align: Align::Center,
|
||||||
// }}}
|
// v_align: Align::Center,
|
||||||
|
// },
|
||||||
|
// &chart.level,
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
// {{{ Display chart name
|
// {{{ Display chart name
|
||||||
// Draw background
|
// Draw background
|
||||||
drawer.fill(bottom_area, (0x82, 0x71, 0xA7, 255));
|
drawer.fill(bottom_area, (0x82, 0x71, 0xA7, 255));
|
||||||
|
|
203
src/jacket.rs
203
src/jacket.rs
|
@ -1,12 +1,11 @@
|
||||||
use std::{fs, path::PathBuf, str::FromStr};
|
use std::{fs, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use freetype::{Face, Library};
|
|
||||||
use image::{imageops::FilterType, GenericImageView, ImageBuffer, Rgb, Rgba};
|
use image::{imageops::FilterType, GenericImageView, ImageBuffer, Rgb, Rgba};
|
||||||
use kd_tree::{KdMap, KdPoint};
|
use kd_tree::{KdMap, KdPoint};
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bitmap::BitmapCanvas,
|
assets::should_skip_jacket_art,
|
||||||
chart::{Difficulty, Jacket, SongCache},
|
chart::{Difficulty, Jacket, SongCache},
|
||||||
context::Error,
|
context::Error,
|
||||||
score::guess_chart_name,
|
score::guess_chart_name,
|
||||||
|
@ -88,6 +87,7 @@ impl JacketCache {
|
||||||
// This is a bit inefficient (using a hash set), but only runs once
|
// This is a bit inefficient (using a hash set), but only runs once
|
||||||
pub fn new(data_dir: &PathBuf, song_cache: &mut SongCache) -> Result<Self, Error> {
|
pub fn new(data_dir: &PathBuf, song_cache: &mut SongCache) -> Result<Self, Error> {
|
||||||
let jacket_dir = data_dir.join("jackets");
|
let jacket_dir = data_dir.join("jackets");
|
||||||
|
let assets_dir = data_dir.join("assets");
|
||||||
|
|
||||||
if jacket_dir.exists() {
|
if jacket_dir.exists() {
|
||||||
fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir");
|
fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir");
|
||||||
|
@ -95,118 +95,121 @@ impl JacketCache {
|
||||||
|
|
||||||
fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
|
fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
|
||||||
|
|
||||||
let mut jackets = Vec::new();
|
let tree_entries = if should_skip_jacket_art() {
|
||||||
let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
let path = assets_dir.join("placeholder-jacket.jpg");
|
||||||
for (i, entry) in entries.enumerate() {
|
let contents: &'static _ = fs::read(path)?.leak();
|
||||||
let dir = entry?;
|
let image = image::load_from_memory(contents)?;
|
||||||
let raw_dir_name = dir.file_name();
|
let bitmap: &'static _ = Box::leak(Box::new(
|
||||||
let dir_name = raw_dir_name.to_str().unwrap();
|
image
|
||||||
for entry in fs::read_dir(dir.path()).expect("Couldn't read song directory") {
|
.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest)
|
||||||
let file = entry?;
|
.into_rgb8(),
|
||||||
let raw_name = file.file_name();
|
));
|
||||||
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
|
||||||
|
|
||||||
if !name.ends_with("_256") {
|
for song in song_cache.songs_mut() {
|
||||||
continue;
|
for chart in song.charts_mut() {
|
||||||
}
|
|
||||||
let name = name.strip_suffix("_256").unwrap();
|
|
||||||
|
|
||||||
let difficulty = match name {
|
|
||||||
"0" => Some(Difficulty::PST),
|
|
||||||
"1" => Some(Difficulty::PRS),
|
|
||||||
"2" => Some(Difficulty::FTR),
|
|
||||||
"3" => Some(Difficulty::BYD),
|
|
||||||
"4" => Some(Difficulty::ETR),
|
|
||||||
"base" => None,
|
|
||||||
"base_night" => None,
|
|
||||||
"base_ja" => None,
|
|
||||||
_ => Err(format!("Unknown jacket suffix {}", name))?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (song, chart) = guess_chart_name(dir_name, &song_cache, difficulty, true)?;
|
|
||||||
|
|
||||||
jackets.push((file.path(), song.id));
|
|
||||||
|
|
||||||
let contents = fs::read(file.path())?.leak();
|
|
||||||
let bitmap = Box::leak(Box::new(
|
|
||||||
image::load_from_memory(contents)?
|
|
||||||
.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest)
|
|
||||||
.into_rgb8(),
|
|
||||||
));
|
|
||||||
|
|
||||||
if name == "base" {
|
|
||||||
let item = song_cache.lookup_mut(song.id).unwrap();
|
|
||||||
|
|
||||||
for chart in item.charts_mut() {
|
|
||||||
let difficulty_num = match chart.difficulty {
|
|
||||||
Difficulty::PST => "0",
|
|
||||||
Difficulty::PRS => "1",
|
|
||||||
Difficulty::FTR => "2",
|
|
||||||
Difficulty::BYD => "3",
|
|
||||||
Difficulty::ETR => "4",
|
|
||||||
};
|
|
||||||
|
|
||||||
// We only want to create this path if there's no overwrite for this
|
|
||||||
// jacket.
|
|
||||||
let specialized_path = PathBuf::from_str(
|
|
||||||
&file
|
|
||||||
.path()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace("base_night", difficulty_num)
|
|
||||||
.replace("base", difficulty_num),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let dest = chart.jacket_path(data_dir);
|
|
||||||
if !specialized_path.exists() && !dest.exists() {
|
|
||||||
std::os::unix::fs::symlink(file.path(), dest)
|
|
||||||
.expect("Could not symlink jacket");
|
|
||||||
chart.cached_jacket = Some(Jacket {
|
|
||||||
raw: contents,
|
|
||||||
bitmap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if difficulty.is_some() {
|
|
||||||
std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir))
|
|
||||||
.expect("Could not symlink jacket");
|
|
||||||
let chart = song_cache.lookup_chart_mut(chart.id).unwrap();
|
|
||||||
chart.cached_jacket = Some(Jacket {
|
chart.cached_jacket = Some(Jacket {
|
||||||
raw: contents,
|
raw: contents,
|
||||||
bitmap,
|
bitmap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries = vec![];
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
let entries =
|
||||||
|
fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
||||||
|
let mut tree_entries = vec![];
|
||||||
|
|
||||||
for (path, song_id) in jackets {
|
for entry in entries {
|
||||||
match image::io::Reader::open(path) {
|
let dir = entry?;
|
||||||
Ok(reader) => {
|
let raw_dir_name = dir.file_name();
|
||||||
let image = reader.decode()?;
|
let dir_name = raw_dir_name.to_str().unwrap();
|
||||||
entries.push((ImageVec::from_image(&image), song_id))
|
for entry in fs::read_dir(dir.path()).expect("Couldn't read song directory") {
|
||||||
|
let file = entry?;
|
||||||
|
let raw_name = file.file_name();
|
||||||
|
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
||||||
|
|
||||||
|
if !name.ends_with("_256") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = name.strip_suffix("_256").unwrap();
|
||||||
|
|
||||||
|
let difficulty = match name {
|
||||||
|
"0" => Some(Difficulty::PST),
|
||||||
|
"1" => Some(Difficulty::PRS),
|
||||||
|
"2" => Some(Difficulty::FTR),
|
||||||
|
"3" => Some(Difficulty::BYD),
|
||||||
|
"4" => Some(Difficulty::ETR),
|
||||||
|
"base" => None,
|
||||||
|
"base_night" => None,
|
||||||
|
"base_ja" => None,
|
||||||
|
_ => Err(format!("Unknown jacket suffix {}", name))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (song, chart) = guess_chart_name(dir_name, &song_cache, difficulty, true)?;
|
||||||
|
|
||||||
|
let contents: &'static _ = fs::read(file.path())?.leak();
|
||||||
|
|
||||||
|
let image = image::load_from_memory(contents)?;
|
||||||
|
tree_entries.push((ImageVec::from_image(&image), song.id));
|
||||||
|
|
||||||
|
let bitmap: &'static _ = Box::leak(Box::new(
|
||||||
|
image
|
||||||
|
.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest)
|
||||||
|
.into_rgb8(),
|
||||||
|
));
|
||||||
|
|
||||||
|
if name == "base" {
|
||||||
|
let item = song_cache.lookup_mut(song.id).unwrap();
|
||||||
|
|
||||||
|
for chart in item.charts_mut() {
|
||||||
|
let difficulty_num = match chart.difficulty {
|
||||||
|
Difficulty::PST => "0",
|
||||||
|
Difficulty::PRS => "1",
|
||||||
|
Difficulty::FTR => "2",
|
||||||
|
Difficulty::BYD => "3",
|
||||||
|
Difficulty::ETR => "4",
|
||||||
|
};
|
||||||
|
|
||||||
|
// We only want to create this path if there's no overwrite for this
|
||||||
|
// jacket.
|
||||||
|
let specialized_path = PathBuf::from_str(
|
||||||
|
&file
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.replace("base_night", difficulty_num)
|
||||||
|
.replace("base", difficulty_num),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let dest = chart.jacket_path(data_dir);
|
||||||
|
if !specialized_path.exists() && !dest.exists() {
|
||||||
|
std::os::unix::fs::symlink(file.path(), dest)
|
||||||
|
.expect("Could not symlink jacket");
|
||||||
|
chart.cached_jacket = Some(Jacket {
|
||||||
|
raw: contents,
|
||||||
|
bitmap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if difficulty.is_some() {
|
||||||
|
std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir))
|
||||||
|
.expect("Could not symlink jacket");
|
||||||
|
let chart = song_cache.lookup_chart_mut(chart.id).unwrap();
|
||||||
|
chart.cached_jacket = Some(Jacket {
|
||||||
|
raw: contents,
|
||||||
|
bitmap,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => continue,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let assets_dir = data_dir.join("assets");
|
tree_entries
|
||||||
|
};
|
||||||
let lib = Library::init()?;
|
|
||||||
let saira_font = lib.new_face(assets_dir.join("saira-variable.ttf"), 0)?;
|
|
||||||
let mut canvas = BitmapCanvas::new(0, 0);
|
|
||||||
canvas.text(
|
|
||||||
(0, 0),
|
|
||||||
saira_font,
|
|
||||||
20,
|
|
||||||
"Yo, this is a test!",
|
|
||||||
(0, 0, 0, 0xff),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let result = Self {
|
let result = Self {
|
||||||
tree: KdMap::build_by_ordered_float(entries),
|
tree: KdMap::build_by_ordered_float(tree_entries),
|
||||||
b30_background: image::open(assets_dir.join("b30_background.jpg"))?
|
b30_background: image::open(assets_dir.join("b30_background.jpg"))?
|
||||||
.resize(2048 * 2, 1535 * 2, FilterType::Nearest)
|
.resize(2048 * 2, 1535 * 2, FilterType::Nearest)
|
||||||
.blur(20.0)
|
.blur(20.0)
|
||||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -4,6 +4,7 @@
|
||||||
#![feature(array_try_map)]
|
#![feature(array_try_map)]
|
||||||
#![feature(async_closure)]
|
#![feature(async_closure)]
|
||||||
|
|
||||||
|
mod assets;
|
||||||
mod bitmap;
|
mod bitmap;
|
||||||
mod chart;
|
mod chart;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
@ -12,6 +13,7 @@ mod jacket;
|
||||||
mod score;
|
mod score;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
|
use assets::DATA_DIR;
|
||||||
use context::{Error, UserContext};
|
use context::{Error, UserContext};
|
||||||
use poise::serenity_prelude::{self as serenity};
|
use poise::serenity_prelude::{self as serenity};
|
||||||
use sqlx::sqlite::SqlitePoolOptions;
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
@ -31,11 +33,14 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let data_dir = var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var");
|
let data_dir = DATA_DIR.with(|d| d.clone());
|
||||||
let cache_dir = var("SHIMMERING_CACHE_DIR").expect("Missing `SHIMMERING_CACHE_DIR` env var");
|
let cache_dir = var("SHIMMERING_CACHE_DIR").expect("Missing `SHIMMERING_CACHE_DIR` env var");
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.connect(&format!("sqlite://{}/db.sqlite", data_dir))
|
.connect(&format!(
|
||||||
|
"sqlite://{}/db.sqlite",
|
||||||
|
data_dir.to_str().unwrap()
|
||||||
|
))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -80,12 +85,7 @@ async fn main() {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
println!("Logged in as {}", _ready.user.name);
|
println!("Logged in as {}", _ready.user.name);
|
||||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||||
let ctx = UserContext::new(
|
let ctx = UserContext::new(data_dir, PathBuf::from_str(&cache_dir)?, pool).await?;
|
||||||
PathBuf::from_str(&data_dir)?,
|
|
||||||
PathBuf::from_str(&cache_dir)?,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ctx)
|
Ok(ctx)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue