1
Fork 0

Implement basic rich presence

This commit is contained in:
prescientmoon 2024-09-24 22:49:09 +02:00
parent 5186c7e8b8
commit 68c46fb7cd
Signed by: prescientmoon
SSH key fingerprint: SHA256:WFp/cO76nbarETAoQcQXuV+0h7XJsEsOCI0UsyPIy6U
26 changed files with 1061 additions and 267 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.allTargets": false
}

568
Cargo.lock generated
View file

@ -182,6 +182,12 @@ dependencies = [
"syn 2.0.77", "syn 2.0.77",
] ]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.3.0"
@ -211,6 +217,73 @@ dependencies = [
"arrayvec", "arrayvec",
] ]
[[package]]
name = "axum"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec"
dependencies = [
"async-trait",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower 0.5.1",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 1.0.1",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.74" version = "0.3.74"
@ -475,7 +548,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -498,7 +571,7 @@ checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5"
dependencies = [ dependencies = [
"core-foundation", "core-foundation",
"core-graphics", "core-graphics",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -687,6 +760,18 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "discord-rich-presence"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f632a41e3e97febf8edff46b1405f9875894c29e20a25c5abe566872226b3f84"
dependencies = [
"serde",
"serde_derive",
"serde_json",
"uuid 0.8.2",
]
[[package]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.2" version = "0.5.2"
@ -859,6 +944,15 @@ dependencies = [
"yeslogic-fontconfig-sys", "yeslogic-fontconfig-sys",
] ]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@ -866,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [ dependencies = [
"foreign-types-macros", "foreign-types-macros",
"foreign-types-shared", "foreign-types-shared 0.3.1",
] ]
[[package]] [[package]]
@ -880,6 +974,12 @@ dependencies = [
"syn 2.0.77", "syn 2.0.77",
] ]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "foreign-types-shared" name = "foreign-types-shared"
version = "0.3.1" version = "0.3.1"
@ -1077,6 +1177,25 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "h2"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"indexmap 2.5.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "half" name = "half"
version = "2.4.1" version = "2.4.1"
@ -1162,6 +1281,29 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.1.0",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"pin-project-lite",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.9.4" version = "1.9.4"
@ -1184,9 +1326,9 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.26",
"http 0.2.12", "http 0.2.12",
"http-body", "http-body 0.4.6",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -1198,6 +1340,27 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.24.2" version = "0.24.2"
@ -1206,12 +1369,65 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http 0.2.12", "http 0.2.12",
"hyper", "hyper 0.14.30",
"rustls 0.21.12", "rustls 0.21.12",
"tokio", "tokio",
"tokio-rustls 0.24.1", "tokio-rustls 0.24.1",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.4.1",
"hyper-util",
"rustls 0.23.13",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.4.1",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"hyper 1.4.1",
"pin-project-lite",
"socket2",
"tokio",
"tower 0.4.13",
"tower-service",
"tracing",
]
[[package]] [[package]]
name = "hypertesseract" name = "hypertesseract"
version = "0.1.0" version = "0.1.0"
@ -1532,6 +1748,12 @@ dependencies = [
"imgref", "imgref",
] ]
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]] [[package]]
name = "matrixmultiply" name = "matrixmultiply"
version = "0.3.9" version = "0.3.9"
@ -1640,6 +1862,23 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "new_debug_unreachable" name = "new_debug_unreachable"
version = "1.0.6" version = "1.0.6"
@ -1768,6 +2007,38 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
version = "0.10.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.103" version = "0.9.103"
@ -1843,6 +2114,26 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.14"
@ -2054,7 +2345,7 @@ checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846"
dependencies = [ dependencies = [
"r2d2", "r2d2",
"rusqlite", "rusqlite",
"uuid", "uuid 1.10.0",
] ]
[[package]] [[package]]
@ -2232,11 +2523,11 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.26",
"http 0.2.12", "http 0.2.12",
"http-body", "http-body 0.4.6",
"hyper", "hyper 0.14.30",
"hyper-rustls", "hyper-rustls 0.24.2",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
@ -2246,12 +2537,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.21.12", "rustls 0.21.12",
"rustls-pemfile", "rustls-pemfile 1.0.4",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper 0.1.2",
"system-configuration", "system-configuration 0.5.1",
"tokio", "tokio",
"tokio-rustls 0.24.1", "tokio-rustls 0.24.1",
"tokio-util", "tokio-util",
@ -2265,6 +2556,49 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "reqwest"
version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
"hyper-rustls 0.27.3",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 2.1.3",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.1",
"system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.50" version = "0.8.50"
@ -2369,6 +2703,19 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls"
version = "0.23.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.4" version = "1.0.4"
@ -2378,6 +2725,16 @@ dependencies = [
"base64 0.21.7", "base64 0.21.7",
] ]
[[package]]
name = "rustls-pemfile"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
dependencies = [
"base64 0.22.1",
"rustls-pki-types",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.8.0" version = "1.8.0"
@ -2405,6 +2762,12 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -2429,6 +2792,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "scheduled-thread-pool" name = "scheduled-thread-pool"
version = "0.2.7" version = "0.2.7"
@ -2464,6 +2836,29 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.23" version = "1.0.23"
@ -2514,6 +2909,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
"itoa",
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.7" version = "0.6.7"
@ -2584,7 +2989,7 @@ dependencies = [
"mime_guess", "mime_guess",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"reqwest", "reqwest 0.11.27",
"secrecy", "secrecy",
"serde", "serde",
"serde_cow", "serde_cow",
@ -2625,20 +3030,24 @@ name = "shimmeringmoon"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum",
"base16ct", "base16ct",
"chrono", "chrono",
"clap", "clap",
"discord-rich-presence",
"freetype-rs", "freetype-rs",
"hypertesseract", "hypertesseract",
"image 0.25.2", "image 0.25.2",
"imageproc", "imageproc",
"include_dir", "include_dir",
"num", "num",
"paste",
"plotters", "plotters",
"poise", "poise",
"postcard", "postcard",
"r2d2", "r2d2",
"r2d2_sqlite", "r2d2_sqlite",
"reqwest 0.12.7",
"rusqlite", "rusqlite",
"rusqlite_migration", "rusqlite_migration",
"serde", "serde",
@ -2772,6 +3181,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "sys" name = "sys"
version = "0.1.0" version = "0.1.0"
@ -2790,7 +3208,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"system-configuration-sys 0.6.0",
] ]
[[package]] [[package]]
@ -2803,6 +3232,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "system-deps" name = "system-deps"
version = "6.2.2" version = "6.2.2"
@ -2953,6 +3392,16 @@ dependencies = [
"syn 2.0.77", "syn 2.0.77",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.24.1" version = "0.24.1"
@ -2974,6 +3423,17 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.13",
"rustls-pki-types",
"tokio",
]
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.21.0" version = "0.21.0"
@ -3037,6 +3497,43 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 0.1.2",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -3221,6 +3718,15 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.10.0" version = "1.10.0"
@ -3446,6 +3952,36 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -16,6 +16,14 @@ path = "src/bin/discord-bot/main.rs"
name = "shimmering-cli" name = "shimmering-cli"
path = "src/bin/cli/main.rs" path = "src/bin/cli/main.rs"
[[bin]]
name = "shimmering-server"
path = "src/bin/server/main.rs"
[[bin]]
name = "shimmering-discord-presence"
path = "src/bin/discord-presence/main.rs"
[dependencies] [dependencies]
chrono = "0.4.38" chrono = "0.4.38"
freetype-rs = "0.36.0" freetype-rs = "0.36.0"
@ -40,6 +48,10 @@ serde_with = "3.9.0"
anyhow = "1.0.87" anyhow = "1.0.87"
sha2 = "0.10.8" sha2 = "0.10.8"
base16ct = { version = "0.2.0", features = ["alloc"] } base16ct = { version = "0.2.0", features = ["alloc"] }
axum = { version = "0.7.6", features = ["macros"] }
paste = "1.0.15"
discord-rich-presence = "0.2.4"
reqwest = { version = "0.12.7", features = ["json"] }
[profile.dev.package."*"] # [profile.dev.package."*"]
opt-level = 3 # opt-level = 3

