From 68c46fb7cd2b2b06bd1447d8b46d276cb2b3aa8f Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Tue, 24 Sep 2024 22:49:09 +0200 Subject: [PATCH] Implement basic rich presence --- .vscode/settings.json | 3 + Cargo.lock | 568 +++++++++++++++++++++++++- Cargo.toml | 16 +- flake.lock | 6 +- flake.nix | 7 +- src/arcaea/chart.rs | 21 +- src/arcaea/jacket.rs | 2 +- src/arcaea/play.rs | 18 +- src/arcaea/score.rs | 3 +- src/bin/discord-bot/main.rs | 10 +- src/bin/discord-presence/main.rs | 73 ++++ src/bin/server/context.rs | 12 + src/bin/server/error.rs | 34 ++ src/bin/server/main.rs | 33 ++ src/bin/server/routes/jacket.rs | 44 ++ src/bin/server/routes/mod.rs | 2 + src/bin/server/routes/recent_plays.rs | 50 +++ src/bitmap.rs | 29 +- src/commands/chart.rs | 71 ++-- src/commands/score.rs | 220 +++++----- src/context.rs | 21 +- src/lib.rs | 11 +- src/recognition/fuzzy_song_name.rs | 30 +- src/recognition/hyperglass.rs | 8 +- src/recognition/recognize.rs | 16 +- src/utils.rs | 20 + 26 files changed, 1061 insertions(+), 267 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/bin/discord-presence/main.rs create mode 100644 src/bin/server/context.rs create mode 100644 src/bin/server/error.rs create mode 100644 src/bin/server/main.rs create mode 100644 src/bin/server/routes/jacket.rs create mode 100644 src/bin/server/routes/mod.rs create mode 100644 src/bin/server/routes/recent_plays.rs create mode 100644 src/utils.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..480dae9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.allTargets": false +} diff --git a/Cargo.lock b/Cargo.lock index a9880cd..5b0e276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,12 @@ dependencies = [ "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]] name = "autocfg" version = "1.3.0" @@ -211,6 +217,73 @@ dependencies = [ "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]] name = "backtrace" version = "0.3.74" @@ -475,7 +548,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -498,7 +571,7 @@ checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -687,6 +760,18 @@ dependencies = [ "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]] name = "dlib" version = "0.5.2" @@ -859,6 +944,15 @@ dependencies = [ "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]] name = "foreign-types" version = "0.5.0" @@ -866,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -880,6 +974,12 @@ dependencies = [ "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]] name = "foreign-types-shared" version = "0.3.1" @@ -1077,6 +1177,25 @@ dependencies = [ "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]] name = "half" version = "2.4.1" @@ -1162,6 +1281,29 @@ dependencies = [ "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]] name = "httparse" version = "1.9.4" @@ -1184,9 +1326,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1198,6 +1340,27 @@ dependencies = [ "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]] name = "hyper-rustls" version = "0.24.2" @@ -1206,12 +1369,65 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper", + "hyper 0.14.30", "rustls 0.21.12", "tokio", "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]] name = "hypertesseract" version = "0.1.0" @@ -1532,6 +1748,12 @@ dependencies = [ "imgref", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matrixmultiply" version = "0.3.9" @@ -1640,6 +1862,23 @@ dependencies = [ "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]] name = "new_debug_unreachable" version = "1.0.6" @@ -1768,6 +2007,38 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "openssl-sys" version = "0.9.103" @@ -1843,6 +2114,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "pin-project-lite" version = "0.2.14" @@ -2054,7 +2345,7 @@ checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846" dependencies = [ "r2d2", "rusqlite", - "uuid", + "uuid 1.10.0", ] [[package]] @@ -2232,11 +2523,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", - "http-body", - "hyper", - "hyper-rustls", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -2246,12 +2537,12 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", "tokio-util", @@ -2265,6 +2556,49 @@ dependencies = [ "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]] name = "rgb" version = "0.8.50" @@ -2369,6 +2703,19 @@ dependencies = [ "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]] name = "rustls-pemfile" version = "1.0.4" @@ -2378,6 +2725,16 @@ dependencies = [ "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]] name = "rustls-pki-types" version = "1.8.0" @@ -2405,6 +2762,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -2429,6 +2792,15 @@ dependencies = [ "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]] name = "scheduled-thread-pool" version = "0.2.7" @@ -2464,6 +2836,29 @@ dependencies = [ "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]] name = "semver" version = "1.0.23" @@ -2514,6 +2909,16 @@ dependencies = [ "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]] name = "serde_spanned" version = "0.6.7" @@ -2584,7 +2989,7 @@ dependencies = [ "mime_guess", "parking_lot", "percent-encoding", - "reqwest", + "reqwest 0.11.27", "secrecy", "serde", "serde_cow", @@ -2625,20 +3030,24 @@ name = "shimmeringmoon" version = "0.1.0" dependencies = [ "anyhow", + "axum", "base16ct", "chrono", "clap", + "discord-rich-presence", "freetype-rs", "hypertesseract", "image 0.25.2", "imageproc", "include_dir", "num", + "paste", "plotters", "poise", "postcard", "r2d2", "r2d2_sqlite", + "reqwest 0.12.7", "rusqlite", "rusqlite_migration", "serde", @@ -2772,6 +3181,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "sys" version = "0.1.0" @@ -2790,7 +3208,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "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]] @@ -2803,6 +3232,16 @@ dependencies = [ "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]] name = "system-deps" version = "6.2.2" @@ -2953,6 +3392,16 @@ dependencies = [ "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]] name = "tokio-rustls" version = "0.24.1" @@ -2974,6 +3423,17 @@ dependencies = [ "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]] name = "tokio-tungstenite" version = "0.21.0" @@ -3037,6 +3497,43 @@ dependencies = [ "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]] name = "tower-service" version = "0.3.3" @@ -3221,6 +3718,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "uuid" version = "1.10.0" @@ -3446,6 +3952,36 @@ dependencies = [ "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]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 48ea38a..4ab2634 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,14 @@ path = "src/bin/discord-bot/main.rs" name = "shimmering-cli" 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] chrono = "0.4.38" freetype-rs = "0.36.0" @@ -40,6 +48,10 @@ serde_with = "3.9.0" anyhow = "1.0.87" sha2 = "0.10.8" 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."*"] -opt-level = 3 +# [profile.dev.package."*"] +# opt-level = 3 diff --git a/flake.lock b/flake.lock index 210a78e..0d0f552 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1726755586, - "narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=", + "lastModified": 1726937504, + "narHash": "sha256-bvGoiQBvponpZh8ClUcmJ6QnsNKw0EMrCQJARK3bI1c=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e", + "rev": "9357f4f23713673f310988025d9dc261c20e70c6", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 58766ed..7c91921 100644 --- a/flake.nix +++ b/flake.nix @@ -41,7 +41,12 @@ }; devShell = pkgs.mkShell rec { nativeBuildInputs = with pkgs; [ - toolchain + cargo + rustc + clippy + rust-analyzer + rustfmt + ruff imagemagick pkg-config diff --git a/src/arcaea/chart.rs b/src/arcaea/chart.rs index 8b93689..81f86da 100644 --- a/src/arcaea/chart.rs +++ b/src/arcaea/chart.rs @@ -5,13 +5,14 @@ use std::{fmt::Display, num::NonZeroU16, path::PathBuf}; use anyhow::anyhow; use image::{ImageBuffer, Rgb}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef}; +use serde::{Deserialize, Serialize}; use crate::bitmap::Color; use crate::context::{DbConnection, Error}; // }}} // {{{ Difficuly -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum Difficulty { PST, PRS, @@ -69,7 +70,7 @@ pub const DIFFICULTY_MENU_PIXEL_COLORS: [Color; Difficulty::DIFFICULTIES.len()] ]; // }}} // {{{ Level -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum Level { Unknown, One, @@ -144,7 +145,7 @@ impl FromSql for Level { } // }}} // {{{ Side -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Side { Light, Conflict, @@ -178,7 +179,7 @@ impl FromSql for Side { } // }}} // {{{ Song -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Song { pub id: u32, pub title: String, @@ -199,7 +200,7 @@ pub struct Jacket { pub bitmap: &'static ImageBuffer, Vec>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Chart { pub id: u32, pub song_id: u32, @@ -212,17 +213,9 @@ pub struct Chart { pub note_count: u32, pub chart_constant: u32, + #[serde(skip)] pub cached_jacket: Option, } - -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 #[derive(Debug, Clone)] diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs index 7ea511b..1597070 100644 --- a/src/arcaea/jacket.rs +++ b/src/arcaea/jacket.rs @@ -26,6 +26,7 @@ pub struct ImageVec { impl ImageVec { // {{{ (Image => vector) encoding + #[allow(clippy::identity_op)] pub fn from_image(image: &impl GenericImageView>) -> Self { let mut colors = [0.0; IMAGE_VEC_DIM]; let chunk_width = image.width() / SPLIT_FACTOR; @@ -55,7 +56,6 @@ impl ImageVec { let r = (r as f64 / count).sqrt(); let g = (g 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 + 1] = g as f32; colors[i as usize * 3 + 2] = b as f32; diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs index e0459f6..05725f9 100644 --- a/src/arcaea/play.rs +++ b/src/arcaea/play.rs @@ -12,6 +12,8 @@ use num::Rational32; use num::Zero; use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp}; use rusqlite::Row; +use serde::Deserialize; +use serde::Serialize; use crate::arcaea::chart::{Chart, Song}; use crate::context::ErrorKind; @@ -140,7 +142,7 @@ impl CreatePlay { } // }}} // {{{ Score data -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct ScoreCollection([Score; ScoringSystem::SCORING_SYSTEMS.len()]); impl ScoreCollection { @@ -152,7 +154,7 @@ impl ScoreCollection { } // }}} // {{{ Play -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Play { pub id: u32, #[allow(unused)] @@ -267,9 +269,7 @@ impl Play { } else { Some('P') } - } else if let Some(distribution) = self.distribution(chart.note_count) - && distribution.3 == 0 - { + } else if let Some((_, _, _, 0)) = self.distribution(chart.note_count) { Some('F') } else { Some('C') @@ -555,3 +555,11 @@ pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { Ok(()) } // }}} +// {{{ Play + chart + song triplet +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlayWithDetails { + pub play: Play, + pub song: Song, + pub chart: Chart, +} +// }}} diff --git a/src/arcaea/score.rs b/src/arcaea/score.rs index da93b3b..5782906 100644 --- a/src/arcaea/score.rs +++ b/src/arcaea/score.rs @@ -2,6 +2,7 @@ use std::fmt::{Display, Write}; use num::{Rational32, Rational64}; +use serde::{Deserialize, Serialize}; use crate::context::Error; @@ -71,7 +72,7 @@ impl Display for Grade { } // }}} // {{{ Score -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Score(pub u32); impl Score { diff --git a/src/bin/discord-bot/main.rs b/src/bin/discord-bot/main.rs index c1663c5..3090402 100644 --- a/src/bin/discord-bot/main.rs +++ b/src/bin/discord-bot/main.rs @@ -6,12 +6,8 @@ use std::{env::var, sync::Arc, time::Duration}; // {{{ Error handler async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { - match error { - error => { - if let Err(e) = poise::builtins::on_error(error).await { - 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("!") { Ok(Some(message.content.split_at(1))) } else if message.guild_id.is_none() { - if message.content.trim().len() == 0 { + if message.content.trim().is_empty() { Ok(Some(("", "score magic"))) } else { Ok(Some(("", &message.content[..]))) diff --git a/src/bin/discord-presence/main.rs b/src/bin/discord-presence/main.rs new file mode 100644 index 0000000..3be806a --- /dev/null +++ b/src/bin/discord-presence/main.rs @@ -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::().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(()) +} diff --git a/src/bin/server/context.rs b/src/bin/server/context.rs new file mode 100644 index 0000000..e54283a --- /dev/null +++ b/src/bin/server/context.rs @@ -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 } + } +} diff --git a/src/bin/server/error.rs b/src/bin/server/error.rs new file mode 100644 index 0000000..2ddf70b --- /dev/null +++ b/src/bin/server/error.rs @@ -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 From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self::new(err.into(), StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs new file mode 100644 index 0000000..fcd7c0c --- /dev/null +++ b/src/bin/server/main.rs @@ -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(()) +} diff --git a/src/bin/server/routes/jacket.rs b/src/bin/server/routes/jacket.rs new file mode 100644 index 0000000..a6f6d07 --- /dev/null +++ b/src/bin/server/routes/jacket.rs @@ -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, + Path(filename): Path, +) -> Result<([(HeaderName, String); 2], Vec), AppError> { + let chart_id = filename + .strip_suffix(".png") + .unwrap_or(&filename) + .parse::() + .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)) +} diff --git a/src/bin/server/routes/mod.rs b/src/bin/server/routes/mod.rs new file mode 100644 index 0000000..4ce858f --- /dev/null +++ b/src/bin/server/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod jacket; +pub mod recent_plays; diff --git a/src/bin/server/routes/recent_plays.rs b/src/bin/server/routes/recent_plays.rs new file mode 100644 index 0000000..08803d8 --- /dev/null +++ b/src/bin/server/routes/recent_plays.rs @@ -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, +) -> Result, 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(), + })) +} diff --git a/src/bitmap.rs b/src/bitmap.rs index 7f3f25e..639ef94 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -363,13 +363,15 @@ impl BitmapCanvas { })?; let face = &mut faces[face_index]; - if let Some((prev_face_index, prev_glyth_index)) = previous - && prev_face_index == face_index - && kerning[face_index] - { - let delta = - face.get_kerning(prev_glyth_index, glyph_index, KerningMode::KerningDefault)?; - pen_x += delta.x >> 6; // we shift to get rid of sub-pixel accuracy + if let Some((prev_face_index, prev_glyth_index)) = previous { + if prev_face_index == face_index && kerning[face_index] { + let delta = face.get_kerning( + prev_glyth_index, + glyph_index, + KerningMode::KerningDefault, + )?; + pen_x += delta.x >> 6; // we shift to get rid of sub-pixel accuracy + } } face.load_glyph(glyph_index, LoadFlag::DEFAULT)?; @@ -579,12 +581,13 @@ impl LayoutManager { ) { let current = self.boxes[id.0]; - if let Some((current_points_to, dx, dy)) = current.relative_to - && current_points_to != id_relative_to - { - 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)); + match current.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.boxes[id.0].relative_to = Some((id_relative_to, x, y)); + } } { diff --git a/src/commands/chart.rs b/src/commands/chart.rs index 2e1fc03..8db25e3 100644 --- a/src/commands/chart.rs +++ b/src/commands/chart.rs @@ -101,13 +101,13 @@ async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), Tagg // {{{ Tests #[cfg(test)] 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::*; #[tokio::test] 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?; Ok(()) }) @@ -115,23 +115,21 @@ mod info_tests { #[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?; + "commands/commands/chart/info/specify_difficulty", + |ctx| async move { + info_impl(ctx, "Hellohell [ETR]").await?; 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 @@ -208,46 +206,41 @@ async fn best_impl(ctx: &mut C, name: &str) -> Result 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?; Ok(()) }) } - #[tokio::test] - async fn pick_correct_score() -> Result<(), Error> { - with_test_ctx!( - "test/commands/chart/best/pick_correct_score", - async |ctx: &mut MockContext| { - let plays = magic_impl( - ctx, - &[ - PathBuf::from_str("test/screenshots/fracture_ray_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(()) - } + golden_test!(pick_correct_score, "commands/chart/best/pick_correct_score"); + async fn pick_correct_score(ctx: &mut MockContext) -> Result<(), TaggedError> { + let plays = magic_impl( + ctx, + &[ + PathBuf::from_str("test/screenshots/fracture_ray_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(()) } } // }}} diff --git a/src/commands/score.rs b/src/commands/score.rs index 6500307..97e9846 100644 --- a/src/commands/score.rs +++ b/src/commands/score.rs @@ -4,7 +4,7 @@ use crate::arcaea::score::Score; use crate::context::{Context, Error, ErrorKind, TagError, TaggedError}; use crate::recognition::recognize::{ImageAnalyzer, ScoreKind}; use crate::user::User; -use crate::{get_user_error, timed}; +use crate::{get_user_error, timed, try_block}; use anyhow::anyhow; use image::DynamicImage; use poise::{serenity_prelude as serenity, CreateReply}; @@ -48,7 +48,7 @@ pub async fn magic_impl( let mut grayscale_image = DynamicImage::ImageLuma8(image.to_luma8()); // }}} - let result: Result<(), TaggedError> = try { + let result: Result<(), TaggedError> = try_block!({ // {{{ Detection let kind = timed!("read_score_kind", { @@ -113,7 +113,7 @@ pub async fn magic_impl( embeds.push(embed); attachments.extend(attachment); // }}} - }; + }); if let Err(err) = result { let user_err = get_user_error!(err); @@ -140,63 +140,52 @@ pub async fn magic_impl( #[cfg(test)] mod magic_tests { - use std::path::PathBuf; + use std::{path::PathBuf, str::FromStr}; use crate::{ arcaea::score::ScoringSystem, commands::discord::{mock::MockContext, play_song_title}, - with_test_ctx, + golden_test, with_test_ctx, }; use super::*; #[tokio::test] 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?; Ok(()) }) } - #[tokio::test] - async fn simple_pic() -> Result<(), Error> { - with_test_ctx!( - "test/commands/score/magic/single_pic", - async |ctx: &mut MockContext| { - let plays = - magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]) - .await?; - 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(()) - } - ) + golden_test!(simple_pic, "score/magic/single_pic"); + async fn simple_pic(ctx: &mut MockContext) -> Result<(), TaggedError> { + let plays = + magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?; + 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] - async fn weird_kerning() -> Result<(), Error> { - with_test_ctx!( - "test/commands/score/magic/weird_kerning", - async |ctx: &mut MockContext| { - let plays = magic_impl( - ctx, - &[ - 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(()) - } + golden_test!(weird_kerning, "score/magic/weird_kerning"); + async fn weird_kerning(ctx: &mut MockContext) -> Result<(), TaggedError> { + let plays = magic_impl( + ctx, + &[ + 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(()) } } // }}} @@ -293,12 +282,12 @@ pub async fn show_impl( #[cfg(test)] mod show_tests { use super::*; - use crate::{commands::discord::mock::MockContext, with_test_ctx}; - use std::path::PathBuf; + use crate::{commands::discord::mock::MockContext, golden_test, with_test_ctx}; + use std::{path::PathBuf, str::FromStr}; #[tokio::test] 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?; Ok(()) }) @@ -306,35 +295,30 @@ mod show_tests { #[tokio::test] 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?; Ok(()) }) } - #[tokio::test] - async fn agrees_with_magic() -> Result<(), Error> { - with_test_ctx!( - "test/commands/score/show/agrees_with_magic", - async |ctx: &mut MockContext| { - let created_plays = magic_impl( - ctx, - &[ - 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::>(); - let plays = show_impl(ctx, &ids).await?; - - assert_eq!(plays.len(), 3); - assert_eq!(created_plays, plays); - Ok(()) - } + golden_test!(agrees_with_magic, "commands/score/show/agrees_with_magic"); + async fn agrees_with_magic(ctx: &mut MockContext) -> Result<(), TaggedError> { + let created_plays = magic_impl( + ctx, + &[ + 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::>(); + 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 crate::{ 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] 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?; Ok(()) }) @@ -406,74 +390,60 @@ mod delete_tests { #[tokio::test] 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?; Ok(()) }) } - #[tokio::test] - async fn delete_twice() -> Result<(), Error> { - with_test_ctx!( - "test/commands/score/delete/delete_twice", - async |ctx: &mut MockContext| { - let plays = - magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]) - .await?; + golden_test!(delete_twice, "commands/score/delete/delete_twice"); + async fn delete_twice(ctx: &mut MockContext) -> Result<(), TaggedError> { + let plays = + magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?; - let id = plays[0].id; - delete_impl(ctx, &[id, id]).await?; - Ok(()) - } - ) + let id = plays[0].id; + delete_impl(ctx, &[id, id]).await?; + Ok(()) } - #[tokio::test] - async fn no_show_after_delete() -> Result<(), Error> { - with_test_ctx!( - "test/commands/score/delete/no_show_after_delete", - async |ctx: &mut MockContext| { - let plays = - magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]) - .await?; + golden_test!( + no_show_after_delete, + "commands/score/delete/no_show_after_delete" + ); + async fn no_show_after_delete(ctx: &mut MockContext) -> Result<(), TaggedError> { + let plays = + magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?; - // Showcase proper usage - let ids = [plays[0].id]; - delete_impl(ctx, &ids).await?; + // Showcase proper usage + let ids = [plays[0].id]; + delete_impl(ctx, &ids).await?; - // This will tell the user the play doesn't exist - let shown_plays = show_impl(ctx, &ids).await?; - assert_eq!(shown_plays.len(), 0); + // This will tell the user the play doesn't exist + let shown_plays = show_impl(ctx, &ids).await?; + assert_eq!(shown_plays.len(), 0); - Ok(()) - } - ) + Ok(()) } - #[tokio::test] - async fn delete_multiple() -> Result<(), Error> { - with_test_ctx!( - "test/commands/score/delete/delete_multiple", - async |ctx: &mut MockContext| { - let plays = magic_impl( - ctx, - &[ - 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(()) - } + golden_test!(delete_multiple, "commands/score/delete/delete_multiple"); + async fn delete_multiple(ctx: &mut MockContext) -> Result<(), TaggedError> { + let plays = magic_impl( + ctx, + &[ + 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(()) } } // }}} diff --git a/src/context.rs b/src/context.rs index e967332..8f0c86b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -145,7 +145,7 @@ pub mod testing { pub async fn get_shared_context() -> &'static UserContext { static CELL: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); - CELL.get_or_init(async || { + CELL.get_or_init(|| async move { // env::set_var("SHIMMERING_DATA_DIR", "") 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_rules! with_test_ctx { ($test_path:expr, $f:expr) => {{ @@ -179,10 +193,11 @@ pub mod testing { let res = $crate::user::User::create_from_context(&ctx); 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.golden(&std::path::PathBuf::from_str($test_path)?)?; + ctx.golden(&std::path::PathBuf::from_str("test")?.join($test_path))?; Ok(()) }}; } diff --git a/src/lib.rs b/src/lib.rs index bbc7a4e..979f3f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,6 @@ #![allow(async_fn_in_trait)] #![allow(clippy::needless_range_loop)] #![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 assets; @@ -23,3 +13,4 @@ pub mod recognition; pub mod time; pub mod transform; pub mod user; +pub mod utils; diff --git a/src/recognition/fuzzy_song_name.rs b/src/recognition/fuzzy_song_name.rs index 5563c7e..e56efd0 100644 --- a/src/recognition/fuzzy_song_name.rs +++ b/src/recognition/fuzzy_song_name.rs @@ -74,9 +74,7 @@ pub fn guess_chart_name<'a>( let mut close_enough: Vec<_> = cache .charts() .filter_map(|chart| { - if let Some(difficulty) = difficulty - && chart.difficulty != difficulty - { + if difficulty.map_or(false, |d| d != chart.difficulty) { return None; } @@ -92,22 +90,24 @@ pub fn guess_chart_name<'a>( // Cut title to the length of the text, and then check let shortest_len = Ord::min(song_title.len(), text.len()); - if let Some(sliced) = &song_title.get(..shortest_len) - && (text.len() >= 6 || unsafe_heuristics) - { - let slice_distance = edit_distance_with(text, sliced, &mut levenshtein_vec); - if slice_distance == 0 { - distance_vec.push(3); + if let Some(sliced) = &song_title.get(..shortest_len) { + if text.len() >= 6 || unsafe_heuristics { + let slice_distance = edit_distance_with(text, sliced, &mut levenshtein_vec); + if slice_distance == 0 { + distance_vec.push(3); + } } } // Shorthand-based matching - if let Some(shorthand) = &chart.shorthand - && unsafe_heuristics - { - let short_distance = edit_distance_with(text, shorthand, &mut levenshtein_vec); - if short_distance <= shorthand.len() / 3 { - distance_vec.push(short_distance * 10 + 1); + if let Some(shorthand) = &chart.shorthand { + if unsafe_heuristics { + let short_distance = + edit_distance_with(text, shorthand, &mut levenshtein_vec); + + if short_distance <= shorthand.len() / 3 { + distance_vec.push(short_distance * 10 + 1); + } } } diff --git a/src/recognition/hyperglass.rs b/src/recognition/hyperglass.rs index 594aaa0..5c0fa89 100644 --- a/src/recognition/hyperglass.rs +++ b/src/recognition/hyperglass.rs @@ -70,10 +70,10 @@ impl ComponentVec { for x in x_start..x_end { for y in y_start..y_end { - if let Some(p) = components.components.get_pixel_checked(x, y) - && p.0[0] == component - { - count += 255 - components.image[(x, y)].0[0] as u32; + if let Some(p) = components.components.get_pixel_checked(x, y) { + if p.0[0] == component { + count += 255 - components.image[(x, y)].0[0] as u32; + } } } } diff --git a/src/recognition/recognize.rs b/src/recognition/recognize.rs index a8bbcf0..10fb790 100644 --- a/src/recognition/recognize.rs +++ b/src/recognition/recognize.rs @@ -162,14 +162,14 @@ impl ImageAnalyzer { ); // Discard scores if it's impossible - if result.0 <= 10_010_000 - && note_count.map_or(true, |note_count| { - let (zeta, shinies, score_units) = result.analyse(note_count); - 8_000_000 <= zeta.0 - && zeta.0 <= 10_000_000 - && shinies <= note_count - && score_units <= 2 * note_count - }) { + let valid_analysis = note_count.map_or(true, |note_count| { + let (zeta, shinies, score_units) = result.analyse(note_count); + 8_000_000 <= zeta.0 + && zeta.0 <= 10_000_000 + && shinies <= note_count + && score_units <= 2 * note_count + }); + if result.0 <= 10_010_000 && valid_analysis { Ok(result) } else { Err(anyhow!("Score {result} is not vaild")) diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..f5f86ac --- /dev/null +++ b/src/utils.rs @@ -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)* + }))() + } +}