Figured out plotting!
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
		
					parent
					
						
							
								b2e88e703b
							
						
					
				
			
			
				commit
				
					
						49d50bf88b
					
				
			
		
					 12 changed files with 1297 additions and 777 deletions
				
			
		
							
								
								
									
										378
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										378
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -401,6 +401,42 @@ version = "0.8.6" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "core-graphics" | ||||||
|  | version = "0.23.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 1.3.2", | ||||||
|  |  "core-foundation", | ||||||
|  |  "core-graphics-types", | ||||||
|  |  "foreign-types", | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "core-graphics-types" | ||||||
|  | version = "0.1.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 1.3.2", | ||||||
|  |  "core-foundation", | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "core-text" | ||||||
|  | version = "20.1.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" | ||||||
|  | dependencies = [ | ||||||
|  |  "core-foundation", | ||||||
|  |  "core-graphics", | ||||||
|  |  "foreign-types", | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "cpufeatures" | name = "cpufeatures" | ||||||
| version = "0.2.12" | version = "0.2.12" | ||||||
|  | @ -494,24 +530,13 @@ dependencies = [ | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "csv" | name = "cstr" | ||||||
| version = "1.3.0" | version = "0.2.12" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" | checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "csv-core", |  "proc-macro2", | ||||||
|  "itoa", |  "quote", | ||||||
|  "ryu", |  | ||||||
|  "serde", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "csv-core" |  | ||||||
| version = "0.1.11" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" |  | ||||||
| dependencies = [ |  | ||||||
|  "memchr", |  | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -634,12 +659,33 @@ dependencies = [ | ||||||
|  "winapi", |  "winapi", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "dlib" | ||||||
|  | version = "0.5.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" | ||||||
|  | dependencies = [ | ||||||
|  |  "libloading", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "dotenvy" | name = "dotenvy" | ||||||
| version = "0.15.7" | version = "0.15.7" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "dwrote" | ||||||
|  | version = "0.11.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" | ||||||
|  | dependencies = [ | ||||||
|  |  "lazy_static", | ||||||
|  |  "libc", | ||||||
|  |  "winapi", | ||||||
|  |  "wio", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "edit-distance" | name = "edit-distance" | ||||||
| version = "2.1.0" | version = "2.1.0" | ||||||
|  | @ -655,12 +701,6 @@ dependencies = [ | ||||||
|  "serde", |  "serde", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "encode_unicode" |  | ||||||
| version = "1.0.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "encoding_rs" | name = "encoding_rs" | ||||||
| version = "0.8.34" | version = "0.8.34" | ||||||
|  | @ -728,28 +768,6 @@ dependencies = [ | ||||||
|  "zune-inflate", |  "zune-inflate", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "failure" |  | ||||||
| version = "0.1.8" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" |  | ||||||
| dependencies = [ |  | ||||||
|  "backtrace", |  | ||||||
|  "failure_derive", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "failure_derive" |  | ||||||
| version = "0.1.8" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" |  | ||||||
| dependencies = [ |  | ||||||
|  "proc-macro2", |  | ||||||
|  "quote", |  | ||||||
|  "syn 1.0.109", |  | ||||||
|  "synstructure", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "fastrand" | name = "fastrand" | ||||||
| version = "2.1.0" | version = "2.1.0" | ||||||
|  | @ -775,6 +793,12 @@ dependencies = [ | ||||||
|  "miniz_oxide", |  "miniz_oxide", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "float-ord" | ||||||
|  | version = "0.3.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "flume" | name = "flume" | ||||||
| version = "0.11.0" | version = "0.11.0" | ||||||
|  | @ -792,6 +816,58 @@ version = "1.0.7" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "font-kit" | ||||||
|  | version = "0.13.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "2845a73bbd781e691ab7c2a028c579727cd254942e8ced57ff73e0eafd60de87" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 2.5.0", | ||||||
|  |  "byteorder", | ||||||
|  |  "core-foundation", | ||||||
|  |  "core-graphics", | ||||||
|  |  "core-text", | ||||||
|  |  "dirs-next", | ||||||
|  |  "dwrote", | ||||||
|  |  "float-ord", | ||||||
|  |  "freetype-sys", | ||||||
|  |  "lazy_static", | ||||||
|  |  "libc", | ||||||
|  |  "log", | ||||||
|  |  "pathfinder_geometry", | ||||||
|  |  "pathfinder_simd", | ||||||
|  |  "walkdir", | ||||||
|  |  "winapi", | ||||||
|  |  "yeslogic-fontconfig-sys", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "foreign-types" | ||||||
|  | version = "0.5.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" | ||||||
|  | dependencies = [ | ||||||
|  |  "foreign-types-macros", | ||||||
|  |  "foreign-types-shared", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "foreign-types-macros" | ||||||
|  | version = "0.2.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "syn 2.0.66", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "foreign-types-shared" | ||||||
|  | version = "0.3.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "form_urlencoded" | name = "form_urlencoded" | ||||||
| version = "1.2.1" | version = "1.2.1" | ||||||
|  | @ -801,6 +877,17 @@ dependencies = [ | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "freetype-sys" | ||||||
|  | version = "0.20.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" | ||||||
|  | dependencies = [ | ||||||
|  |  "cc", | ||||||
|  |  "libc", | ||||||
|  |  "pkg-config", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "futures" | name = "futures" | ||||||
| version = "0.3.30" | version = "0.3.30" | ||||||
|  | @ -930,6 +1017,16 @@ dependencies = [ | ||||||
|  "wasi", |  "wasi", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "gif" | ||||||
|  | version = "0.11.4" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" | ||||||
|  | dependencies = [ | ||||||
|  |  "color_quant", | ||||||
|  |  "weezl", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "gif" | name = "gif" | ||||||
| version = "0.13.1" | version = "0.13.1" | ||||||
|  | @ -1176,6 +1273,20 @@ dependencies = [ | ||||||
|  "unicode-normalization", |  "unicode-normalization", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "image" | ||||||
|  | version = "0.24.9" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" | ||||||
|  | dependencies = [ | ||||||
|  |  "bytemuck", | ||||||
|  |  "byteorder", | ||||||
|  |  "color_quant", | ||||||
|  |  "jpeg-decoder", | ||||||
|  |  "num-traits", | ||||||
|  |  "png", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "image" | name = "image" | ||||||
| version = "0.25.1" | version = "0.25.1" | ||||||
|  | @ -1186,7 +1297,7 @@ dependencies = [ | ||||||
|  "byteorder", |  "byteorder", | ||||||
|  "color_quant", |  "color_quant", | ||||||
|  "exr", |  "exr", | ||||||
|  "gif", |  "gif 0.13.1", | ||||||
|  "image-webp", |  "image-webp", | ||||||
|  "num-traits", |  "num-traits", | ||||||
|  "png", |  "png", | ||||||
|  | @ -1242,17 +1353,6 @@ version = "2.9.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "is-terminal" |  | ||||||
| version = "0.4.12" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" |  | ||||||
| dependencies = [ |  | ||||||
|  "hermit-abi", |  | ||||||
|  "libc", |  | ||||||
|  "windows-sys 0.52.0", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "itertools" | name = "itertools" | ||||||
| version = "0.12.1" | version = "0.12.1" | ||||||
|  | @ -1709,6 +1809,25 @@ version = "1.0.15" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pathfinder_geometry" | ||||||
|  | version = "0.5.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" | ||||||
|  | dependencies = [ | ||||||
|  |  "log", | ||||||
|  |  "pathfinder_simd", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pathfinder_simd" | ||||||
|  | version = "0.5.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "ebf45976c56919841273f2a0fc684c28437e2f304e264557d9c72be5d5a718be" | ||||||
|  | dependencies = [ | ||||||
|  |  "rustc_version", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "peeking_take_while" | name = "peeking_take_while" | ||||||
| version = "0.1.2" | version = "0.1.2" | ||||||
|  | @ -1770,13 +1889,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "plotlib" | name = "plotters" | ||||||
| version = "0.5.1" | version = "0.4.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c" | ||||||
| checksum = "9462104f987d8d0f6625f0c7764f1c8b890bd1dc8584d8293e031f25c5a0d242" |  | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "failure", |  "chrono", | ||||||
|  "svg", |  "font-kit", | ||||||
|  |  "image 0.24.9", | ||||||
|  |  "lazy_static", | ||||||
|  |  "num-traits", | ||||||
|  |  "pathfinder_geometry", | ||||||
|  |  "plotters-backend", | ||||||
|  |  "plotters-bitmap", | ||||||
|  |  "plotters-svg", | ||||||
|  |  "ttf-parser", | ||||||
|  |  "wasm-bindgen", | ||||||
|  |  "web-sys", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "plotters-backend" | ||||||
|  | version = "0.4.0" | ||||||
|  | source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "plotters-bitmap" | ||||||
|  | version = "0.4.0" | ||||||
|  | source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c" | ||||||
|  | dependencies = [ | ||||||
|  |  "gif 0.11.4", | ||||||
|  |  "image 0.24.9", | ||||||
|  |  "plotters-backend", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "plotters-svg" | ||||||
|  | version = "0.4.0" | ||||||
|  | source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c" | ||||||
|  | dependencies = [ | ||||||
|  |  "plotters-backend", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -1833,20 +1984,6 @@ version = "0.2.17" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "prettytable-rs" |  | ||||||
| version = "0.10.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" |  | ||||||
| dependencies = [ |  | ||||||
|  "csv", |  | ||||||
|  "encode_unicode", |  | ||||||
|  "is-terminal", |  | ||||||
|  "lazy_static", |  | ||||||
|  "term", |  | ||||||
|  "unicode-width", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "proc-macro2" | name = "proc-macro2" | ||||||
| version = "1.0.85" | version = "1.0.85" | ||||||
|  | @ -2168,6 +2305,15 @@ version = "1.1.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "rustc_version" | ||||||
|  | version = "0.4.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" | ||||||
|  | dependencies = [ | ||||||
|  |  "semver", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "rustix" | name = "rustix" | ||||||
| version = "0.38.34" | version = "0.38.34" | ||||||
|  | @ -2243,12 +2389,6 @@ dependencies = [ | ||||||
|  "untrusted", |  "untrusted", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "rustversion" |  | ||||||
| version = "1.0.17" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "ryu" | name = "ryu" | ||||||
| version = "1.0.18" | version = "1.0.18" | ||||||
|  | @ -2420,14 +2560,12 @@ name = "shimmeringmoon" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "chrono", |  "chrono", | ||||||
|  "csv", |  | ||||||
|  "edit-distance", |  "edit-distance", | ||||||
|  "image", |  "image 0.25.1", | ||||||
|  "kd-tree", |  "kd-tree", | ||||||
|  "num", |  "num", | ||||||
|  "plotlib", |  "plotters", | ||||||
|  "poise", |  "poise", | ||||||
|  "prettytable-rs", |  | ||||||
|  "sqlx", |  "sqlx", | ||||||
|  "tesseract", |  "tesseract", | ||||||
|  "tokio", |  "tokio", | ||||||
|  | @ -2761,12 +2899,6 @@ version = "2.5.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "svg" |  | ||||||
| version = "0.7.2" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "3685c82a045a6af0c488f0550b0f52b4c77d2a52b0ca8aba719f9d268fa96965" |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "syn" | name = "syn" | ||||||
| version = "1.0.109" | version = "1.0.109" | ||||||
|  | @ -2795,18 +2927,6 @@ version = "0.1.2" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "synstructure" |  | ||||||
| version = "0.12.6" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" |  | ||||||
| dependencies = [ |  | ||||||
|  "proc-macro2", |  | ||||||
|  "quote", |  | ||||||
|  "syn 1.0.109", |  | ||||||
|  "unicode-xid", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "system-configuration" | name = "system-configuration" | ||||||
| version = "0.5.1" | version = "0.5.1" | ||||||
|  | @ -2865,17 +2985,6 @@ dependencies = [ | ||||||
|  "windows-sys 0.52.0", |  "windows-sys 0.52.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "term" |  | ||||||
| version = "0.7.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" |  | ||||||
| dependencies = [ |  | ||||||
|  "dirs-next", |  | ||||||
|  "rustversion", |  | ||||||
|  "winapi", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "tesseract" | name = "tesseract" | ||||||
| version = "0.15.1" | version = "0.15.1" | ||||||
|  | @ -3160,6 +3269,12 @@ version = "0.2.5" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "ttf-parser" | ||||||
|  | version = "0.15.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "tungstenite" | name = "tungstenite" | ||||||
| version = "0.21.0" | version = "0.21.0" | ||||||
|  | @ -3264,18 +3379,6 @@ version = "1.11.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "unicode-width" |  | ||||||
| version = "0.1.13" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "unicode-xid" |  | ||||||
| version = "0.2.4" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "unicode_categories" | name = "unicode_categories" | ||||||
| version = "0.1.1" | version = "0.1.1" | ||||||
|  | @ -3702,6 +3805,27 @@ dependencies = [ | ||||||
|  "windows-sys 0.48.0", |  "windows-sys 0.48.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "wio" | ||||||
|  | version = "0.2.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" | ||||||
|  | dependencies = [ | ||||||
|  |  "winapi", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "yeslogic-fontconfig-sys" | ||||||
|  | version = "5.0.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" | ||||||
|  | dependencies = [ | ||||||
|  |  "cstr", | ||||||
|  |  "dlib", | ||||||
|  |  "once_cell", | ||||||
|  |  "pkg-config", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "zerocopy" | name = "zerocopy" | ||||||
| version = "0.7.34" | version = "0.7.34" | ||||||
|  |  | ||||||
|  | @ -5,14 +5,12 @@ edition = "2021" | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| chrono = "0.4.38" | chrono = "0.4.38" | ||||||
| csv = "1.3.0" |  | ||||||
| edit-distance = "2.1.0" | edit-distance = "2.1.0" | ||||||
| image = "0.25.1" | image = "0.25.1" | ||||||
| kd-tree = "0.6.0" | kd-tree = "0.6.0" | ||||||
| num = "0.4.3" | num = "0.4.3" | ||||||
| plotlib = "0.5.1" | plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c" } | ||||||
| poise = "0.6.1" | poise = "0.6.1" | ||||||
| prettytable-rs = "0.10.0" |  | ||||||
| sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] } | sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] } | ||||||
| tesseract = "0.15.1" | tesseract = "0.15.1" | ||||||
| tokio = {version="1.38.0", features=["rt-multi-thread"]} | tokio = {version="1.38.0", features=["rt-multi-thread"]} | ||||||
|  |  | ||||||
|  | @ -28,6 +28,8 @@ | ||||||
|               rust-analyzer-nightly |               rust-analyzer-nightly | ||||||
|               ruff |               ruff | ||||||
|               imagemagick |               imagemagick | ||||||
|  |               fontconfig | ||||||
|  |               freetype | ||||||
| 
 | 
 | ||||||
|               clang |               clang | ||||||
|               llvmPackages.clang |               llvmPackages.clang | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS plays ( | ||||||
|     creation_zeta_ptt INTEGER, |     creation_zeta_ptt INTEGER, | ||||||
| 
 | 
 | ||||||
|     score INTEGER NOT NULL, |     score INTEGER NOT NULL, | ||||||
|     zeta_score INTEGER, |     zeta_score INTEGER NOT NULL, | ||||||
| 
 | 
 | ||||||
|     max_recall INTEGER, |     max_recall INTEGER, | ||||||
|     far_notes INTEGER, |     far_notes INTEGER, | ||||||
|  |  | ||||||
							
								
								
									
										42
									
								
								src/chart.rs
									
										
									
									
									
								
							
							
						
						
									
										42
									
								
								src/chart.rs
									
										
									
									
									
								
							|  | @ -5,7 +5,7 @@ use sqlx::{prelude::FromRow, SqlitePool}; | ||||||
| use crate::context::Error; | use crate::context::Error; | ||||||
| 
 | 
 | ||||||
| // {{{ Difficuly
 | // {{{ Difficuly
 | ||||||
| #[derive(Debug, Clone, Copy, sqlx::Type)] | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)] | ||||||
| pub enum Difficulty { | pub enum Difficulty { | ||||||
| 	PST, | 	PST, | ||||||
| 	PRS, | 	PRS, | ||||||
|  | @ -85,17 +85,31 @@ impl CachedSong { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	#[inline] | 	#[inline] | ||||||
| 	pub fn lookup(&self, difficulty: Difficulty) -> Option<&Chart> { | 	pub fn lookup(&self, difficulty: Difficulty) -> Result<&Chart, Error> { | ||||||
| 		self.charts | 		self.charts | ||||||
| 			.get(difficulty.to_index()) | 			.get(difficulty.to_index()) | ||||||
| 			.and_then(|c| c.as_ref()) | 			.and_then(|c| c.as_ref()) | ||||||
|  | 			.ok_or_else(|| { | ||||||
|  | 				format!( | ||||||
|  | 					"Could not find difficulty {:?} for song {}", | ||||||
|  | 					difficulty, self.song.title | ||||||
|  | 				) | ||||||
|  | 				.into() | ||||||
|  | 			}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	#[inline] | 	#[inline] | ||||||
| 	pub fn lookup_mut(&mut self, difficulty: Difficulty) -> Option<&mut Chart> { | 	pub fn lookup_mut(&mut self, difficulty: Difficulty) -> Result<&mut Chart, Error> { | ||||||
| 		self.charts | 		self.charts | ||||||
| 			.get_mut(difficulty.to_index()) | 			.get_mut(difficulty.to_index()) | ||||||
| 			.and_then(|c| c.as_mut()) | 			.and_then(|c| c.as_mut()) | ||||||
|  | 			.ok_or_else(|| { | ||||||
|  | 				format!( | ||||||
|  | 					"Could not find difficulty {:?} for song {}", | ||||||
|  | 					difficulty, self.song.title | ||||||
|  | 				) | ||||||
|  | 				.into() | ||||||
|  | 			}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	#[inline] | 	#[inline] | ||||||
|  | @ -117,8 +131,26 @@ impl SongCache { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	#[inline] | 	#[inline] | ||||||
| 	pub fn lookup_mut(&mut self, id: u32) -> Option<&mut CachedSong> { | 	pub fn lookup_chart(&self, chart_id: u32) -> Result<(&Song, &Chart), Error> { | ||||||
| 		self.songs.get_mut(id as usize).and_then(|i| i.as_mut()) | 		self.songs() | ||||||
|  | 			.find_map(|item| { | ||||||
|  | 				item.charts().find_map(|chart| { | ||||||
|  | 					if chart.id == chart_id { | ||||||
|  | 						Some((&item.song, chart)) | ||||||
|  | 					} else { | ||||||
|  | 						None | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 			.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	#[inline] | ||||||
|  | 	pub fn lookup_mut(&mut self, id: u32) -> Result<&mut CachedSong, Error> { | ||||||
|  | 		self.songs | ||||||
|  | 			.get_mut(id as usize) | ||||||
|  | 			.and_then(|i| i.as_mut()) | ||||||
|  | 			.ok_or_else(|| format!("Could not find song with id {}", id).into()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	#[inline] | 	#[inline] | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								src/commands/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/commands/mod.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | pub mod score; | ||||||
|  | pub mod stats; | ||||||
|  | @ -1,8 +1,10 @@ | ||||||
| use std::fmt::Display; | use std::fmt::Display; | ||||||
| 
 | 
 | ||||||
| use crate::context::{Context, Error}; | use crate::context::{Context, Error}; | ||||||
| use crate::score::{jacket_rects, CreatePlay, ImageCropper, ImageDimensions, RelativeRect, Score}; | use crate::score::{ | ||||||
| use crate::user::User; | 	jacket_rects, CreatePlay, ImageCropper, ImageDimensions, Play, RelativeRect, Score, | ||||||
|  | }; | ||||||
|  | use crate::user::{discord_it_to_discord_user, User}; | ||||||
| use image::imageops::FilterType; | use image::imageops::FilterType; | ||||||
| use image::ImageFormat; | use image::ImageFormat; | ||||||
| use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; | use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; | ||||||
|  | @ -36,7 +38,7 @@ pub async fn help( | ||||||
| #[poise::command(
 | #[poise::command(
 | ||||||
| 	prefix_command, | 	prefix_command, | ||||||
| 	slash_command, | 	slash_command, | ||||||
| 	subcommands("magic", "delete"), | 	subcommands("magic", "delete", "show"), | ||||||
| 	subcommand_required | 	subcommand_required | ||||||
| )] | )] | ||||||
| pub async fn score(_ctx: Context<'_>) -> Result<(), Error> { | pub async fn score(_ctx: Context<'_>) -> Result<(), Error> { | ||||||
|  | @ -88,8 +90,8 @@ pub async fn magic( | ||||||
| 	if files.len() == 0 { | 	if files.len() == 0 { | ||||||
| 		ctx.reply("No images found attached to message").await?; | 		ctx.reply("No images found attached to message").await?; | ||||||
| 	} else { | 	} else { | ||||||
| 		let mut embeds: Vec<CreateEmbed> = vec![]; | 		let mut embeds = Vec::with_capacity(files.len()); | ||||||
| 		let mut attachments: Vec<CreateAttachment> = vec![]; | 		let mut attachments = Vec::with_capacity(files.len()); | ||||||
| 		let handle = ctx | 		let handle = ctx | ||||||
| 			.reply(format!("Processed 0/{} scores", files.len())) | 			.reply(format!("Processed 0/{} scores", files.len())) | ||||||
| 			.await?; | 			.await?; | ||||||
|  | @ -116,7 +118,7 @@ pub async fn magic( | ||||||
| 					.content(format!("Image {}: reading jacket", i + 1)); | 					.content(format!("Image {}: reading jacket", i + 1)); | ||||||
| 				handle.edit(ctx, edited).await?; | 				handle.edit(ctx, edited).await?; | ||||||
| 
 | 
 | ||||||
| 				let song_by_jacket = cropper.read_jacket(ctx.data(), &image); | 				let song_by_jacket = cropper.read_jacket(ctx.data(), &image).await; | ||||||
| 
 | 
 | ||||||
| 				// This makes OCR more likely to work
 | 				// This makes OCR more likely to work
 | ||||||
| 				let mut ocr_image = image.grayscale().blur(1.); | 				let mut ocr_image = image.grayscale().blur(1.); | ||||||
|  | @ -151,8 +153,10 @@ pub async fn magic( | ||||||
| 					.content(format!("Image {}: reading title", i + 1)); | 					.content(format!("Image {}: reading title", i + 1)); | ||||||
| 				handle.edit(ctx, edited).await?; | 				handle.edit(ctx, edited).await?; | ||||||
| 
 | 
 | ||||||
| 				let song_by_name = cropper.read_song(&ocr_image, &ctx.data().song_cache); | 				let song_by_name = cropper | ||||||
| 				let cached_song = match (song_by_jacket, song_by_name) { | 					.read_song(&ocr_image, &ctx.data().song_cache, difficulty) | ||||||
|  | 					.await; | ||||||
|  | 				let (song, chart) = match (song_by_jacket, song_by_name) { | ||||||
| 					// {{{ Both errors
 | 					// {{{ Both errors
 | ||||||
| 					(Err(err_jacket), Err(err_name)) => { | 					(Err(err_jacket), Err(err_name)) => { | ||||||
| 						error_with_image( | 						error_with_image( | ||||||
|  | @ -196,16 +200,8 @@ Title error: {} | ||||||
| 						.ok_or_else(|| "Could not find jacket area in picture")? | 						.ok_or_else(|| "Could not find jacket area in picture")? | ||||||
| 						.to_absolute(); | 						.to_absolute(); | ||||||
| 						// }}}
 | 						// }}}
 | ||||||
| 						// {{{ Find chart
 |  | ||||||
| 						let chart = by_name.lookup(difficulty).ok_or_else(|| { |  | ||||||
| 							format!( |  | ||||||
| 								"Cannot find difficulty {:?} for chart {:?}", |  | ||||||
| 								difficulty, by_name.song.title |  | ||||||
| 							) |  | ||||||
| 						})?; |  | ||||||
| 						// }}}
 |  | ||||||
| 						// {{{ Build path
 | 						// {{{ Build path
 | ||||||
| 						let filename = format!("{}-{}", by_name.song.id, chart.id); | 						let filename = format!("{}-{}", by_name.0.id, by_name.1.id); | ||||||
| 						let jacket = format!("user/{}", filename); | 						let jacket = format!("user/{}", filename); | ||||||
| 
 | 
 | ||||||
| 						let jacket_dir = ctx.data().data_dir.join("jackets/user"); | 						let jacket_dir = ctx.data().data_dir.join("jackets/user"); | ||||||
|  | @ -221,42 +217,27 @@ Title error: {} | ||||||
| 						sqlx::query!( | 						sqlx::query!( | ||||||
| 							"UPDATE charts SET jacket=? WHERE song_id=? AND difficulty=?", | 							"UPDATE charts SET jacket=? WHERE song_id=? AND difficulty=?", | ||||||
| 							jacket, | 							jacket, | ||||||
| 							chart.song_id, | 							by_name.1.song_id, | ||||||
| 							chart.difficulty, | 							by_name.1.difficulty, | ||||||
| 						) | 						) | ||||||
| 						.execute(&ctx.data().db) | 						.execute(&ctx.data().db) | ||||||
| 						.await?; | 						.await?; | ||||||
| 						// }}}
 | 						// }}}
 | ||||||
| 						// {{{ Aquire and use song cache lock
 | 						// {{{ Aquire and use song cache lock
 | ||||||
| 						{ | 						{ | ||||||
| 							let mut song_cache = ctx | 							let mut song_cache = ctx.data().song_cache.lock().await; | ||||||
| 								.data() |  | ||||||
| 								.song_cache |  | ||||||
| 								.lock() |  | ||||||
| 								.map_err(|_| "Poisoned song cache")?; |  | ||||||
| 
 | 
 | ||||||
| 							let chart = song_cache | 							let chart = song_cache | ||||||
| 								.lookup_mut(by_name.song.id) | 								.lookup_mut(by_name.0.id)? | ||||||
| 								.ok_or_else(|| { | 								.lookup_mut(difficulty)?; | ||||||
| 									format!("Could not find song for id {}", by_name.song.id) |  | ||||||
| 								})? |  | ||||||
| 								.lookup_mut(difficulty) |  | ||||||
| 								.ok_or_else(|| { |  | ||||||
| 									format!( |  | ||||||
| 										"Could not find difficulty {:?} for song {}", |  | ||||||
| 										difficulty, by_name.song.title |  | ||||||
| 									) |  | ||||||
| 								})?; |  | ||||||
| 
 | 
 | ||||||
| 							if chart.jacket.is_none() { | 							if chart.jacket.is_none() { | ||||||
| 								if let Some(chart) = by_name.lookup_mut(difficulty) { | 								by_name.1.jacket = Some(jacket_path.clone()); | ||||||
| 									chart.jacket = Some(jacket_path.clone()); |  | ||||||
| 								}; |  | ||||||
| 								chart.jacket = Some(jacket_path); | 								chart.jacket = Some(jacket_path); | ||||||
| 							} else { | 							} else { | ||||||
| 								println!( | 								println!( | ||||||
| 									"Jacket not detected for chart {} [{:?}]", | 									"Jacket not detected for chart {} [{:?}]", | ||||||
| 									by_name.song.id, difficulty | 									by_name.0.id, difficulty | ||||||
| 								) | 								) | ||||||
| 							}; | 							}; | ||||||
| 						} | 						} | ||||||
|  | @ -267,10 +248,10 @@ Title error: {} | ||||||
| 					// }}}
 | 					// }}}
 | ||||||
| 					// {{{ Both succeeded
 | 					// {{{ Both succeeded
 | ||||||
| 					(Ok(by_jacket), Ok(by_name)) => { | 					(Ok(by_jacket), Ok(by_name)) => { | ||||||
| 						if by_name.song.id != by_jacket.song.id { | 						if by_name.0.id != by_jacket.0.id { | ||||||
| 							println!( | 							println!( | ||||||
| 								"Got diverging choices between '{:?}' and '{:?}'", | 								"Got diverging choices between '{:?}' and '{:?}'", | ||||||
| 								by_jacket.song.id, by_name.song.id | 								by_jacket.0.id, by_name.0.id | ||||||
| 							); | 							); | ||||||
| 						}; | 						}; | ||||||
| 
 | 
 | ||||||
|  | @ -278,16 +259,6 @@ Title error: {} | ||||||
| 					} // }}}
 | 					} // }}}
 | ||||||
| 				}; | 				}; | ||||||
| 
 | 
 | ||||||
| 				// {{{ Build chart
 |  | ||||||
| 				let song = &cached_song.song; |  | ||||||
| 				let chart = cached_song.lookup(difficulty).ok_or_else(|| { |  | ||||||
| 					format!( |  | ||||||
| 						"Could not find difficulty {:?} for song {}", |  | ||||||
| 						difficulty, song.title |  | ||||||
| 					) |  | ||||||
| 				})?; |  | ||||||
| 				// }}}
 |  | ||||||
| 
 |  | ||||||
| 				let edited = CreateReply::default() | 				let edited = CreateReply::default() | ||||||
| 					.reply(true) | 					.reply(true) | ||||||
| 					.content(format!("Image {}: reading score", i + 1)); | 					.content(format!("Image {}: reading score", i + 1)); | ||||||
|  | @ -315,7 +286,7 @@ Title error: {} | ||||||
| 				// {{{ Build play
 | 				// {{{ Build play
 | ||||||
| 				let (score, maybe_fars, score_warning) = | 				let (score, maybe_fars, score_warning) = | ||||||
| 					Score::resolve_ambiguities(score_possibilities, None, chart.note_count)?; | 					Score::resolve_ambiguities(score_possibilities, None, chart.note_count)?; | ||||||
| 				let play = CreatePlay::new(score, chart, &user) | 				let play = CreatePlay::new(score, &chart, &user) | ||||||
| 					.with_attachment(file) | 					.with_attachment(file) | ||||||
| 					.with_fars(maybe_fars) | 					.with_fars(maybe_fars) | ||||||
| 					.save(&ctx.data()) | 					.save(&ctx.data()) | ||||||
|  | @ -323,15 +294,13 @@ Title error: {} | ||||||
| 				// }}}
 | 				// }}}
 | ||||||
| 				// }}}
 | 				// }}}
 | ||||||
| 				// {{{ Deliver embed
 | 				// {{{ Deliver embed
 | ||||||
| 				let (mut embed, attachment) = play.to_embed(&song, &chart, i).await?; | 				let (mut embed, attachment) = play.to_embed(&song, &chart, i, None).await?; | ||||||
| 				if let Some(warning) = score_warning { | 				if let Some(warning) = score_warning { | ||||||
| 					embed = embed.description(warning); | 					embed = embed.description(warning); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				embeds.push(embed); | 				embeds.push(embed); | ||||||
| 				if let Some(attachment) = attachment { | 				attachments.extend(attachment); | ||||||
| 					attachments.push(attachment); |  | ||||||
| 				} |  | ||||||
| 			// }}}
 | 			// }}}
 | ||||||
| 			} else { | 			} else { | ||||||
| 				ctx.reply("One of the attached files is not an image!") | 				ctx.reply("One of the attached files is not an image!") | ||||||
|  | @ -350,10 +319,8 @@ Title error: {} | ||||||
| 
 | 
 | ||||||
| 		handle.delete(ctx).await?; | 		handle.delete(ctx).await?; | ||||||
| 
 | 
 | ||||||
| 		let msg = CreateMessage::new().embeds(embeds); |  | ||||||
| 
 |  | ||||||
| 		ctx.channel_id() | 		ctx.channel_id() | ||||||
| 			.send_files(ctx.http(), attachments, msg) | 			.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds)) | ||||||
| 			.await?; | 			.await?; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -403,3 +370,66 @@ pub async fn delete( | ||||||
| 	Ok(()) | 	Ok(()) | ||||||
| } | } | ||||||
| // }}}
 | // }}}
 | ||||||
|  | // {{{ Score show
 | ||||||
|  | /// Show scores given their ides
 | ||||||
|  | #[poise::command(prefix_command, slash_command)] | ||||||
|  | pub async fn show( | ||||||
|  | 	ctx: Context<'_>, | ||||||
|  | 	#[description = "Ids of score to show"] ids: Vec<u32>, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  | 	if ids.len() == 0 { | ||||||
|  | 		ctx.reply("Empty ID list provided").await?; | ||||||
|  | 		return Ok(()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	let lock = ctx.data().song_cache.lock().await; | ||||||
|  | 
 | ||||||
|  | 	let mut embeds = Vec::with_capacity(ids.len()); | ||||||
|  | 	let mut attachments = Vec::with_capacity(ids.len()); | ||||||
|  | 	for (i, id) in ids.iter().enumerate() { | ||||||
|  | 		let res = query!( | ||||||
|  | 			" | ||||||
|  |                 SELECT 
 | ||||||
|  |                     p.id,p.chart_id,p.user_id,p.score,p.zeta_score, | ||||||
|  |                     p.max_recall,p.created_at,p.far_notes, | ||||||
|  |                     u.discord_id | ||||||
|  |                 FROM plays p 
 | ||||||
|  |                 JOIN users u ON p.user_id = u.id | ||||||
|  |                 WHERE p.id=? | ||||||
|  |             ",
 | ||||||
|  | 			id | ||||||
|  | 		) | ||||||
|  | 		.fetch_one(&ctx.data().db) | ||||||
|  | 		.await | ||||||
|  | 		.map_err(|_| format!("Could not find play with id {}", id))?; | ||||||
|  | 
 | ||||||
|  | 		let play = Play { | ||||||
|  | 			id: res.id as u32, | ||||||
|  | 			chart_id: res.chart_id as u32, | ||||||
|  | 			user_id: res.user_id as u32, | ||||||
|  | 			score: Score(res.score as u32), | ||||||
|  | 			zeta_score: Score(res.zeta_score as u32), | ||||||
|  | 			max_recall: res.max_recall.map(|r| r as u32), | ||||||
|  | 			far_notes: res.far_notes.map(|r| r as u32), | ||||||
|  | 			created_at: res.created_at, | ||||||
|  | 			discord_attachment_id: None, | ||||||
|  | 			creation_ptt: None, | ||||||
|  | 			creation_zeta_ptt: None, | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		let user = discord_it_to_discord_user(&ctx, &res.discord_id).await?; | ||||||
|  | 
 | ||||||
|  | 		let (song, chart) = lock.lookup_chart(play.chart_id)?; | ||||||
|  | 		let (embed, attachment) = play.to_embed(song, chart, i, Some(&user)).await?; | ||||||
|  | 
 | ||||||
|  | 		embeds.push(embed); | ||||||
|  | 		attachments.extend(attachment); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx.channel_id() | ||||||
|  | 		.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds)) | ||||||
|  | 		.await?; | ||||||
|  | 
 | ||||||
|  | 	Ok(()) | ||||||
|  | } | ||||||
|  | // }}}
 | ||||||
							
								
								
									
										245
									
								
								src/commands/stats.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								src/commands/stats.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,245 @@ | ||||||
|  | use std::io::Cursor; | ||||||
|  | 
 | ||||||
|  | use chrono::{DateTime, NaiveDateTime}; | ||||||
|  | use image::{ImageBuffer, Rgb}; | ||||||
|  | use plotters::{ | ||||||
|  | 	backend::{BitMapBackend, PixelFormat, RGBPixel}, | ||||||
|  | 	chart::{ChartBuilder, LabelAreaPosition}, | ||||||
|  | 	drawing::IntoDrawingArea, | ||||||
|  | 	element::Circle, | ||||||
|  | 	series::LineSeries, | ||||||
|  | 	style::{ | ||||||
|  | 		text_anchor::{HPos, Pos, VPos}, | ||||||
|  | 		Color, FontTransform, IntoFont, TextStyle, BLUE, WHITE, | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
|  | use poise::{ | ||||||
|  | 	serenity_prelude::{CreateAttachment, CreateMessage}, | ||||||
|  | 	CreateReply, | ||||||
|  | }; | ||||||
|  | use sqlx::query_as; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  | 	chart::Difficulty, | ||||||
|  | 	context::{Context, Error}, | ||||||
|  | 	score::{guess_chart_name, DbPlay, Score}, | ||||||
|  | 	user::{discord_it_to_discord_user, User}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // {{{ Stats
 | ||||||
|  | /// Stats display
 | ||||||
|  | #[poise::command(
 | ||||||
|  | 	prefix_command, | ||||||
|  | 	slash_command, | ||||||
|  | 	subcommands("chart"), | ||||||
|  | 	subcommand_required | ||||||
|  | )] | ||||||
|  | pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> { | ||||||
|  | 	Ok(()) | ||||||
|  | } | ||||||
|  | // }}}
 | ||||||
|  | // {{{ Chart
 | ||||||
|  | /// Chart-related stats
 | ||||||
|  | #[poise::command(
 | ||||||
|  | 	prefix_command, | ||||||
|  | 	slash_command, | ||||||
|  | 	subcommands("best", "plot"), | ||||||
|  | 	subcommand_required | ||||||
|  | )] | ||||||
|  | pub async fn chart(_ctx: Context<'_>) -> Result<(), Error> { | ||||||
|  | 	Ok(()) | ||||||
|  | } | ||||||
|  | // }}}
 | ||||||
|  | // {{{ Best score
 | ||||||
|  | /// Show the best score on a given chart
 | ||||||
|  | #[poise::command(prefix_command, slash_command)] | ||||||
|  | pub async fn best( | ||||||
|  | 	ctx: Context<'_>, | ||||||
|  | 	#[rest] | ||||||
|  | 	#[description = "Name of chart to show (difficulty at the end)"] | ||||||
|  | 	name: String, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  | 	let user = match User::from_context(&ctx).await { | ||||||
|  | 		Ok(user) => user, | ||||||
|  | 		Err(_) => { | ||||||
|  | 			ctx.say("You are not an user in my database, sorry!") | ||||||
|  | 				.await?; | ||||||
|  | 			return Ok(()); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	let name = name.trim(); | ||||||
|  | 	let (name, difficulty) = name | ||||||
|  | 		.strip_suffix("PST") | ||||||
|  | 		.zip(Some(Difficulty::PST)) | ||||||
|  | 		.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS))) | ||||||
|  | 		.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR))) | ||||||
|  | 		.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR))) | ||||||
|  | 		.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD))) | ||||||
|  | 		.unwrap_or((&name, Difficulty::FTR)); | ||||||
|  | 
 | ||||||
|  | 	let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, difficulty).await?; | ||||||
|  | 
 | ||||||
|  | 	let play = query_as!( | ||||||
|  | 		DbPlay, | ||||||
|  | 		" | ||||||
|  |             SELECT * FROM plays | ||||||
|  |             WHERE user_id=? | ||||||
|  |             AND chart_id=? | ||||||
|  |             ORDER BY score DESC | ||||||
|  |         ",
 | ||||||
|  | 		user.id, | ||||||
|  | 		chart.id | ||||||
|  | 	) | ||||||
|  | 	.fetch_one(&ctx.data().db) | ||||||
|  | 	.await | ||||||
|  | 	.map_err(|_| format!("Could not find any scores for chart"))? | ||||||
|  | 	.to_play(); | ||||||
|  | 
 | ||||||
|  | 	let (embed, attachment) = play | ||||||
|  | 		.to_embed( | ||||||
|  | 			&song, | ||||||
|  | 			&chart, | ||||||
|  | 			0, | ||||||
|  | 			Some(&discord_it_to_discord_user(&ctx, &user.discord_id).await?), | ||||||
|  | 		) | ||||||
|  | 		.await?; | ||||||
|  | 
 | ||||||
|  | 	ctx.channel_id() | ||||||
|  | 		.send_files(ctx.http(), attachment, CreateMessage::new().embed(embed)) | ||||||
|  | 		.await?; | ||||||
|  | 
 | ||||||
|  | 	Ok(()) | ||||||
|  | } | ||||||
|  | // }}}
 | ||||||
|  | //  Score plot
 | ||||||
|  | /// Show the best score on a given chart
 | ||||||
|  | #[poise::command(prefix_command, slash_command)] | ||||||
|  | pub async fn plot( | ||||||
|  | 	ctx: Context<'_>, | ||||||
|  | 	#[rest] | ||||||
|  | 	#[description = "Name of chart to show (difficulty at the end)"] | ||||||
|  | 	name: String, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  | 	let user = match User::from_context(&ctx).await { | ||||||
|  | 		Ok(user) => user, | ||||||
|  | 		Err(_) => { | ||||||
|  | 			ctx.say("You are not an user in my database, sorry!") | ||||||
|  | 				.await?; | ||||||
|  | 			return Ok(()); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	let name = name.trim(); | ||||||
|  | 	let (name, difficulty) = name | ||||||
|  | 		.strip_suffix("PST") | ||||||
|  | 		.zip(Some(Difficulty::PST)) | ||||||
|  | 		.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS))) | ||||||
|  | 		.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR))) | ||||||
|  | 		.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR))) | ||||||
|  | 		.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD))) | ||||||
|  | 		.unwrap_or((&name, Difficulty::FTR)); | ||||||
|  | 
 | ||||||
|  | 	let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, difficulty).await?; | ||||||
|  | 
 | ||||||
|  | 	let plays = query_as!( | ||||||
|  | 		DbPlay, | ||||||
|  | 		" | ||||||
|  |             SELECT * FROM plays | ||||||
|  |             WHERE user_id=? | ||||||
|  |             AND chart_id=? | ||||||
|  |             ORDER BY created_at ASC | ||||||
|  |         ",
 | ||||||
|  | 		user.id, | ||||||
|  | 		chart.id | ||||||
|  | 	) | ||||||
|  | 	.fetch_all(&ctx.data().db) | ||||||
|  | 	.await?; | ||||||
|  | 
 | ||||||
|  | 	if plays.len() == 0 { | ||||||
|  | 		ctx.reply("No plays found").await?; | ||||||
|  | 		return Ok(()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	let min_time = plays.iter().map(|p| p.created_at).min().unwrap(); | ||||||
|  | 	let max_time = plays.iter().map(|p| p.created_at).max().unwrap(); | ||||||
|  | 	let mut min_score = plays.iter().map(|p| p.score).min().unwrap(); | ||||||
|  | 
 | ||||||
|  | 	if min_score > 9_900_000 { | ||||||
|  | 		min_score = 9_800_000; | ||||||
|  | 	} else if min_score > 9_800_000 { | ||||||
|  | 		min_score = 9_800_000; | ||||||
|  | 	} else if min_score > 9_500_000 { | ||||||
|  | 		min_score = 9_500_000; | ||||||
|  | 	} else { | ||||||
|  | 		min_score = 9_000_000 | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	let max_score = 10_010_000; | ||||||
|  | 	let width = 1024; | ||||||
|  | 	let height = 768; | ||||||
|  | 
 | ||||||
|  | 	let mut buffer = vec![u8::MAX; RGBPixel::PIXEL_SIZE * (width * height) as usize]; | ||||||
|  | 
 | ||||||
|  | 	{ | ||||||
|  | 		let mut root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area(); | ||||||
|  | 
 | ||||||
|  | 		let mut chart = ChartBuilder::on(&root) | ||||||
|  | 			.margin(25) | ||||||
|  | 			.caption( | ||||||
|  | 				format!("{} [{:?}]", song.title, chart.difficulty), | ||||||
|  | 				("sans-serif", 40), | ||||||
|  | 			) | ||||||
|  | 			.set_label_area_size(LabelAreaPosition::Left, 100) | ||||||
|  | 			.set_label_area_size(LabelAreaPosition::Bottom, 40) | ||||||
|  | 			.build_cartesian_2d( | ||||||
|  | 				min_time.and_utc().timestamp_millis()..max_time.and_utc().timestamp_millis(), | ||||||
|  | 				min_score..max_score, | ||||||
|  | 			)?; | ||||||
|  | 
 | ||||||
|  | 		chart | ||||||
|  | 			.configure_mesh() | ||||||
|  | 			.light_line_style(WHITE) | ||||||
|  | 			.y_label_formatter(&|s| format!("{}", Score(*s as u32))) | ||||||
|  | 			.y_desc("Score") | ||||||
|  | 			.x_label_formatter(&|d| { | ||||||
|  | 				format!( | ||||||
|  | 					"{}", | ||||||
|  | 					DateTime::from_timestamp_millis(*d).unwrap().date_naive() | ||||||
|  | 				) | ||||||
|  | 			}) | ||||||
|  | 			.y_label_style(TextStyle::from(("sans-serif", 20).into_font())) | ||||||
|  | 			.x_label_style(TextStyle::from(("sans-serif", 20).into_font())) | ||||||
|  | 			.draw()?; | ||||||
|  | 
 | ||||||
|  | 		let mut points: Vec<_> = plays | ||||||
|  | 			.iter() | ||||||
|  | 			.map(|play| (play.created_at.and_utc().timestamp_millis(), play.score)) | ||||||
|  | 			.collect(); | ||||||
|  | 
 | ||||||
|  | 		points.sort(); | ||||||
|  | 		points.dedup(); | ||||||
|  | 
 | ||||||
|  | 		chart.draw_series(LineSeries::new(points.iter().map(|(t, s)| (*t, *s)), &BLUE))?; | ||||||
|  | 
 | ||||||
|  | 		chart.draw_series( | ||||||
|  | 			points | ||||||
|  | 				.iter() | ||||||
|  | 				.map(|(t, s)| Circle::new((*t, *s), 3, BLUE.filled())), | ||||||
|  | 		)?; | ||||||
|  | 
 | ||||||
|  | 		root.present()?; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	let image: ImageBuffer<Rgb<u8>, _> = ImageBuffer::from_raw(width, height, buffer).unwrap(); | ||||||
|  | 
 | ||||||
|  | 	let mut buffer = Vec::new(); | ||||||
|  | 	let mut cursor = Cursor::new(&mut buffer); | ||||||
|  | 	image.write_to(&mut cursor, image::ImageFormat::Png)?; | ||||||
|  | 
 | ||||||
|  | 	let reply = CreateReply::default().attachment(CreateAttachment::bytes(buffer, "plot.png")); | ||||||
|  | 	ctx.send(reply).await?; | ||||||
|  | 
 | ||||||
|  | 	Ok(()) | ||||||
|  | } | ||||||
|  | //
 | ||||||
|  | @ -1,9 +1,7 @@ | ||||||
| use std::{ | use std::{path::PathBuf, sync::Arc}; | ||||||
| 	path::PathBuf, |  | ||||||
| 	sync::{Arc, Mutex}, |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| use sqlx::SqlitePool; | use sqlx::SqlitePool; | ||||||
|  | use tokio::sync::Mutex; | ||||||
| 
 | 
 | ||||||
| use crate::{chart::SongCache, jacket::JacketCache}; | use crate::{chart::SongCache, jacket::JacketCache}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| #![warn(clippy::str_to_string)] | #![warn(clippy::str_to_string)] | ||||||
| #![feature(iter_map_windows)] | #![feature(iter_map_windows)] | ||||||
| #![feature(let_chains)] | #![feature(let_chains)] | ||||||
|  | #![feature(async_closure)] | ||||||
| 
 | 
 | ||||||
| mod chart; | mod chart; | ||||||
| mod commands; | mod commands; | ||||||
|  | @ -9,7 +10,6 @@ mod jacket; | ||||||
| mod score; | mod score; | ||||||
| mod user; | mod user; | ||||||
| 
 | 
 | ||||||
| use chart::Difficulty; |  | ||||||
| use context::{Error, UserContext}; | use context::{Error, UserContext}; | ||||||
| use poise::serenity_prelude::{self as serenity}; | use poise::serenity_prelude::{self as serenity}; | ||||||
| use sqlx::sqlite::SqlitePoolOptions; | use sqlx::sqlite::SqlitePoolOptions; | ||||||
|  | @ -38,7 +38,11 @@ async fn main() { | ||||||
| 
 | 
 | ||||||
| 	// {{{ Poise options
 | 	// {{{ Poise options
 | ||||||
| 	let options = poise::FrameworkOptions { | 	let options = poise::FrameworkOptions { | ||||||
| 		commands: vec![commands::help(), commands::score()], | 		commands: vec![ | ||||||
|  | 			commands::score::help(), | ||||||
|  | 			commands::score::score(), | ||||||
|  | 			commands::stats::stats(), | ||||||
|  | 		], | ||||||
| 		prefix_options: poise::PrefixFrameworkOptions { | 		prefix_options: poise::PrefixFrameworkOptions { | ||||||
| 			stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { | 			stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { | ||||||
| 				Box::pin(async { | 				Box::pin(async { | ||||||
|  |  | ||||||
							
								
								
									
										1222
									
								
								src/score.rs
									
										
									
									
									
								
							
							
						
						
									
										1222
									
								
								src/score.rs
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										15
									
								
								src/user.rs
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								src/user.rs
									
										
									
									
									
								
							|  | @ -1,3 +1,7 @@ | ||||||
|  | use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  | use poise::serenity_prelude::UserId; | ||||||
|  | 
 | ||||||
| use crate::context::{Context, Error}; | use crate::context::{Context, Error}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
|  | @ -21,3 +25,14 @@ impl User { | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[inline] | ||||||
|  | pub async fn discord_it_to_discord_user( | ||||||
|  | 	&ctx: &Context<'_>, | ||||||
|  | 	discord_id: &str, | ||||||
|  | ) -> Result<poise::serenity_prelude::User, Error> { | ||||||
|  | 	UserId::from_str(discord_id)? | ||||||
|  | 		.to_user(ctx.http()) | ||||||
|  | 		.await | ||||||
|  | 		.map_err(|e| e.into()) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue