Revampt jacket loading system
This commit is contained in:
parent
cba88c5def
commit
ac4145ee40
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,7 +4,7 @@
|
||||||
shimmering/data
|
shimmering/data
|
||||||
shimmering/logs
|
shimmering/logs
|
||||||
shimmering/assets/fonts
|
shimmering/assets/fonts
|
||||||
shimmering/assets/songs
|
shimmering/assets/songs*
|
||||||
shimmering/assets/b30_background.*
|
shimmering/assets/b30_background.*
|
||||||
|
|
||||||
target
|
target
|
||||||
|
|
215
Cargo.lock
generated
215
Cargo.lock
generated
|
@ -76,10 +76,59 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anstream"
|
||||||
version = "1.0.86"
|
version = "0.6.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.87"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "approx"
|
name = "approx"
|
||||||
|
@ -331,12 +380,64 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cobs"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -509,7 +610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
|
@ -601,6 +702,18 @@ version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
|
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-io"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-io"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.34"
|
version = "0.8.34"
|
||||||
|
@ -945,7 +1058,7 @@ dependencies = [
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"indexmap",
|
"indexmap 2.2.6",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -962,6 +1075,12 @@ dependencies = [
|
||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
@ -977,7 +1096,7 @@ version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -992,6 +1111,12 @@ version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
@ -1214,6 +1339,17 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.2.6"
|
||||||
|
@ -1221,7 +1357,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1241,6 +1378,12 @@ version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -1316,7 +1459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1791,6 +1934,18 @@ dependencies = [
|
||||||
"syn 2.0.66",
|
"syn 2.0.66",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "postcard"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e"
|
||||||
|
dependencies = [
|
||||||
|
"cobs",
|
||||||
|
"embedded-io 0.4.0",
|
||||||
|
"embedded-io 0.6.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -2366,6 +2521,36 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.2.6",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serenity"
|
name = "serenity"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
|
@ -2414,7 +2599,9 @@ dependencies = [
|
||||||
name = "shimmeringmoon"
|
name = "shimmeringmoon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"freetype-rs",
|
"freetype-rs",
|
||||||
"hypertesseract",
|
"hypertesseract",
|
||||||
"image 0.25.2",
|
"image 0.25.2",
|
||||||
|
@ -2423,11 +2610,13 @@ dependencies = [
|
||||||
"num",
|
"num",
|
||||||
"plotters",
|
"plotters",
|
||||||
"poise",
|
"poise",
|
||||||
|
"postcard",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"r2d2_sqlite",
|
"r2d2_sqlite",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"rusqlite_migration",
|
"rusqlite_migration",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_with",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -2815,7 +3004,7 @@ version = "0.22.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.2.6",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
|
@ -2925,7 +3114,7 @@ checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
"mini-moka",
|
"mini-moka",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"secrecy",
|
"secrecy",
|
||||||
|
@ -3000,6 +3189,12 @@ version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
|
|
|
@ -21,6 +21,10 @@ include_dir = "0.7.4"
|
||||||
serde = "1.0.209"
|
serde = "1.0.209"
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
tempfile = "3.12.0"
|
tempfile = "3.12.0"
|
||||||
|
clap = { version = "4.5.17", features = ["derive"] }
|
||||||
|
postcard = { version = "1.0.10", features = ["use-std"], default-features = false }
|
||||||
|
serde_with = "3.9.0"
|
||||||
|
anyhow = "1.0.87"
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
14
flake.nix
14
flake.nix
|
@ -15,6 +15,20 @@
|
||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
packages.shimmeringmoon = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "shimmeringmoon";
|
||||||
|
version = "unstable-2024-09-06";
|
||||||
|
|
||||||
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./Cargo.lock;
|
||||||
|
outputHashes = {
|
||||||
|
"hypertesseract-0.1.0" = "sha256-G0dos5yvvcfBKznAo1IIzLgXqRDxmyZwB93QQ6hVZSo=";
|
||||||
|
"plotters-0.4.0" = "sha256-9wtd7lig1vQ2RJVaEHdicfPZy2AyuoNav8shPMZ1EuE=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
devShell = pkgs.mkShell rec {
|
devShell = pkgs.mkShell rec {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
(fenix.complete.withComponents [
|
(fenix.complete.withComponents [
|
||||||
|
|
13
scripts/copy-chart-info.sh
Executable file
13
scripts/copy-chart-info.sh
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$#" != 2 ]; then
|
||||||
|
echo "Usage: $0 <from> <to>"
|
||||||
|
echo "This script copies the chart/song data from a db to another. Useful for creating new dbs for testing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
from="$1/db.sqlite"
|
||||||
|
to ="$2/db.sqlite"
|
||||||
|
|
||||||
|
sqlite3 $from ".dump songs" | sqlite3 $to
|
||||||
|
sqlite3 $from ".dump charts" | sqlite3 $to
|
|
@ -47,3 +47,15 @@ Mistempered Malignance,,,mismal
|
||||||
Twilight Concerto,,,tasogare
|
Twilight Concerto,,,tasogare
|
||||||
Heart,,,kokoro
|
Heart,,,kokoro
|
||||||
Dancin' on a Cat's Paw,,,nekonote
|
Dancin' on a Cat's Paw,,,nekonote
|
||||||
|
Bookmaker (2D Version),,,bookmaker
|
||||||
|
Dement ~after legend~,,,dement
|
||||||
|
Einherjar Joker,,,einherjar
|
||||||
|
GOODTEK (Arcaea Edit),,,goodtek
|
||||||
|
Kanagawa Cyber Culvert,,,kanagawa
|
||||||
|
La'qryma of the Wasteland,,,laqryma
|
||||||
|
PRAGMATISM -RESURRECTION-,,,pragmatism
|
||||||
|
qualia -ideaesthesia-,,,qualia
|
||||||
|
Shades of Light in a Transcendent Realm,,,shadesoflight
|
||||||
|
trappola bewitching,,,trappola
|
||||||
|
Vicious [ANTi] Heroism,,,viciousheroism
|
||||||
|
eden,,,edenwacca
|
||||||
|
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -121,7 +122,8 @@ impl GoalStats {
|
||||||
user: &User,
|
user: &User,
|
||||||
scoring_system: ScoringSystem,
|
scoring_system: ScoringSystem,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let plays = get_best_plays(ctx, user.id, scoring_system, 0, usize::MAX, None)??;
|
let plays = get_best_plays(ctx, user.id, scoring_system, 0, usize::MAX, None)?
|
||||||
|
.map_err(|s| anyhow!("{s}"))?;
|
||||||
let conn = ctx.db.get()?;
|
let conn = ctx.db.get()?;
|
||||||
|
|
||||||
// {{{ PM count
|
// {{{ PM count
|
||||||
|
@ -157,7 +159,7 @@ impl GoalStats {
|
||||||
),
|
),
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.map_err(|_| "No ptt history data found")?;
|
.map_err(|_| anyhow!("No ptt history data found"))?;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Peak PM relay
|
// {{{ Peak PM relay
|
||||||
let peak_pm_relay = {
|
let peak_pm_relay = {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
|
use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use image::{ImageBuffer, Rgb};
|
use image::{ImageBuffer, Rgb};
|
||||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
|
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
|
||||||
|
|
||||||
|
@ -237,6 +238,14 @@ impl CachedSong {
|
||||||
chart_ids: [None; 5],
|
chart_ids: [None; 5],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn charts(&self) -> impl Iterator<Item = u32> {
|
||||||
|
self.chart_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|i| i)
|
||||||
|
.map(|i| i.get() as u32)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Song cache
|
// {{{ Song cache
|
||||||
|
@ -252,7 +261,7 @@ impl SongCache {
|
||||||
self.songs
|
self.songs
|
||||||
.get(id as usize)
|
.get(id as usize)
|
||||||
.and_then(|i| i.as_ref())
|
.and_then(|i| i.as_ref())
|
||||||
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
.ok_or_else(|| anyhow!("Could not find song with id {}", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -261,7 +270,7 @@ impl SongCache {
|
||||||
.charts
|
.charts
|
||||||
.get(chart_id as usize)
|
.get(chart_id as usize)
|
||||||
.and_then(|i| i.as_ref())
|
.and_then(|i| i.as_ref())
|
||||||
.ok_or_else(|| format!("Could not find chart with id {}", chart_id))?;
|
.ok_or_else(|| anyhow!("Could not find chart with id {}", chart_id))?;
|
||||||
let song = &self.lookup_song(chart.song_id)?.song;
|
let song = &self.lookup_song(chart.song_id)?.song;
|
||||||
|
|
||||||
Ok((song, chart))
|
Ok((song, chart))
|
||||||
|
@ -272,7 +281,7 @@ impl SongCache {
|
||||||
self.songs
|
self.songs
|
||||||
.get_mut(id as usize)
|
.get_mut(id as usize)
|
||||||
.and_then(|i| i.as_mut())
|
.and_then(|i| i.as_mut())
|
||||||
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
.ok_or_else(|| anyhow!("Could not find song with id {}", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -280,7 +289,7 @@ impl SongCache {
|
||||||
self.charts
|
self.charts
|
||||||
.get_mut(chart_id as usize)
|
.get_mut(chart_id as usize)
|
||||||
.and_then(|i| i.as_mut())
|
.and_then(|i| i.as_mut())
|
||||||
.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into())
|
.ok_or_else(|| anyhow!("Could not find chart with id {}", chart_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -292,7 +301,7 @@ impl SongCache {
|
||||||
let cached_song = self.lookup_song(id)?;
|
let cached_song = self.lookup_song(id)?;
|
||||||
let chart_id = cached_song.chart_ids[difficulty.to_index()]
|
let chart_id = cached_song.chart_ids[difficulty.to_index()]
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
format!(
|
anyhow!(
|
||||||
"Cannot find chart {} [{difficulty:?}]",
|
"Cannot find chart {} [{difficulty:?}]",
|
||||||
cached_song.song.title
|
cached_song.song.title
|
||||||
)
|
)
|
||||||
|
@ -302,6 +311,25 @@ impl SongCache {
|
||||||
Ok((&cached_song.song, chart))
|
Ok((&cached_song.song, chart))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn lookup_by_difficulty_mut(
|
||||||
|
&mut self,
|
||||||
|
id: u32,
|
||||||
|
difficulty: Difficulty,
|
||||||
|
) -> Result<&mut Chart, Error> {
|
||||||
|
let cached_song = self.lookup_song(id)?;
|
||||||
|
let chart_id = cached_song.chart_ids[difficulty.to_index()]
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow!(
|
||||||
|
"Cannot find chart {} [{difficulty:?}]",
|
||||||
|
cached_song.song.title
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.get() as u32;
|
||||||
|
let chart = self.lookup_chart_mut(chart_id)?;
|
||||||
|
Ok(chart)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn charts(&self) -> impl Iterator<Item = &Chart> {
|
pub fn charts(&self) -> impl Iterator<Item = &Chart> {
|
||||||
self.charts.iter().filter_map(|i| i.as_ref())
|
self.charts.iter().filter_map(|i| i.as_ref())
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use std::{fs, io::Cursor};
|
use std::fs;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use image::{imageops::FilterType, GenericImageView, Rgba};
|
use image::{imageops::FilterType, GenericImageView, Rgba};
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arcaea::chart::{Difficulty, Jacket, SongCache},
|
arcaea::chart::{Difficulty, Jacket, SongCache},
|
||||||
assets::{get_asset_dir, should_blur_jacket_art, should_skip_jacket_art},
|
assets::{get_asset_dir, should_skip_jacket_art},
|
||||||
context::Error,
|
context::Error,
|
||||||
recognition::fuzzy_song_name::guess_chart_name,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How many sub-segments to split each side into
|
/// How many sub-segments to split each side into
|
||||||
|
@ -15,14 +17,16 @@ pub const SPLIT_FACTOR: u32 = 8;
|
||||||
pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
||||||
pub const BITMAP_IMAGE_SIZE: u32 = 174;
|
pub const BITMAP_IMAGE_SIZE: u32 = 174;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[serde_as]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ImageVec {
|
pub struct ImageVec {
|
||||||
|
#[serde_as(as = "[_; IMAGE_VEC_DIM]")]
|
||||||
pub colors: [f32; IMAGE_VEC_DIM],
|
pub colors: [f32; IMAGE_VEC_DIM],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageVec {
|
impl ImageVec {
|
||||||
// {{{ (Image => vector) encoding
|
// {{{ (Image => vector) encoding
|
||||||
fn from_image(image: &impl GenericImageView<Pixel = Rgba<u8>>) -> Self {
|
pub fn from_image(image: &impl GenericImageView<Pixel = Rgba<u8>>) -> Self {
|
||||||
let mut colors = [0.0; IMAGE_VEC_DIM];
|
let mut colors = [0.0; IMAGE_VEC_DIM];
|
||||||
let chunk_width = image.width() / SPLIT_FACTOR;
|
let chunk_width = image.width() / SPLIT_FACTOR;
|
||||||
let chunk_height = image.height() / SPLIT_FACTOR;
|
let chunk_height = image.height() / SPLIT_FACTOR;
|
||||||
|
@ -101,91 +105,61 @@ impl JacketCache {
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
|
let songs_dir = get_asset_dir().join("songs/by_id");
|
||||||
let entries =
|
let entries =
|
||||||
fs::read_dir(get_asset_dir().join("songs")).expect("Couldn't read songs directory");
|
fs::read_dir(songs_dir).with_context(|| "Couldn't read songs directory")?;
|
||||||
let mut jacket_vectors = vec![];
|
let bytes = fs::read(get_asset_dir().join("songs/recognition_matrix"))
|
||||||
|
.with_context(|| "Could not read jacket recognition matrix")?;
|
||||||
|
let jacket_vectors = postcard::from_bytes(&bytes)
|
||||||
|
.with_context(|| "Could not decode jacket recognition matrix")?;
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let dir = entry?;
|
let dir = entry?;
|
||||||
let raw_dir_name = dir.file_name();
|
let raw_dir_name = dir.file_name();
|
||||||
let dir_name = raw_dir_name.to_str().unwrap();
|
let dir_name = raw_dir_name.to_str().unwrap();
|
||||||
for entry in fs::read_dir(dir.path()).expect("Couldn't read song directory") {
|
let song_id = dir_name.parse().with_context(|| {
|
||||||
|
format!("Dir name {dir_name} could not be parsed as `u32` song id")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let entries =
|
||||||
|
fs::read_dir(dir.path()).with_context(|| "Couldn't read song directory")?;
|
||||||
|
for entry in entries {
|
||||||
let file = entry?;
|
let file = entry?;
|
||||||
let raw_name = file.file_name();
|
let raw_name = file.file_name();
|
||||||
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
||||||
|
|
||||||
if !name.ends_with("_256") {
|
let difficulty = Difficulty::DIFFICULTY_SHORTHANDS
|
||||||
continue;
|
.iter()
|
||||||
}
|
.zip(Difficulty::DIFFICULTIES)
|
||||||
|
.find_map(|(s, d)| Some(d).filter(|_| name == s.to_lowercase()));
|
||||||
|
|
||||||
let name = name.strip_suffix("_256").unwrap();
|
let contents: &'static _ = fs::read(file.path())
|
||||||
|
.with_context(|| "Coult not read prepared jacket image")?
|
||||||
|
.leak();
|
||||||
|
|
||||||
let difficulty = match name {
|
let image = image::load_from_memory(contents)
|
||||||
"0" => Some(Difficulty::PST),
|
.with_context(|| "Could not load jacket image from prepared bytes")?;
|
||||||
"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_id, chart_id) = {
|
|
||||||
let (song, chart) =
|
|
||||||
guess_chart_name(dir_name, &song_cache, difficulty, true)?;
|
|
||||||
(song.id, chart.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
let contents: &'static _ = fs::read(file.path())?.leak();
|
|
||||||
|
|
||||||
let image = image::load_from_memory(contents)?;
|
|
||||||
jacket_vectors.push((song_id, ImageVec::from_image(&image)));
|
|
||||||
let mut image =
|
|
||||||
image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest);
|
|
||||||
|
|
||||||
if should_blur_jacket_art() {
|
|
||||||
image = image.blur(40.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let encoded_pic = {
|
|
||||||
let mut processed_pic = Vec::new();
|
|
||||||
image.write_to(
|
|
||||||
&mut Cursor::new(&mut processed_pic),
|
|
||||||
image::ImageFormat::Jpeg,
|
|
||||||
)?;
|
|
||||||
processed_pic.leak()
|
|
||||||
};
|
|
||||||
let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8()));
|
let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8()));
|
||||||
|
|
||||||
if name == "base" {
|
if let Some(difficulty) = difficulty {
|
||||||
// Inefficiently iterates over everything, but it's fine for ~1k entries
|
let chart = song_cache
|
||||||
for chart in song_cache.charts_mut() {
|
.lookup_by_difficulty_mut(song_id, difficulty)
|
||||||
if chart.song_id == song_id && chart.cached_jacket.is_none() {
|
.unwrap();
|
||||||
chart.cached_jacket = Some(Jacket {
|
chart.cached_jacket = Some(Jacket {
|
||||||
raw: encoded_pic,
|
raw: contents,
|
||||||
bitmap,
|
bitmap,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
}
|
for chart_id in song_cache.lookup_song(song_id)?.charts() {
|
||||||
} else if difficulty.is_some() {
|
let chart = song_cache.lookup_chart_mut(chart_id)?;
|
||||||
let chart = song_cache.lookup_chart_mut(chart_id).unwrap();
|
|
||||||
chart.cached_jacket = Some(Jacket {
|
|
||||||
raw: encoded_pic,
|
|
||||||
bitmap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for chart in song_cache.charts() {
|
|
||||||
if chart.cached_jacket.is_none() {
|
if chart.cached_jacket.is_none() {
|
||||||
println!(
|
chart.cached_jacket = Some(Jacket {
|
||||||
"No jacket found for '{} [{:?}]'",
|
raw: contents,
|
||||||
song_cache.lookup_song(chart.song_id)?.song.title,
|
bitmap,
|
||||||
chart.difficulty
|
});
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ pub fn should_skip_jacket_art() -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn should_blur_jacket_art() -> bool {
|
pub fn should_blur_jacket_art() -> bool {
|
||||||
var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1"
|
var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//! There's still stuff to be implemented here, like a cache for glyphs and
|
//! There's still stuff to be implemented here, like a cache for glyphs and
|
||||||
//! whatnot, but this does run pretty stably for the b30 renderer.
|
//! whatnot, but this does run pretty stably for the b30 renderer.
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use freetype::{
|
use freetype::{
|
||||||
bitmap::PixelMode,
|
bitmap::PixelMode,
|
||||||
face::{KerningMode, LoadFlag},
|
face::{KerningMode, LoadFlag},
|
||||||
|
@ -355,7 +356,7 @@ impl BitmapCanvas {
|
||||||
Some((i, glyph_index))
|
Some((i, glyph_index))
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
format!("Could not get glyph index for char '{}' in \"{}\"", c, text)
|
anyhow!("Could not get glyph index for char '{}' in \"{}\"", c, text)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let face = &mut faces[face_index];
|
let face = &mut faces[face_index];
|
||||||
|
|
15
src/cli/mod.rs
Normal file
15
src/cli/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
pub mod prepare_jackets;
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Start the discord bot
|
||||||
|
Discord {},
|
||||||
|
PrepareJackets {},
|
||||||
|
}
|
155
src/cli/prepare_jackets.rs
Normal file
155
src/cli/prepare_jackets.rs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{stdout, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Context};
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
arcaea::{
|
||||||
|
chart::{Difficulty, SongCache},
|
||||||
|
jacket::{ImageVec, BITMAP_IMAGE_SIZE},
|
||||||
|
},
|
||||||
|
assets::{get_asset_dir, get_data_dir},
|
||||||
|
context::{connect_db, Error},
|
||||||
|
recognition::fuzzy_song_name::guess_chart_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn prepare_jackets() -> Result<(), Error> {
|
||||||
|
let db = connect_db(&get_data_dir());
|
||||||
|
let song_cache = SongCache::new(&db)?;
|
||||||
|
|
||||||
|
let songs_dir = get_asset_dir().join("songs");
|
||||||
|
let raw_songs_dir = songs_dir.join("raw");
|
||||||
|
|
||||||
|
let by_id_dir = songs_dir.join("by_id");
|
||||||
|
if by_id_dir.exists() {
|
||||||
|
fs::remove_dir_all(&by_id_dir).with_context(|| "Could not remove `by_id` dir")?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&by_id_dir).with_context(|| "Could not create `by_id` dir")?;
|
||||||
|
|
||||||
|
let mut jacket_vectors = vec![];
|
||||||
|
|
||||||
|
let entries = fs::read_dir(&raw_songs_dir)
|
||||||
|
.with_context(|| "Couldn't read songs directory")?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.with_context(|| format!("Could not read member of `songs/raw`"))?;
|
||||||
|
|
||||||
|
for (i, dir) in entries.iter().enumerate() {
|
||||||
|
let raw_dir_name = dir.file_name();
|
||||||
|
let dir_name = raw_dir_name.to_str().unwrap();
|
||||||
|
|
||||||
|
// {{{ Update progress live
|
||||||
|
print!(
|
||||||
|
"{}/{}: {dir_name} \r",
|
||||||
|
i,
|
||||||
|
entries.len()
|
||||||
|
);
|
||||||
|
if i % 5 == 0 {
|
||||||
|
stdout().flush()?;
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
let entries = fs::read_dir(dir.path())
|
||||||
|
.with_context(|| "Couldn't read song directory")?
|
||||||
|
.map(|f| f.unwrap())
|
||||||
|
.filter(|f| f.file_name().to_str().unwrap().ends_with("_256.jpg"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for file in &entries {
|
||||||
|
let raw_name = file.file_name();
|
||||||
|
let name = raw_name
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.strip_suffix("_256.jpg")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow!("No '_256.jpg' suffix to remove from filename {raw_name:?}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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,
|
||||||
|
_ => bail!("Unknown jacket suffix {}", name),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sometimes it's useful to distinguish between separate (but related)
|
||||||
|
// charts like "Vicious Heroism" and "Vicious [ANTi] Heroism" being in
|
||||||
|
// the same directory. To do this, we only allow the base jacket to refer
|
||||||
|
// to the FUTURE difficulty, unless it's the only jacket present
|
||||||
|
// (or unless we are parsing the tutorial)
|
||||||
|
let search_difficulty =
|
||||||
|
if entries.len() > 1 && difficulty.is_none() && dir_name != "tutorial" {
|
||||||
|
Some(Difficulty::FTR)
|
||||||
|
} else {
|
||||||
|
difficulty
|
||||||
|
};
|
||||||
|
|
||||||
|
let (song, _) = guess_chart_name(dir_name, &song_cache, search_difficulty, true)
|
||||||
|
.with_context(|| format!("Could not recognise chart name from '{dir_name}'"))?;
|
||||||
|
|
||||||
|
// {{{ Set up `out_dir` paths
|
||||||
|
let out_dir = {
|
||||||
|
let out = by_id_dir.join(song.id.to_string());
|
||||||
|
if !out.exists() {
|
||||||
|
fs::create_dir_all(&out).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Could not create parent dir for song '{}' inside `by_id`",
|
||||||
|
song.title
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
};
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
let difficulty_string = if let Some(difficulty) = difficulty {
|
||||||
|
&Difficulty::DIFFICULTY_SHORTHANDS[difficulty.to_index()].to_lowercase()
|
||||||
|
} else {
|
||||||
|
"def"
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents: &'static _ = fs::read(file.path())
|
||||||
|
.with_context(|| format!("Could not read image for file {:?}", file.path()))?
|
||||||
|
.leak();
|
||||||
|
let image = image::load_from_memory(contents)?;
|
||||||
|
|
||||||
|
jacket_vectors.push((song.id, ImageVec::from_image(&image)));
|
||||||
|
|
||||||
|
let image = image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian);
|
||||||
|
let image_out_path =
|
||||||
|
out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg"));
|
||||||
|
image
|
||||||
|
.save(&image_out_path)
|
||||||
|
.with_context(|| format!("Could not save image to {image_out_path:?}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this is N^2, but it's a one-off warning thing, so it's fine
|
||||||
|
for chart in song_cache.charts() {
|
||||||
|
if jacket_vectors.iter().all(|(i, _)| chart.song_id != *i) {
|
||||||
|
println!(
|
||||||
|
"No jacket found for '{} [{:?}]'",
|
||||||
|
song_cache.lookup_song(chart.song_id)?.song.title,
|
||||||
|
chart.difficulty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
println!("Encoded {} images", jacket_vectors.len());
|
||||||
|
let bytes = postcard::to_allocvec(&jacket_vectors)
|
||||||
|
.with_context(|| format!("Coult not encode jacket matrix"))?;
|
||||||
|
fs::write(songs_dir.join("recognition_matrix"), bytes)
|
||||||
|
.with_context(|| format!("Could not write jacket matrix"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
|
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -25,6 +26,8 @@ use crate::{
|
||||||
user::discord_id_to_discord_user,
|
user::discord_id_to_discord_user,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::discord::MessageContext;
|
||||||
|
|
||||||
// {{{ Top command
|
// {{{ Top command
|
||||||
/// Chart-related stats
|
/// Chart-related stats
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
|
@ -38,15 +41,9 @@ pub async fn chart(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Info
|
// {{{ Info
|
||||||
/// Show a chart given it's name
|
// {{{ Implementation
|
||||||
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), Error> {
|
||||||
async fn info(
|
let (song, chart) = guess_song_and_chart(&ctx.data(), name)?;
|
||||||
ctx: Context<'_>,
|
|
||||||
#[rest]
|
|
||||||
#[description = "Name of chart to show (difficulty at the end)"]
|
|
||||||
name: String,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
|
||||||
|
|
||||||
let attachement_name = "chart.png";
|
let attachement_name = "chart.png";
|
||||||
let icon_attachement = match chart.cached_jacket.as_ref() {
|
let icon_attachement = match chart.cached_jacket.as_ref() {
|
||||||
|
@ -95,17 +92,62 @@ async fn info(
|
||||||
embed = embed.thumbnail(format!("attachment://{}", &attachement_name));
|
embed = embed.thumbnail(format!("attachment://{}", &attachement_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.channel_id()
|
ctx.send_files(icon_attachement, CreateMessage::new().embed(embed))
|
||||||
.send_files(
|
|
||||||
ctx.http(),
|
|
||||||
icon_attachement,
|
|
||||||
CreateMessage::new().embed(embed),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
// {{{ Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod info_tests {
|
||||||
|
use crate::{commands::discord::mock::MockContext, with_test_ctx};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn no_suffix() -> Result<(), Error> {
|
||||||
|
with_test_ctx!("test/commands/chart/info/no_suffix", async |ctx| {
|
||||||
|
info_impl(ctx, "Pentiment").await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn specify_difficulty() -> Result<(), Error> {
|
||||||
|
with_test_ctx!("test/commands/chart/info/specify_difficulty", async |ctx| {
|
||||||
|
info_impl(ctx, "Hellohell [ETR]").await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn last_byd() -> Result<(), Error> {
|
||||||
|
with_test_ctx!(
|
||||||
|
"test/commands/chart/info/last_byd",
|
||||||
|
async |ctx: &mut MockContext| {
|
||||||
|
info_impl(ctx, "Last | Moment [BYD]").await?;
|
||||||
|
info_impl(ctx, "Last | Eternity [BYD]").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/// Show a chart given it's name
|
||||||
|
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||||
|
async fn info(
|
||||||
|
mut ctx: Context<'_>,
|
||||||
|
#[rest]
|
||||||
|
#[description = "Name of chart to show (difficulty at the end)"]
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
info_impl(&mut ctx, &name).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
// {{{ Best score
|
// {{{ Best score
|
||||||
/// Show the best score on a given chart
|
/// Show the best score on a given chart
|
||||||
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||||
|
@ -138,9 +180,10 @@ async fn best(
|
||||||
)?
|
)?
|
||||||
.query_row((user.id, chart.id), |row| Play::from_sql(chart, row))
|
.query_row((user.id, chart.id), |row| Play::from_sql(chart, row))
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
format!(
|
anyhow!(
|
||||||
"Could not find any scores for {} [{:?}]",
|
"Could not find any scores for {} [{:?}]",
|
||||||
song.title, chart.difficulty
|
song.title,
|
||||||
|
chart.difficulty
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
@ -53,9 +53,7 @@ pub trait MessageContext {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Poise implementation
|
// {{{ Poise implementation
|
||||||
impl<'a, 'b> MessageContext
|
impl<'a> MessageContext for poise::Context<'a, UserContext, Error> {
|
||||||
for poise::Context<'a, UserContext, Box<dyn std::error::Error + Send + Sync + 'b>>
|
|
||||||
{
|
|
||||||
type Attachment = poise::serenity_prelude::Attachment;
|
type Attachment = poise::serenity_prelude::Attachment;
|
||||||
|
|
||||||
fn data(&self) -> &UserContext {
|
fn data(&self) -> &UserContext {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::context::{Context, Error};
|
||||||
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
|
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
|
||||||
use crate::user::{discord_id_to_discord_user, User};
|
use crate::user::{discord_id_to_discord_user, User};
|
||||||
use crate::{get_user, timed};
|
use crate::{get_user, timed};
|
||||||
|
use anyhow::anyhow;
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use poise::serenity_prelude as serenity;
|
use poise::serenity_prelude as serenity;
|
||||||
use poise::serenity_prelude::CreateMessage;
|
use poise::serenity_prelude::CreateMessage;
|
||||||
|
@ -90,9 +91,10 @@ async fn magic_impl<C: MessageContext>(
|
||||||
analyzer
|
analyzer
|
||||||
.read_score(ctx.data(), Some(chart.note_count), &grayscale_image, kind)
|
.read_score(ctx.data(), Some(chart.note_count), &grayscale_image, kind)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
format!(
|
anyhow!(
|
||||||
"Could not read score for chart {} [{:?}]: {err}",
|
"Could not read score for chart {} [{:?}]: {err}",
|
||||||
song.title, chart.difficulty
|
song.title,
|
||||||
|
chart.difficulty
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
});
|
});
|
||||||
|
@ -137,52 +139,24 @@ async fn magic_impl<C: MessageContext>(
|
||||||
// {{{ Tests
|
// {{{ Tests
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod magic_tests {
|
mod magic_tests {
|
||||||
use std::{path::PathBuf, process::Command, str::FromStr};
|
|
||||||
|
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{
|
use crate::with_test_ctx;
|
||||||
commands::discord::mock::MockContext,
|
|
||||||
context::{connect_db, get_shared_context},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
macro_rules! with_ctx {
|
|
||||||
($test_path:expr, $f:expr) => {{
|
|
||||||
let mut data = (*get_shared_context().await).clone();
|
|
||||||
let dir = tempfile::tempdir()?;
|
|
||||||
let path = dir.path().join("db.sqlite");
|
|
||||||
println!("path {path:?}");
|
|
||||||
data.db = connect_db(SqliteConnectionManager::file(path));
|
|
||||||
|
|
||||||
Command::new("scripts/import-charts.py")
|
|
||||||
.env("SHIMMERING_DATA_DIR", dir.path().to_str().unwrap())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut ctx = MockContext::new(data);
|
|
||||||
User::create_from_context(&ctx)?;
|
|
||||||
|
|
||||||
let res: Result<(), Error> = $f(&mut ctx).await;
|
|
||||||
res?;
|
|
||||||
|
|
||||||
ctx.write_to(&PathBuf::from_str($test_path)?)?;
|
|
||||||
Ok(())
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn no_pics() -> Result<(), Error> {
|
async fn no_pics() -> Result<(), Error> {
|
||||||
with_ctx!("test/commands/score/magic/no_pics", async |ctx| {
|
with_test_ctx!("test/commands/score/magic/no_pics", async |ctx| {
|
||||||
magic_impl(ctx, vec![]).await?;
|
magic_impl(ctx, vec![]).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn basic_pic() -> Result<(), Error> {
|
async fn simple_pic() -> Result<(), Error> {
|
||||||
with_ctx!("test/commands/score/magic/single_pic", async |ctx| {
|
with_test_ctx!("test/commands/score/magic/single_pic", async |ctx| {
|
||||||
magic_impl(
|
magic_impl(
|
||||||
ctx,
|
ctx,
|
||||||
vec![PathBuf::from_str("test/screenshots/alter_ego.jpg")?],
|
vec![PathBuf::from_str("test/screenshots/alter_ego.jpg")?],
|
||||||
|
@ -194,7 +168,7 @@ mod magic_tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn weird_kerning() -> Result<(), Error> {
|
async fn weird_kerning() -> Result<(), Error> {
|
||||||
with_ctx!("test/commands/score/magic/weird_kerning", async |ctx| {
|
with_test_ctx!("test/commands/score/magic/weird_kerning", async |ctx| {
|
||||||
magic_impl(
|
magic_impl(
|
||||||
ctx,
|
ctx,
|
||||||
vec![
|
vec![
|
||||||
|
@ -298,7 +272,7 @@ pub async fn show(
|
||||||
Ok((song, chart, play, discord_id))
|
Ok((song, chart, play, discord_id))
|
||||||
})?
|
})?
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| format!("Could not find play with id {}", id))??;
|
.ok_or_else(|| anyhow!("Could not find play with id {}", id))??;
|
||||||
|
|
||||||
let author = discord_id_to_discord_user(&ctx, &discord_id).await?;
|
let author = discord_id_to_discord_user(&ctx, &discord_id).await?;
|
||||||
let user = User::by_id(ctx.data(), play.user_id)?;
|
let user = User::by_id(ctx.data(), play.user_id)?;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use image::{DynamicImage, ImageBuffer};
|
use image::{DynamicImage, ImageBuffer};
|
||||||
use poise::{
|
use poise::{
|
||||||
serenity_prelude::{CreateAttachment, CreateEmbed},
|
serenity_prelude::{CreateAttachment, CreateEmbed},
|
||||||
|
@ -194,9 +195,10 @@ async fn best_plays(
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display jacket
|
// {{{ Display jacket
|
||||||
let jacket = chart.cached_jacket.as_ref().ok_or_else(|| {
|
let jacket = chart.cached_jacket.as_ref().ok_or_else(|| {
|
||||||
format!(
|
anyhow!(
|
||||||
"Cannot find jacket for chart {} [{:?}]",
|
"Cannot find jacket for chart {} [{:?}]",
|
||||||
song.title, chart.difficulty
|
song.title,
|
||||||
|
chart.difficulty
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -289,7 +291,7 @@ async fn best_plays(
|
||||||
// {{{ Display status text
|
// {{{ Display status text
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
let status = play.short_status(scoring_system, chart).ok_or_else(|| {
|
let status = play.short_status(scoring_system, chart).ok_or_else(|| {
|
||||||
format!(
|
anyhow!(
|
||||||
"Could not get status for score {}",
|
"Could not get status for score {}",
|
||||||
play.score(scoring_system)
|
play.score(scoring_system)
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,7 @@ use r2d2::Pool;
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
use rusqlite_migration::Migrations;
|
use rusqlite_migration::Migrations;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -13,7 +14,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
// Types used by all command functions
|
// Types used by all command functions
|
||||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
pub type Error = anyhow::Error;
|
||||||
pub type Context<'a> = poise::Context<'a, UserContext, Error>;
|
pub type Context<'a> = poise::Context<'a, UserContext, Error>;
|
||||||
|
|
||||||
pub type DbConnection = r2d2::Pool<SqliteConnectionManager>;
|
pub type DbConnection = r2d2::Pool<SqliteConnectionManager>;
|
||||||
|
@ -33,21 +34,24 @@ pub struct UserContext {
|
||||||
pub kazesawa_bold_measurements: CharMeasurements,
|
pub kazesawa_bold_measurements: CharMeasurements,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect_db(manager: SqliteConnectionManager) -> DbConnection {
|
pub fn connect_db(data_dir: &Path) -> DbConnection {
|
||||||
timed!("create_sqlite_pool", {
|
timed!("create_sqlite_pool", {
|
||||||
Pool::new(manager.with_init(|conn| {
|
fs::create_dir_all(data_dir).expect("Could not create $SHIMMERING_DATA_DIR");
|
||||||
|
|
||||||
|
let data_dir = data_dir.to_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
let db_path = format!("{}/db.sqlite", data_dir);
|
||||||
|
let mut conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||||
static MIGRATIONS: LazyLock<Migrations> = LazyLock::new(|| {
|
static MIGRATIONS: LazyLock<Migrations> = LazyLock::new(|| {
|
||||||
Migrations::from_directory(&MIGRATIONS_DIR).expect("Could not load migrations")
|
Migrations::from_directory(&MIGRATIONS_DIR).expect("Could not load migrations")
|
||||||
});
|
});
|
||||||
|
|
||||||
MIGRATIONS
|
MIGRATIONS
|
||||||
.to_latest(conn)
|
.to_latest(&mut conn)
|
||||||
.expect("Could not run migrations");
|
.expect("Could not run migrations");
|
||||||
|
|
||||||
Ok(())
|
Pool::new(SqliteConnectionManager::file(&db_path)).expect("Could not open sqlite database.")
|
||||||
}))
|
|
||||||
.expect("Could not open sqlite database.")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +59,7 @@ impl UserContext {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn new() -> Result<Self, Error> {
|
pub async fn new() -> Result<Self, Error> {
|
||||||
timed!("create_context", {
|
timed!("create_context", {
|
||||||
fs::create_dir_all(get_data_dir())?;
|
let db = connect_db(&get_data_dir());
|
||||||
|
|
||||||
let db = connect_db(SqliteConnectionManager::file(&format!(
|
|
||||||
"{}/db.sqlite",
|
|
||||||
get_data_dir().to_str().unwrap()
|
|
||||||
)));
|
|
||||||
|
|
||||||
let mut song_cache = timed!("make_song_cache", { SongCache::new(&db)? });
|
let mut song_cache = timed!("make_song_cache", { SongCache::new(&db)? });
|
||||||
let jacket_cache = timed!("make_jacket_cache", { JacketCache::new(&mut song_cache)? });
|
let jacket_cache = timed!("make_jacket_cache", { JacketCache::new(&mut song_cache)? });
|
||||||
|
@ -93,8 +92,45 @@ impl UserContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod testing {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
pub async fn get_shared_context() -> &'static UserContext {
|
pub async fn get_shared_context() -> &'static UserContext {
|
||||||
static CELL: tokio::sync::OnceCell<UserContext> = tokio::sync::OnceCell::const_new();
|
static CELL: tokio::sync::OnceCell<UserContext> = tokio::sync::OnceCell::const_new();
|
||||||
CELL.get_or_init(async || UserContext::new().await.unwrap())
|
CELL.get_or_init(async || {
|
||||||
|
// env::set_var("SHIMMERING_DATA_DIR", "")
|
||||||
|
UserContext::new().await.unwrap()
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn import_songs_and_jackets_from(to: &Path) -> () {
|
||||||
|
std::process::Command::new("scripts/copy-chart-info.sh")
|
||||||
|
.arg(get_data_dir())
|
||||||
|
.arg(to)
|
||||||
|
.output()
|
||||||
|
.expect("Could not run sh chart info copy script");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! with_test_ctx {
|
||||||
|
($test_path:expr, $f:expr) => {{
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
let mut data = (*crate::context::testing::get_shared_context().await).clone();
|
||||||
|
let dir = tempfile::tempdir()?;
|
||||||
|
data.db = crate::context::connect_db(dir.path());
|
||||||
|
crate::context::testing::import_songs_and_jackets_from(dir.path());
|
||||||
|
|
||||||
|
let mut ctx = crate::commands::discord::mock::MockContext::new(data);
|
||||||
|
crate::user::User::create_from_context(&ctx)?;
|
||||||
|
|
||||||
|
let res: Result<(), Error> = $f(&mut ctx).await;
|
||||||
|
res?;
|
||||||
|
|
||||||
|
ctx.write_to(&std::path::PathBuf::from_str($test_path)?)?;
|
||||||
|
Ok(())
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -7,10 +7,12 @@
|
||||||
#![feature(thread_local)]
|
#![feature(thread_local)]
|
||||||
#![feature(generic_arg_infer)]
|
#![feature(generic_arg_infer)]
|
||||||
#![feature(lazy_cell_consume)]
|
#![feature(lazy_cell_consume)]
|
||||||
|
#![feature(iter_collect_into)]
|
||||||
|
|
||||||
mod arcaea;
|
mod arcaea;
|
||||||
mod assets;
|
mod assets;
|
||||||
mod bitmap;
|
mod bitmap;
|
||||||
|
mod cli;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod context;
|
mod context;
|
||||||
mod levenshtein;
|
mod levenshtein;
|
||||||
|
@ -21,6 +23,8 @@ mod transform;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
use arcaea::play::generate_missing_scores;
|
use arcaea::play::generate_missing_scores;
|
||||||
|
use clap::Parser;
|
||||||
|
use cli::{prepare_jackets::prepare_jackets, Cli, Command};
|
||||||
use context::{Error, UserContext};
|
use context::{Error, UserContext};
|
||||||
use poise::serenity_prelude::{self as serenity};
|
use poise::serenity_prelude::{self as serenity};
|
||||||
use std::{env::var, sync::Arc, time::Duration};
|
use std::{env::var, sync::Arc, time::Duration};
|
||||||
|
@ -39,6 +43,9 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
match cli.command {
|
||||||
|
Command::Discord {} => {
|
||||||
// {{{ Poise options
|
// {{{ Poise options
|
||||||
let options = poise::FrameworkOptions {
|
let options = poise::FrameworkOptions {
|
||||||
commands: vec![
|
commands: vec![
|
||||||
|
@ -79,7 +86,8 @@ async fn main() {
|
||||||
.setup(move |ctx, _ready, framework| {
|
.setup(move |ctx, _ready, framework| {
|
||||||
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().await?;
|
let ctx = UserContext::new().await?;
|
||||||
|
|
||||||
if var("SHIMMERING_REGEN_SCORES").unwrap_or_default() == "1" {
|
if var("SHIMMERING_REGEN_SCORES").unwrap_or_default() == "1" {
|
||||||
|
@ -94,10 +102,10 @@ async fn main() {
|
||||||
.options(options)
|
.options(options)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let token =
|
let token = var("SHIMMERING_DISCORD_TOKEN")
|
||||||
var("SHIMMERING_DISCORD_TOKEN").expect("Missing `SHIMMERING_DISCORD_TOKEN` env var");
|
.expect("Missing `SHIMMERING_DISCORD_TOKEN` env var");
|
||||||
let intents =
|
let intents = serenity::GatewayIntents::non_privileged()
|
||||||
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
|
| serenity::GatewayIntents::MESSAGE_CONTENT;
|
||||||
|
|
||||||
let client = serenity::ClientBuilder::new(token, intents)
|
let client = serenity::ClientBuilder::new(token, intents)
|
||||||
.framework(framework)
|
.framework(framework)
|
||||||
|
@ -106,3 +114,8 @@ async fn main() {
|
||||||
client.unwrap().start().await.unwrap()
|
client.unwrap().start().await.unwrap()
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
|
Command::PrepareJackets {} => {
|
||||||
|
prepare_jackets().expect("Could not prepare jackets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
//! databases extracted from the game, but this is still useful for having a
|
//! databases extracted from the game, but this is still useful for having a
|
||||||
//! "canonical" way to refer to some weirdly-named charts).
|
//! "canonical" way to refer to some weirdly-named charts).
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
use crate::arcaea::chart::{Chart, Difficulty, Song, SongCache};
|
use crate::arcaea::chart::{Chart, Difficulty, Song, SongCache};
|
||||||
use crate::context::{Error, UserContext};
|
use crate::context::{Error, UserContext};
|
||||||
use crate::levenshtein::edit_distance_with;
|
use crate::levenshtein::edit_distance_with;
|
||||||
|
@ -82,26 +84,29 @@ pub fn guess_chart_name<'a>(
|
||||||
let song_title = &song.lowercase_title;
|
let song_title = &song.lowercase_title;
|
||||||
distance_vec.clear();
|
distance_vec.clear();
|
||||||
|
|
||||||
|
// Apply raw distance
|
||||||
let base_distance = edit_distance_with(&text, &song_title, &mut levenshtein_vec);
|
let base_distance = edit_distance_with(&text, &song_title, &mut levenshtein_vec);
|
||||||
if base_distance < 1.max(song.title.len() / 3) {
|
if base_distance <= song.title.len() / 3 {
|
||||||
distance_vec.push(base_distance * 10 + 2);
|
distance_vec.push(base_distance * 10 + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cut title to the length of the text, and then check
|
||||||
let shortest_len = Ord::min(song_title.len(), text.len());
|
let shortest_len = Ord::min(song_title.len(), text.len());
|
||||||
if let Some(sliced) = &song_title.get(..shortest_len)
|
if let Some(sliced) = &song_title.get(..shortest_len)
|
||||||
&& (text.len() >= 6 || unsafe_heuristics)
|
&& (text.len() >= 6 || unsafe_heuristics)
|
||||||
{
|
{
|
||||||
let slice_distance = edit_distance_with(&text, sliced, &mut levenshtein_vec);
|
let slice_distance = edit_distance_with(&text, sliced, &mut levenshtein_vec);
|
||||||
if slice_distance < 1 {
|
if slice_distance == 0 {
|
||||||
distance_vec.push(slice_distance * 10 + 3);
|
distance_vec.push(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shorthand-based matching
|
||||||
if let Some(shorthand) = &chart.shorthand
|
if let Some(shorthand) = &chart.shorthand
|
||||||
&& unsafe_heuristics
|
&& unsafe_heuristics
|
||||||
{
|
{
|
||||||
let short_distance = edit_distance_with(&text, shorthand, &mut levenshtein_vec);
|
let short_distance = edit_distance_with(&text, shorthand, &mut levenshtein_vec);
|
||||||
if short_distance < 1.max(shorthand.len() / 3) {
|
if short_distance <= shorthand.len() / 3 {
|
||||||
distance_vec.push(short_distance * 10 + 1);
|
distance_vec.push(short_distance * 10 + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,12 +118,16 @@ pub fn guess_chart_name<'a>(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
close_enough.sort_by_key(|(song, _, _)| song.id);
|
||||||
|
close_enough.dedup_by_key(|(song, _, _)| song.id);
|
||||||
|
|
||||||
if close_enough.len() == 0 {
|
if close_enough.len() == 0 {
|
||||||
if text.len() <= 1 {
|
if text.len() <= 1 {
|
||||||
Err(format!(
|
bail!(
|
||||||
"Could not find match for chart name '{}' [{:?}]",
|
"Could not find match for chart name '{}' [{:?}]",
|
||||||
raw_text, difficulty
|
raw_text,
|
||||||
))?;
|
difficulty
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
text = &text[..text.len() - 1];
|
text = &text[..text.len() - 1];
|
||||||
}
|
}
|
||||||
|
@ -129,7 +138,7 @@ pub fn guess_chart_name<'a>(
|
||||||
close_enough.sort_by_key(|(_, _, distance)| *distance);
|
close_enough.sort_by_key(|(_, _, distance)| *distance);
|
||||||
break (close_enough[0].0, close_enough[0].1);
|
break (close_enough[0].0, close_enough[0].1);
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Name '{}' is too vague to choose a match", raw_text).into());
|
bail!("Name '{}' is too vague to choose a match", raw_text);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
//! aforementioned precomputed vectors are generated using almost the exact
|
//! aforementioned precomputed vectors are generated using almost the exact
|
||||||
//! procedure described in steps 1-6, except the images are generated at
|
//! procedure described in steps 1-6, except the images are generated at
|
||||||
//! startup using my very own bitmap rendering module (`crate::bitmap`).
|
//! startup using my very own bitmap rendering module (`crate::bitmap`).
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
use freetype::Face;
|
use freetype::Face;
|
||||||
use image::{DynamicImage, ImageBuffer, Luma};
|
use image::{DynamicImage, ImageBuffer, Luma};
|
||||||
use imageproc::{
|
use imageproc::{
|
||||||
|
@ -58,7 +59,7 @@ impl ComponentVec {
|
||||||
.bounds
|
.bounds
|
||||||
.get(component as usize - 1)
|
.get(component as usize - 1)
|
||||||
.and_then(|o| o.as_ref())
|
.and_then(|o| o.as_ref())
|
||||||
.ok_or_else(|| "Missing bounds for given connected component")?;
|
.ok_or_else(|| anyhow!("Missing bounds for given connected component"))?;
|
||||||
|
|
||||||
for i in 0..(SPLIT_FACTOR * SPLIT_FACTOR) {
|
for i in 0..(SPLIT_FACTOR * SPLIT_FACTOR) {
|
||||||
let (iy, ix) = i.div_rem_euclid(&SPLIT_FACTOR);
|
let (iy, ix) = i.div_rem_euclid(&SPLIT_FACTOR);
|
||||||
|
@ -82,10 +83,7 @@ impl ComponentVec {
|
||||||
let size = (x_end + 1 - x_start) * (y_end + 1 - y_start);
|
let size = (x_end + 1 - x_start) * (y_end + 1 - y_start);
|
||||||
|
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
return Err(format!(
|
bail!("Got zero size for chunk [{x_start},{x_end}]x[{y_start},{y_end}]");
|
||||||
"Got zero size for chunk [{x_start},{x_end}]x[{y_start},{y_end}]"
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks[i as usize] = count as f32 / size as f32;
|
chunks[i as usize] = count as f32 / size as f32;
|
||||||
|
@ -256,7 +254,7 @@ impl CharMeasurements {
|
||||||
canvas.text(padding, &mut [face], style, &string)?;
|
canvas.text(padding, &mut [face], style, &string)?;
|
||||||
let buffer =
|
let buffer =
|
||||||
ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec())
|
ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec())
|
||||||
.ok_or_else(|| "Failed to turn buffer into canvas")?;
|
.ok_or_else(|| anyhow!("Failed to turn buffer into canvas"))?;
|
||||||
let image = DynamicImage::ImageRgb8(buffer);
|
let image = DynamicImage::ImageRgb8(buffer);
|
||||||
|
|
||||||
debug_image_log(&image);
|
debug_image_log(&image);
|
||||||
|
@ -270,14 +268,14 @@ impl CharMeasurements {
|
||||||
.filter_map(|o| o.as_ref())
|
.filter_map(|o| o.as_ref())
|
||||||
.map(|b| b.x_max - b.x_min)
|
.map(|b| b.x_max - b.x_min)
|
||||||
.max()
|
.max()
|
||||||
.ok_or_else(|| "No connected components found")?;
|
.ok_or_else(|| anyhow!("No connected components found"))?;
|
||||||
let max_height = components
|
let max_height = components
|
||||||
.bounds
|
.bounds
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|o| o.as_ref())
|
.filter_map(|o| o.as_ref())
|
||||||
.map(|b| b.y_max - b.y_min)
|
.map(|b| b.y_max - b.y_min)
|
||||||
.max()
|
.max()
|
||||||
.ok_or_else(|| "No connected components found")?;
|
.ok_or_else(|| anyhow!("No connected components found"))?;
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
let mut chars = Vec::with_capacity(string.len());
|
let mut chars = Vec::with_capacity(string.len());
|
||||||
|
@ -318,7 +316,7 @@ impl CharMeasurements {
|
||||||
.filter_map(|o| o.as_ref())
|
.filter_map(|o| o.as_ref())
|
||||||
.map(|b| b.y_max - b.y_min)
|
.map(|b| b.y_max - b.y_min)
|
||||||
.max()
|
.max()
|
||||||
.ok_or_else(|| "No connected components found")?;
|
.ok_or_else(|| anyhow!("No connected components found"))?;
|
||||||
let max_width = self.max_width * max_height / self.max_height;
|
let max_width = self.max_width * max_height / self.max_height;
|
||||||
|
|
||||||
for i in &components.bounds_by_position {
|
for i in &components.bounds_by_position {
|
||||||
|
@ -334,7 +332,7 @@ impl CharMeasurements {
|
||||||
d1.partial_cmp(d2).expect("NaN distance encountered")
|
d1.partial_cmp(d2).expect("NaN distance encountered")
|
||||||
})
|
})
|
||||||
.map(|(i, _, d)| (d.sqrt(), i))
|
.map(|(i, _, d)| (d.sqrt(), i))
|
||||||
.ok_or_else(|| "No chars in cache")?;
|
.ok_or_else(|| anyhow!("No chars in cache"))?;
|
||||||
|
|
||||||
println!("char '{}', distance {}", best_match.1, best_match.0);
|
println!("char '{}', distance {}", best_match.1, best_match.0);
|
||||||
if best_match.0 <= 0.75 {
|
if best_match.0 <= 0.75 {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
use hypertesseract::{PageSegMode, Tesseract};
|
use hypertesseract::{PageSegMode, Tesseract};
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::{DynamicImage, GenericImageView};
|
||||||
|
@ -170,7 +171,7 @@ impl ImageAnalyzer {
|
||||||
}) {
|
}) {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Score {result} is not vaild").into())
|
Err(anyhow!("Score {result} is not vaild"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -228,7 +229,7 @@ impl ImageAnalyzer {
|
||||||
.zip(Difficulty::DIFFICULTY_STRINGS)
|
.zip(Difficulty::DIFFICULTY_STRINGS)
|
||||||
.min_by_key(|(_, difficulty_string)| edit_distance(difficulty_string, &text))
|
.min_by_key(|(_, difficulty_string)| edit_distance(difficulty_string, &text))
|
||||||
.map(|(difficulty, _)| *difficulty)
|
.map(|(difficulty, _)| *difficulty)
|
||||||
.ok_or_else(|| format!("Unrecognised difficulty '{}'", text))?;
|
.ok_or_else(|| anyhow!("Unrecognised difficulty '{}'", text))?;
|
||||||
|
|
||||||
Ok(difficulty)
|
Ok(difficulty)
|
||||||
}
|
}
|
||||||
|
@ -272,12 +273,11 @@ impl ImageAnalyzer {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if conf < 20 && conf != 0 {
|
if conf < 20 && conf != 0 {
|
||||||
return Err(format!(
|
bail!(
|
||||||
"Title text is not readable (confidence = {}, text = {}).",
|
"Title text is not readable (confidence = {}, text = {}).",
|
||||||
conf,
|
conf,
|
||||||
text.trim()
|
text.trim()
|
||||||
)
|
);
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guess_chart_name(&text, &ctx.song_cache, Some(difficulty), false)
|
guess_chart_name(&text, &ctx.song_cache, Some(difficulty), false)
|
||||||
|
@ -319,10 +319,10 @@ impl ImageAnalyzer {
|
||||||
let (distance, song_id) = ctx
|
let (distance, song_id) = ctx
|
||||||
.jacket_cache
|
.jacket_cache
|
||||||
.recognise(&*cropped)
|
.recognise(&*cropped)
|
||||||
.ok_or_else(|| "Could not recognise jacket")?;
|
.ok_or_else(|| anyhow!("Could not recognise jacket"))?;
|
||||||
|
|
||||||
if distance > (IMAGE_VEC_DIM * 3) as f32 {
|
if distance > (IMAGE_VEC_DIM * 3) as f32 {
|
||||||
Err("No known jacket looks like this")?;
|
bail!("No known jacket looks like this");
|
||||||
}
|
}
|
||||||
|
|
||||||
let (song, chart) = ctx.song_cache.lookup_by_difficulty(*song_id, difficulty)?;
|
let (song, chart) = ctx.song_cache.lookup_by_difficulty(*song_id, difficulty)?;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use image::GenericImage;
|
use image::GenericImage;
|
||||||
|
|
||||||
use crate::{assets::get_config_dir, bitmap::Rect, context::Error};
|
use crate::{assets::get_config_dir, bitmap::Rect, context::Error};
|
||||||
|
@ -172,7 +173,7 @@ impl UIMeasurements {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(format!("Could no find rect for {rect:?} in image").into())
|
Err(anyhow!("Could no find rect for {rect:?} in image"))
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use poise::serenity_prelude::UserId;
|
use poise::serenity_prelude::UserId;
|
||||||
use rusqlite::Row;
|
use rusqlite::Row;
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ impl User {
|
||||||
)?
|
)?
|
||||||
.query_map([&discord_id], |row| row.get("id"))?
|
.query_map([&discord_id], |row| row.get("id"))?
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "Failed to create user")??;
|
.ok_or_else(|| anyhow!("Failed to create user"))??;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
discord_id,
|
discord_id,
|
||||||
|
@ -57,7 +58,7 @@ impl User {
|
||||||
.prepare_cached("SELECT * FROM users WHERE discord_id = ?")?
|
.prepare_cached("SELECT * FROM users WHERE discord_id = ?")?
|
||||||
.query_map([id], Self::from_row)?
|
.query_map([id], Self::from_row)?
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "You are not an user in my database, sowwy ^~^")??;
|
.ok_or_else(|| anyhow!("You are not an user in my database, sowwy ^~^"))??;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
@ -69,7 +70,7 @@ impl User {
|
||||||
.prepare_cached("SELECT * FROM users WHERE id = ?")?
|
.prepare_cached("SELECT * FROM users WHERE id = ?")?
|
||||||
.query_map([id], Self::from_row)?
|
.query_map([id], Self::from_row)?
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "You are not an user in my database, sowwy ^~^")??;
|
.ok_or_else(|| anyhow!("You are not an user in my database, sowwy ^~^"))??;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue