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",
]
[[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"

View file

@ -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

View file

@ -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": {

View file

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

View file

@ -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<Rgb<u8>, Vec<u8>>,
}
#[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<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
#[derive(Debug, Clone)]

View file

@ -26,6 +26,7 @@ pub struct ImageVec {
impl ImageVec {
// {{{ (Image => vector) encoding
#[allow(clippy::identity_op)]
pub fn from_image(image: &impl GenericImageView<Pixel = Rgba<u8>>) -> 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;

View file

@ -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,
}
// }}}

View file

@ -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 {

View file

@ -6,13 +6,9 @@ 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)
}
}
}
}
// }}}
@ -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[..])))

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,14 +363,16 @@ 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)?;
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,13 +581,14 @@ 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
{
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);
} else {
}
_ => {
self.boxes[id.0].relative_to = Some((id_relative_to, x, y));
}
}
{
let a = self.lookup(id);

View file

@ -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,28 +206,25 @@ async fn best_impl<C: MessageContext>(ctx: &mut C, name: &str) -> Result<Play, T
// {{{ Tests
#[cfg(test)]
mod best_tests {
use std::path::PathBuf;
use std::{path::PathBuf, str::FromStr};
use crate::{
commands::{discord::mock::MockContext, score::magic_impl},
with_test_ctx,
golden_test, with_test_ctx,
};
use super::*;
#[tokio::test]
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?;
Ok(())
})
}
#[tokio::test]
async fn pick_correct_score() -> Result<(), Error> {
with_test_ctx!(
"test/commands/chart/best/pick_correct_score",
async |ctx: &mut MockContext| {
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,
&[
@ -247,8 +242,6 @@ mod best_tests {
Ok(())
}
)
}
}
// }}}
// }}}

View file

@ -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<C: MessageContext>(
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<C: MessageContext>(
embeds.push(embed);
attachments.extend(attachment);
// }}}
};
});
if let Err(err) = result {
let user_err = get_user_error!(err);
@ -140,45 +140,36 @@ pub async fn magic_impl<C: MessageContext>(
#[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| {
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?;
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| {
golden_test!(weird_kerning, "score/magic/weird_kerning");
async fn weird_kerning(ctx: &mut MockContext) -> Result<(), TaggedError> {
let plays = magic_impl(
ctx,
&[
@ -196,8 +187,6 @@ mod magic_tests {
Ok(())
}
)
}
}
// }}}
// {{{ Discord wrapper
@ -293,12 +282,12 @@ pub async fn show_impl<C: MessageContext>(
#[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,17 +295,14 @@ 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| {
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,
&[
@ -334,8 +320,6 @@ mod show_tests {
assert_eq!(created_plays, plays);
Ok(())
}
)
}
}
// }}}
// {{{ Discord wrapper
@ -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,36 +390,29 @@ 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| {
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?;
magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?;
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| {
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?;
magic_impl(ctx, &[PathBuf::from_str("test/screenshots/alter_ego.jpg")?]).await?;
// Showcase proper usage
let ids = [plays[0].id];
@ -447,14 +424,9 @@ mod delete_tests {
Ok(())
}
)
}
#[tokio::test]
async fn delete_multiple() -> Result<(), Error> {
with_test_ctx!(
"test/commands/score/delete/delete_multiple",
async |ctx: &mut MockContext| {
golden_test!(delete_multiple, "commands/score/delete/delete_multiple");
async fn delete_multiple(ctx: &mut MockContext) -> Result<(), TaggedError> {
let plays = magic_impl(
ctx,
&[
@ -473,8 +445,6 @@ mod delete_tests {
Ok(())
}
)
}
}
// }}}
// {{{ Discord wrapper

View file

@ -145,7 +145,7 @@ pub mod testing {
pub async fn get_shared_context() -> &'static UserContext {
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", "")
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(())
}};
}

View file

@ -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;

View file

@ -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,24 +90,26 @@ 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)
{
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 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);
}
}
}
distance_vec
.iter()

View file

@ -70,13 +70,13 @@ 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
{
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;
}
}
}
}
let size = (x_end + 1 - x_start) * (y_end + 1 - y_start);

View file

@ -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 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"))

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)*
}))()
}
}