From 3dc320d524e4b9cce3dd496646dcd49fd2d88618 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Thu, 18 Jul 2024 20:17:39 +0200 Subject: [PATCH] So much progress on b30 Signed-off-by: prescientmoon --- Cargo.lock | 12 + Cargo.toml | 5 +- data/assets/count_background.png | Bin 0 -> 4920 bytes data/assets/diff-byd.png | Bin 0 -> 6037 bytes data/assets/diff-etr.png | Bin 0 -> 9122 bytes data/assets/diff-ftr.png | Bin 0 -> 5465 bytes data/assets/diff-prs.png | Bin 0 -> 5897 bytes data/assets/diff-pst.png | Bin 0 -> 5618 bytes src/bitmap.rs | 411 +++++++++++++++++++++++++++++++ src/chart.rs | 15 +- src/commands/chart.rs | 4 +- src/commands/stats.rs | 231 ++++++++++++++++- src/jacket.rs | 74 ++++-- src/main.rs | 2 + src/score.rs | 30 ++- 15 files changed, 745 insertions(+), 39 deletions(-) create mode 100644 data/assets/count_background.png create mode 100644 data/assets/diff-byd.png create mode 100644 data/assets/diff-etr.png create mode 100644 data/assets/diff-ftr.png create mode 100644 data/assets/diff-prs.png create mode 100644 data/assets/diff-pst.png create mode 100644 src/bitmap.rs diff --git a/Cargo.lock b/Cargo.lock index 2972394..169b446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -904,6 +904,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freetype-rs" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17" +dependencies = [ + "bitflags 2.5.0", + "freetype-sys", + "libc", +] + [[package]] name = "freetype-sys" version = "0.20.1" @@ -2672,6 +2683,7 @@ version = "0.1.0" dependencies = [ "chrono", "edit-distance", + "freetype-rs", "image 0.25.1", "kd-tree", "num", diff --git a/Cargo.toml b/Cargo.toml index c5e8f46..5b23459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,11 @@ edition = "2021" [dependencies] chrono = "0.4.38" edit-distance = "2.1.0" +freetype-rs = "0.36.0" image = "0.25.1" kd-tree = { version="0.6.0", features=["serde"] } num = "0.4.3" -plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c" } +plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] } poise = "0.6.1" postcard = { version="1.0.8", features=["use-std"] } serde = "1.0.204" @@ -19,5 +20,5 @@ tesseract = "0.15.1" tokio = {version="1.38.0", features=["rt-multi-thread"]} typenum = "1.17.0" -[profile.dev.package.sqlx-macros] +[profile.dev.package."*"] opt-level = 3 diff --git a/data/assets/count_background.png b/data/assets/count_background.png new file mode 100644 index 0000000000000000000000000000000000000000..aed324db4faeb80c74d62c10b699deb2de9f3c7a GIT binary patch literal 4920 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 P00000NkvXXu0mjfCO$1r literal 0 HcmV?d00001 diff --git a/data/assets/diff-prs.png b/data/assets/diff-prs.png new file mode 100644 index 0000000000000000000000000000000000000000..0265965a18fc1ffd18c63e41680d8695168e102d GIT binary patch literal 5897 zcmV+k7xw6hP)2Olu4~DnNoD#d>`C^1bBz5&!Xje+FlC6Sy=kN%wixZ^|yd?hI}| zobAoW<=LZ_0<-`ST7w`^DqJN2^v4E{t))Z$yoW5?3NUOz`=~2oPPbnL*XFWRQHujNwuuVDthWFC#GLy|6pl-q~IH(NBK% zkB=X(zfK07-Ar_sG>EHs-6CDjuk&z;KQ!hDTplx~Qv;?CL9#s<>@K|W^8LROm0o*z z@5$9LXnc8TdFggmz;dWl5TLXMN$-*J`;~nBt(KYhr+@jjXAR7v27Nh)WC?-U8V-i5 z4_z7H9Rb^#ztE-n2m%FBO7z=Upf^Pl6Wwbt!)l$27(x~&c;S#Nh^@Gnw|=X(piJ6U_bPLo<`=`$^>*@1}>m}F~jH0p1y@7{=`_S$?mQOzLGQ5ZrL27sV|z<*K- zEkU%0!|}U>P`_?=y7wyWEqIVM1?UVwRJ9V*X8_L3z)-uUZgVw*?xIGGNr%JL%||<* zNy6s!xlUWP!q5N{1Puhn2V##Dv;;I70lisgYkT-X1n6!v?!HuNf6+GpsggBF(K-Wg zW*1*PULuVlV8&_MM__KW!|3{aH&*Q^(qUlP;^C6g`a>b?@_nDqYh9^!IvT%Idg516 zJ3$7?1^<{q;z>tCK0|+do?h-VTZPb`^n-8}?8#i0mdss^CW=n_2jD|-5 zNCX3yCQtx`lm;kIB4~Wy_o$1FKcJzyizW7Qg$%+Hi>4T)Y5gt=a-A_fVhm{v1f9(^ zO&2#FZQV$k?Q8Q%teR1%L!ZjXZe9pscZ5fY%ibl~#q&x73bYQxP~pji;a2)jc*z&% z7Z$LK=@sl^g1Z>-P1m&O%$Xx=?K%Na3j)NI3Hlg~j2Vx{%gC6|w1epSa(_W5?T#k4 zG(3;MF)ZO5fmA1>1x*7;F0lr#vaC8gJHz*b&@VLOxtB}rISneNM4Ev*D{$(OfV&c& zpi(kjX*^1oHy`i(YaBG!NMkzfwr)ic0b@ni!zE}3PHUhB6rbb!68~vM4voUCPa6}y# z=VOR56bVM@aAp1B){Ra;U8$=`Ga~Lv*61NXg3E@E{P{HlL?Dz^g+~PzIRc26U|^-x z2p&a^_X>#&io35`fb!|6p`Il;^{(X7HE9g_m~Dy#PabT2rV}>TdP$;MQA;<&5cR^g zYtL?u;ovsM&(k#~^_L6$l)9KSHuS7^LqJp1#%W%ppLs&t1%EP+99@ z;wiwHJw%emjL_gcdANBa4qI!zE&+pGrqN)7=N*0PqEf6YmS4!>89#$EgSxV_jX0sATf^i?kmki$8e3Dp$*D#=1=PSIr z;JUWWZCp%$uXNsd9qLpTB)&+<<4Z}@ecvPeTBFhayFmhp^gUN%EYj*}G>E`?no{b! zQUV0VhREGd&$Z*V1=N^k5b6Mdu>wrxWSKDahV@U$qO9xf+?1EZjfY`C?_pi^5xYDK zu5=9Y>E)f_VDteX26Yl_^FA_Y(h{J_C>B#)Ow0(*=^=8Qj`|yqwr_OXoweQ^Io~D$ zLmdg1*$n5UQ$C9ZQ+CsTSt|n=w|$9#DNwj9D0cb$w}c6+&_~C$yz2CFzf0km+K(=?681)da(;db+UczedJTh+NP?yg9Or`Dg4(*Q~}MW4K+@ z0w0$G^9|>DP94W=Y8oKgz4VN(up{QuQi{EewTMMgn$ilRkY0n2slKeJE3H5PmY?}T zBGUFsO3@o`kNyz>{^Hz1?}bUJmz#QcSs<(&HPmTQgP8N0LL`>b4vrm`af0_*oZwxb zi#sZAq4RAvA!s1h%Kjaqd8tZU3aX+_x{cc~S<6!%>k>Ur*YXntD4jQg9yAcZMqq%X zo6pujvar*2LfiZFAbu8Jv?tWX>USG)$K6-T~5rHHap)-l$Eeu6JySy`2K4rij2k(Ftl00|G3jw zZE+4*Yv*KX^JbeR=?IkzmxC)4(8oE-!{uI2pwPpBS~Wig3kU-<<&BO23c!euraEsc zy!D4$qkk4sJ~zKGe>*ER4ehTWKze#6&5M~c#TbE%sfEa$@o>1f`DFJqZ8O@8t@9-o zc%%a!vB3b~O2~oYm{Sx5ef3mE1XBHZyQDC$Z?I`p+Fnbb&~*(4#!!rPSTGpV{ZH~3 zL;x*)95t*DYVYn2--lS-ZFNo$tByHxdQfG635YSH)%C|a^z+HJUJ_%7Y-!pAZC1ER z9WHmH=QLBFnMB^i?u*6Pl^$JJB5J=%au8bn~zm%zKU(L-n(CX_zW7SJB4 zy(_OlFEE>xT~TSR`a4es|Ee`S*I(-2$_jlh)I-N0Rr~2#`L&r0WswdZ&k-=f_?UF@ z$)l}*Nl;@J(Z{qcV>s$)5VhKOEFmyx(Z&?B#Ib^IHAe9Q(3PSm9H>rrEQmrlVBF5_ z=3Qc+wz}&0e5EXNj4Ab``8+tDt%|Z-pr``?&o3@5-p&fKgaEA~KobP$XeLc%MfK1S zpxYW?5e?oY8Fhz)ox#feS0CNzV2E6rpI4ZywHZ*6Sjt*15(JR-Az7Y5o)>hVH*Q-l zX_M#16%G_9z=Ez}W=u1{&2rqA;Ov0Y8=>o+>uB6s*-*>+W!YFzC!{Zd#~t{$JDc0P z7*^p{JlA`*RI(>=2NLwYiyE%1JPiEzHfSZj$KGv)zck z1p?^kD^pfveJM~Inh1jREiN)>-bh_|L4Y-Q2#(KRFcZJ3EVe*}6EpFSDF>Bhes-mO8%O^Gp$wsm~Hh-*N1ir%SQDGr36?qyU z*dhjwZ%*WGMpG483sH>5V!oT)dh1(X|D|59mzIZvID*k50I{z@s5&_GAlD$r5$tbF zCUB|U?(Mzf9dG{buYPqW9PjUc0ONtz@O34H%|RZyTH1j0sMT@@bIkFM9V{(7_GFu4 z(lc!qm$Dwl^NzpprFCTo@06#wS-260uu5h12N#x= z9PJ&@={s+J^XtAkKi^BM!@)vC!~ih_5V;$zPQ?eItx9L2tjeV@>P~vS?hl8<(fNb@ zgG&gEz|CkktD(8D`3))vNdVdNa7Xh9M0=fJ+fVm_4IBaFvq(!GZTS3pa?fU441mvV z-eV!AMPLxPB=pE?T%pN#QP42MF<*GJ8@k5W3KU()vRtHlV~M4A`z>#I{d2vA-mabu z`XP)r0K@|@;J`j%?tBh7=I|^|(#H+qz{_)3Z8jIuH@vT&scnh!q|bFUU{@_IRoP$J zRu6*AV*wSr@!Z_6NQY#)w=c9OZolabYsi@Sp`HvE5tvN^MuQmCf%#3rlV`1>6nK$` zc&V~lYBZC{++6a*bTqnMZ9)sg1f09R;91n;cuI*YQ=-Wk%piR@}s9RNv z!14L_EE56jP~P`xk#IB9d|K8dYQr4*e01b4$9~6Yq7iH=rBYyLl6dv)H@xB6bMy0a zyE-Fa_J}d}5SSeR*dv0t2r(Tz^>1L14#d7B2DMuA>DueBe*2Y|9*FV5;fIAFd^`$L zJjAlTh8B4C9BqWOMGHk6XYsDhoDr+O~6g z4jo1k4mT(*V&6gd(B_u&1(2 zUud;^>GjuL{>Do$-1|oq=?{4bPZWiCjnM=C=8fLe2J4NQ#pj^u0wfEj>+(nL8BCfc zWw#|ukHNaz2D3>~$>h0h{oIX$Ffgyr^1^tTtjOiw;epUn-C0|^@+AZ&g=}avMuT`2 zKRyVI)yZF^EmJ$MU z#2B!Q*`EgHRB)!5!~hiy$V5c|Me$*rw0}GtjxQH^c>(o7NYWsWH$u$8C4dE)TfF3< zJAV$&N(yyB(b0xYeh+#*F$%u`Vh6FpflbpxDyZfOZ-A5yTP(&3d7NjVOUAY)j{OK(V0|!||Aaxq0F8 z%2(&+y1RmanbMfkfN`30Ds#BKk#hvw=J!Qbz)G_>kEuu0Sgz$ggJxr(p#2^{WdiooF6G!4s$Ns~MmyJ;#9i;Niae9}#J zF(Z)pu2U%i$n9yy%=X_toXnts0`U+T^jH?dZWKlO!liTHQ9}PX9q&V-O3zI6XhQ(T zYanE_8Jo>aO0EnL4bOTZr%%MLFo?h-UxVOA2(BA<*pjv27I|(Mqd-tzl33nyQWkg4 zUtalIoFoI`7-LImw8k3IXev#d}aAoB=%NPR2F-Dxo znAw?gGJ_5%sA-UoFlsy&#V`qxK`WP*zniP_$2-FmUFm^Glt+|+j#??#Q3x2jQ4&+o zjuvL=sh_e9Xw>o~btJytlf)dkGe{Ab$(_|p%U_M-I2HMbobPX9jCyAOv$r~fsDV&e zRgOVP>xXGNGF=R{f{MnFgO)EnPVdKaXJ7*f!ZO4OINj);1B^Ogg}`DIAR#tgX+Jx- zy>fB+M8?#8%(DRJWTOa$r&YD+g>e=&gCD2q-lc4kUz9@FN+Msv<>6*5w2prI<G)zkDK1H0N@RwX(s>_yJaWb!gJRt-o#u8; zt~aN0lEV>}Qhs=A`NHDYk|ai8#wUcx8Zc)H&U6=J4WbfjM9E<*j(&(H(Ucg1An>M- zhF~s;3(3LNy&1=WihwEL0JG9=Iud)6gF6V!bDcOINb6%ZIYge2uFw9z@vNu$eR852 zk~jZ!VqLG|p%wj8N56&Ts4<7qbieHI zW`A|z%k55Q$iSFVniZI*iVriic>>4?Oo(^0T%V_T5IWayJ-WB<4@aX93KD4$3XgRY z=tdK!oF-WY2l+%0Fnig-ou$?O^X*QYa#z}fdb>VZW6r*;P7bT!!YXyKNwXFHXozhd zb%ffyae#tg;a?^xF1XCJpG_zdkjC6xTIqkalXP}vIeLl^dB)(JKJG98^}Q(0n$*Px z=~XP73jwBiO#rdj5HOP*yV3+TW;BwhF?W|%7oP9L$pC@rM@J!2o)99>9-NuOYS;>Y zh+S+2Mfx(SF`1ysFeZnCNhY?jT^|&ao6F}H5ttYmGoB$tp0(zj9#-iDjXPvk&`f%6 zv~uy>ckaLPD3}bg56&lvhn)B{O?=@esOSkSYEq!@qhaL{yOBt9y8|t3?cFi zG-qa`NcTyErA7@ZvmBN??QZezcfRev03cM^uoJ=FLjbYv7&8sb9~d~(B}PmT<~{;6 z%qy{4idG`^WDnc*695<@FvOOFX^r{A1Lx#lR_Xp>0Q+e}9|Qt4mD~hCWCY4|i2Oqb zXBr^Vpb5?P?3h^Sn!8qijQ^iq4j>0WNx)1!$Ls8F{|_2j(+4A}1<0rMFR_Z)y>%m4-zj)_>0{Swlv1ZN4w;xmL` z1xQTHdGLNm&Y#M84QAeg6kZ`X023P@qCk{IW$~WNvq`o4^uhe`<5Q*z4-G>>dw}!| z6vWR6ic>JDAsR%T{NozTe0OKgN!*XR-H#3P1os=0=;K)?z07cUrI=8IF@4v`-;aB} z=;5s!<1_#K>+kMfI{&(;Xc6_w_YBj-gpfQdK>Q0U24F&S!2{bV-Vv4Xlg-8%3~ zPM_GwlH|?7f!BN-Mdi+H`ektLL_R(+C8pQ>-h64&VcFha1~_fSB(-A5n4uHJxNgVn z%w~hf=TC3sY5GQQJoZVNh9u9804jFwZhq?79#4nE88ICYK)c5K8W5p-y##Pr!KCmQ z0TXqKcLfY>Pd5*4UpjxN%=0%ogP~8;EW~ka;v_L~T0b|DOE(Gf6R0h1(2NRm{bB*t3TJlVQn*%VuMW}j4s zk7uLN9Vlo%-wi(Okpg-Vm1A3A5g9 zoG6KdDIedy`L#E6I-N{zljUFey}fNCfK)*N^96y^HeYllS2iBAEgmCaHu5ZeqZ|yH zQmUauk+D{QFhMog%RlQ}JwxzSv2*XvC&Aid$zXKHz=NQmr%MGr?{KKCn5D|Qi2juHY-plvj7i5>3JU-kii!M{WC8fl#Z4P+Slg8vxKuhL^44! zQSC8ON{`Q3P3v_Gz$A&TVH67MKFDm` zR0$~rUsba4xQCWXva~X8fgDWb%dOe;3(tH!9oHpB2T@{B(AsStaMliy%SaG)1k7yO ze|+K0VJRhmNwX}ZNxEv+fTA7~Oxs(6nlUwCg5FC6NWlP_J_ZvDMG9ilv(3*u(aCf; zo;85x=wfRE0-W_jWLFf^k*>6P`{0ucXEw@ABSDsiC`ycjY7~qrMax9}r9P(>DZpb| zf%FPWFLhaJFmoFLP)7iXN%KjK8taDDHk|cCUX;+ zr8r3&Fb)g6=He{xBdtPM9fTTF3t(Md)>#uLv0=+l9{{VW41fa`Oq}YldPl;l;ZxG) zH_dus6>!!Hk=u8sgL;A2EsHn0(v`BTK)OaoVv7qP9FzIp5$bE^eeWE6P=8_E?<|#pHys*XG00A77F?~VHI%J3XuZlP)ES@Mw6y1rKyo1sU1nbZ6!?f zoq%v(6gNt%78N{6j^WD-zW|W=d=6N+UCx)qg|f9MFBre^zTBSGQG+CE{Oqs_I6o;w zS_zS{{_G%GX`|4tR1}4PLA%z#7|CRO;L3aBWg4RWfNBWVwlWr(O3KLxz4#Ec#5`0I zr-{H|T$z}%Jp5c!VlbnKRt($3{`2$4mvFQp`ksg1E#g0-t? zpe3}rAceK+GQRRK=l1=p#h$*7lf4gkY_p6}FaUy@k^ccZg{q?km)YWZ`WzhFC;}k) z8`&2XeP>wxZ#h@h+)Q#sr@KdE5*R;!%uHeC=lqr6TlPJ)Og|w{dCYHEW;QG{8id2$ zo!duuy5y)Ts#;a81KRBxf11%)bA0Z(=Uk0#c^v>p@#1#v7~|4)QxMum`ftB}M`L~g z8bk9KFh&Z23t@*^l@cz}{{+QQRLi2o)i4MsFLu2!dYJ35SE=d@EHf7SLvQ~01`Gyt zTuK?Rs;sSl4nUJgyAx7ar8F|7>85 z14y8(_)0@ri&m_)*+(0%{i6Nx$LCj1pAE8|efxQw!Bk176JU? ze0QnDliRQ5UEi6BY083AIq)KKq^T z|BbFMD&yo+IxgnUUAh}4y&I+-b82Ds*8KAFQqt+jH=lX(S6Pz$tg*6Yqk7$7wyQOK z1NdPrn51T-gJQwu;k96NUc-#3TrA$OHHZ)>4`Ufqi!MdS;ar+8*m2Hl2_J8ZlE63iYs z6Tux@loc6Bs{+aTWQ27B%?z+Z&By2GCD>@J1{)OEYaIUQn!dJsGG!B#k zj4`H^X=mTPdi%`Rzj;?PMO1nZyWoe-`A_3EiT&v`n4VDmo`bW!Ke&v~D0Vq6?yHM7O5JJvDk{Kr*SYjy7 zRUZ(RE2h*q|IFNMK%)|jJg76$;&Z#ha{`3eqBCMl5{p55Ukal)&wS%se{8NF?WAo2 zra#!EL%YnFoWZf9;8-j$Ls&w&Z_2VU(`wBuEG<7xp67HP_fL^$psG?fl$8OLw46f* zD8mIH0(`AFD**&40Rn*Fj#9BdM0e6YBVoK+C2h1{;Q&QK2M8g4D1HVcAr!1zuFRYk zY1-yF|2vmSd(WL}>%{G^-$kX=$vR!D`!N9{I)M-w1EYb%I5@l&Ku+7d=;~6pW*1vw zeR<`Hcz5@BnIxwvHV7qzRSNT6W{$wccs9tPi5o%#{csmYOZj1yqCF7-8)(DK0a!ye zMl~Fd@dLjulp%f?V4!8gR~mqTuN|XMR=l-_fN{Pn6KTIAjWt^*Z-4!d^|d42tliOi z;DDjdBt*)0td^q@!h~`b8YKnLgjHNV!w!QYko*~x72eZWw$_i^Hm@a6yz`zYt1@b5A&9Af|soq2=a@7?v)wO-za#%u-R z-!f+M40*i7gdq*;8LeH3)n;4EPsO`moHBWGs^&CE$zc~`k)|!P1PiB-0FXl?#{px| z(tWIeU}Gul*Li_sQMx=9)ny5hMR^WObENzp5Xy;6s3{DE6q!fb3B;(kdmugqf&@tuY!5;IQcLpg?EiPNO8E0stVP$8$y_ z7x7rbvJEJQ<%1F`5DBs@72UmEnGj@%n zz;G!+YeEJ8!#>8kRt|PSjIDNMCS56|WwE_@>FQksOwW~)9t@nuc!M`KC!T^i6GR$Y zO&8mB8Wc5~wWzh^qQ=oO?w_U4%Y{_RR-=h91Pj>u!)3CL@B|RPXY__9yFd-`|%>o^Q4;U;88dNyfy(pLh`I6$~i zQ?>bs+I(1VJ{|AvoGRnwl#ogaDXs7Wc7{j6I36e?f13%H8Y2nD=%R$y#Dl)<5o7vU zx>Y}S>8`ou)lS*#S)}U;r8EUNQ!@x*)wnL!YSvVv^(aGre5#Se1KDfuIbb#y&R@EFZgtK5KInuH zd8puw0|W*YWE4q*hDLGmiT>{Ar*s^jlC>J5sLNee2;ZFqk3NMjBg^AaO~B-iF|xbA zC;CacwQ%9`pHND}NU%9ML>~UHF`CgXMyu!2Jaw^ttzHkBm#;sN3i(KnE-=e8MS>bw zHhBJ~QToz@ z)>z}UWbxqjY*xxxOSZ++wrHigKAw(V~IA0>HLZoL@ zqv^8Px-GJ^B)}5m8V5Iq8IzaR5A454Q$dW|%_V)>xb@Iy#AjVUxAZ8rcIJ*4lMYKM&o>s%Q7J92kiqLu2$6>f&h!j|uQAlc23DhsMdYNP z>VE#|nY`aSDU^~$QCbpUNun*|Jlk$uxO^uxrZ=uJ6NL9r!I?U&1_V&8iFwfDWE79a z?Y*;F8$qQcQeAE?U%N#iat)DSiV%5Nt$g~hnz!2nng@xM8()7oHNle~zx}2dkj5-u zy?GZIyg}aWP7xvxnK{#k)uLmxuG1&0Gh`HBNe2Dm$c|J)@U5F);d;2Wjxp~djkhd@2f1O!~gp^, + pub width: u32, +} + +impl BitmapCanvas { + // {{{ Draw pixel + pub fn set_pixel(&mut self, pos: (u32, u32), color: (u8, u8, u8, u8)) { + let index = 3 * (pos.1 * self.width + pos.0) as usize; + let alpha = color.3 as u32; + self.buffer[index + 0] = + ((alpha * color.0 as u32 + (255 - alpha) * self.buffer[index + 0] as u32) / 255) as u8; + self.buffer[index + 1] = + ((alpha * color.1 as u32 + (255 - alpha) * self.buffer[index + 1] as u32) / 255) as u8; + self.buffer[index + 2] = + ((alpha * color.2 as u32 + (255 - alpha) * self.buffer[index + 2] as u32) / 255) as u8; + } + // }}} + // {{{ Draw RBG image + /// Draws a bitmap image + pub fn blit_rbg(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), src: &[u8]) { + let height = self.buffer.len() as u32 / 3 / self.width; + for dx in 0..iw { + for dy in 0..ih { + let x = pos.0 + dx as i32; + let y = pos.1 + dy as i32; + if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height { + let r = src[(dx + dy * iw) as usize * 3]; + let g = src[(dx + dy * iw) as usize * 3 + 1]; + let b = src[(dx + dy * iw) as usize * 3 + 2]; + + let color = (r, g, b, 255); + + self.set_pixel((x as u32, y as u32), color); + } + } + } + } + // }}} + // {{{ Draw RGBA image + /// Draws a bitmap image taking care of the alpha channel. + pub fn blit_rbga(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), src: &[u8]) { + let height = self.buffer.len() as u32 / 3 / self.width; + for dx in 0..iw { + for dy in 0..ih { + let x = pos.0 + dx as i32; + let y = pos.1 + dy as i32; + if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height { + let r = src[(dx + dy * iw) as usize * 4]; + let g = src[(dx + dy * iw) as usize * 4 + 1]; + let b = src[(dx + dy * iw) as usize * 4 + 2]; + let a = src[(dx + dy * iw) as usize * 4 + 3]; + + let color = (r, g, b, a); + + self.set_pixel((x as u32, y as u32), color); + } + } + } + } + // }}} + // {{{ Fill + /// Fill with solid color + pub fn fill(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), color: (u8, u8, u8, u8)) { + let height = self.buffer.len() as u32 / 3 / self.width; + for dx in 0..iw { + for dy in 0..ih { + let x = pos.0 + dx as i32; + let y = pos.1 + dy as i32; + if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height { + self.set_pixel((x as u32, y as u32), color); + } + } + } + } + // }}} + // {{{ Draw text + /// Render text + pub fn text( + &mut self, + pos: (i32, i32), + face: Face, + size: u32, + text: &str, + color: (u8, u8, u8, u8), + ) -> Result<(), Error> { + face.set_char_size(0, (size as isize) << 6, 300, 300)?; + + let mut pen_x = 0; + let kerning = face.has_kerning(); + let mut previous = None; + let mut data = Vec::new(); + + for c in text.chars() { + let glyph_index = face + .get_char_index(c as usize) + .ok_or_else(|| format!("Could not get glyph index for char {:?}", c))?; + + if let Some(previous) = previous + && kerning + { + let delta = face.get_kerning(previous, 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)?; + + data.push((pen_x, face.glyph().get_glyph()?)); + pen_x += face.glyph().advance().x >> 6; + previous = Some(glyph_index); + } + + let mut x_min = 32000; + let mut y_min = 32000; + let mut x_max = -32000; + let mut y_max = -32000; + + for (pen_x, glyph) in &data { + let mut bbox = glyph.get_cbox(FT_GLYPH_BBOX_PIXELS); + + bbox.xMin += pen_x; + bbox.xMax += pen_x; + + if bbox.xMin < x_min { + x_min = bbox.xMin + } + + if bbox.xMax < x_max { + x_max = bbox.xMax + } + + if bbox.yMin < y_min { + y_min = bbox.yMin + } + + if bbox.yMax < y_max { + y_max = bbox.yMax + } + } + + // Check that we really grew the string bbox + if x_min > x_max { + x_min = 0; + x_max = 0; + y_min = 0; + y_max = 0; + } + + for (pos_x, glyph) in &data { + let b_glyph = glyph.to_bitmap(freetype::RenderMode::Normal, None)?; + let bitmap = b_glyph.bitmap(); + let pixel_mode = bitmap.pixel_mode()?; + println!( + "Pixel mode: {:?}, width {:?}, height {:?}, len {:?}, pen x {:?}", + pixel_mode, + bitmap.width(), + bitmap.rows(), + bitmap.buffer().len(), + pos_x + ); + } + + Ok(()) + } + // }}} + + #[inline] + pub fn new(width: u32, height: u32) -> Self { + let buffer = vec![u8::MAX; 8 * 3 * (width * height) as usize].into_boxed_slice(); + Self { buffer, width } + } +} +// }}} +// {{{ Layout types +#[derive(Clone, Copy, Debug)] +pub struct LayoutBox { + relative_to: Option<(LayoutBoxId, i32, i32)>, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct LayoutBoxId(usize); + +#[derive(Default, Debug)] +pub struct LayoutManager { + boxes: Vec, +} + +pub struct LayoutDrawer { + pub layout: LayoutManager, + pub canvas: BitmapCanvas, +} + +impl LayoutManager { + // {{{ Trivial box creation + pub fn make_box(&mut self, width: u32, height: u32) -> LayoutBoxId { + let id = self.boxes.len(); + self.boxes.push(LayoutBox { + relative_to: None, + width, + height, + }); + + LayoutBoxId(id) + } + + pub fn make_relative_box( + &mut self, + to: LayoutBoxId, + x: i32, + y: i32, + width: u32, + height: u32, + ) -> LayoutBoxId { + let id = self.make_box(width, height); + self.edit_to_relative(id, to, x, y); + + id + } + // }}} + // {{{ Chage box to be relative + pub fn edit_to_relative( + &mut self, + id: LayoutBoxId, + id_relative_to: LayoutBoxId, + x: i32, + y: i32, + ) { + let current = self.boxes[id.0]; + let to = self.boxes[id_relative_to.0]; + if let Some((current_points_to, dx, dy)) = current.relative_to + && current_points_to != id_relative_to + { + self.edit_to_relative(current_points_to, id_relative_to, x - dx, y - dy); + } else { + self.boxes[id.0].relative_to = Some((id_relative_to, x, y)); + } + + { + let a = self.lookup(id); + let b = self.lookup(id_relative_to); + assert_eq!((a.0 - b.0, a.1 - b.1), (x, y)); + } + } + // }}} + // {{{ Margins + #[inline] + pub fn margin(&mut self, id: LayoutBoxId, t: i32, r: i32, b: i32, l: i32) -> LayoutBoxId { + let inner = self.boxes[id.0]; + let out = self.make_box( + (inner.width as i32 + l + r) as u32, + (inner.height as i32 + t + b) as u32, + ); + self.edit_to_relative(id, out, l, t); + out + } + + #[inline] + pub fn margin_xy(&mut self, inner: LayoutBoxId, x: i32, y: i32) -> LayoutBoxId { + self.margin(inner, y, x, y, x) + } + + #[inline] + pub fn margin_uniform(&mut self, inner: LayoutBoxId, amount: i32) -> LayoutBoxId { + self.margin(inner, amount, amount, amount, amount) + } + // }}} + // {{{ Glueing + #[inline] + pub fn glue_horizontally( + &mut self, + first_id: LayoutBoxId, + second_id: LayoutBoxId, + ) -> LayoutBoxId { + let first = self.boxes[first_id.0]; + let second = self.boxes[second_id.0]; + let id = self.make_box(first.width.max(second.width), first.height + second.height); + + self.edit_to_relative(first_id, id, 0, 0); + self.edit_to_relative(second_id, id, 0, first.height as i32); + id + } + + #[inline] + pub fn glue_vertically( + &mut self, + first_id: LayoutBoxId, + second_id: LayoutBoxId, + ) -> LayoutBoxId { + let first = self.boxes[first_id.0]; + let second = self.boxes[second_id.0]; + let id = self.make_box(first.width + second.width, first.height.max(second.height)); + + self.edit_to_relative(first_id, id, 0, 0); + self.edit_to_relative(second_id, id, first.width as i32, 0); + id + } + // }}} + // {{{ Repeating + pub fn repeated_evenly( + &mut self, + id: LayoutBoxId, + amount: (u32, u32), + ) -> (LayoutBoxId, impl Iterator) { + let inner = self.boxes[id.0]; + let outer_id = self.make_box(inner.width * amount.0, inner.height * amount.1); + self.edit_to_relative(id, outer_id, 0, 0); + + ( + outer_id, + (0..amount.0 * amount.1).into_iter().map(move |i| { + let (y, x) = i.div_rem_euclid(&amount.0); + ((x * inner.width) as i32, (y * inner.height) as i32) + }), + ) + } + // }}} + // {{{ Lookup box + pub fn lookup(&self, id: LayoutBoxId) -> (i32, i32, u32, u32) { + let current = self.boxes[id.0]; + if let Some((to, dx, dy)) = current.relative_to { + let (x, y, _, _) = self.lookup(to); + (x + dx, y + dy, current.width, current.height) + } else { + (0, 0, current.width, current.height) + } + } + + #[inline] + pub fn width(&self, id: LayoutBoxId) -> u32 { + self.boxes[id.0].width + } + + #[inline] + pub fn height(&self, id: LayoutBoxId) -> u32 { + self.boxes[id.0].height + } + + #[inline] + pub fn position_relative_to(&self, id: LayoutBoxId, pos: (i32, i32)) -> (i32, i32) { + let current = self.lookup(id); + ((pos.0 as i32 + current.0), (pos.1 as i32 + current.1)) + } + // }}} +} + +impl LayoutDrawer { + pub fn new(layout: LayoutManager, canvas: BitmapCanvas) -> Self { + Self { layout, canvas } + } + + // {{{ Drawing + // {{{ Draw pixel + pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: (u8, u8, u8, u8)) { + let pos = self + .layout + .position_relative_to(id, (pos.0 as i32, pos.1 as i32)); + self.canvas.set_pixel((pos.0 as u32, pos.1 as u32), color); + } + // }}} + // {{{ Draw RGB image + /// Draws a bitmap image + pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: (i32, i32), dims: (u32, u32), src: &[u8]) { + let pos = self.layout.position_relative_to(id, pos); + self.canvas.blit_rbg(pos, dims, src); + } + // }}} + // {{{ Draw RGBA image + /// Draws a bitmap image taking care of the alpha channel. + pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: (i32, i32), dims: (u32, u32), src: &[u8]) { + let pos = self.layout.position_relative_to(id, pos); + self.canvas.blit_rbga(pos, dims, src); + } + // }}} + // {{{ Fill + /// Fills with solid color + pub fn fill(&mut self, id: LayoutBoxId, color: (u8, u8, u8, u8)) { + let current = self.layout.lookup(id); + self.canvas + .fill((current.0, current.1), (current.2, current.3), color); + } + // }}} + // {{{ Draw text + /// Render text + pub fn text( + &mut self, + id: LayoutBoxId, + pos: (i32, i32), + face: Face, + size: u32, + text: &str, + color: (u8, u8, u8, u8), + ) -> Result<(), Error> { + let pos = self.layout.position_relative_to(id, pos); + self.canvas.text(pos, face, size, text, color) + } + // }}} + // }}} +} +// }}} diff --git a/src/chart.rs b/src/chart.rs index 2442121..3d21f5b 100644 --- a/src/chart.rs +++ b/src/chart.rs @@ -1,7 +1,8 @@ use std::path::PathBuf; +use image::{ImageBuffer, Rgb}; use serde::{Deserialize, Serialize}; -use sqlx::{prelude::FromRow, SqlitePool}; +use sqlx::SqlitePool; use crate::context::Error; @@ -78,7 +79,7 @@ impl TryFrom for Side { } // }}} // {{{ Song -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] pub struct Song { pub id: u32, pub title: String, @@ -91,7 +92,13 @@ pub struct Song { } // }}} // {{{ Chart -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy)] +pub struct Jacket { + pub raw: &'static [u8], + pub bitmap: &'static ImageBuffer, Vec>, +} + +#[derive(Debug, Clone)] pub struct Chart { pub id: u32, pub song_id: u32, @@ -104,7 +111,7 @@ pub struct Chart { pub note_count: u32, pub chart_constant: u32, - pub cached_jacket: Option<&'static [u8]>, + pub cached_jacket: Option, } impl Chart { diff --git a/src/commands/chart.rs b/src/commands/chart.rs index 989df40..d4ca796 100644 --- a/src/commands/chart.rs +++ b/src/commands/chart.rs @@ -19,8 +19,8 @@ pub async fn chart( let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; let attachement_name = "chart.png"; - let icon_attachement = match chart.cached_jacket { - Some(bytes) => Some(CreateAttachment::bytes(bytes, attachement_name)), + let icon_attachement = match chart.cached_jacket.as_ref() { + Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, attachement_name)), None => None, }; diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 1da3d34..9a8dc21 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -17,8 +17,11 @@ use poise::{ use sqlx::query_as; use crate::{ + bitmap::{BitmapCanvas, LayoutDrawer, LayoutManager}, + chart::{Chart, Song}, context::{Context, Error}, - score::{guess_song_and_chart, DbPlay, Score}, + jacket::BITMAP_IMAGE_SIZE, + score::{guess_song_and_chart, DbPlay, Play, Score}, user::{discord_it_to_discord_user, User}, }; @@ -27,7 +30,7 @@ use crate::{ #[poise::command( prefix_command, slash_command, - subcommands("chart"), + subcommands("chart", "b30"), subcommand_required )] pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> { @@ -212,7 +215,6 @@ pub async fn plot( .iter() .map(|(t, s)| Circle::new((*t, *s), 3, BLUE.filled())), )?; - root.present()?; } @@ -228,3 +230,226 @@ pub async fn plot( Ok(()) } // }}} +// {{{ B30 +/// Show the 30 best scores +#[poise::command(prefix_command, slash_command)] +pub async fn b30(ctx: Context<'_>) -> 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 plays: Vec = query_as( + " + SELECT id, chart_id, user_id, + created_at, MAX(score) as score, zeta_score, + creation_ptt, creation_zeta_ptt, far_notes, max_recall, discord_attachment_id + FROM plays p + WHERE user_id = ? + GROUP BY chart_id + ORDER BY score DESC + ", + ) + .bind(user.id) + .fetch_all(&ctx.data().db) + .await?; + + if plays.len() < 30 { + ctx.reply("Not enough plays found").await?; + return Ok(()); + } + + // TODO: consider not reallocating everything here + let mut plays: Vec<(Play, &Song, &Chart)> = plays + .into_iter() + .map(|play| { + let play = play.to_play(); + // TODO: change the .lookup to perform binary search or something + let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?; + Ok((play, song, chart)) + }) + .collect::, Error>>()?; + + plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant)); + plays.truncate(30); + + let mut layout = LayoutManager::default(); + let jacket_area = layout.make_box(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE); + let jacket_margin = 10; + let jacket_with_margin = + layout.margin(jacket_area, jacket_margin, jacket_margin, 5, jacket_margin); + let top_left_area = layout.make_box(90, layout.height(jacket_with_margin)); + let top_area = layout.glue_vertically(top_left_area, jacket_with_margin); + let bottom_area = layout.make_box(layout.width(top_area), 40); + let item_area = layout.glue_horizontally(top_area, bottom_area); + let item_with_margin = layout.margin_xy(item_area, 25, 20); + let (item_grid, item_origins) = layout.repeated_evenly(item_with_margin, (5, 6)); + let root = item_grid; + + // layout.normalize(root); + let width = layout.width(root); + let height = layout.height(root); + + let canvas = BitmapCanvas::new(width, height); + let mut drawer = LayoutDrawer::new(layout, canvas); + + let asset_cache = &ctx.data().jacket_cache; + let bg = &asset_cache.b30_background; + + drawer.blit_rbg( + root, + ( + -((bg.width() - width) as i32) / 2, + -((bg.height() - height) as i32) / 2, + ), + bg.dimensions(), + bg.as_raw(), + ); + + for (i, origin) in item_origins.enumerate() { + drawer + .layout + .edit_to_relative(item_with_margin, item_grid, origin.0, origin.1); + + drawer.fill(top_area, (59, 78, 102, 255)); + + let (_play, song, chart) = &plays[i]; + + // {{{ Display jacket + let jacket = chart.cached_jacket.as_ref().ok_or_else(|| { + format!( + "Cannot find jacket for chart {} [{:?}]", + song.title, chart.difficulty + ) + })?; + + drawer.blit_rbg( + jacket_area, + (0, 0), + jacket.bitmap.dimensions(), + &jacket.bitmap.as_raw(), + ); + // }}} + // {{{ Display difficulty background + let diff_bg = &asset_cache.diff_backgrounds[chart.difficulty.to_index()]; + drawer.blit_rbga( + jacket_area, + ( + BITMAP_IMAGE_SIZE as i32 - (diff_bg.width() as i32) / 2, + -(diff_bg.height() as i32) / 2, + ), + diff_bg.dimensions(), + &diff_bg.as_raw(), + ); + // }}} + // {{{ Display difficulty text + let x_offset = if chart.level.ends_with("+") { + 3 + } else if chart.level == "11" { + -2 + } else { + 0 + }; + // jacket_area.draw_text( + // &chart.level, + // &TextStyle::from(("Exo", 30).into_font()) + // .color(&WHITE) + // .with_anchor::(Pos { + // h_pos: HPos::Center, + // v_pos: VPos::Center, + // }) + // .into_text_style(&jacket_area), + // (BITMAP_IMAGE_SIZE as i32 + x_offset, 2), + // )?; + // }}} + // {{{ Display chart name + // Draw background + drawer.fill(bottom_area, (0x82, 0x71, 0xA7, 255)); + + let tx = 10; + let ty = drawer.layout.height(bottom_area) as i32 / 2; + + // let text = &song.title; + // let mut size = 30; + // let mut text_style = TextStyle::from(("Exo", size).into_font().style(FontStyle::Bold)) + // .with_anchor::(Pos { + // h_pos: HPos::Left, + // v_pos: VPos::Center, + // }) + // .into_text_style(&bottom_area); + // + // while text_style.font.layout_box(text).unwrap().1 .0 >= item_area.0 as i32 - 20 { + // size -= 3; + // text_style.font = ("Exo", size).into_font(); + // } + // + // Draw drop shadow + // bottom_area.draw_text( + // &song.title, + // &text_style.color(&RGBAColor(0, 0, 0, 0.2)), + // (tx + 3, ty + 3), + // )?; + // bottom_area.draw_text( + // &song.title, + // &text_style.color(&RGBAColor(0, 0, 0, 0.2)), + // (tx - 3, ty + 3), + // )?; + // bottom_area.draw_text( + // &song.title, + // &text_style.color(&RGBAColor(0, 0, 0, 0.2)), + // (tx + 3, ty - 3), + // )?; + // bottom_area.draw_text( + // &song.title, + // &text_style.color(&RGBAColor(0, 0, 0, 0.2)), + // (tx - 3, ty - 3), + // )?; + + // Draw text + // bottom_area.draw_text(&song.title, &text_style.color(&WHITE), (tx, ty))?; + // }}} + // {{{ Display index + let bg = &asset_cache.count_background; + + // Draw background + drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg); + + // let text_style = TextStyle::from(("Exo", 30).into_font().style(FontStyle::Bold)) + // .with_anchor::(Pos { + // h_pos: HPos::Left, + // v_pos: VPos::Center, + // }) + // .into_text_style(&area); + + let tx = 7; + let ty = (jacket_margin + bg.height() as i32 / 2) - 3; + + // Draw drop shadow + // area.draw_text( + // &format!("#{}", i + 1), + // &text_style.color(&BLACK), + // (tx + 2, ty + 2), + // )?; + + // Draw main text + // area.draw_text(&format!("#{}", i + 1), &text_style.color(&WHITE), (tx, ty))?; + // }}} + } + + let mut out_buffer = Vec::new(); + let image: ImageBuffer, _> = + ImageBuffer::from_raw(width, height, drawer.canvas.buffer).unwrap(); + + let mut cursor = Cursor::new(&mut out_buffer); + image.write_to(&mut cursor, image::ImageFormat::Png)?; + + let reply = CreateReply::default().attachment(CreateAttachment::bytes(out_buffer, "b30.png")); + ctx.send(reply).await?; + + Ok(()) +} +// }}} diff --git a/src/jacket.rs b/src/jacket.rs index d6c26e7..4267bd2 100644 --- a/src/jacket.rs +++ b/src/jacket.rs @@ -1,13 +1,13 @@ use std::{fs, path::PathBuf, str::FromStr}; -use image::{GenericImageView, Rgba}; +use freetype::{Face, Library}; +use image::{imageops::FilterType, GenericImageView, ImageBuffer, Rgb, Rgba}; use kd_tree::{KdMap, KdPoint}; use num::Integer; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; use crate::{ - chart::{Difficulty, SongCache}, + bitmap::BitmapCanvas, + chart::{Difficulty, Jacket, SongCache}, context::Error, score::guess_chart_name, }; @@ -15,11 +15,10 @@ use crate::{ /// How many sub-segments to split each side into pub const SPLIT_FACTOR: u32 = 8; pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize; +pub const BITMAP_IMAGE_SIZE: u32 = 192; -#[serde_as] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ImageVec { - #[serde_as(as = "[_; IMAGE_VEC_DIM]")] pub colors: [f32; IMAGE_VEC_DIM], } @@ -44,16 +43,16 @@ impl ImageVec { let mut count = 0; for (_, _, pixel) in cropped.pixels() { - r += pixel.0[0] as u64; - g += pixel.0[1] as u64; - b += pixel.0[2] as u64; + r += (pixel.0[0] as u64).pow(2); + g += (pixel.0[1] as u64).pow(2); + b += (pixel.0[2] as u64).pow(2); count += 1; } let count = count as f64; - let r = r as f64 / count; - let g = g as f64 / count; - let b = b as f64 / count; + let r = (r as f64 / count).sqrt(); + let g = (g as f64 / count).sqrt(); + let b = (b as f64 / count).sqrt(); 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; @@ -77,9 +76,11 @@ impl KdPoint for ImageVec { } } -#[derive(Serialize, Deserialize)] pub struct JacketCache { tree: KdMap, + pub b30_background: ImageBuffer, Vec>, + pub count_background: ImageBuffer, Vec>, + pub diff_backgrounds: [ImageBuffer, Vec>; 5], } impl JacketCache { @@ -96,7 +97,7 @@ impl JacketCache { let mut jackets = Vec::new(); let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory"); - for entry in entries { + for (i, entry) in entries.enumerate() { let dir = entry?; let raw_dir_name = dir.file_name(); let dir_name = raw_dir_name.to_str().unwrap(); @@ -127,6 +128,11 @@ impl JacketCache { jackets.push((file.path(), song.id)); let contents = fs::read(file.path())?.leak(); + let bitmap = Box::leak(Box::new( + image::load_from_memory(contents)? + .resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest) + .into_rgb8(), + )); if name == "base" { let item = song_cache.lookup_mut(song.id).unwrap(); @@ -156,14 +162,20 @@ impl JacketCache { if !specialized_path.exists() && !dest.exists() { std::os::unix::fs::symlink(file.path(), dest) .expect("Could not symlink jacket"); - chart.cached_jacket = Some(contents); + chart.cached_jacket = Some(Jacket { + raw: contents, + bitmap, + }); } } } else if difficulty.is_some() { std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir)) .expect("Could not symlink jacket"); let chart = song_cache.lookup_chart_mut(chart.id).unwrap(); - chart.cached_jacket = Some(contents); + chart.cached_jacket = Some(Jacket { + raw: contents, + bitmap, + }); } } } @@ -180,8 +192,36 @@ impl JacketCache { } } + let assets_dir = data_dir.join("assets"); + + let lib = Library::init()?; + let saira_font = lib.new_face(assets_dir.join("saira-variable.ttf"), 0)?; + let mut canvas = BitmapCanvas::new(0, 0); + canvas.text( + (0, 0), + saira_font, + 20, + "Yo, this is a test!", + (0, 0, 0, 0xff), + )?; + let result = Self { tree: KdMap::build_by_ordered_float(entries), + b30_background: image::open(assets_dir.join("b30_background.jpg"))? + .resize(2048 * 2, 1535 * 2, FilterType::Nearest) + .blur(20.0) + .into_rgb8(), + count_background: image::open(assets_dir.join("count_background.png"))? + .blur(1.0) + .into_rgba8(), + diff_backgrounds: Difficulty::DIFFICULTY_SHORTHANDS.try_map( + |shorthand| -> Result<_, Error> { + Ok(image::open( + assets_dir.join(format!("diff-{}.png", shorthand.to_lowercase())), + )? + .into_rgba8()) + }, + )?, }; Ok(result) diff --git a/src/main.rs b/src/main.rs index de9d412..8b88cc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ #![warn(clippy::str_to_string)] #![feature(iter_map_windows)] #![feature(let_chains)] +#![feature(array_try_map)] #![feature(async_closure)] +mod bitmap; mod chart; mod commands; mod context; diff --git a/src/score.rs b/src/score.rs index 127d709..11bc09e 100644 --- a/src/score.rs +++ b/src/score.rs @@ -153,8 +153,16 @@ impl Score { // Compute score from note breakdown subpairs let pf_score = Score::compute_naive(note_count, pures, fars); - let fl_score = Score::compute_naive(note_count, note_count - losts - fars, fars); - let lp_score = Score::compute_naive(note_count, pures, note_count - losts - pures); + let fl_score = Score::compute_naive( + note_count, + note_count.checked_sub(losts + fars).unwrap_or(0), + fars, + ); + let lp_score = Score::compute_naive( + note_count, + pures, + note_count.checked_sub(losts + pures).unwrap_or(0), + ); if no_shiny_scores.len() == 1 { // {{{ Score is fixed, gotta figure out the exact distribution @@ -450,14 +458,14 @@ impl Play { pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> { if let Some(fars) = self.far_notes { let (_, shinies, units) = self.score.analyse(note_count); - let (pures, rem) = (units - fars).div_rem_euclid(&2); + let (pures, rem) = units.checked_sub(fars)?.div_rem_euclid(&2); if rem == 1 { println!("The impossible happened: got an invalid amount of far notes!"); return None; } - let lost = note_count - fars - pures; - let non_max_pures = pures - shinies; + let lost = note_count.checked_sub(fars + pures)?; + let non_max_pures = pures.checked_sub(shinies)?; Some((shinies, non_max_pures, fars, lost)) } else { None @@ -474,7 +482,7 @@ impl Play { return None; } - let non_max_pures = chart.note_count + 10_000_000 - score; + let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?; if non_max_pures == 0 { Some("MPM".to_string()) } else { @@ -507,8 +515,8 @@ impl Play { author: Option<&poise::serenity_prelude::User>, ) -> Result<(CreateEmbed, Option), Error> { let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index); - let icon_attachement = match chart.cached_jacket { - Some(bytes) => Some(CreateAttachment::bytes(bytes, &attachement_name)), + let icon_attachement = match chart.cached_jacket.as_ref() { + Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)), None => None, }; @@ -527,16 +535,16 @@ impl Play { true, ) .field("Grade", self.score.grade(), true) - .field("ζ-Score", format!("{} (+?)", self.zeta_score), true) + .field("ξ-Score", format!("{} (+?)", self.zeta_score), true) .field( - "ζ-Rating", + "ξ-Rating", format!( "{:.2} (+?)", (self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100. ), true, ) - .field("ζ-Grade", self.zeta_score.grade(), true) + .field("ξ-Grade", self.zeta_score.grade(), true) .field( "Status", self.status(chart).unwrap_or("?".to_string()),