Introduce missing assets and better folder system
14
.gitignore
vendored
|
@ -1,10 +1,12 @@
|
||||||
target
|
|
||||||
.direnv
|
.direnv
|
||||||
.envrc
|
.envrc
|
||||||
data/db.sqlite
|
|
||||||
data/jackets
|
shimmering/data
|
||||||
data/songs
|
shimmering/logs
|
||||||
|
shimmering/assets/fonts
|
||||||
|
shimmering/assets/songs
|
||||||
|
shimmering/assets/b30_background.*
|
||||||
|
|
||||||
|
target
|
||||||
backups
|
backups
|
||||||
dump.sql
|
dump.sql
|
||||||
logs
|
|
||||||
cache
|
|
||||||
|
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
shimmering/assets/diff_byd.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
shimmering/assets/diff_etr.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
shimmering/assets/diff_ftr.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
shimmering/assets/diff_prs.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
shimmering/assets/diff_pst.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 752 B |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
@ -1,11 +1,11 @@
|
||||||
use std::{fs, io::Cursor, path::PathBuf};
|
use std::{fs, io::Cursor};
|
||||||
|
|
||||||
use image::{imageops::FilterType, GenericImageView, Rgba};
|
use image::{imageops::FilterType, GenericImageView, Rgba};
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arcaea::chart::{Difficulty, Jacket, SongCache},
|
arcaea::chart::{Difficulty, Jacket, SongCache},
|
||||||
assets::{get_assets_dir, should_blur_jacket_art, should_skip_jacket_art},
|
assets::{get_asset_dir, should_blur_jacket_art, should_skip_jacket_art},
|
||||||
context::Error,
|
context::Error,
|
||||||
recognition::fuzzy_song_name::guess_chart_name,
|
recognition::fuzzy_song_name::guess_chart_name,
|
||||||
};
|
};
|
||||||
|
@ -80,17 +80,9 @@ pub struct JacketCache {
|
||||||
impl JacketCache {
|
impl JacketCache {
|
||||||
// {{{ Generate
|
// {{{ Generate
|
||||||
// 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(song_cache: &mut SongCache) -> Result<Self, Error> {
|
||||||
let jacket_dir = data_dir.join("jackets");
|
|
||||||
|
|
||||||
if jacket_dir.exists() {
|
|
||||||
fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
|
|
||||||
|
|
||||||
let jacket_vectors = if should_skip_jacket_art() {
|
let jacket_vectors = if should_skip_jacket_art() {
|
||||||
let path = get_assets_dir().join("placeholder_jacket.jpg");
|
let path = get_asset_dir().join("placeholder_jacket.jpg");
|
||||||
let contents: &'static _ = fs::read(path)?.leak();
|
let contents: &'static _ = fs::read(path)?.leak();
|
||||||
let image = image::load_from_memory(contents)?;
|
let image = image::load_from_memory(contents)?;
|
||||||
let bitmap: &'static _ = Box::leak(Box::new(
|
let bitmap: &'static _ = Box::leak(Box::new(
|
||||||
|
@ -109,7 +101,7 @@ impl JacketCache {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
let entries =
|
let entries =
|
||||||
fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
fs::read_dir(get_asset_dir().join("songs")).expect("Couldn't read songs directory");
|
||||||
let mut jacket_vectors = vec![];
|
let mut jacket_vectors = vec![];
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
|
|
195
src/assets.rs
|
@ -1,44 +1,56 @@
|
||||||
use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock, thread::LocalKey};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
env::var,
|
||||||
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
|
sync::{LazyLock, OnceLock},
|
||||||
|
thread::LocalKey,
|
||||||
|
};
|
||||||
|
|
||||||
use freetype::{Face, Library};
|
use freetype::{Face, Library};
|
||||||
use image::{ImageBuffer, Rgb, Rgba};
|
use image::{DynamicImage, RgbaImage};
|
||||||
|
|
||||||
use crate::{arcaea::chart::Difficulty, timed};
|
use crate::{arcaea::chart::Difficulty, timed};
|
||||||
|
|
||||||
|
// {{{ Path helpers
|
||||||
|
#[inline]
|
||||||
|
pub fn get_var(name: &str) -> String {
|
||||||
|
var(name).unwrap_or_else(|_| panic!("Missing `{name}` environment variable"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_path(name: &str) -> PathBuf {
|
||||||
|
PathBuf::from_str(&get_var(name))
|
||||||
|
.unwrap_or_else(|_| panic!("`{name}` environment variable is not a valid path"))
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_data_dir() -> PathBuf {
|
pub fn get_data_dir() -> PathBuf {
|
||||||
PathBuf::from_str(&var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var"))
|
get_path("SHIMMERING_DATA_DIR")
|
||||||
.expect("`SHIMMERING_DATA_DIR` is not a valid path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_assets_dir() -> PathBuf {
|
pub fn get_config_dir() -> PathBuf {
|
||||||
get_data_dir().join("assets")
|
get_path("SHIMMERING_CONFIG_DIR")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_asset_dir() -> PathBuf {
|
||||||
|
get_path("SHIMMERING_ASSET_DIR")
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Font helpers
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_font(name: &str) -> RefCell<Face> {
|
fn get_font(name: &str) -> RefCell<Face> {
|
||||||
let face = timed!(format!("load font \"{name}\""), {
|
let face = timed!(format!("load font \"{name}\""), {
|
||||||
FREETYPE_LIB.with(|lib| {
|
FREETYPE_LIB.with(|lib| {
|
||||||
lib.new_face(get_assets_dir().join(name), 0)
|
lib.new_face(get_asset_dir().join("fonts").join(name), 0)
|
||||||
.expect(&format!("Could not load {} font", name))
|
.expect(&format!("Could not load {} font", name))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
RefCell::new(face)
|
RefCell::new(face)
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
pub static FREETYPE_LIB: Library = Library::init().unwrap();
|
|
||||||
pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable.ttf");
|
|
||||||
pub static EXO_FONT: RefCell<Face> = get_font("exo-variable.ttf");
|
|
||||||
pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light.ttf");
|
|
||||||
pub static KAZESAWA_FONT: RefCell<Face> = get_font("kazesawa-regular.ttf");
|
|
||||||
pub static KAZESAWA_BOLD_FONT: RefCell<Face> = get_font("kazesawa-bold.ttf");
|
|
||||||
pub static NOTO_SANS_FONT: RefCell<Face> = get_font("noto-sans.ttf");
|
|
||||||
pub static ARIAL_FONT: RefCell<Face> = get_font("arial.ttf");
|
|
||||||
pub static UNI_FONT: RefCell<Face> = get_font("unifont.otf");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn with_font<T>(
|
pub fn with_font<T>(
|
||||||
primary: &'static LocalKey<RefCell<Face>>,
|
primary: &'static LocalKey<RefCell<Face>>,
|
||||||
|
@ -52,7 +64,21 @@ pub fn with_font<T>(
|
||||||
// })
|
// })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Font loading
|
||||||
|
thread_local! {
|
||||||
|
pub static FREETYPE_LIB: Library = Library::init().unwrap();
|
||||||
|
pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable.ttf");
|
||||||
|
pub static EXO_FONT: RefCell<Face> = get_font("exo-variable.ttf");
|
||||||
|
pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light.ttf");
|
||||||
|
pub static KAZESAWA_FONT: RefCell<Face> = get_font("kazesawa-regular.ttf");
|
||||||
|
pub static KAZESAWA_BOLD_FONT: RefCell<Face> = get_font("kazesawa-bold.ttf");
|
||||||
|
pub static NOTO_SANS_FONT: RefCell<Face> = get_font("noto-sans.ttf");
|
||||||
|
pub static ARIAL_FONT: RefCell<Face> = get_font("arial.ttf");
|
||||||
|
pub static UNI_FONT: RefCell<Face> = get_font("unifont.otf");
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Asset art helpers
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn should_skip_jacket_art() -> bool {
|
pub fn should_skip_jacket_art() -> bool {
|
||||||
var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1"
|
var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1"
|
||||||
|
@ -63,110 +89,49 @@ pub fn should_blur_jacket_art() -> bool {
|
||||||
var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1"
|
var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
|
macro_rules! get_asset {
|
||||||
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
|
($name: ident, $path:expr) => {
|
||||||
CELL.get_or_init(|| {
|
get_asset!($name, $path, |d: DynamicImage| d);
|
||||||
timed!("load_b30_background", {
|
};
|
||||||
let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg"))
|
($name: ident, $path:expr, $f:expr) => {
|
||||||
.expect("Could not open b30 background");
|
pub static $name: LazyLock<RgbaImage> = LazyLock::new(move || {
|
||||||
|
timed!($path, {
|
||||||
raw_b30_background.blur(7.0).into_rgb8()
|
let image = image::open(get_asset_dir().join($path))
|
||||||
})
|
.unwrap_or_else(|_| panic!("Could no read asset `{}`", $path));
|
||||||
|
let f = $f;
|
||||||
|
f(image).into_rgba8()
|
||||||
})
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Asset art loading
|
||||||
|
get_asset!(COUNT_BACKGROUND, "count_background.png");
|
||||||
|
get_asset!(SCORE_BACKGROUND, "score_background.png");
|
||||||
|
get_asset!(STATUS_BACKGROUND, "status_background.png");
|
||||||
|
get_asset!(GRADE_BACKGROUND, "grade_background.png");
|
||||||
|
get_asset!(TOP_BACKGROUND, "top_background.png");
|
||||||
|
get_asset!(NAME_BACKGROUND, "name_background.png");
|
||||||
|
get_asset!(PTT_EMBLEM, "ptt_emblem.png");
|
||||||
|
get_asset!(
|
||||||
|
B30_BACKGROUND,
|
||||||
|
"b30_background.jpg",
|
||||||
|
|image: DynamicImage| image.blur(7.0)
|
||||||
|
);
|
||||||
|
|
||||||
pub fn get_count_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
pub fn get_difficulty_background(difficulty: Difficulty) -> &'static RgbaImage {
|
||||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
static CELL: OnceLock<[RgbaImage; 5]> = OnceLock::new();
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_count_backound", {
|
|
||||||
image::open(get_assets_dir().join("count_background.png"))
|
|
||||||
.expect("Could not open count background")
|
|
||||||
.into_rgba8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_score_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_score_background", {
|
|
||||||
image::open(get_assets_dir().join("score_background.png"))
|
|
||||||
.expect("Could not open score background")
|
|
||||||
.into_rgba8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_status_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_status_background", {
|
|
||||||
image::open(get_assets_dir().join("status_background.png"))
|
|
||||||
.expect("Could not open status background")
|
|
||||||
.into_rgba8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_grade_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_grade_background", {
|
|
||||||
image::open(get_assets_dir().join("grade_background.png"))
|
|
||||||
.expect("Could not open grade background")
|
|
||||||
.into_rgba8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_top_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_top_background", {
|
|
||||||
image::open(get_assets_dir().join("top_background.png"))
|
|
||||||
.expect("Could not open top background")
|
|
||||||
.into_rgb8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_name_background", {
|
|
||||||
image::open(get_assets_dir().join("name_background.png"))
|
|
||||||
.expect("Could not open name background")
|
|
||||||
.into_rgb8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ptt_emblem() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| {
|
|
||||||
timed!("load_ptt_emblem", {
|
|
||||||
image::open(get_assets_dir().join("ptt_emblem.png"))
|
|
||||||
.expect("Could not open ptt emblem")
|
|
||||||
.into_rgba8()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_difficulty_background(
|
|
||||||
difficulty: Difficulty,
|
|
||||||
) -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
|
||||||
static CELL: OnceLock<[ImageBuffer<Rgba<u8>, Vec<u8>>; 5]> = OnceLock::new();
|
|
||||||
&CELL.get_or_init(|| {
|
&CELL.get_or_init(|| {
|
||||||
timed!("load_difficulty_background", {
|
timed!("load_difficulty_background", {
|
||||||
let assets_dir = get_assets_dir();
|
let assets_dir = get_asset_dir();
|
||||||
Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| {
|
Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| {
|
||||||
image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase())))
|
image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase())))
|
||||||
.expect(&format!(
|
.unwrap_or_else(|_| {
|
||||||
"Could not get background for difficulty {:?}",
|
panic!("Could not get background for difficulty {shorthand:?}")
|
||||||
shorthand
|
})
|
||||||
))
|
|
||||||
.into_rgba8()
|
.into_rgba8()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})[difficulty.to_index()]
|
})[difficulty.to_index()]
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use freetype::{
|
||||||
ffi::{FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS},
|
ffi::{FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS},
|
||||||
Bitmap, BitmapGlyph, Face, Glyph, StrokerLineCap, StrokerLineJoin,
|
Bitmap, BitmapGlyph, Face, Glyph, StrokerLineCap, StrokerLineJoin,
|
||||||
};
|
};
|
||||||
use image::GenericImage;
|
use image::{GenericImage, RgbImage, RgbaImage};
|
||||||
use num::traits::Euclid;
|
use num::traits::Euclid;
|
||||||
|
|
||||||
use crate::{assets::FREETYPE_LIB, context::Error};
|
use crate::{assets::FREETYPE_LIB, context::Error};
|
||||||
|
@ -184,7 +184,8 @@ impl BitmapCanvas {
|
||||||
((alpha * color.2 as u32 + (255 - alpha) * self.buffer[index + 2] as u32) / 255) as u8;
|
((alpha * color.2 as u32 + (255 - alpha) * self.buffer[index + 2] as u32) / 255) as u8;
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Draw RBG image
|
// {{{ Draw RGB image
|
||||||
|
/// Draws a bitmap image with no alpha channel.
|
||||||
pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
|
pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
|
||||||
let iw = iw as i32;
|
let iw = iw as i32;
|
||||||
let ih = ih as i32;
|
let ih = ih as i32;
|
||||||
|
@ -242,8 +243,8 @@ impl BitmapCanvas {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Draw scaled up RBG image
|
// {{{ Draw scaled up RBGA image
|
||||||
pub fn blit_rbg_scaled_up(
|
pub fn blit_rbga_scaled_up(
|
||||||
&mut self,
|
&mut self,
|
||||||
pos: Position,
|
pos: Position,
|
||||||
(iw, ih): (u32, u32),
|
(iw, ih): (u32, u32),
|
||||||
|
@ -269,11 +270,12 @@ impl BitmapCanvas {
|
||||||
// but would not perform division.
|
// but would not perform division.
|
||||||
let dx = (x - pos.0) / scale;
|
let dx = (x - pos.0) / scale;
|
||||||
let dy = (y - pos.1) / scale;
|
let dy = (y - pos.1) / scale;
|
||||||
let r = src[(dx + dy * iw) as usize * 3];
|
let r = src[(dx + dy * iw) as usize * 4];
|
||||||
let g = src[(dx + dy * iw) as usize * 3 + 1];
|
let g = src[(dx + dy * iw) as usize * 4 + 1];
|
||||||
let b = src[(dx + dy * iw) as usize * 3 + 2];
|
let b = src[(dx + dy * iw) as usize * 4 + 2];
|
||||||
|
let a = src[(dx + dy * iw) as usize * 4 + 3];
|
||||||
|
|
||||||
let color = Color(r, g, b, 0xff);
|
let color = Color(r, g, b, a);
|
||||||
|
|
||||||
self.set_pixel((x as u32, y as u32), color);
|
self.set_pixel((x as u32, y as u32), color);
|
||||||
}
|
}
|
||||||
|
@ -712,11 +714,21 @@ impl LayoutDrawer {
|
||||||
self.canvas.set_pixel((pos.0 as u32, pos.1 as u32), color);
|
self.canvas.set_pixel((pos.0 as u32, pos.1 as u32), color);
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Draw RGB image
|
// {{{ Draw images
|
||||||
|
/// Draws a bitmap image taking with no alpha channel.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
|
pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, image: &RgbImage) {
|
||||||
let pos = self.layout.position_relative_to(id, pos);
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
self.canvas.blit_rbg(pos, dims, src);
|
self.canvas
|
||||||
|
.blit_rbg(pos, image.dimensions(), image.as_raw());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a bitmap image taking care of the alpha channel.
|
||||||
|
#[inline]
|
||||||
|
pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, image: &RgbaImage) {
|
||||||
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
|
self.canvas
|
||||||
|
.blit_rbga(pos, image.dimensions(), image.as_raw());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -729,15 +741,7 @@ impl LayoutDrawer {
|
||||||
scale: u32,
|
scale: u32,
|
||||||
) {
|
) {
|
||||||
let pos = self.layout.position_relative_to(id, pos);
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
self.canvas.blit_rbg_scaled_up(pos, dims, src, scale);
|
self.canvas.blit_rbga_scaled_up(pos, dims, src, scale);
|
||||||
}
|
|
||||||
// }}}
|
|
||||||
// {{{ Draw RGBA image
|
|
||||||
/// Draws a bitmap image taking care of the alpha channel.
|
|
||||||
#[inline]
|
|
||||||
pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
|
|
||||||
let pos = self.layout.position_relative_to(id, pos);
|
|
||||||
self.canvas.blit_rbga(pos, dims, src);
|
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Fill
|
// {{{ Fill
|
||||||
|
|
|
@ -24,9 +24,9 @@ use crate::{
|
||||||
},
|
},
|
||||||
assert_is_pookie,
|
assert_is_pookie,
|
||||||
assets::{
|
assets::{
|
||||||
get_b30_background, get_count_background, get_difficulty_background, get_grade_background,
|
get_difficulty_background, with_font, B30_BACKGROUND, COUNT_BACKGROUND, EXO_FONT,
|
||||||
get_name_backgound, get_ptt_emblem, get_score_background, get_status_background,
|
GRADE_BACKGROUND, NAME_BACKGROUND, PTT_EMBLEM, SCORE_BACKGROUND, STATUS_BACKGROUND,
|
||||||
get_top_backgound, with_font, EXO_FONT,
|
TOP_BACKGROUND,
|
||||||
},
|
},
|
||||||
bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect},
|
bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect},
|
||||||
context::{Context, Error},
|
context::{Context, Error},
|
||||||
|
@ -300,7 +300,7 @@ async fn best_plays(
|
||||||
let mut drawer = LayoutDrawer::new(layout, canvas);
|
let mut drawer = LayoutDrawer::new(layout, canvas);
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Render background
|
// {{{ Render background
|
||||||
let bg = get_b30_background();
|
let bg = &*B30_BACKGROUND;
|
||||||
|
|
||||||
let scale = (drawer.layout.width(root) as f32 / bg.width() as f32)
|
let scale = (drawer.layout.width(root) as f32 / bg.width() as f32)
|
||||||
.max(drawer.layout.height(root) as f32 / bg.height() as f32)
|
.max(drawer.layout.height(root) as f32 / bg.height() as f32)
|
||||||
|
@ -325,8 +325,8 @@ async fn best_plays(
|
||||||
.layout
|
.layout
|
||||||
.edit_to_relative(item_with_margin, item_grid, origin.0, origin.1);
|
.edit_to_relative(item_with_margin, item_grid, origin.0, origin.1);
|
||||||
|
|
||||||
let top_bg = get_top_backgound();
|
let top_bg = &*TOP_BACKGROUND;
|
||||||
drawer.blit_rbg(top_area, (0, 0), top_bg.dimensions(), top_bg);
|
drawer.blit_rbga(top_area, (0, 0), top_bg);
|
||||||
|
|
||||||
let (play, song, chart) = if let Some(item) = plays.get(i) {
|
let (play, song, chart) = if let Some(item) = plays.get(i) {
|
||||||
item
|
item
|
||||||
|
@ -335,11 +335,11 @@ async fn best_plays(
|
||||||
};
|
};
|
||||||
|
|
||||||
// {{{ Display index
|
// {{{ Display index
|
||||||
let bg = get_count_background();
|
let bg = &*COUNT_BACKGROUND;
|
||||||
let bg_center = Rect::from_image(bg).center();
|
let bg_center = Rect::from_image(bg).center();
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg);
|
drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg);
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
drawer.text(
|
drawer.text(
|
||||||
item_area,
|
item_area,
|
||||||
|
@ -359,8 +359,8 @@ async fn best_plays(
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display chart name
|
// {{{ Display chart name
|
||||||
// Draw background
|
// Draw background
|
||||||
let bg = get_name_backgound();
|
let bg = &*NAME_BACKGROUND;
|
||||||
drawer.blit_rbg(bottom_area, (0, 0), bg.dimensions(), bg.as_raw());
|
drawer.blit_rbga(bottom_area, (0, 0), bg);
|
||||||
|
|
||||||
// Draw text
|
// Draw text
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
|
@ -403,12 +403,7 @@ async fn best_plays(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
drawer.fill(jacket_with_border, Color::from_rgb_int(0x271E35));
|
drawer.fill(jacket_with_border, Color::from_rgb_int(0x271E35));
|
||||||
drawer.blit_rbg(
|
drawer.blit_rbg(jacket_area, (0, 0), jacket.bitmap);
|
||||||
jacket_area,
|
|
||||||
(0, 0),
|
|
||||||
jacket.bitmap.dimensions(),
|
|
||||||
&jacket.bitmap.as_raw(),
|
|
||||||
);
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display difficulty background
|
// {{{ Display difficulty background
|
||||||
let diff_bg = get_difficulty_background(chart.difficulty);
|
let diff_bg = get_difficulty_background(chart.difficulty);
|
||||||
|
@ -417,12 +412,7 @@ async fn best_plays(
|
||||||
(drawer.layout.width(jacket_with_border) as i32, 0),
|
(drawer.layout.width(jacket_with_border) as i32, 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
drawer.blit_rbga(
|
drawer.blit_rbga(jacket_with_border, diff_bg_area.top_left(), diff_bg);
|
||||||
jacket_with_border,
|
|
||||||
diff_bg_area.top_left(),
|
|
||||||
diff_bg.dimensions(),
|
|
||||||
&diff_bg.as_raw(),
|
|
||||||
);
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display difficulty text
|
// {{{ Display difficulty text
|
||||||
let x_offset = if chart.level.ends_with("+") {
|
let x_offset = if chart.level.ends_with("+") {
|
||||||
|
@ -453,7 +443,7 @@ async fn best_plays(
|
||||||
})?;
|
})?;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display score background
|
// {{{ Display score background
|
||||||
let score_bg = get_score_background();
|
let score_bg = &*SCORE_BACKGROUND;
|
||||||
let score_bg_pos = Rect::from_image(score_bg).align(
|
let score_bg_pos = Rect::from_image(score_bg).align(
|
||||||
(Align::End, Align::End),
|
(Align::End, Align::End),
|
||||||
(
|
(
|
||||||
|
@ -462,12 +452,7 @@ async fn best_plays(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
drawer.blit_rbga(
|
drawer.blit_rbga(jacket_area, score_bg_pos, score_bg);
|
||||||
jacket_area,
|
|
||||||
score_bg_pos,
|
|
||||||
score_bg.dimensions(),
|
|
||||||
&score_bg.as_raw(),
|
|
||||||
);
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display score text
|
// {{{ Display score text
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
|
@ -491,7 +476,7 @@ async fn best_plays(
|
||||||
})?;
|
})?;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display status background
|
// {{{ Display status background
|
||||||
let status_bg = get_status_background();
|
let status_bg = &*STATUS_BACKGROUND;
|
||||||
let status_bg_area = Rect::from_image(status_bg).align_whole(
|
let status_bg_area = Rect::from_image(status_bg).align_whole(
|
||||||
(Align::Center, Align::Center),
|
(Align::Center, Align::Center),
|
||||||
(
|
(
|
||||||
|
@ -500,12 +485,7 @@ async fn best_plays(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
drawer.blit_rbga(
|
drawer.blit_rbga(jacket_area, status_bg_area.top_left(), status_bg);
|
||||||
jacket_area,
|
|
||||||
status_bg_area.top_left(),
|
|
||||||
status_bg.dimensions(),
|
|
||||||
&status_bg.as_raw(),
|
|
||||||
);
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display status text
|
// {{{ Display status text
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
|
@ -543,18 +523,13 @@ async fn best_plays(
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display grade background
|
// {{{ Display grade background
|
||||||
let top_left_center = (drawer.layout.width(top_left_area) as i32 + jacket_margin) / 2;
|
let top_left_center = (drawer.layout.width(top_left_area) as i32 + jacket_margin) / 2;
|
||||||
let grade_bg = get_grade_background();
|
let grade_bg = &*GRADE_BACKGROUND;
|
||||||
let grade_bg_area = Rect::from_image(grade_bg).align_whole(
|
let grade_bg_area = Rect::from_image(grade_bg).align_whole(
|
||||||
(Align::Center, Align::Center),
|
(Align::Center, Align::Center),
|
||||||
(top_left_center, jacket_margin + 140),
|
(top_left_center, jacket_margin + 140),
|
||||||
);
|
);
|
||||||
|
|
||||||
drawer.blit_rbga(
|
drawer.blit_rbga(top_area, grade_bg_area.top_left(), grade_bg);
|
||||||
top_area,
|
|
||||||
grade_bg_area.top_left(),
|
|
||||||
grade_bg.dimensions(),
|
|
||||||
&grade_bg.as_raw(),
|
|
||||||
);
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display grade text
|
// {{{ Display grade text
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
|
@ -614,13 +589,12 @@ async fn best_plays(
|
||||||
})?;
|
})?;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display ptt emblem
|
// {{{ Display ptt emblem
|
||||||
let ptt_emblem = get_ptt_emblem();
|
let ptt_emblem = &*PTT_EMBLEM;
|
||||||
drawer.blit_rbga(
|
drawer.blit_rbga(
|
||||||
top_left_area,
|
top_left_area,
|
||||||
Rect::from_image(ptt_emblem)
|
Rect::from_image(ptt_emblem)
|
||||||
.align((Align::Center, Align::Center), (top_left_center, 115)),
|
.align((Align::Center, Align::Center), (top_left_center, 115)),
|
||||||
ptt_emblem.dimensions(),
|
ptt_emblem,
|
||||||
ptt_emblem.as_raw(),
|
|
||||||
);
|
);
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use std::{fs, path::PathBuf};
|
use std::fs;
|
||||||
|
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arcaea::{chart::SongCache, jacket::JacketCache},
|
arcaea::{chart::SongCache, jacket::JacketCache},
|
||||||
assets::{EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT},
|
assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT},
|
||||||
recognition::{hyperglass::CharMeasurements, ui::UIMeasurements},
|
recognition::{hyperglass::CharMeasurements, ui::UIMeasurements},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,9 +14,6 @@ pub type Context<'a> = poise::Context<'a, UserContext, Error>;
|
||||||
|
|
||||||
// Custom user data passed to all command functions
|
// Custom user data passed to all command functions
|
||||||
pub struct UserContext {
|
pub struct UserContext {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub data_dir: PathBuf,
|
|
||||||
|
|
||||||
pub db: SqlitePool,
|
pub db: SqlitePool,
|
||||||
pub song_cache: SongCache,
|
pub song_cache: SongCache,
|
||||||
pub jacket_cache: JacketCache,
|
pub jacket_cache: JacketCache,
|
||||||
|
@ -31,14 +28,14 @@ pub struct UserContext {
|
||||||
|
|
||||||
impl UserContext {
|
impl UserContext {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn new(data_dir: PathBuf, cache_dir: PathBuf, db: SqlitePool) -> Result<Self, Error> {
|
pub async fn new(db: SqlitePool) -> Result<Self, Error> {
|
||||||
fs::create_dir_all(&cache_dir)?;
|
fs::create_dir_all(get_data_dir())?;
|
||||||
fs::create_dir_all(&data_dir)?;
|
|
||||||
|
|
||||||
let mut song_cache = SongCache::new(&db).await?;
|
let mut song_cache = SongCache::new(&db).await?;
|
||||||
let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?;
|
let jacket_cache = JacketCache::new(&mut song_cache)?;
|
||||||
let ui_measurements = UIMeasurements::read(&data_dir)?;
|
let ui_measurements = UIMeasurements::read()?;
|
||||||
|
|
||||||
|
// {{{ Font measurements
|
||||||
static WHITELIST: &str = "0123456789'abcdefghklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ";
|
static WHITELIST: &str = "0123456789'abcdefghklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ";
|
||||||
|
|
||||||
let geosans_measurements = GEOSANS_FONT
|
let geosans_measurements = GEOSANS_FONT
|
||||||
|
@ -49,11 +46,11 @@ impl UserContext {
|
||||||
.with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?;
|
.with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?;
|
||||||
let exo_measurements = EXO_FONT
|
let exo_measurements = EXO_FONT
|
||||||
.with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, Some(700)))?;
|
.with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, Some(700)))?;
|
||||||
|
// }}}
|
||||||
|
|
||||||
println!("Created user context");
|
println!("Created user context");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
data_dir,
|
|
||||||
db,
|
db,
|
||||||
song_cache,
|
song_cache,
|
||||||
jacket_cache,
|
jacket_cache,
|
||||||
|
|
19
src/logs.rs
|
@ -6,11 +6,11 @@
|
||||||
//! allows for a convenient way to throw images into a `logs` directory with
|
//! allows for a convenient way to throw images into a `logs` directory with
|
||||||
//! a simple env var.
|
//! a simple env var.
|
||||||
|
|
||||||
use std::{env, ops::Deref, sync::OnceLock, time::Instant};
|
use std::{env, ops::Deref, path::PathBuf, sync::OnceLock, time::Instant};
|
||||||
|
|
||||||
use image::{DynamicImage, EncodableLayout, ImageBuffer, PixelWithColorType};
|
use image::{DynamicImage, EncodableLayout, ImageBuffer, PixelWithColorType};
|
||||||
|
|
||||||
use crate::context::Error;
|
use crate::{assets::get_path, context::Error};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn should_save_debug_images() -> bool {
|
fn should_save_debug_images() -> bool {
|
||||||
|
@ -19,6 +19,11 @@ fn should_save_debug_images() -> bool {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_log_dir() -> PathBuf {
|
||||||
|
get_path("SHIMMERING_LOG_DIR")
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_startup_time() -> Instant {
|
fn get_startup_time() -> Instant {
|
||||||
static CELL: OnceLock<Instant> = OnceLock::new();
|
static CELL: OnceLock<Instant> = OnceLock::new();
|
||||||
|
@ -28,10 +33,10 @@ fn get_startup_time() -> Instant {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn debug_image_log(image: &DynamicImage) -> Result<(), Error> {
|
pub fn debug_image_log(image: &DynamicImage) -> Result<(), Error> {
|
||||||
if should_save_debug_images() {
|
if should_save_debug_images() {
|
||||||
image.save(format!(
|
image.save(get_log_dir().join(format!(
|
||||||
"./logs/{:0>15}.png",
|
"{:0>15}.png",
|
||||||
get_startup_time().elapsed().as_nanos()
|
get_startup_time().elapsed().as_nanos()
|
||||||
))?;
|
)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -45,10 +50,10 @@ where
|
||||||
C: Deref<Target = [P::Subpixel]>,
|
C: Deref<Target = [P::Subpixel]>,
|
||||||
{
|
{
|
||||||
if should_save_debug_images() {
|
if should_save_debug_images() {
|
||||||
image.save(format!(
|
image.save(get_log_dir().join(format!(
|
||||||
"./logs/{:0>15}.png",
|
"./logs/{:0>15}.png",
|
||||||
get_startup_time().elapsed().as_nanos()
|
get_startup_time().elapsed().as_nanos()
|
||||||
))?;
|
)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
10
src/main.rs
|
@ -4,6 +4,7 @@
|
||||||
#![feature(array_try_map)]
|
#![feature(array_try_map)]
|
||||||
#![feature(async_closure)]
|
#![feature(async_closure)]
|
||||||
#![feature(try_blocks)]
|
#![feature(try_blocks)]
|
||||||
|
#![feature(thread_local)]
|
||||||
|
|
||||||
mod arcaea;
|
mod arcaea;
|
||||||
mod assets;
|
mod assets;
|
||||||
|
@ -21,7 +22,7 @@ use assets::get_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;
|
||||||
use std::{env::var, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
|
use std::{env::var, sync::Arc, time::Duration};
|
||||||
|
|
||||||
// {{{ Error handler
|
// {{{ Error handler
|
||||||
async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
|
async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
|
||||||
|
@ -37,13 +38,10 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let data_dir = get_data_dir();
|
|
||||||
let cache_dir = var("SHIMMERING_CACHE_DIR").expect("Missing `SHIMMERING_CACHE_DIR` env var");
|
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.connect(&format!(
|
.connect(&format!(
|
||||||
"sqlite://{}/db.sqlite",
|
"sqlite://{}/db.sqlite",
|
||||||
data_dir.to_str().unwrap()
|
get_data_dir().to_str().unwrap()
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -89,7 +87,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(data_dir, PathBuf::from_str(&cache_dir)?, pool).await?;
|
let ctx = UserContext::new(pool).await?;
|
||||||
|
|
||||||
Ok(ctx)
|
Ok(ctx)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::{fs, path::PathBuf};
|
use std::fs;
|
||||||
|
|
||||||
use image::GenericImage;
|
use image::GenericImage;
|
||||||
|
|
||||||
use crate::{bitmap::Rect, context::Error};
|
use crate::{assets::get_config_dir, bitmap::Rect, context::Error};
|
||||||
|
|
||||||
// {{{ Rects
|
// {{{ Rects
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
@ -94,11 +94,11 @@ pub struct UIMeasurements {
|
||||||
|
|
||||||
impl UIMeasurements {
|
impl UIMeasurements {
|
||||||
// {{{ Read
|
// {{{ Read
|
||||||
pub fn read(data_dir: &PathBuf) -> Result<Self, Error> {
|
pub fn read() -> Result<Self, Error> {
|
||||||
let mut measurements = Vec::new();
|
let mut measurements = Vec::new();
|
||||||
let mut measurement = UIMeasurement::default();
|
let mut measurement = UIMeasurement::default();
|
||||||
|
|
||||||
let path = data_dir.join("ui.txt");
|
let path = get_config_dir().join("ui.txt");
|
||||||
let contents = fs::read_to_string(path)?;
|
let contents = fs::read_to_string(path)?;
|
||||||
|
|
||||||
// {{{ Parse measurement file
|
// {{{ Parse measurement file
|
||||||
|
|