View file

@ -41,11 +41,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1726755586, "lastModified": 1726937504,
"narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=", "narHash": "sha256-bvGoiQBvponpZh8ClUcmJ6QnsNKw0EMrCQJARK3bI1c=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e", "rev": "9357f4f23713673f310988025d9dc261c20e70c6",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -41,7 +41,12 @@
}; };
devShell = pkgs.mkShell rec { devShell = pkgs.mkShell rec {
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
toolchain cargo
rustc
clippy
rust-analyzer
rustfmt
ruff ruff
imagemagick imagemagick
pkg-config pkg-config

View file

@ -5,13 +5,14 @@ use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
use anyhow::anyhow; 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};
use serde::{Deserialize, Serialize};
use crate::bitmap::Color; use crate::bitmap::Color;
use crate::context::{DbConnection, Error}; use crate::context::{DbConnection, Error};
// }}} // }}}
// {{{ Difficuly // {{{ Difficuly
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Difficulty { pub enum Difficulty {
PST, PST,
PRS, PRS,
@ -69,7 +70,7 @@ pub const DIFFICULTY_MENU_PIXEL_COLORS: [Color; Difficulty::DIFFICULTIES.len()]
]; ];
// }}} // }}}
// {{{ Level // {{{ Level
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Level { pub enum Level {
Unknown, Unknown,
One, One,
@ -144,7 +145,7 @@ impl FromSql for Level {
} }
// }}} // }}}
// {{{ Side // {{{ Side
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Side { pub enum Side {
Light, Light,
Conflict, Conflict,
@ -178,7 +179,7 @@ impl FromSql for Side {
} }
// }}} // }}}
// {{{ Song // {{{ Song
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Song { pub struct Song {
pub id: u32, pub id: u32,
pub title: String, pub title: String,
@ -199,7 +200,7 @@ pub struct Jacket {
pub bitmap: &'static ImageBuffer<Rgb<u8>, Vec<u8>>, pub bitmap: &'static ImageBuffer<Rgb<u8>, Vec<u8>>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chart { pub struct Chart {
pub id: u32, pub id: u32,
pub song_id: u32, pub song_id: u32,
@ -212,17 +213,9 @@ pub struct Chart {
pub note_count: u32, pub note_count: u32,
pub chart_constant: u32, pub chart_constant: u32,
#[serde(skip)]
pub cached_jacket: Option<Jacket>, pub cached_jacket: Option<Jacket>,
} }
impl Chart {
#[inline]
pub fn jacket_path(&self, data_dir: &Path) -> PathBuf {
data_dir
.join("jackets")
.join(format!("{}-{}.jpg", self.song_id, self.id))
}
}
// }}} // }}}
// {{{ Cached song // {{{ Cached song
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -26,6 +26,7 @@ pub struct ImageVec {
impl ImageVec { impl ImageVec {
// {{{ (Image => vector) encoding // {{{ (Image => vector) encoding
#[allow(clippy::identity_op)]
pub 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;
@ -55,7 +56,6 @@ impl ImageVec {
let r = (r as f64 / count).sqrt(); let r = (r as f64 / count).sqrt();
let g = (g as f64 / count).sqrt(); let g = (g as f64 / count).sqrt();
let b = (b as f64 / count).sqrt(); let b = (b as f64 / count).sqrt();
#[allow(clippy::identity_op)]
colors[i as usize * 3 + 0] = r as f32; colors[i as usize * 3 + 0] = r as f32;
colors[i as usize * 3 + 1] = g as f32; colors[i as usize * 3 + 1] = g as f32;
colors[i as usize * 3 + 2] = b as f32; colors[i as usize * 3 + 2] = b as f32;

View file

@ -12,6 +12,8 @@ use num::Rational32;
use num::Zero; use num::Zero;
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp}; use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp};
use rusqlite::Row; use rusqlite::Row;
use serde::Deserialize;
use serde::Serialize;
use crate::arcaea::chart::{Chart, Song}; use crate::arcaea::chart::{Chart, Song};
use crate::context::ErrorKind; use crate::context::ErrorKind;
@ -140,7 +142,7 @@ impl CreatePlay {
} }
// }}} // }}}
// {{{ Score data // {{{ Score data
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScoreCollection([Score; ScoringSystem::SCORING_SYSTEMS.len()]); pub struct ScoreCollection([Score; ScoringSystem::SCORING_SYSTEMS.len()]);
impl ScoreCollection { impl ScoreCollection {
@ -152,7 +154,7 @@ impl ScoreCollection {
} }
// }}} // }}}
// {{{ Play // {{{ Play
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Play { pub struct Play {
pub id: u32, pub id: u32,
#[allow(unused)] #[allow(unused)]
@ -267,9 +269,7 @@ impl Play {
} else { } else {
Some('P') Some('P')
} }
} else if let Some(distribution) = self.distribution(chart.note_count) } else if let Some((_, _, _, 0)) = self.distribution(chart.note_count) {
&& distribution.3 == 0
{
Some('F') Some('F')
} else { } else {
Some('C') Some('C')
@ -555,3 +555,11 @@ pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> {
Ok(()) Ok(())
} }
// }}} // }}}
// {{{ Play + chart + song triplet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayWithDetails {
pub play: Play,
pub song: Song,
pub chart: Chart,
}
// }}}

View file

@ -2,6 +2,7 @@
use std::fmt::{Display, Write}; use std::fmt::{Display, Write};
use num::{Rational32, Rational64}; use num::{Rational32, Rational64};
use serde::{Deserialize, Serialize};
use crate::context::Error; use crate::context::Error;
@ -71,7 +72,7 @@ impl Display for Grade {
} }
// }}} // }}}
// {{{ Score // {{{ Score
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Score(pub u32); pub struct Score(pub u32);
impl Score { impl Score {

View file

@ -6,12 +6,8 @@ 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>) {
match error { if let Err(e) = poise::builtins::on_error(error).await {
error => { println!("Error while handling error: {}", e)
if let Err(e) = poise::builtins::on_error(error).await {
println!("Error while handling error: {}", e)
}
}
} }
} }
// }}} // }}}
@ -34,7 +30,7 @@ async fn main() {
} else if message.content.starts_with("!") { } else if message.content.starts_with("!") {
Ok(Some(message.content.split_at(1))) Ok(Some(message.content.split_at(1)))
} else if message.guild_id.is_none() { } else if message.guild_id.is_none() {
if message.content.trim().len() == 0 { if message.content.trim().is_empty() {
Ok(Some(("", "score magic"))) Ok(Some(("", "score magic")))
} else { } else {
Ok(Some(("", &message.content[..]))) Ok(Some(("", &message.content[..])))

View file

@ -0,0 +1,73 @@
use std::time::Duration;
use anyhow::anyhow;
// {{{ Imports
use discord_rich_presence::activity::{Activity, Assets};
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use shimmeringmoon::arcaea::chart::Difficulty;
use shimmeringmoon::arcaea::play::PlayWithDetails;
use shimmeringmoon::arcaea::score::ScoringSystem;
use shimmeringmoon::assets::get_var;
use shimmeringmoon::context::Error;
// }}}
#[tokio::main]
async fn main() -> Result<(), Error> {
let server_url = get_var("SHIMMERING_SERVER_URL");
let client_id = get_var("SHIMMERING_DISCORD_ID");
println!("Connecting to discord...");
let mut ipc = DiscordIpcClient::new(&client_id).map_err(|e| anyhow!("{}", e))?;
ipc.connect().map_err(|e| anyhow!("{}", e))?;
println!("Starting presence loop...");
for i in 0.. {
println!("Getting most recent score...");
let res = reqwest::get(format!("{}/plays/latest", server_url)).await;
let res = match res.and_then(|r| r.error_for_status()) {
Ok(v) => v,
Err(e) => {
ipc.clear_activity().map_err(|e| anyhow!("{}", e))?;
println!("{e}");
tokio::time::sleep(Duration::from_secs(10)).await;
continue;
}
};
let triplet = res.json::<PlayWithDetails>().await?;
let jacket_url = format!(
"{}/jackets/by_chart_id/{}.png",
server_url, &triplet.chart.id
);
let jacket_url = "https://static.wikia.nocookie.net/iowiro/images/c/c2/Fracture_Ray.jpg/revision/latest?cb=20230928061927";
println!("Jacket url: {}", jacket_url);
let jacket_text = format!("{}{}", &triplet.song.title, &triplet.song.artist);
let assets = Assets::new()
.large_image(&jacket_url)
.large_text(&jacket_text);
let details = format!(
"{} [{} {}]",
&triplet.song.title,
Difficulty::DIFFICULTY_SHORTHANDS[triplet.chart.difficulty.to_index()],
&triplet.chart.level,
);
let state = format!("{}", &triplet.play.score(ScoringSystem::Standard));
let activity = Activity::new()
.assets(assets)
.details(&details)
.state(&state);
println!("Sending activity");
ipc.set_activity(activity).map_err(|e| anyhow!("{}", e))?;
tokio::time::sleep(Duration::from_secs(30)).await;
}
Ok(())
}

12
src/bin/server/context.rs Normal file
View file

@ -0,0 +1,12 @@
use shimmeringmoon::context::UserContext;
#[derive(Clone, Copy)]
pub struct AppContext {
pub ctx: &'static UserContext,
}
impl AppContext {
pub fn new(ctx: &'static UserContext) -> Self {
Self { ctx }
}
}

34
src/bin/server/error.rs Normal file
View file

@ -0,0 +1,34 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
pub struct AppError {
pub error: anyhow::Error,
pub status_code: StatusCode,
}
impl AppError {
pub fn new(error: anyhow::Error, status_code: StatusCode) -> Self {
Self { error, status_code }
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
self.status_code,
format!("Something went wrong: {}", self.error),
)
.into_response()
}
}
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self::new(err.into(), StatusCode::INTERNAL_SERVER_ERROR)
}
}

33
src/bin/server/main.rs Normal file
View file

@ -0,0 +1,33 @@
use context::AppContext;
use routes::jacket::get_jacket_image;
use routes::recent_plays::get_recent_play;
use shimmeringmoon::assets::get_var;
use shimmeringmoon::context::{Error, UserContext};
mod context;
mod error;
mod routes;
#[tokio::main]
async fn main() -> Result<(), Error> {
let ctx = Box::leak(Box::new(UserContext::new().await?));
let app = axum::Router::new()
.route("/plays/latest", axum::routing::get(get_recent_play))
.route(
"/jackets/by_chart_id/:chart_id",
axum::routing::get(get_jacket_image),
)
.with_state(AppContext::new(ctx));
let port: u32 = get_var("SHIMMERING_SERVER_PORT").parse()?;
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await?;
Ok(())
}

View file

@ -0,0 +1,44 @@
use std::io::Cursor;
use axum::extract::{Path, State};
use axum::http::{header, HeaderName, StatusCode};
use crate::{context::AppContext, error::AppError};
pub async fn get_jacket_image(
State(state): State<AppContext>,
Path(filename): Path<String>,
) -> Result<([(HeaderName, String); 2], Vec<u8>), AppError> {
let chart_id = filename
.strip_suffix(".png")
.unwrap_or(&filename)
.parse::<u32>()
.map_err(|e| AppError::new(e.into(), StatusCode::NOT_FOUND))?;
let (_song, chart) = state
.ctx
.song_cache
.lookup_chart(chart_id)
.map_err(|e| AppError::new(e, StatusCode::NOT_FOUND))?;
let headers = [
(header::CONTENT_TYPE, "image/png".to_owned()),
(
header::HeaderName::from_static("pngrok-skip-browser-warning"),
"-".to_owned(),
),
// (
// header::CONTENT_DISPOSITION,
// format!("attachment; filename=\"chart_{}.jpg\"", chart_id),
// ),
];
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
chart
.cached_jacket
.unwrap()
.bitmap
.write_to(&mut cursor, image::ImageFormat::Png)?;
Ok((headers, buffer))
}

View file

@ -0,0 +1,2 @@
pub mod jacket;
pub mod recent_plays;

View file

@ -0,0 +1,50 @@
// {{{ Imports
use crate::context::AppContext;
use crate::error::AppError;
use anyhow::anyhow;
use axum::{extract::State, http::StatusCode, Json};
use chrono::{TimeDelta, Utc};
use shimmeringmoon::arcaea::play::{Play, PlayWithDetails};
// }}}
pub async fn get_recent_play(
State(state): State<AppContext>,
) -> Result<Json<PlayWithDetails>, AppError> {
let after = Utc::now()
.checked_sub_signed(TimeDelta::minutes(30))
.unwrap()
.naive_utc();
let (play, song, chart) = state
.ctx
.db
.get()?
.prepare_cached(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
AND p.user_id=?
AND p.created_at>=?
ORDER BY p.created_at DESC
LIMIT 1
",
)?
.query_and_then((2, after), |row| -> Result<_, AppError> {
let (song, chart) = state.ctx.song_cache.lookup_chart(row.get("chart_id")?)?;
let play = Play::from_sql(chart, row)?;
Ok((play, song, chart))
})?
.next()
.ok_or_else(|| AppError::new(anyhow!("No recent plays found"), StatusCode::NOT_FOUND))??;
// Perhaps I need to make a Serialize-only version of this type which takes refs?
Ok(axum::response::Json(PlayWithDetails {
play,
song: song.clone(),
chart: chart.clone(),
}))
}

View file

@ -363,13 +363,15 @@ impl BitmapCanvas {
})?; })?;
let face = &mut faces[face_index]; let face = &mut faces[face_index];
if let Some((prev_face_index, prev_glyth_index)) = previous if let Some((prev_face_index, prev_glyth_index)) = previous {
&& prev_face_index == face_index if prev_face_index == face_index && kerning[face_index] {
&& kerning[face_index] let delta = face.get_kerning(
{ prev_glyth_index,
let delta = glyph_index,
face.get_kerning(prev_glyth_index, glyph_index, KerningMode::KerningDefault)?; KerningMode::KerningDefault,
pen_x += delta.x >> 6; // we shift to get rid of sub-pixel accuracy )?;
pen_x += delta.x >> 6; // we shift to get rid of sub-pixel accuracy
}
} }
face.load_glyph(glyph_index, LoadFlag::DEFAULT)?; face.load_glyph(glyph_index, LoadFlag::DEFAULT)?;
@ -579,12 +581,13 @@ impl LayoutManager {
) { ) {
let current = self.boxes[id.0]; let current = self.boxes[id.0];
if let Some((current_points_to, dx, dy)) = current.relative_to match current.relative_to {
&& current_points_to != id_relative_to Some((current_points_to, dx, dy)) if current_points_to != id_relative_to => {
{ self.edit_to_relative(current_points_to, id_relative_to, x - dx, y - dy);
self.edit_to_relative(current_points_to, id_relative_to, x - dx, y - dy); }
} else { _ => {
self.boxes[id.0].relative_to = Some((id_relative_to, x, y)); self.boxes[id.0].relative_to = Some((id_relative_to, x, y));
}
} }
{ {

View file

@ -101,13 +101,13 @@ async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), Tagg
// {{{ Tests // {{{ Tests
#[cfg(test)] #[cfg(test)]
mod info_tests { mod info_tests {
use crate::{commands::discord::mock::MockContext, with_test_ctx}; use crate::{commands::discord::mock::MockContext, golden_test, with_test_ctx};
use super::*; use super::*;
#[tokio::test] #[tokio::test]
async fn no_suffix() -> Result<(), Error> { async fn no_suffix() -> Result<(), Error> {
with_test_ctx!("test/commands/chart/info/no_suffix", async |ctx| { with_test_ctx!("commands/commands/chart/info/no_suffix", |ctx| async move {
info_impl(ctx, "Pentiment").await?; info_impl(ctx, "Pentiment").await?;
Ok(()) Ok(())
}) })
@ -115,23 +115,21 @@ mod info_tests {
#[tokio::test] #[tokio::test]
async fn specify_difficulty() -> Result<(), Error> { 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!( with_test_ctx!(
"test/commands/chart/info/last_byd", "commands/commands/chart/info/specify_difficulty",
async |ctx: &mut MockContext| { |ctx| async move {
info_impl(ctx, "Last | Moment [BYD]").await?; info_impl(ctx, "Hellohell [ETR]").await?;
info_impl(ctx, "Last | Eternity [BYD]").await?;
Ok(()) Ok(())
} }
) )
} }
golden_test!(last_byd, "commands/chart/info/last_byd");
async fn last_byd(ctx: &mut MockContext) -> Result<(), TaggedError> {
info_impl(ctx, "Last | Moment [BYD]").await?;
info_impl(ctx, "Last | Eternity [BYD]").await?;
Ok(())
}
} }
// }}} // }}}
// {{{ Discord wrapper // {{{ Discord wrapper
@ -208,46 +206,41 @@ async fn best_impl<C: MessageContext>(ctx: &mut C, name: &str) -> Result<Play, T
// {{{ Tests // {{{ Tests
#[cfg(test)] #[cfg(test)]
mod best_tests { mod best_tests {
use std::path::PathBuf; use std::{path::PathBuf, str::FromStr};
use crate::{ use crate::{
commands::{discord::mock::MockContext, score::magic_impl}, commands::{discord::mock::MockContext, score::magic_impl},
with_test_ctx, golden_test, with_test_ctx,
}; };
use super::*; use super::*;
#[tokio::test] #[tokio::test]
async fn no_scores() -> Result<(), Error> { async fn no_scores() -> Result<(), Error> {
with_test_ctx!("test/commands/chart/best/no_scores", async |ctx| { with_test_ctx!("commands/chart/best/no_scores", |ctx| async move {
best_impl(ctx, "Pentiment").await?; best_impl(ctx, "Pentiment").await?;
Ok(()) Ok(())
}) })
} }
#[tokio::test] golden_test!(pick_correct_score, "commands/chart/best/pick_correct_score");
async fn pick_correct_score() -> Result<(), Error> { async fn pick_correct_score(ctx: &mut MockContext) -> Result<(), TaggedError> {
with_test_ctx!( let plays = magic_impl(
"test/commands/chart/best/pick_correct_score", ctx,
async |ctx: &mut MockContext| { &[
let plays = magic_impl( PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?,
ctx, // Make sure we aren't considering higher scores from other stuff
&[ PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?, PathBuf::from_str("test/screenshots/fracture_ray_missed_ex.jpg")?,
// Make sure we aren't considering higher scores from other stuff ],
PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
PathBuf::from_str("test/screenshots/fracture_ray_missed_ex.jpg")?,
],
)
.await?;
let play = best_impl(ctx, "Fracture ray").await?;
assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651);
assert_eq!(plays[0], play);
Ok(())
}
) )
.await?;
let play = best_impl(ctx, "Fracture ray").await?;
assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651);
assert_eq!(plays[0], play);
Ok(())
} }
} }
// }}} // }}}

View file

@ -4,7 +4,7 @@ use crate::arcaea::score::Score;
use crate::context::{Context, Error, ErrorKind, TagError, TaggedError}; use crate::context::{Context, Error, ErrorKind, TagError, TaggedError};
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind}; use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
use crate::user::User; use crate::user::User;
use crate::{get_user_error, timed}; use crate::{get_user_error, timed, try_block};
use anyhow::anyhow; use anyhow::anyhow;
use image::DynamicImage; use image::DynamicImage;
use poise::{serenity_prelude as serenity, CreateReply}; use poise::{serenity_prelude as serenity, CreateReply};
@ -48,7 +48,7 @@ pub async fn magic_impl<C: MessageContext>(
let mut grayscale_image = DynamicImage::ImageLuma8(image.to_luma8()); let mut grayscale_image = DynamicImage::ImageLuma8(image.to_luma8());
// }}} // }}}
let result: Result<(), TaggedError> = try { let result: Result<(), TaggedError> = try_block!({
// {{{ Detection // {{{ Detection
let kind = timed!("read_score_kind", { let kind = timed!("read_score_kind", {
@ -113,7 +113,7 @@ pub async fn magic_impl<C: MessageContext>(
embeds.push(embed); embeds.push(embed);
attachments.extend(attachment); attachments.extend(attachment);
// }}} // }}}
}; });
if let Err(err) = result { if let Err(err) = result {
let user_err = get_user_error!(err); let user_err = get_user_error!(err);
@ -140,63 +140,52 @@ pub async fn magic_impl<C: MessageContext>(
#[cfg(test)] #[cfg(test)]
mod magic_tests { mod magic_tests {
use std::path::PathBuf; use std::{path::PathBuf, str::FromStr};
use crate::{ use crate::{
arcaea::score::ScoringSystem, arcaea::score::ScoringSystem,
commands::discord::{mock::MockContext, play_song_title}, commands::discord::{mock::MockContext, play_song_title},
with_test_ctx, golden_test, with_test_ctx,
}; };
use super::*; use super::*;
#[tokio::test] #[tokio::test]
async fn no_pics() -> Result<(), Error> { async fn no_pics() -> Result<(), Error> {
with_test_ctx!("test/commands/score/magic/no_pics", async |ctx| { with_test_ctx!("commands/score/magic/no_pics", |ctx| async move {
magic_impl(ctx, &[]).await?; magic_impl(ctx, &[]).await?;
Ok(()) Ok(())
}) })
} }
#[tokio::test] golden_test!(simple_pic, "score/magic/single_pic");
async fn simple_pic() -> Result<(), Error> { async fn simple_pic(ctx: &mut MockContext) -> Result<(), TaggedError> {
with_test_ctx!( let plays =
"test/commands/score/magic/single_pic", magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?;
async |ctx: &mut MockContext| { assert_eq!(plays.len(), 1);
let plays = assert_eq!(plays[0].score(ScoringSystem::Standard).0, 9926250);
magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]) assert_eq!(play_song_title(ctx, &plays[0])?, "ALTER EGO");
.await?; Ok(())
assert_eq!(plays.len(), 1);
assert_eq!(plays[0].score(ScoringSystem::Standard).0, 9926250);
assert_eq!(play_song_title(ctx, &plays[0])?, "ALTER EGO");
Ok(())
}
)
} }
#[tokio::test] golden_test!(weird_kerning, "score/magic/weird_kerning");
async fn weird_kerning() -> Result<(), Error> { async fn weird_kerning(ctx: &mut MockContext) -> Result<(), TaggedError> {
with_test_ctx!( let plays = magic_impl(
"test/commands/score/magic/weird_kerning", ctx,
async |ctx: &mut MockContext| { &[
let plays = magic_impl( PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
ctx, PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
&[ ],
PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
],
)
.await?;
assert_eq!(plays.len(), 2);
assert_eq!(plays[0].score(ScoringSystem::Standard).0, 9983744);
assert_eq!(play_song_title(ctx, &plays[0])?, "Antithese");
assert_eq!(plays[1].score(ScoringSystem::Standard).0, 9724775);
assert_eq!(play_song_title(ctx, &plays[1])?, "GENOCIDER");
Ok(())
}
) )
.await?;
assert_eq!(plays.len(), 2);
assert_eq!(plays[0].score(ScoringSystem::Standard).0, 9983744);
assert_eq!(play_song_title(ctx, &plays[0])?, "Antithese");
assert_eq!(plays[1].score(ScoringSystem::Standard).0, 9724775);
assert_eq!(play_song_title(ctx, &plays[1])?, "GENOCIDER");
Ok(())
} }
} }
// }}} // }}}
@ -293,12 +282,12 @@ pub async fn show_impl<C: MessageContext>(
#[cfg(test)] #[cfg(test)]
mod show_tests { mod show_tests {
use super::*; use super::*;
use crate::{commands::discord::mock::MockContext, with_test_ctx}; use crate::{commands::discord::mock::MockContext, golden_test, with_test_ctx};
use std::path::PathBuf; use std::{path::PathBuf, str::FromStr};
#[tokio::test] #[tokio::test]
async fn no_ids() -> Result<(), Error> { async fn no_ids() -> Result<(), Error> {
with_test_ctx!("test/commands/score/show/no_ids", async |ctx| { with_test_ctx!("commands/score/show/no_ids", |ctx| async move {
show_impl(ctx, &[]).await?; show_impl(ctx, &[]).await?;
Ok(()) Ok(())
}) })
@ -306,35 +295,30 @@ mod show_tests {
#[tokio::test] #[tokio::test]
async fn nonexistent_id() -> Result<(), Error> { async fn nonexistent_id() -> Result<(), Error> {
with_test_ctx!("test/commands/score/show/nonexistent_id", async |ctx| { with_test_ctx!("commands/score/show/nonexistent_id", |ctx| async move {
show_impl(ctx, &[666]).await?; show_impl(ctx, &[666]).await?;
Ok(()) Ok(())
}) })
} }
#[tokio::test] golden_test!(agrees_with_magic, "commands/score/show/agrees_with_magic");
async fn agrees_with_magic() -> Result<(), Error> { async fn agrees_with_magic(ctx: &mut MockContext) -> Result<(), TaggedError> {
with_test_ctx!( let created_plays = magic_impl(
"test/commands/score/show/agrees_with_magic", ctx,
async |ctx: &mut MockContext| { &[
let created_plays = magic_impl( PathBuf::from_str("test/screenshots/alter_ego.jpg")?,
ctx, PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
&[ PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
PathBuf::from_str("test/screenshots/alter_ego.jpg")?, ],
PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
],
)
.await?;
let ids = created_plays.iter().map(|p| p.id).collect::<Vec<_>>();
let plays = show_impl(ctx, &ids).await?;
assert_eq!(plays.len(), 3);
assert_eq!(created_plays, plays);
Ok(())
}
) )
.await?;
let ids = created_plays.iter().map(|p| p.id).collect::<Vec<_>>();
let plays = show_impl(ctx, &ids).await?;
assert_eq!(plays.len(), 3);
assert_eq!(created_plays, plays);
Ok(())
} }
} }
// }}} // }}}
@ -392,13 +376,13 @@ mod delete_tests {
use super::*; use super::*;
use crate::{ use crate::{
commands::discord::{mock::MockContext, play_song_title}, commands::discord::{mock::MockContext, play_song_title},
with_test_ctx, golden_test, with_test_ctx,
}; };
use std::path::PathBuf; use std::{path::PathBuf, str::FromStr};
#[tokio::test] #[tokio::test]
async fn no_ids() -> Result<(), Error> { async fn no_ids() -> Result<(), Error> {
with_test_ctx!("test/commands/score/delete/no_ids", async |ctx| { with_test_ctx!("commands/score/delete/no_ids", |ctx| async move {
delete_impl(ctx, &[]).await?; delete_impl(ctx, &[]).await?;
Ok(()) Ok(())
}) })
@ -406,74 +390,60 @@ mod delete_tests {
#[tokio::test] #[tokio::test]
async fn nonexistent_id() -> Result<(), Error> { async fn nonexistent_id() -> Result<(), Error> {
with_test_ctx!("test/commands/score/delete/nonexistent_id", async |ctx| { with_test_ctx!("commands/score/delete/nonexistent_id", |ctx| async move {
delete_impl(ctx, &[666]).await?; delete_impl(ctx, &[666]).await?;
Ok(()) Ok(())
}) })
} }
#[tokio::test] golden_test!(delete_twice, "commands/score/delete/delete_twice");
async fn delete_twice() -> Result<(), Error> { async fn delete_twice(ctx: &mut MockContext) -> Result<(), TaggedError> {
with_test_ctx!( let plays =
"test/commands/score/delete/delete_twice", magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?;
async |ctx: &mut MockContext| {
let plays =
magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?])
.await?;
let id = plays[0].id; let id = plays[0].id;
delete_impl(ctx, &[id, id]).await?; delete_impl(ctx, &[id, id]).await?;
Ok(()) Ok(())
}
)
} }
#[tokio::test] golden_test!(
async fn no_show_after_delete() -> Result<(), Error> { no_show_after_delete,
with_test_ctx!( "commands/score/delete/no_show_after_delete"
"test/commands/score/delete/no_show_after_delete", );
async |ctx: &mut MockContext| { async fn no_show_after_delete(ctx: &mut MockContext) -> Result<(), TaggedError> {
let plays = let plays =
magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]) magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?;
.await?;
// Showcase proper usage // Showcase proper usage
let ids = [plays[0].id]; let ids = [plays[0].id];
delete_impl(ctx, &ids).await?; delete_impl(ctx, &ids).await?;
// This will tell the user the play doesn't exist // This will tell the user the play doesn't exist
let shown_plays = show_impl(ctx, &ids).await?; let shown_plays = show_impl(ctx, &ids).await?;
assert_eq!(shown_plays.len(), 0); assert_eq!(shown_plays.len(), 0);
Ok(()) Ok(())
}
)
} }
#[tokio::test] golden_test!(delete_multiple, "commands/score/delete/delete_multiple");
async fn delete_multiple() -> Result<(), Error> { async fn delete_multiple(ctx: &mut MockContext) -> Result<(), TaggedError> {
with_test_ctx!( let plays = magic_impl(
"test/commands/score/delete/delete_multiple", ctx,
async |ctx: &mut MockContext| { &[
let plays = magic_impl( PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
ctx, PathBuf::from_str("test/screenshots/alter_ego.jpg")?,
&[ PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?, ],
PathBuf::from_str("test/screenshots/alter_ego.jpg")?,
PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
],
)
.await?;
delete_impl(ctx, &[plays[0].id, plays[2].id]).await?;
// Ensure the second play still exists
let shown_plays = show_impl(ctx, &[plays[1].id]).await?;
assert_eq!(play_song_title(ctx, &shown_plays[0])?, "ALTER EGO");
Ok(())
}
) )
.await?;
delete_impl(ctx, &[plays[0].id, plays[2].id]).await?;
// Ensure the second play still exists
let shown_plays = show_impl(ctx, &[plays[1].id]).await?;
assert_eq!(play_song_title(ctx, &shown_plays[0])?, "ALTER EGO");
Ok(())
} }
} }
// }}} // }}}

View file

@ -145,7 +145,7 @@ pub mod testing {
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 || { CELL.get_or_init(|| async move {
// env::set_var("SHIMMERING_DATA_DIR", "") // env::set_var("SHIMMERING_DATA_DIR", "")
UserContext::new().await.unwrap() UserContext::new().await.unwrap()
}) })
@ -165,6 +165,20 @@ pub mod testing {
); );
} }
// rustfmt fucks up the formatting here,
// but the skip attribute doesn't seem to work well on macros 🤔
#[macro_export]
macro_rules! golden_test {
($name:ident, $test_path:expr) => {
paste::paste! {
#[tokio::test]
async fn [<$name _test>]() -> Result<(), $crate::context::Error> {
$crate::with_test_ctx!($test_path, $name)
}
}
};
}
#[macro_export] #[macro_export]
macro_rules! with_test_ctx { macro_rules! with_test_ctx {
($test_path:expr, $f:expr) => {{ ($test_path:expr, $f:expr) => {{
@ -179,10 +193,11 @@ pub mod testing {
let res = $crate::user::User::create_from_context(&ctx); let res = $crate::user::User::create_from_context(&ctx);
ctx.handle_error(res).await?; ctx.handle_error(res).await?;
let res: Result<(), $crate::context::TaggedError> = $f(&mut ctx).await; let ctx: &mut $crate::commands::discord::mock::MockContext = &mut ctx;
let res: Result<(), $crate::context::TaggedError> = $f(ctx).await;
ctx.handle_error(res).await?; ctx.handle_error(res).await?;
ctx.golden(&std::path::PathBuf::from_str($test_path)?)?; ctx.golden(&std::path::PathBuf::from_str("test")?.join($test_path))?;
Ok(()) Ok(())
}}; }};
} }

View file

@ -1,16 +1,6 @@
#![allow(async_fn_in_trait)] #![allow(async_fn_in_trait)]
#![allow(clippy::needless_range_loop)] #![allow(clippy::needless_range_loop)]
#![allow(clippy::redundant_closure)] #![allow(clippy::redundant_closure)]
#![feature(iter_map_windows)]
#![feature(anonymous_lifetime_in_impl_trait)]
#![feature(let_chains)]
#![feature(array_try_map)]
#![feature(async_closure)]
#![feature(try_blocks)]
#![feature(thread_local)]
#![feature(generic_arg_infer)]
#![feature(iter_collect_into)]
#![feature(stmt_expr_attributes)]
pub mod arcaea; pub mod arcaea;
pub mod assets; pub mod assets;
@ -23,3 +13,4 @@ pub mod recognition;
pub mod time; pub mod time;
pub mod transform; pub mod transform;
pub mod user; pub mod user;
pub mod utils;

View file

@ -74,9 +74,7 @@ pub fn guess_chart_name<'a>(
let mut close_enough: Vec<_> = cache let mut close_enough: Vec<_> = cache
.charts() .charts()
.filter_map(|chart| { .filter_map(|chart| {
if let Some(difficulty) = difficulty if difficulty.map_or(false, |d| d != chart.difficulty) {
&& chart.difficulty != difficulty
{
return None; return None;
} }
@ -92,22 +90,24 @@ pub fn guess_chart_name<'a>(
// Cut title to the length of the text, and then check // 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) if 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 == 0 {
if slice_distance == 0 { distance_vec.push(3);
distance_vec.push(3); }
} }
} }
// Shorthand-based matching // Shorthand-based matching
if let Some(shorthand) = &chart.shorthand if let Some(shorthand) = &chart.shorthand {
&& unsafe_heuristics if unsafe_heuristics {
{ let short_distance =
let short_distance = edit_distance_with(text, shorthand, &mut levenshtein_vec); edit_distance_with(text, shorthand, &mut levenshtein_vec);
if short_distance <= shorthand.len() / 3 {
distance_vec.push(short_distance * 10 + 1); if short_distance <= shorthand.len() / 3 {
distance_vec.push(short_distance * 10 + 1);
}
} }
} }

View file

@ -70,10 +70,10 @@ impl ComponentVec {
for x in x_start..x_end { for x in x_start..x_end {
for y in y_start..y_end { for y in y_start..y_end {
if let Some(p) = components.components.get_pixel_checked(x, y) if let Some(p) = components.components.get_pixel_checked(x, y) {
&& p.0[0] == component if p.0[0] == component {
{ count += 255 - components.image[(x, y)].0[0] as u32;
count += 255 - components.image[(x, y)].0[0] as u32; }
} }
} }
} }

View file

@ -162,14 +162,14 @@ impl ImageAnalyzer {
); );
// Discard scores if it's impossible // Discard scores if it's impossible
if result.0 <= 10_010_000 let valid_analysis = note_count.map_or(true, |note_count| {
&& note_count.map_or(true, |note_count| { let (zeta, shinies, score_units) = result.analyse(note_count);
let (zeta, shinies, score_units) = result.analyse(note_count); 8_000_000 <= zeta.0
8_000_000 <= zeta.0 && zeta.0 <= 10_000_000
&& zeta.0 <= 10_000_000 && shinies <= note_count
&& shinies <= note_count && score_units <= 2 * note_count
&& score_units <= 2 * note_count });
}) { if result.0 <= 10_010_000 && valid_analysis {
Ok(result) Ok(result)
} else { } else {
Err(anyhow!("Score {result} is not vaild")) Err(anyhow!("Score {result} is not vaild"))

20
src/utils.rs Normal file
View file

@ -0,0 +1,20 @@
/// Performs "Ok-wrapping" on the result of an expression.
/// This is compatible with [`Result`], [`Option`], [`ControlFlow`], and any type that
/// implements the unstable [`std::ops::Try`] trait.
///
/// The destination type must be specified with a type ascription somewhere.
#[macro_export]
macro_rules! wrap_ok {
($e:expr) => {
::core::iter::empty().try_fold($e, |_, __x: ::core::convert::Infallible| match __x {})
};
}
#[macro_export]
macro_rules! try_block {
{ $($token:tt)* } => {
(|| $crate::wrap_ok!({
$($token)*
}))()
}
}