From 29053048ff8ec0e33aef5f2d9ef8966e59b29c75 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 21:40:13 +0300 Subject: [PATCH 01/32] Add support to use legacy combo fonts for the counter on legacy skins --- osu.Game.Rulesets.Catch/CatchSkinComponents.cs | 3 ++- .../Skinning/CatchLegacySkinTransformer.cs | 11 +++++++++++ .../Skinning/OsuLegacySkinTransformer.cs | 4 +--- osu.Game/Skinning/LegacySkinConfiguration.cs | 2 ++ osu.Game/Skinning/LegacySkinTransformer.cs | 2 ++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 80390705fe..23d8428fec 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch Droplet, CatcherIdle, CatcherFail, - CatcherKiai + CatcherKiai, + CatchComboCounter } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index d929da1a29..c2432e1dbb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning { @@ -51,6 +52,16 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.CatcherKiai: return this.GetAnimation("fruit-catcher-kiai", true, true, true) ?? this.GetAnimation("fruit-ryuuta", true, true, true); + + case CatchSkinComponents.CatchComboCounter: + var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; + var fontOverlap = GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; + + // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. + if (HasFont(comboFont)) + return new LegacyComboCounter(Source, comboFont, fontOverlap); + + break; } return null; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 81d1d05b66..b955150c90 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2; - return !hasFont(font) + return !HasFont(font) ? null : new LegacySpriteText(Source, font) { @@ -145,7 +145,5 @@ namespace osu.Game.Rulesets.Osu.Skinning return Source.GetConfig(lookup); } - - private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null; } } diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 41b7aea34b..1d5412d93f 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -15,6 +15,8 @@ namespace osu.Game.Skinning public enum LegacySetting { Version, + ComboPrefix, + ComboOverlap, AnimationFramerate, LayeredHitSounds, } diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index ebc4757e75..acca53835c 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -47,5 +47,7 @@ namespace osu.Game.Skinning } public abstract IBindable GetConfig(TLookup lookup); + + protected bool HasFont(string fontPrefix) => GetTexture($"{fontPrefix}-0") != null; } } From f5af81d775874f928c8bf8cdeb12fffee23d9cce Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 21:41:22 +0300 Subject: [PATCH 02/32] Add legacy combo font glyphs to legacy skins of test purposes --- .../Resources/old-skin/score-0.png | Bin 0 -> 3092 bytes .../Resources/old-skin/score-1.png | Bin 0 -> 1237 bytes .../Resources/old-skin/score-2.png | Bin 0 -> 3134 bytes .../Resources/old-skin/score-3.png | Bin 0 -> 3712 bytes .../Resources/old-skin/score-4.png | Bin 0 -> 2395 bytes .../Resources/old-skin/score-5.png | Bin 0 -> 3067 bytes .../Resources/old-skin/score-6.png | Bin 0 -> 3337 bytes .../Resources/old-skin/score-7.png | Bin 0 -> 1910 bytes .../Resources/old-skin/score-8.png | Bin 0 -> 3652 bytes .../Resources/old-skin/score-9.png | Bin 0 -> 3561 bytes .../Resources/special-skin/score-0@2x.png | Bin 0 -> 2184 bytes .../Resources/special-skin/score-1@2x.png | Bin 0 -> 923 bytes .../Resources/special-skin/score-2@2x.png | Bin 0 -> 2196 bytes .../Resources/special-skin/score-3@2x.png | Bin 0 -> 2510 bytes .../Resources/special-skin/score-4@2x.png | Bin 0 -> 1551 bytes .../Resources/special-skin/score-5@2x.png | Bin 0 -> 2265 bytes .../Resources/special-skin/score-6@2x.png | Bin 0 -> 2438 bytes .../Resources/special-skin/score-7@2x.png | Bin 0 -> 1700 bytes .../Resources/special-skin/score-8@2x.png | Bin 0 -> 2498 bytes .../Resources/special-skin/score-9@2x.png | Bin 0 -> 2494 bytes 20 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png new file mode 100644 index 0000000000000000000000000000000000000000..8304617d8c94a8400b50a90f364941bb02983065 GIT binary patch literal 3092 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 Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png new file mode 100644 index 0000000000000000000000000000000000000000..82bec3babebd59263dc486e5900a3a01ea4aaed7 GIT binary patch literal 3712 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 JPDHLkV1kmK?iT<6 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png new file mode 100644 index 0000000000000000000000000000000000000000..b4cf81f26e5cab5a068ce282ee22b15b92d0df12 GIT binary patch literal 3337 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 TZ*tsI00000NkvXXu0mjfuoP_o literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png new file mode 100644 index 0000000000000000000000000000000000000000..a23f5379b223d61079e055162fdd93f107f0ec02 GIT binary patch literal 1910 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..508cc85e4ae2ad01c53c93c314502071de8a5e1f GIT binary patch literal 2184 zcmV;32zU31P)EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00v-5L_t(|+U=cNh*eb>$Ny_|9B0f+-ZC$lm$b}CYw}WN z6j2r=`p|=E(gh8=M+BigWDrGxR0JhdP+;kUg_4=l1xjX6Obr#y#Ika7GR<4gIL^$k zhqFZ(^6a(tIeYJQ&g?&&w{_0<-@o;pz1BYa?C+B@Wy+K(Q>KVAX&V4~Ntz_-MoBkF z8YHP-@aitF6jF)gK>brrx1Kx>|iISafCT;Z6_bAf#!*|-%LQ>yST0@jBd;JQK%5U2zm2TtXQ zaSFJ*)Zq08Hiy7_6{sjuya~X8kSw29D)5E?yF=hT97EiJz>YjI+kq#2hu1sgAOK#B zG5&DiP)PiFzQXGkY$5V#Wdc4X11)(xv;epH2=Jp2c!z<033+)r#LF>YoKNr`ve@9x zPtZ@b#TI7=(A@`kV?wqO0Gm_rHQQoR-%cA|CGdlV(b*|@GZwo6fSG9me9pq)o?`9y zBKUjI!rvc2k2K5;22NTSd?MwY!BGp3ucTq_V+&)gz`&GGGs`S|o=NnY8JJ@E)6zN* z7P;w`URNh+>I+L#3q1g6fWIvLPDsK~t;3I-S9-Ef{nXO$swB*;wlwoMU_1f%tfl96 zV7%++S2!3ME$JT1;Tw%Hcyh6{(Q=%T^i*sBB`uKsEr5;kM_VN|S&p3p^ej3+piKxcT3U$qhw&Kuk?&DZAk;{UQPfD@Yk=9D-l<tK`0F5y}D_6%VPNPX_gyZ<{3kDeI%b1P&n1R?Hw++D0 zN<9OCK8}mXCSy!Pu>fw6^lyo0psOeAqzMIRjA@p%J_8s%1EpL6MO$4RJe=s0u5}o@ zC0IO*3UH0XYFsVpI?n)3a~QkK<%6vN8e>`|eGrr3xEGMFviP%jpp%kT6dhoo_k!iU zqyS2qDEnvqJ~76eigPwsS=Jvo>`?|jW9hjq_L|f|xlLP*^#q{pq_0TI&>Bla3p{{l zfJV!T9oHmfsx}B1rnMdboaRl6UpDz2SG)}&#+N7Hqc$k% znXlaEE%H%7Phfip#Kkej9~$&o%C~6Dkb6ve|SFY0J1w%?& zDrr$kclGK3u{y}cVmIXLsIP#DDp$qW*|5H>I~)EF$eQ31@Y@1ILrGG)rtiS-|NH3fs%kaO4o0000< KMNUMnLSTYH3*fx~ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..84f74e1ec9d45818d76353aa66ed1a728a2ad118 GIT binary patch literal 923 zcmeAS@N?(olHy`uVBq!ia0vp^mOz}v!3HGRn10@2U|?*?baoE#baqxKD9TUE%t>Wn z(3n^|(bnUzgUr$R;G;^S?A;v}E-U=RTwGNj4Vmj_xx~^;pkw0NMhQ|)H9wzLi--SmSfy^?`Xxe|2ys7`)6&I5D@50pP6{;i1d-mv$q|r zoNsZcYm--vpH12&)=v4I>ni>n+xcMA0b}JfD^q_NMKSK&HCqg|XGwh&DGa$pH808$vxazra!lJ)B`s*)z^?nj` zB9uw_IFr@|h23=@?0?){J9Yk?50efRF8F-3(9W%4UF?Fu;yLe=k7v$dIGFyxEB~i< zX~MI~U!uIbmCDp`+`Ki;O^-g5Z=fq4vg+%PZ!6Kid%2*?DskxAmQ+__%h$F5U#QS8JRDvEEBsp z9z96gT`K%h@y+TToPBvs&HLK&AG~<Km8VxsGZpplN_U0GQbAsLngbaTCSj)V3+1^c`<~?C7xb^LO(J_0EC4Xxk z`7Mr6p1;InqLOQuh-MJkjMS?$KYs0XSSPxtZfod?jtGW6u6t&B)$*F@lWn`|A8CI4 zm)Ya{K<38(qx;r1az7BPI2miOboqV-{mg^uhCyB@Uus=CbLP$MdkmNE&1rvga=AM& Pju<>${an^LB{Ts5NqUv} literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..49625c6623c2d07188e263708d2649af82eefdd7 GIT binary patch literal 2196 zcmV;F2y6F=P)UW&V0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00wMHL_t(|+U=ZMY!p=($NvXf5DMjvqNv3JMHCQ_fG8jk zLeX&1hy)?gfRd2-q%q;e3sGaDz9=zb5Ce!3yd(*mBmh`Qp&BmBRmoyZhr=$rEh|!YDBgpoEq|MIjJ!8zlOCt1uk-$5^ z1>)5O-~*r%Xpuw^#lURfdzwdkfdxR@L=+!jh@=&gZfN4GCnX(~bj*3xICV;QNtZkC ztwOr9N76iF%=+evjw0Z3;JnMf_ZRRsFc0YKBfplwt-!Ou_K=bD2GBXzKq@?n$`8N; zKBZ2K|C zR#2S<#>eGU8E_!O;6K2W92VOdj!hdd6QXve1o$n(&^cgIGgR=!MttYhAd%7Fn{{+U;rxwB}1b(UbJL}C1!j7;~$U8(x?tp`wT0p$SDK8)d6 zSu&lQpy9;}meKN57^7PrEU`>C*#=5wmS=kt2xW_;T8ng*fuK0$ZCI9%4ke9oKOFvl&^l?H)Q z?~&1)&Va0S9zbaq1WJ`hMy(}1CuukEA}|hU)tp3(F=s5B#KmvkIh z1Iz)6n~6=0MViw=pd7Xc!^x7CIuSGnXqjUur50(bf{Z6x0;fVQGkg@);*<}6+gkcK z(|4p8V=k&;y*7;Qko0nnqNJl`y4^v?J{u!&b}n#Z4xn_kNK-9oM^GrsB-KUW@QfTl z8DNp-V`EHRASlL|Ba)(+=d{B{lxN*Zj8`CIuaU|N6(sRM3PKFetlw*`Q*K>041|S1`?pj*e+$#&Y z0#X4S@d4x+r9nWsv+Kx9HNfn&0O<$pYXZm)pkG>mjPf!1eLe5{AdP_B+r(D$)4;5> zz%am*O*FeZfQqyLDFNPZqPQ$cQ*kK+e)ND*?Y6v`uRv}Gj(Gt24!AB&K<0bwmaccU zm=~u7MltYO6KDS>rYSnQ0bh9lSr_R+rAq+FO&&eeI%gl9NfVIi&dE==)8`}71j7JN zdlZ+|shA9E1f)Iip~rdQhmG0dtY z&8!1$GOiuz6PKYe&YcIDT9mZjDKIR<$F6Z0=?xsu0%UPQ8-WKi{49#vKq;`xrO23s zLRr+v&y#T&Se^yQ_9XW~fG->0*AR8)Q(P`>DNitv%QE^;TcgapbC&Ce0G>#Ar*dNE zyiJsuuX4G;rcJ`3yp%aQk1FHQF5{V36Aq*Z_#=qVLGE01Dk z{B(8iY2x#TfrFA3hdM(O4n!+8ocS3eX|5W!#(T-LPBA_VZjt0?m|dra)0@?DIT;?Y ziX?SV!(|mguWcP@jM-@g#krDszjAmNc8=83>%Q!!>XZ?A2qbhxJ2Vf0)EZ;Xg@H08 zZBgO}N<{(6|JA(!<-aXYsib^dxH=IgQaUL=6{`5ODnPLt0_0`!DX5@=3M#0e-0NR0 W2Bg8WSc5SD0000UW&V0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00*W?L_t(|+U=cvj9o<;hkx%D`T^a3%Ju^*(pJ7xsudAY zP!UamL>t3LRYF448c`&v{l||e5eX)0MATRm15ukm8Uz%C7HAYLl>oM-O-n6Bl9ugu z+itt@S^uzx0-P%8T1kr>h-n!g*(2$B2WXX< zts6FB2V4UD8aPC;dJ(u07+udW769vruie0RfJt>zeSj&F?vS)ZQp*sR9Fw%(h5k#D zIwW0d!V)gRcO$022eAdj{||U~NF;+zYgp z1>`#5=nzhI0ypMp=UQN=CzyW&^I`+}3UGV~XVw9y6<}{X@FPz!dw}y|0lA@q6KjhO zTi`ZNFzvuuMFW`ybPZv=8#p76L@D)&a?n)Dj5D|eY5bllhWddsOMZk`8&^bADDRoq^7t zENOYsV-GVMkhI0~wz*lM?2$AO=<^s!cSeYo{hrrP%ml^Edfd!esNNR=$^p;oTeC)r zBt4VC;aMTGZv{g+kO!1|GgP5|Nkat*5SYs6253H!W=m0JQeha+nJ_37z*MPqPzXU$(vs}YflW)}+jrRff0q-i}Z_2u4 z^kzx-rM$dDQoG(-HhNf6pQNdhW=NXp$|+Tz@w}uJk{&d(&Z1tlWdPEmWD+>m63xfV z?2|DJWr1?Gq^%KaYOkcHm1Gy5F|*C}2xX$H23AsYwp~f4;r_gx1`gQ?Dm);>4TCMO1dz`bNeJcEa`SL+p4k!v;y}7M=C6PcotY1 zI#~?N0PX+|ryT3t1H7$XK9*ZupW;g3y})$9a(sRpaCe23-hx05jY5p3~@Xtd&*u0OnI3bF^L2x4V=G#%G;=J20mapq%P* z)@k)xn#Oln&p)`8RzB&A*^<832+=adbIWf(u%HpZ{32jq^QaM7eNND7&+8XR`b;BK zp#jfmd8!er(0WfO{lK)5Tzw?zub$VBl5~A$C~ma61bD=~nO(pZ;0a(^gf+9(^hUrv zi?f907~tC}<~8;JUnnF00^Ag^qvKf61}G50|ji>zcrmH7@X~K5j$PpCw)9 z>Ej|v%OVHM-oX3j`nD;zOL~8xv2WzIkujb22i`X^H59ieFw5JXAnD@~p`2Ve6iHvr zVCa9E9}!CEX*!26w+7kg}Zb|>B zQg!MjN=8fidcdu>RXg9dS~q`1&Yh$pv{aJsveC!Q?2n;uVS0s)0jugHO5UDwh11}C z^7O2RS5`RN?YxqKv;dC<%-&y~hw;u1PJFR~p=BjO`H3fx zBWaEt3VAa_9W-|w_;N%TV*-G5=Tc=VIMiBYM*nw3&&2|ly92O%)KU0mC}5mB=&n+} zXDKi)XPa#S?FYbb-1*zlLiqPJ-pEX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00ZYqL_t(|+U=Z6h!j^8hQD*waio>V2#z9dL??(a2vI~3 zT!;(7fN>+^!q?zNB#4S=Fd%}u7;zMQfl3gKOBadaYw5zR4>W<`+86^#-1t~{TvQ8s zr_w!DGu3r()%ljE3hMIlSNGpl)dhxO7=~dOh7l>M+ECR$^h<_1acox+ho^w~WgOU5 zAbsnB!Lp9;B9}J@xKQRAT*UGOxD51|zbe++B}URwfN%bV1!EDdkd2@$zxpMmW;41D9w9s)numq`2ctpxUY<2S&7h`cv} zt>sMzuB5DGw}||1HKsl1fw|u77ZEv61h|sZw+YygQdtIx1XrTGdBEwk$|53vo50rc zCIDA~Y2K#ckSj|_3H%XQo>Ey35E-t7^eqPtrc{=Vp;wmD1*+<_=BYa&rLz1+gmgNA zr-6Ci?AKT-OQ`}?bpx=;8~+JxBVt^K`}%t1LkXRMjOUm#NY!%R3X;T3!SA8JOg4+9M**s0c+Sa2V)IsVwKH5U!x~tpm2F zRF*+1hATkcY?9lOer4GrBCkwfYk8lZoS)%sIw>M|s37B=z+J#@jB7v8HUT=1)s+DD21*+-}U~Nie*-wpdIpr-R`ODSkyrn`{Sqc-Vs$I>> zG9{(5Jf&vjN#Idnu{XOZB4?-}E@S#u0lU5NOJFNC#buD!!^kJ+YgJjsn!pRdoRrFP zn;PRrnZ7|_y*GYRx5|=BpsFq)d9jUOSqAD>SuzV$)rllGh8k8`@+I&Pu+%Ec|06BZ zHx>9VMe=pvuM*bU3w)Ph|3|=sF&^_tyGwGd1bzklTl0Y35Rq>SxtL&>Koe+W`yIYa zvte(rv{Tpw8Ya*Lnm`k1m_QR~0!^S{0!^R^G=YW*G=U~CAB}?due`voGw$DK=SLG& z?N-&t8Sja_SJh=juT456pofuvylzJq$-DFWf$tglRP1Q@hva1!w@4lYeNFOAV-{YC z$cM58-Un7hdIHQ6k(c%@Y+oWK&@c?cFbrcH`3Qg`jXy1_BiaA}002ovPDHLkV1je@ BxF!Gq literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d8250b0c63d96fe5e5ecfa92b639d8fc5f61ded7 GIT binary patch literal 2265 zcmV;~2qyQ5P)EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00yy1L_t(|+U=cbY!y`$$N#4mfwm|DvWc-GihwJEDA8zC zM3D~=7ec^&P1FR97=p_e6B9pRB8ef8sEG=KalsFUXo6UciW^}O1rci^xI%@33Y7Ne zhq;8;rmvm3Z{Ezb&Of;cDKm4<`@Q?_a?ZVXq+Gdj<;vCd1WX1t1NQvC6kj^fVM(*iY(*SSRVo{b z1I)jYW|`UBaXFP0IHhWfq?^oaQ=AX2=mubxN*W(Gn63`zCrQ)IY_^&0FNcAy2iVJZ3scnIi|*iQ1f&DSSI1GFOJOYg9661?DgTNhTc5*w{^pte5q%o34yV31nH{z{vrKw~aB}5vayU7h98Nk) zJT>F;=_e`3UXGA7Ku2Rem6G7g|mdAP9e?oGWR%B%crZucUcq zwlEVl2e`gSN@f)>4d~%v%Ycc%v%v2jHEB`Y3#X#M`OW=|ZUdUzIQAKEQH*v^0ABI{ z^geKW8gP#E7%gp*ar1#=%4Gj)pgtm?Z-GmK95_k?DlY+ws_ogRSu`PCj4LE)O*}h+tGNZ zJHheUR@6&DfIo**L?`8NimN(@Q(RR!oQ~U_ZV$&dT>DS4|M1ER0>dSZQ;@Q4mb61s zotZVH2&XFH8rKR)1#l~{se>JMOMvQ>=2x>k=U3;HYPc3y7t+|4k`4O#c#kKXq26r{ zFjms%lFkq5=SWF!0LLcG$EU{rZDzlC!2wQ`^p2!~5sjWMX->j$j`O(Zhf?NNFG?Eb z>FnHu;naHE^Su`wmmQp1g0lmF!3o0g*=D`gx8}@?!Qn9(hf^0Bj@uu8bqo$Ca@d($ zViL8MHcDFWQ*$nn)H?>hw`RE3yk}<3K5)jx;P5^L$G9XRpVbpfJG-D5oPjYoys)!x zCPqz;c>Q0JR{6p?u|#M8ko0Uq?MF~Kf8>Q8F9nbnt4Rp)rDT9~1P1yn{{e%l0$F4gFbA&vdz!WgLp z%{;;s3*s;uX3fhkpjIgg*cbuKFR>kM)y~nmZFPUjV5UR>(+o_F)A;;O3|^Qbm}7vo z5#T%;w*#kjVsLzlaGs6;=9O|ev8)55GgAUHwatc>EcJFSei!d#CEY){W0DU^?!8QT+0fRiB zKM1%B_%uRSy}n#qh0OCq+#qR@f^#U^Q;UK`;5r4T_it0MRk~5Z_U0-VX6UOR33#@G zq~UmlU)<0j=?O`T6AY?F`j%fS!%+-jdZKwiUu9juDZn#sw{W@C41DNbmy+1)TP#LM zy(P_b!KX_l4Jq-F`y_oT>3vBn(l|Za%2=a@J4h20hH>tBVztm>$7sUDac=c nC24);+LBzka^=dED|7k}Cp+Yxt)OTj00000NkvXXu0mjfFn}8V literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..75d3cbd3bd5931289026636b1426e69e97eca4ca GIT binary patch literal 2438 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 Ef(UtvIRF3v literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe2021df40680208e385ab41ee12ab157899712 GIT binary patch literal 1700 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 IM6N<$g4va<@Bjb+ literal 0 HcmV?d00001 From d6f36457a8736963b7710cab79ceca1f5512ac35 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 21:43:03 +0300 Subject: [PATCH 03/32] Fix legacy font glyphs being mistaken for animation and getting "extrapolated" --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 81c13112d0..49ba02fdea 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -152,25 +153,38 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; + private readonly HashSet legacyFontPrefixes = new HashSet(); + public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) : base(skin, storage, audioManager, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; + + legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); + legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); + legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); } public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { // extrapolate frames to test longer animations - if (extrapolateAnimations) - { - var match = Regex.Match(componentName, "-([0-9]*)"); - - if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60) - return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); - } + if (extrapolateAnimations && isAnimationComponent(componentName, out var number) && number < 60) + return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); return base.GetTexture(componentName, wrapModeS, wrapModeT); } + + private bool isAnimationComponent(string componentName, out int number) + { + number = 0; + + // legacy font glyph textures have the pattern "{fontPrefix}-{character}", which could be mistaken for an animation frame. + if (legacyFontPrefixes.Any(p => componentName.StartsWith($"{p}-"))) + return false; + + var match = Regex.Match(componentName, "-([0-9]*)"); + return match.Length > 0 && int.TryParse(match.Groups[1].Value, out number); + } } } } From f37ba49f7f624614b99ddfadc5e54785d0722b5b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:13:02 +0300 Subject: [PATCH 04/32] Add catch-specific combo counter with its legacy design --- .../Skinning/LegacyComboCounter.cs | 139 ++++++++++++++++++ .../UI/CatchComboDisplay.cs | 59 ++++++++ osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 32 +++- .../UI/ICatchComboCounter.cs | 34 +++++ 4 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs create mode 100644 osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs create mode 100644 osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs new file mode 100644 index 0000000000..9700bd0e08 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -0,0 +1,139 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Skinning +{ + internal class LegacyComboCounter : CompositeDrawable, ICatchComboCounter + { + private readonly ISkin skin; + + private readonly string fontName; + private readonly float fontOverlap; + + private readonly LegacyRollingCounter counter; + private LegacyRollingCounter lastExplosion; + + public LegacyComboCounter(ISkin skin, string fontName, float fontOverlap) + { + this.skin = skin; + + this.fontName = fontName; + this.fontOverlap = fontOverlap; + + AutoSizeAxes = Axes.Both; + + Alpha = 0f; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Scale = new Vector2(0.8f); + + InternalChild = counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); + public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); + + private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) + { + // Combo fell to zero, roll down and fade out the counter. + if (combo == 0) + { + counter.Current.Value = 0; + if (lastExplosion != null) + lastExplosion.Current.Value = 0; + + this.FadeOut(immediate ? 0.0 : 400.0, Easing.Out); + return; + } + + // There may still be previous transforms being applied, finish them and remove explosion. + FinishTransforms(true); + if (lastExplosion != null) + RemoveInternal(lastExplosion); + + this.FadeIn().Delay(1000.0).FadeOut(300.0); + + // For simplicity, in the case of rewinding we'll just set the counter to the current combo value. + immediate |= Time.Elapsed < 0; + + if (immediate) + { + counter.SetCountWithoutRolling(combo); + return; + } + + counter.ScaleTo(1.5f).ScaleTo(0.8f, 250.0, Easing.Out) + .OnComplete(c => c.SetCountWithoutRolling(combo)); + + counter.Delay(250.0).ScaleTo(1f).ScaleTo(1.1f, 60.0).Then().ScaleTo(1f, 30.0); + + var explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Alpha = 0.65f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), + Colour = hitObjectColour ?? Color4.White, + Depth = 1f, + }; + + AddInternal(explosion); + + explosion.SetCountWithoutRolling(combo); + explosion.ScaleTo(1.9f, 400.0, Easing.Out) + .FadeOut(400.0) + .Expire(true); + + lastExplosion = explosion; + } + + private class LegacyRollingCounter : RollingCounter + { + private readonly ISkin skin; + + private readonly string fontName; + private readonly float fontOverlap; + + protected override bool IsRollingProportional => true; + + public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) + { + this.skin = skin; + this.fontName = fontName; + this.fontOverlap = fontOverlap; + } + + public override void Increment(int amount) => Current.Value += amount; + + protected override double GetProportionalDuration(int currentValue, int newValue) + { + return Math.Abs(newValue - currentValue) * 75.0; + } + + protected override OsuSpriteText CreateSpriteText() + { + return new LegacySpriteText(skin, fontName) + { + Spacing = new Vector2(-fontOverlap, 0f) + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs new file mode 100644 index 0000000000..10aee2ea31 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatchComboDisplay : SkinnableDrawable + { + private int currentCombo; + + [CanBeNull] + public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter; + + public CatchComboDisplay() + : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty()) + { + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + ComboCounter?.DisplayInitialCombo(currentCombo); + } + + public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result) + { + if (!result.Judgement.AffectsCombo || !result.HasResult) + return; + + if (result.Type == HitResult.Miss) + { + updateCombo(0, null); + return; + } + + updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value); + } + + public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result) + { + if (!result.Judgement.AffectsCombo || !result.HasResult) + return; + + updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); + } + + private void updateCombo(int newCombo, Color4? hitObjectColour) + { + currentCombo = newCombo; + ComboCounter?.UpdateCombo(newCombo, hitObjectColour); + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 154e1576db..b5c040f80d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI public const float CENTER_X = WIDTH / 2; internal readonly CatcherArea CatcherArea; + private readonly CatchComboDisplay comboDisplay; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // only check the X position; handle all vertical space. @@ -48,12 +49,22 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopLeft, }; + comboDisplay = new CatchComboDisplay + { + AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.None, + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Y = 30f, + }; + InternalChildren = new[] { explodingFruitContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer, - CatcherArea + CatcherArea, + comboDisplay, }; } @@ -62,6 +73,7 @@ namespace osu.Game.Rulesets.Catch.UI public override void Add(DrawableHitObject h) { h.OnNewResult += onNewResult; + h.OnRevertResult += onRevertResult; base.Add(h); @@ -69,7 +81,23 @@ namespace osu.Game.Rulesets.Catch.UI fruit.CheckPosition = CheckIfWeCanCatch; } + protected override void Update() + { + base.Update(); + comboDisplay.X = CatcherArea.MovableCatcher.X; + } + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) - => CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result); + { + var catchObject = (DrawableCatchHitObject)judgedObject; + CatcherArea.OnResult(catchObject, result); + + comboDisplay.OnNewResult(catchObject, result); + } + + private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result) + { + comboDisplay.OnRevertResult((DrawableCatchHitObject)judgedObject, result); + } } } diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs new file mode 100644 index 0000000000..1363ed1352 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + /// + /// An interface providing a set of methods to update the combo counter. + /// + public interface ICatchComboCounter : IDrawable + { + /// + /// Updates the counter to display the provided as initial value. + /// The value should be immediately displayed without any animation. + /// + /// + /// This is required for when instantiating a combo counter in middle of accumulating combo (via skin change). + /// + /// The combo value to be displayed as initial. + void DisplayInitialCombo(int combo); + + /// + /// Updates the counter to animate a transition from the old combo value it had to the current provided one. + /// + /// + /// This is called regardless of whether the clock is rewinding. + /// + /// The new combo value. + /// The colour of the object if hit, null on miss. + void UpdateCombo(int combo, Color4? hitObjectColour); + } +} From 21eaf0e99515fb708f5e00787295f94296f997ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:14:00 +0300 Subject: [PATCH 05/32] Expose "is break time" bindable within GameplayBeatmap --- osu.Game/Screens/Play/GameplayBeatmap.cs | 5 +++++ osu.Game/Screens/Play/Player.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 64894544f4..d7eed73275 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -16,6 +16,11 @@ namespace osu.Game.Screens.Play { public readonly IBeatmap PlayableBeatmap; + /// + /// Whether the gameplay is currently in a break. + /// + public IBindable IsBreakTime { get; } = new Bindable(); + public GameplayBeatmap(IBeatmap playableBeatmap) { PlayableBeatmap = playableBeatmap; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..ee32ded93d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -612,6 +612,7 @@ namespace osu.Game.Screens.Play // bind component bindables. Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); + gameplayBeatmap.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From 65c269e473d15abb7bebd072dbb01f96f9849869 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:17:11 +0300 Subject: [PATCH 06/32] Hide combo counter on gameplay break Intentionally inside LegacyComboCounter and not in CatchComboDisplay, to avoid conflicting with how the legacy combo counter fades away after 1 second of no combo update, can move to parent once a DefaultComboCounter design is decided and code is shareable between. --- .../Skinning/LegacyComboCounter.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 9700bd0e08..90dd1f4e9f 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -47,6 +47,23 @@ namespace osu.Game.Rulesets.Catch.Skinning }; } + private IBindable isBreakTime; + + [Resolved(canBeNull: true)] + private GameplayBeatmap beatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + isBreakTime = beatmap?.IsBreakTime.GetBoundCopy(); + isBreakTime?.BindValueChanged(b => + { + if (b.NewValue) + this.FadeOut(400.0, Easing.OutQuint); + }); + } + public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); From 5cd2841080caf8e41b135d740a246f5d6535abae Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:17:42 +0300 Subject: [PATCH 07/32] Add test scene showing off the skinnable catch-specific combo counter --- .../TestSceneComboCounter.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs new file mode 100644 index 0000000000..2581e305dd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneComboCounter : CatchSkinnableTestScene + { + private ScoreProcessor scoreProcessor; + private GameplayBeatmap gameplayBeatmap; + private readonly Bindable isBreakTime = new BindableBool(); + + [BackgroundDependencyLoader] + private void load() + { + gameplayBeatmap = new GameplayBeatmap(CreateBeatmapForSkinProvider()); + gameplayBeatmap.IsBreakTime.BindTo(isBreakTime); + Dependencies.Cache(gameplayBeatmap); + Add(gameplayBeatmap); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + scoreProcessor = new ScoreProcessor(); + + SetContents(() => new CatchComboDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2.5f), + }); + }); + + [Test] + public void TestCatchComboCounter() + { + AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); + AddStep("perform miss", () => performJudgement(HitResult.Miss)); + AddToggleStep("toggle gameplay break", v => isBreakTime.Value = v); + } + + private void performJudgement(HitResult type, Judgement judgement = null) + { + var judgedObject = new TestDrawableCatchHitObject(new TestCatchHitObject()); + var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type }; + scoreProcessor.ApplyResult(result); + + foreach (var counter in CreatedDrawables.Cast()) + counter.OnNewResult(judgedObject, result); + } + + private class TestDrawableCatchHitObject : DrawableCatchHitObject + { + public TestDrawableCatchHitObject(CatchHitObject hitObject) + : base(hitObject) + { + AccentColour.Value = Color4.White; + } + } + + private class TestCatchHitObject : CatchHitObject + { + } + } +} From 4dbf695bca47f62ef35bc8724ae64871a752720c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 4 Aug 2020 00:04:00 +0300 Subject: [PATCH 08/32] Fix wrong ordering --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index b5c040f80d..d4a1740c12 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay = new CatchComboDisplay { - AutoSizeAxes = Axes.Both, RelativeSizeAxes = Axes.None, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, Y = 30f, From 422100192c3a6c14628b03369be6dbfdb93cf18d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 07:58:23 +0300 Subject: [PATCH 09/32] Move HasFont to legacy skin extensions class instead --- osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 2 +- osu.Game/Skinning/LegacySkinExtensions.cs | 3 +++ osu.Game/Skinning/LegacySkinTransformer.cs | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index c2432e1dbb..0e434291c1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Skinning var fontOverlap = GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. - if (HasFont(comboFont)) + if (this.HasFont(comboFont)) return new LegacyComboCounter(Source, comboFont, fontOverlap); break; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index b955150c90..851a8d56c9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2; - return !HasFont(font) + return !this.HasFont(font) ? null : new LegacySpriteText(Source, font) { diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index bb46dc8b9f..0ee02a2442 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -62,6 +62,9 @@ namespace osu.Game.Skinning } } + public static bool HasFont(this ISkin source, string fontPrefix) + => source.GetTexture($"{fontPrefix}-0") != null; + public class SkinnableTextureAnimation : TextureAnimation { [Resolved(canBeNull: true)] diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index acca53835c..ebc4757e75 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -47,7 +47,5 @@ namespace osu.Game.Skinning } public abstract IBindable GetConfig(TLookup lookup); - - protected bool HasFont(string fontPrefix) => GetTexture($"{fontPrefix}-0") != null; } } From 885f8104f566cb350aef38bc8e99649f49a4a7ea Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:00:57 +0300 Subject: [PATCH 10/32] Always use public accessors even on legacy classes Because of https://github.com/ppy/osu-framework/issues/3727 --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 90dd1f4e9f..13c751ac5d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { - internal class LegacyComboCounter : CompositeDrawable, ICatchComboCounter + public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { private readonly ISkin skin; From d4bde0afe57c04a01fe5ab880f7dc70686fc0f0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:18:30 +0300 Subject: [PATCH 11/32] Do not pass accent value on a reverted miss judgement --- osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index 10aee2ea31..351610646d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -47,6 +47,12 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Judgement.AffectsCombo || !result.HasResult) return; + if (result.Type == HitResult.Miss) + { + updateCombo(result.ComboAtJudgement, null); + return; + } + updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); } From a59dabca7ed30c48e6c8cae98b8fe6c2558b9e5c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:28:11 +0300 Subject: [PATCH 12/32] Use existing hit objects instead of defining own --- .../TestSceneComboCounter.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 2581e305dd..079cdfc44b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -55,25 +55,13 @@ namespace osu.Game.Rulesets.Catch.Tests private void performJudgement(HitResult type, Judgement judgement = null) { - var judgedObject = new TestDrawableCatchHitObject(new TestCatchHitObject()); + var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } }; + var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type }; scoreProcessor.ApplyResult(result); foreach (var counter in CreatedDrawables.Cast()) counter.OnNewResult(judgedObject, result); } - - private class TestDrawableCatchHitObject : DrawableCatchHitObject - { - public TestDrawableCatchHitObject(CatchHitObject hitObject) - : base(hitObject) - { - AccentColour.Value = Color4.White; - } - } - - private class TestCatchHitObject : CatchHitObject - { - } } } From dde0bc6070f41bf001e4d108bf8a0fe77f22191b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:29:24 +0300 Subject: [PATCH 13/32] Add step to randomize judged object's combo colour --- .../TestSceneComboCounter.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 079cdfc44b..89521d616d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; @@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Tests private GameplayBeatmap gameplayBeatmap; private readonly Bindable isBreakTime = new BindableBool(); + private Color4 judgedObjectColour = Color4.White; + [BackgroundDependencyLoader] private void load() { @@ -50,7 +53,17 @@ namespace osu.Game.Rulesets.Catch.Tests { AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); AddStep("perform miss", () => performJudgement(HitResult.Miss)); + AddToggleStep("toggle gameplay break", v => isBreakTime.Value = v); + AddStep("randomize judged object colour", () => + { + judgedObjectColour = new Color4( + RNG.NextSingle(1f), + RNG.NextSingle(1f), + RNG.NextSingle(1f), + 1f + ); + }); } private void performJudgement(HitResult type, Judgement judgement = null) From af52b73b06ed9b6482ebb7cba4c0765b19ebdc24 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:39:40 +0300 Subject: [PATCH 14/32] Fill out missing documentation --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 3 +++ osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 13c751ac5d..8ea06688cb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -16,6 +16,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { + /// + /// A combo counter implementation that visually behaves almost similar to osu!stable's combo counter. + /// public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { private readonly ISkin skin; diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index 351610646d..b53711e4ed 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -10,6 +10,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { + /// + /// Represents a component that displays a skinned and handles combo judgement results for updating it accordingly. + /// public class CatchComboDisplay : SkinnableDrawable { private int currentCombo; From 7ae45b29dbc82c98012c4525bbf7a13d10bef161 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 22 Aug 2020 12:20:50 +0300 Subject: [PATCH 15/32] Finish internal counter transformation regardless of the combo --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 8ea06688cb..0d9df4f9a0 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Skinning private readonly float fontOverlap; private readonly LegacyRollingCounter counter; - private LegacyRollingCounter lastExplosion; public LegacyComboCounter(ISkin skin, string fontName, float fontOverlap) { @@ -70,8 +69,14 @@ namespace osu.Game.Rulesets.Catch.Skinning public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); + private LegacyRollingCounter lastExplosion; + private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) { + // There may still be existing transforms to the counter (including value change after 250ms), + // finish them immediately before new transforms. + counter.FinishTransforms(); + // Combo fell to zero, roll down and fade out the counter. if (combo == 0) { @@ -83,8 +88,7 @@ namespace osu.Game.Rulesets.Catch.Skinning return; } - // There may still be previous transforms being applied, finish them and remove explosion. - FinishTransforms(true); + // Remove last explosion to not conflict with the upcoming one. if (lastExplosion != null) RemoveInternal(lastExplosion); From 7e838c80420d35a9c633cfc038fd3afd7ecf728a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 22 Aug 2020 13:07:15 +0300 Subject: [PATCH 16/32] Add comment explaining why direct string lookups are used --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 49ba02fdea..a9e3d36ca0 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -160,6 +160,7 @@ namespace osu.Game.Tests.Visual { this.extrapolateAnimations = extrapolateAnimations; + // Use a direct string lookup for simplicity, as they're legacy settings and not worth creating enums for them. legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); From fafdbb0a81ae98b0070f24f48622494aa2da6f0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 22 Aug 2020 17:26:54 +0300 Subject: [PATCH 17/32] Adjust recently added inline comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index a9e3d36ca0..58e0b23fab 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual { this.extrapolateAnimations = extrapolateAnimations; - // Use a direct string lookup for simplicity, as they're legacy settings and not worth creating enums for them. + // use a direct string lookup instead of enum to avoid having to reference ruleset assemblies. legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); From 0e9242ee9a327e220d7716c812ffb4bb3468790d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Aug 2020 10:28:05 +0300 Subject: [PATCH 18/32] Move combo font retrieval inside the legacy component --- .../Skinning/CatchLegacySkinTransformer.cs | 3 +-- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 0e434291c1..28cd0fb65b 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -55,11 +55,10 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.CatchComboCounter: var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; - var fontOverlap = GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. if (this.HasFont(comboFont)) - return new LegacyComboCounter(Source, comboFont, fontOverlap); + return new LegacyComboCounter(Source); break; } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 0d9df4f9a0..c1c70c3a0e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -13,6 +13,7 @@ using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning { @@ -28,12 +29,12 @@ namespace osu.Game.Rulesets.Catch.Skinning private readonly LegacyRollingCounter counter; - public LegacyComboCounter(ISkin skin, string fontName, float fontOverlap) + public LegacyComboCounter(ISkin skin) { this.skin = skin; - this.fontName = fontName; - this.fontOverlap = fontOverlap; + fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; + fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; AutoSizeAxes = Axes.Both; From ba8a4eb6f0ffd493b346701a8d1886796f6736c5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Aug 2020 23:14:29 +0300 Subject: [PATCH 19/32] Move osu!catch combo counter display to inside CatcherArea --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 19 ++----------------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index d4a1740c12..409ea6dbc6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -28,7 +28,8 @@ namespace osu.Game.Rulesets.Catch.UI public const float CENTER_X = WIDTH / 2; internal readonly CatcherArea CatcherArea; - private readonly CatchComboDisplay comboDisplay; + + private CatchComboDisplay comboDisplay => CatcherArea.ComboDisplay; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // only check the X position; handle all vertical space. @@ -49,22 +50,12 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopLeft, }; - comboDisplay = new CatchComboDisplay - { - RelativeSizeAxes = Axes.None, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - Y = 30f, - }; - InternalChildren = new[] { explodingFruitContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer, CatcherArea, - comboDisplay, }; } @@ -81,12 +72,6 @@ namespace osu.Game.Rulesets.Catch.UI fruit.CheckPosition = CheckIfWeCanCatch; } - protected override void Update() - { - base.Update(); - comboDisplay.X = CatcherArea.MovableCatcher.X; - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { var catchObject = (DrawableCatchHitObject)judgedObject; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 03ebf01b9b..9cfb9f41d7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI public Func> CreateDrawableRepresentation; public readonly Catcher MovableCatcher; + internal readonly CatchComboDisplay ComboDisplay; public Container ExplodingFruitTarget { @@ -34,7 +35,19 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherArea(BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); - Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }; + Children = new Drawable[] + { + ComboDisplay = new CatchComboDisplay + { + RelativeSizeAxes = Axes.None, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopLeft, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 350f }, + X = CatchPlayfield.CENTER_X + }, + MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }, + }; } public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) @@ -105,6 +118,8 @@ namespace osu.Game.Rulesets.Catch.UI if (state?.CatcherX != null) MovableCatcher.X = state.CatcherX.Value; + + ComboDisplay.X = MovableCatcher.X; } } } From a0a45010080308898c088430c8aa3e66db4ce98d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Aug 2020 23:23:36 +0300 Subject: [PATCH 20/32] Merge remote-tracking branch 'upstream/master' into catch-combo-counter --- .../Mods/TestSceneCatchModRelax.cs | 84 ++++++++++++ .../TestSceneHyperDash.cs | 42 ++++-- .../Beatmaps/CatchBeatmapProcessor.cs | 6 + osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 2 +- .../Skinning/CatchLegacySkinTransformer.cs | 8 +- .../Skinning/LegacyFruitPiece.cs | 3 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 9 +- .../UI/CatcherTrailDisplay.cs | 8 +- .../Skinning/ColumnTestContainer.cs | 24 ++-- .../Skinning/ManiaHitObjectTestScene.cs | 4 +- .../Skinning/TestSceneStageBackground.cs | 4 +- .../Skinning/TestSceneStageForeground.cs | 3 +- .../Beatmaps/StageDefinition.cs | 4 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 10 ++ osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 11 +- .../Objects/Drawables/DrawableHoldNote.cs | 92 ++++++++++++- .../Objects/Drawables/DrawableHoldNoteHead.cs | 8 ++ .../Drawables/DrawableManiaHitObject.cs | 2 +- .../Skinning/HitTargetInsetContainer.cs | 46 +++++++ .../Skinning/LegacyBodyPiece.cs | 109 ++++++++++++++-- .../Skinning/LegacyColumnBackground.cs | 65 +-------- .../Skinning/LegacyHitTarget.cs | 7 +- .../Skinning/LegacyKeyArea.cs | 3 + .../Skinning/LegacyStageBackground.cs | 88 ++++++++++++- .../Skinning/ManiaLegacySkinTransformer.cs | 10 +- osu.Game.Rulesets.Mania/UI/Column.cs | 2 - osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 105 +++++++++++++++ osu.Game.Rulesets.Mania/UI/Stage.cs | 69 ++-------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 52 +++++--- .../Skinning/LegacyMainCirclePiece.cs | 3 +- .../Skinning/LegacySliderBall.cs | 6 +- .../Skinning/LegacyCirclePiece.cs | 2 +- .../Skinning/LegacyDrumRoll.cs | 8 +- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 8 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 33 +++-- .../Beatmaps/BeatmapDifficultyManagerTest.cs | 32 +++++ .../Formats/LegacyScoreDecoderTest.cs | 70 ++++++++++ ...tSceneHitObjectComposerDistanceSnapping.cs | 14 +- .../Filtering/FilterQueryParserTest.cs | 16 ++- .../Resources/Replays/mania-replay.osr | Bin 0 -> 1012 bytes .../Resources/skin-zero-alpha-colour.ini | 5 - osu.Game.Tests/Skins/LegacySkinDecoderTest.cs | 10 -- .../TestSceneBeatmapSetOverlaySuccessRate.cs | 28 ++++ .../Ranking/TestSceneSimpleStatisticTable.cs | 68 ++++++++++ osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 14 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 4 - osu.Game/Online/Chat/MessageFormatter.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 13 +- .../Rulesets/Edit/IPositionSnapProvider.cs | 1 + osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 26 ++-- osu.Game/Screens/Menu/IntroSequence.cs | 1 + .../Ranking/Statistics/SimpleStatisticItem.cs | 81 ++++++++++++ .../Statistics/SimpleStatisticTable.cs | 123 ++++++++++++++++++ .../Ranking/Statistics/StatisticContainer.cs | 60 +++++---- .../Ranking/Statistics/StatisticItem.cs | 2 +- .../Ranking/Statistics/StatisticsPanel.cs | 6 +- .../Ranking/Statistics/UnstableRate.cs | 39 ++++++ osu.Game/Screens/Select/BeatmapDetails.cs | 2 +- .../Screens/Select/Details/FailRetryGraph.cs | 20 ++- osu.Game/Screens/Select/FilterQueryParser.cs | 3 +- .../Skinning/LegacyColourCompatibility.cs | 46 +++++++ .../Skinning/LegacyManiaSkinConfiguration.cs | 3 + .../LegacyManiaSkinConfigurationLookup.cs | 3 + osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 9 ++ osu.Game/Skinning/LegacySkin.cs | 20 +++ 65 files changed, 1352 insertions(+), 309 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs create mode 100644 osu.Game.Rulesets.Mania/UI/ColumnFlow.cs create mode 100644 osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs create mode 100644 osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs create mode 100644 osu.Game.Tests/Resources/Replays/mania-replay.osr delete mode 100644 osu.Game.Tests/Resources/skin-zero-alpha-colour.ini create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/UnstableRate.cs create mode 100644 osu.Game/Skinning/LegacyColourCompatibility.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs new file mode 100644 index 0000000000..1eb0975010 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + public class TestSceneCatchModRelax : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Test] + public void TestModRelax() => CreateModTest(new ModTestData + { + Mod = new CatchModRelax(), + Autoplay = false, + PassCondition = passCondition, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Fruit + { + X = CatchPlayfield.CENTER_X, + StartTime = 0 + }, + new Fruit + { + X = 0, + StartTime = 250 + }, + new Fruit + { + X = CatchPlayfield.WIDTH, + StartTime = 500 + }, + new JuiceStream + { + X = CatchPlayfield.CENTER_X, + StartTime = 750, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) + } + } + } + }); + + private bool passCondition() + { + var playfield = this.ChildrenOfType().Single(); + + switch (Player.ScoreProcessor.Combo.Value) + { + case 0: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + break; + + case 1: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft); + break; + + case 2: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight); + break; + + case 3: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + break; + } + + return Player.ScoreProcessor.Combo.Value >= 6; + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 6dab2a0b56..db09b2bc6b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -19,25 +19,43 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override bool Autoplay => true; + private int hyperDashCount; + private bool inHyperDash; + [Test] public void TestHyperDash() { - AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast. - - AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0); - - for (int i = 0; i < 3; i++) + AddStep("reset count", () => { - AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); - AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + inHyperDash = false; + hyperDashCount = 0; + + // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. + Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => + { + var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher; + + if (catcher == null) + return; + + if (catcher.HyperDashing != inHyperDash) + { + inHyperDash = catcher.HyperDashing; + if (catcher.HyperDashing) + hyperDashCount++; + } + }; + }); + + AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); + + for (int i = 0; i < 9; i++) + { + int count = i + 1; + AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count); } - - AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); } - private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 15e6e98f5a..a08c5b6fb1 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; + + // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins. + // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible. + // For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size. + halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE; + int lastDirection = 0; double lastExcess = halfCatcherWidth; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index c1d24395e4..1e42c6a240 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods protected override bool OnMouseMove(MouseMoveEvent e) { - catcher.UpdatePosition(e.MousePosition.X / DrawSize.X); + catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH); return base.OnMouseMove(e); } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 28cd0fb65b..47224bd195 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning @@ -71,7 +72,12 @@ namespace osu.Game.Rulesets.Catch.Skinning switch (lookup) { case CatchSkinColour colour: - return Source.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour)); + if (result == null) + return null; + + result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value); + return (IBindable)result; } return Source.GetConfig(lookup); diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 5be54d3882..381d066750 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning colouredSprite = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 952ff6b0ce..9289a6162c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. /// - private const float allowed_catch_range = 0.8f; + public const float ALLOWED_CATCH_RANGE = 0.8f; /// /// The drawable catcher for . @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The scale of the catcher. internal static float CalculateCatchWidth(Vector2 scale) - => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range; + => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; /// /// Calculates the width of the area used for attempting catches in gameplay. @@ -285,8 +285,6 @@ namespace osu.Game.Rulesets.Catch.UI private void runHyperDashStateTransition(bool hyperDashing) { - trails.HyperDashTrailsColour = hyperDashColour; - trails.EndGlowSpritesColour = hyperDashEndGlowColour; updateTrailVisibility(); if (hyperDashing) @@ -403,6 +401,9 @@ namespace osu.Game.Rulesets.Catch.UI skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashColour; + trails.HyperDashTrailsColour = hyperDashColour; + trails.EndGlowSpritesColour = hyperDashEndGlowColour; + runHyperDashStateTransition(HyperDashing); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index bab3cb748b..f7e9fd19a7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly Container hyperDashTrails; private readonly Container endGlowSprites; - private Color4 hyperDashTrailsColour; + private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; public Color4 HyperDashTrailsColour { @@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Catch.UI return; hyperDashTrailsColour = value; - hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + hyperDashTrails.Colour = hyperDashTrailsColour; } } - private Color4 endGlowSpritesColour; + private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; public Color4 EndGlowSpritesColour { @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI return; endGlowSpritesColour = value; - endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + endGlowSprites.Colour = endGlowSpritesColour; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index ff4865c71d..8ba58e3af3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private readonly Column column; - public ColumnTestContainer(int column, ManiaAction action) + public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false) { - this.column = new Column(column) + InternalChildren = new[] { - Action = { Value = action }, - AccentColour = Color4.Orange, - ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd - }; - - InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) - { - RelativeSizeAxes = Axes.Both + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange, + ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd, + Alpha = showColumn ? 1 : 0 + }, + content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }, + this.column.TopLevelContainer.CreateProxy() }; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 18eebada00..d24c81dac6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ColumnTestContainer(0, ManiaAction.Key1) + new ColumnTestContainer(0, ManiaAction.Key1, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning })); }) }, - new ColumnTestContainer(1, ManiaAction.Key2) + new ColumnTestContainer(1, ManiaAction.Key2, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 87c84cf89c..a15fb392d6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Skinning; @@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }), + _ => new DefaultStageBackground()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs index 4e99068ed5..bceee1c599 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Tests.Skinning @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index 2557f2acdf..3052fc7d34 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// /// The 0-based column index. /// Whether the column is a special column. - public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; + public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; /// /// Get the type of column given a column index. /// /// The 0-based column index. /// The type of the column. - public ColumnType GetTypeOfColumn(int column) + public readonly ColumnType GetTypeOfColumn(int column) { if (IsSpecialColumn(column)) return ColumnType.Special; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2795868c97..f7098faa5d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -326,6 +326,16 @@ namespace osu.Game.Rulesets.Mania Height = 250 }), } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) + } } }; } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index c0c8505f44..f078345fc1 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; @@ -14,15 +15,23 @@ namespace osu.Game.Rulesets.Mania /// public readonly int? TargetColumn; + /// + /// The intended for this component. + /// May be null if the component is not a direct member of a . + /// + public readonly StageDefinition? StageDefinition; + /// /// Creates a new . /// /// The component. /// The intended index for this component. May be null if the component does not exist in a . - public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null) + /// The intended for this component. May be null if the component is not a direct member of a . + public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null) : base(component) { TargetColumn = targetColumn; + StageDefinition = stageDefinition; } protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a44f8a8886..549a71daaa 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -32,6 +33,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tailContainer; private readonly Container tickContainer; + /// + /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. + /// + private readonly Container sizingContainer; + + /// + /// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of . + /// + private readonly Container maskingContainer; + private readonly SkinnableDrawable bodyPiece; /// @@ -44,24 +55,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public bool HasBroken { get; private set; } + /// + /// Whether the hold note has been released potentially without having caused a break. + /// + private double? releaseTime; + public DrawableHoldNote(HoldNote hitObject) : base(hitObject) { RelativeSizeAxes = Axes.X; + Container maskedContents; + AddRangeInternal(new Drawable[] { + sizingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + maskingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = maskedContents = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + } + }, + headContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }, bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, }) { RelativeSizeAxes = Axes.X }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - headContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); + + maskedContents.AddRange(new[] + { + bodyPiece.CreateProxy(), + tickContainer.CreateProxy(), + tailContainer.CreateProxy(), + }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -127,7 +168,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.OnDirectionChanged(e); - bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + if (e.NewValue == ScrollingDirection.Up) + { + bodyPiece.Anchor = bodyPiece.Origin = Anchor.TopLeft; + sizingContainer.Anchor = sizingContainer.Origin = Anchor.BottomLeft; + } + else + { + bodyPiece.Anchor = bodyPiece.Origin = Anchor.BottomLeft; + sizingContainer.Anchor = sizingContainer.Origin = Anchor.TopLeft; + } } public override void PlaySamples() @@ -145,9 +195,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.Update(); - // Make the body piece not lie under the head note + if (Time.Current < releaseTime) + releaseTime = null; + + // Pad the full size container so its contents (i.e. the masking container) reach under the tail. + // This is required for the tail to not be masked away, since it lies outside the bounds of the hold note. + sizingContainer.Padding = new MarginPadding + { + Top = Direction.Value == ScrollingDirection.Down ? -Tail.Height : 0, + Bottom = Direction.Value == ScrollingDirection.Up ? -Tail.Height : 0, + }; + + // Pad the masking container to the starting position of the body piece (half-way under the head). + // This is required to make the body start getting masked immediately as soon as the note is held. + maskingContainer.Padding = new MarginPadding + { + Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0, + Bottom = Direction.Value == ScrollingDirection.Down ? Head.Height / 2 : 0, + }; + + // Position and resize the body to lie half-way under the head and the tail notes. bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; + + // As the note is being held, adjust the size of the sizing container. This has two effects: + // 1. The contained masking container will mask the body and ticks. + // 2. The head note will move along with the new "head position" in the container. + if (Head.IsHit && releaseTime == null) + { + // How far past the hit target this hold note is. Always a positive value. + float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y); + sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1); + } } protected override void UpdateStateTransforms(ArmedState state) @@ -159,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) + { ApplyResult(r => r.Type = HitResult.Perfect); + endHold(); + } if (Tail.Result.Type == HitResult.Miss) HasBroken = true; @@ -212,6 +294,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) HasBroken = true; + + releaseTime = Time.Current; } private void endHold() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index a73fe259e4..cd56b81e10 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects.Drawables; + namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// @@ -17,6 +19,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); + protected override void UpdateStateTransforms(ArmedState state) + { + // This hitobject should never expire, so this is just a safe maximum. + LifetimeEnd = LifetimeStart + 30000; + } + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note public override void OnReleased(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index a44d8b09aa..ab76a5b8f8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables break; case ArmedState.Hit: - this.FadeOut(150, Easing.OutQuint); + this.FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs new file mode 100644 index 0000000000..c8b05ed2f8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9f716428c0..c0f0fcb4af 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); - private Drawable sprite; + [CanBeNull] + private Drawable bodySprite; + + [CanBeNull] + private Drawable lightContainer; + + [CanBeNull] + private Drawable light; public LegacyBodyPiece() { @@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; - sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => + string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value + ?? "lightingL"; + + float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value + ?? 1; + + // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. + // This animation is discarded and re-queried with the appropriate frame length afterwards. + var tmp = skin.GetAnimation(lightImage, true, false); + double frameLength = 0; + if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); + + light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d => + { + if (d == null) + return; + + d.Origin = Anchor.Centre; + d.Blending = BlendingParameters.Additive; + d.Scale = new Vector2(lightScale); + }); + + if (light != null) + { + lightContainer = new HitTargetInsetContainer + { + Alpha = 0, + Child = light + }; + } + + bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { if (d == null) return; @@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning // Todo: Wrap }); - if (sprite != null) - InternalChild = sprite; + if (bodySprite != null) + InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); @@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning private void onIsHittingChanged(ValueChangedEvent isHitting) { - if (!(sprite is TextureAnimation animation)) + if (bodySprite is TextureAnimation bodyAnimation) + { + bodyAnimation.GotoFrame(0); + bodyAnimation.IsPlaying = isHitting.NewValue; + } + + if (lightContainer == null) return; - animation.GotoFrame(0); - animation.IsPlaying = isHitting.NewValue; + if (isHitting.NewValue) + { + // Clear the fade out and, more importantly, the removal. + lightContainer.ClearTransforms(); + + // Only add the container if the removal has taken place. + if (lightContainer.Parent == null) + Column.TopLevelContainer.Add(lightContainer); + + // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). + if (light is TextureAnimation lightAnimation) + lightAnimation.GotoFrame(0); + + lightContainer.FadeIn(80); + } + else + { + lightContainer.FadeOut(120) + .OnComplete(d => Column.TopLevelContainer.Remove(d)); + } } private void onDirectionChanged(ValueChangedEvent direction) { - if (sprite == null) - return; - if (direction.NewValue == ScrollingDirection.Up) { - sprite.Origin = Anchor.BottomCentre; - sprite.Scale = new Vector2(1, -1); + if (bodySprite != null) + { + bodySprite.Origin = Anchor.BottomCentre; + bodySprite.Scale = new Vector2(1, -1); + } + + if (light != null) + light.Anchor = Anchor.TopCentre; } else { - sprite.Origin = Anchor.TopCentre; - sprite.Scale = Vector2.One; + if (bodySprite != null) + { + bodySprite.Origin = Anchor.TopCentre; + bodySprite.Scale = Vector2.One; + } + + if (light != null) + light.Anchor = Anchor.BottomCentre; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + lightContainer?.Expire(); + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index f9286b5095..3bf51b3073 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -5,10 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; @@ -19,17 +17,12 @@ namespace osu.Game.Rulesets.Mania.Skinning public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler { private readonly IBindable direction = new Bindable(); - private readonly bool isLastColumn; - private Container borderLineContainer; private Container lightContainer; private Sprite light; - private float hitPosition; - - public LegacyColumnBackground(bool isLastColumn) + public LegacyColumnBackground() { - this.isLastColumn = isLastColumn; RelativeSizeAxes = Axes.Both; } @@ -39,62 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value ?? "mania-stage-light"; - float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) - ?.Value ?? 1; - float rightLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth) - ?.Value ?? 1; - - bool hasLeftLine = leftLineWidth > 0; - bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m - || isLastColumn; - - hitPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitPosition)?.Value - ?? Stage.HIT_TARGET_POSITION; - float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value ?? 0; - Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value - ?? Color4.White; - - Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value - ?? Color4.Black; - Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; - InternalChildren = new Drawable[] + InternalChildren = new[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - borderLineContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Y, - Width = leftLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = rightLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 - } - } - }, lightContainer = new Container { Origin = Anchor.BottomCentre, @@ -104,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = lightColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, @@ -123,15 +68,11 @@ namespace osu.Game.Rulesets.Mania.Skinning { lightContainer.Anchor = Anchor.TopCentre; lightContainer.Scale = new Vector2(1, -1); - - borderLineContainer.Padding = new MarginPadding { Top = hitPosition }; } else { lightContainer.Anchor = Anchor.BottomCentre; lightContainer.Scale = Vector2.One; - - borderLineContainer.Padding = new MarginPadding { Bottom = hitPosition }; } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index d055ef3480..6eced571d2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning private Container directionContainer; - public LegacyHitTarget() - { - RelativeSizeAxes = Axes.Both; - } - [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { @@ -56,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, - Colour = lineColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour), Alpha = showJudgementLine ? 0.9f : 0 } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 44f3e7d7b3..b269ea25d4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false) + Column.UnderlayElements.Add(CreateProxy()); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 7f5de601ca..b0bab8e760 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -4,19 +4,27 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning { public class LegacyStageBackground : CompositeDrawable { + private readonly StageDefinition stageDefinition; + private Drawable leftSprite; private Drawable rightSprite; + private ColumnFlow columnBackgrounds; - public LegacyStageBackground() + public LegacyStageBackground(StageDefinition stageDefinition) { + this.stageDefinition = stageDefinition; RelativeSizeAxes = Axes.Both; } @@ -44,8 +52,19 @@ namespace osu.Game.Rulesets.Mania.Skinning Origin = Anchor.TopLeft, X = -0.05f, Texture = skin.GetTexture(rightImage) + }, + columnBackgrounds = new ColumnFlow(stageDefinition) + { + RelativeSizeAxes = Axes.Y + }, + new HitTargetInsetContainer + { + Child = new LegacyHitTarget { RelativeSizeAxes = Axes.Both } } }; + + for (int i = 0; i < stageDefinition.Columns; i++) + columnBackgrounds.SetContentForColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1)); } protected override void Update() @@ -58,5 +77,72 @@ namespace osu.Game.Rulesets.Mania.Skinning if (rightSprite?.Height > 0) rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height); } + + private class ColumnBackground : CompositeDrawable + { + private readonly int columnIndex; + private readonly bool isLastColumn; + + public ColumnBackground(int columnIndex, bool isLastColumn) + { + this.columnIndex = columnIndex; + this.isLastColumn = isLastColumn; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + float leftLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftLineWidth, columnIndex)?.Value ?? 1; + float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1; + + bool hasLeftLine = leftLineWidth > 0; + bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m + || isLastColumn; + + Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White; + Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black; + + InternalChildren = new Drawable[] + { + LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, backgroundColour), + new HitTargetInsetContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = leftLineWidth, + Scale = new Vector2(0.740f, 1), + Alpha = hasLeftLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = rightLineWidth, + Scale = new Vector2(0.740f, 1), + Alpha = hasRightLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) + }, + } + } + }; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index e167135556..439e6f7df2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Audio.Sample; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; @@ -88,10 +89,12 @@ namespace osu.Game.Rulesets.Mania.Skinning switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: - return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1); + return new LegacyColumnBackground(); case ManiaSkinComponents.HitTarget: - return new LegacyHitTarget(); + // Legacy skins sandwich the hit target between the column background and the column light. + // To preserve this ordering, it's created manually inside LegacyStageBackground. + return Drawable.Empty(); case ManiaSkinComponents.KeyArea: return new LegacyKeyArea(); @@ -112,7 +115,8 @@ namespace osu.Game.Rulesets.Mania.Skinning return new LegacyHitExplosion(); case ManiaSkinComponents.StageBackground: - return new LegacyStageBackground(); + Debug.Assert(maniaComponent.StageDefinition != null); + return new LegacyStageBackground(maniaComponent.StageDefinition.Value); case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 255ce4c064..de4648e4fa 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -68,8 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); } - public override Axes RelativeSizeAxes => Axes.Y; - public ColumnType ColumnType { get; set; } public bool IsSpecial => ColumnType == ColumnType.Special; diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs new file mode 100644 index 0000000000..aef82d4c08 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// A which flows its contents according to the s in a . + /// Content can be added to individual columns via . + /// + /// The type of content in each column. + public class ColumnFlow : CompositeDrawable + where TContent : Drawable + { + /// + /// All contents added to this . + /// + public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList(); + + private readonly FillFlowContainer columns; + private readonly StageDefinition stageDefinition; + + public ColumnFlow(StageDefinition stageDefinition) + { + this.stageDefinition = stageDefinition; + + AutoSizeAxes = Axes.X; + + InternalChild = columns = new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + }; + + for (int i = 0; i < stageDefinition.Columns; i++) + columns.Add(new Container { RelativeSizeAxes = Axes.Y }); + } + + private ISkinSource currentSkin; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + currentSkin = skin; + + skin.SourceChanged += onSkinChanged; + onSkinChanged(); + } + + private void onSkinChanged() + { + for (int i = 0; i < stageDefinition.Columns; i++) + { + if (i > 0) + { + float spacing = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) + ?.Value ?? Stage.COLUMN_SPACING; + + columns[i].Margin = new MarginPadding { Left = spacing }; + } + + float? width = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) + ?.Value; + + if (width == null) + // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) + columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; + else + columns[i].Width = width.Value; + } + } + + /// + /// Sets the content of one of the columns of this . + /// + /// The index of the column to set the content of. + /// The content. + public void SetContentForColumn(int column, TContent content) => columns[column].Child = content; + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (currentSkin != null) + currentSkin.SourceChanged -= onSkinChanged; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 36780b0f80..e7a2de266d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -11,7 +10,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -31,14 +29,13 @@ namespace osu.Game.Rulesets.Mania.UI public const float HIT_TARGET_POSITION = 110; - public IReadOnlyList Columns => columnFlow.Children; - private readonly FillFlowContainer columnFlow; + public IReadOnlyList Columns => columnFlow.Content; + private readonly ColumnFlow columnFlow; private readonly JudgementContainer judgements; private readonly DrawablePool judgementPool; private readonly Drawable barLineContainer; - private readonly Container topLevelContainer; private readonly Dictionary columnColours = new Dictionary { @@ -62,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; + Container topLevelContainer; + InternalChildren = new Drawable[] { judgementPool = new DrawablePool(2), @@ -73,17 +72,13 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground()) { RelativeSizeAxes = Axes.Both }, - columnFlow = new FillFlowContainer + columnFlow = new ColumnFlow(definition) { - Name = "Columns", RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, }, new Container { @@ -102,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y, } }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null) { RelativeSizeAxes = Axes.Both }, @@ -121,60 +116,22 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { var columnType = definition.GetTypeOfColumn(i); + var column = new Column(firstColumnIndex + i) { + RelativeSizeAxes = Axes.Both, + Width = 1, ColumnType = columnType, AccentColour = columnColours[columnType], Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ } }; - AddColumn(column); + topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); + columnFlow.SetContentForColumn(i, column); + AddNested(column); } } - private ISkin currentSkin; - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - currentSkin = skin; - skin.SourceChanged += onSkinChanged; - - onSkinChanged(); - } - - private void onSkinChanged() - { - foreach (var col in columnFlow) - { - if (col.Index > 0) - { - float spacing = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1)) - ?.Value ?? COLUMN_SPACING; - - col.Margin = new MarginPadding { Left = spacing }; - } - - float? width = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index)) - ?.Value; - - if (width == null) - // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) - col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; - else - col.Width = width.Value; - } - } - - public void AddColumn(Column c) - { - topLevelContainer.Add(c.TopLevelContainer.CreateProxy()); - columnFlow.Add(c); - AddNested(c); - } - public override void Add(DrawableHitObject h) { var maniaObject = (ManiaHitObject)h.HitObject; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index eaa5d8937a..f527eb2312 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), - } - }, - new StatisticRow - { - Columns = new[] + new StatisticItem("Timing Distribution", + new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0ab3e8825b..d15a0a3203 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning hitCircleSprite = new Sprite { Texture = getTextureWithFallback(string.Empty), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 0f586034d5..25ab96445a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { - animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] { @@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.With(d => + LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; - }), + }), ballColour), layerSpec = new Sprite { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs index bfcf268c3d..9b73ccd248 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - backgroundLayer.Colour = accentColour; + backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 8223e3bc01..5ab8e3a8c8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = accentColour; - body.Colour = accentColour; - end.Colour = accentColour; + var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + + headCircle.AccentColour = colour; + body.Colour = colour; + end.Colour = colour; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 656728f6e4..b11b64c22c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load() { - AccentColour = component == TaikoSkinComponents.CentreHit - ? new Color4(235, 69, 44, 255) - : new Color4(67, 142, 172, 255); + AccentColour = LegacyColourCompatibility.DisallowZeroAlpha( + component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255)); } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2011842591..dbc32f2c3e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs new file mode 100644 index 0000000000..0f6d956b3c --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class BeatmapDifficultyManagerTest + { + [Test] + public void TestKeyEqualsWithDifferentModInstances() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + + [Test] + public void TestKeyEqualsWithDifferentModOrder() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + } +} diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs new file mode 100644 index 0000000000..9c71466489 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyScoreDecoderTest + { + [Test] + public void TestDecodeManiaReplay() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID); + + Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); + Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); + + Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(3, score.ScoreInfo.MaxCombo); + Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); + + Assert.That(score.Replay.Frames, Is.Not.Empty); + } + } + + private class TestLegacyScoreDecoder : LegacyScoreDecoder + { + private static readonly Dictionary rulesets = new Ruleset[] + { + new OsuRuleset(), + new TaikoRuleset(), + new CatchRuleset(), + new ManiaRuleset() + }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; + + protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + MD5Hash = md5Hash, + Ruleset = new OsuRuleset().RulesetInfo, + BaseDifficulty = new BeatmapDifficulty() + } + }); + } + } +} diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 168ec0f09d..bd34eaff63 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -169,17 +169,17 @@ namespace osu.Game.Tests.Editing [Test] public void GetSnappedDistanceFromDistance() { - assertSnappedDistance(50, 100); + assertSnappedDistance(50, 0); assertSnappedDistance(100, 100); - assertSnappedDistance(150, 200); + assertSnappedDistance(150, 100); assertSnappedDistance(200, 200); - assertSnappedDistance(250, 300); + assertSnappedDistance(250, 200); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); assertSnappedDistance(50, 0); - assertSnappedDistance(100, 200); - assertSnappedDistance(150, 200); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); @@ -190,8 +190,8 @@ namespace osu.Game.Tests.Editing }); assertSnappedDistance(50, 0); - assertSnappedDistance(100, 200); - assertSnappedDistance(150, 200); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); assertSnappedDistance(400, 400); diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 7b2913b817..d15682b1eb 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual.Filtering } [Test] - public void TestApplyDrainRateQueries() + public void TestApplyDrainRateQueriesByDrKeyword() { const string query = "dr>2 quite specific dr<:6"; var filterCriteria = new FilterCriteria(); @@ -73,6 +73,20 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.Less(filterCriteria.DrainRate.Min, 6.1f); } + [Test] + public void TestApplyDrainRateQueriesByHpKeyword() + { + const string query = "hp>2 quite specific hp<=6"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim()); + Assert.AreEqual(2, filterCriteria.SearchTerms.Length); + Assert.Greater(filterCriteria.DrainRate.Min, 2.0f); + Assert.Less(filterCriteria.DrainRate.Min, 2.1f); + Assert.Greater(filterCriteria.DrainRate.Max, 6.0f); + Assert.Less(filterCriteria.DrainRate.Min, 6.1f); + } + [Test] public void TestApplyBPMQueries() { diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr new file mode 100644 index 0000000000000000000000000000000000000000..da1a7bdd28e5b89eddd9742bce2b27bc8e75e6f5 GIT binary patch literal 1012 zcmV2Fk>_^VL323Gh;F`G&3?}Wi$c+0000000031008T$3;+WF00000 z01FT?FgZ6ed@(FBI50Uld@(FxP`z80O4tZ&0{{SB001BWz5CvGFroXr>*+lrR^_@@;>&b?uOx<^mMIMT(+z#h{kY?X9fc(g(Lwebtx# z7Q6Lh=|qk7uqqmlbl#mwF~34O;`FY?03m(j9XrY3L)0)1?Fw}$4J_CNJKO;4?X$~q z;+wGa#wD!!0q2PlCzFrV3*3>Lz{D|fez@9JR^@L#NXJJJ;9{+KP~}bh@_eim%+NSk zzegnGWya)SS7pW`<9yI$g`b0t!?MZslqBX+4bVy#tZV0pDNGmeSXKuOXGQ|f3zg8y zGrMu+gxo2?L-AA5RBaNH&*&Dv9Dc!%ufdTEO? zp@}%SBej)D4N{g#xwHF)8|Ks=@OJ2^BGnjH?@$dDOeELCl()%CSSocJ#J0 zrMz`0akC4=AW51K>p71-r!saF*3AP)S9{~98w6pYSsUr1=*o_Q#S)6A-dHA@d9~(^0+ax|D@!;n;Z|| zFvVlqvtKZJf2x8EVoS#`3YPzS-*vaun`i1N*BG`MvK_+XgTPh^GgHpdy!e==KZIaT z3!YC}(8nGj*i;imyixwy9zyF`AooHY90sBzR$Obzb-Vi~&7$v#i3~s7*O{}|SLmeL zoMGZCnRwCc_XbwVIh5g#M@wt{FGra$aqhxs9vn18+0v`>fSkg3XKs=0k_AUDZ~8>a zDG78!sD4dHOSBcg!q83SKA#^J)eG3=oY`jU3QIF2QVgemNl8@7S4(W%aYN@e0jDu% zmqiUorSh0*IbBRsW~4cH;@gJJOutPU2TVP<=OEnHm=mZ{2D~6Z=U*j5r!6K@YHyj* zwhC8cbzc~TkcHU?&2W;Km_To^u4o{p7%^`#txPglTyDDFl%vh successRate.Beatmap = new BeatmapInfo + { + Metrics = new BeatmapMetrics + { + Fails = Enumerable.Range(1, 100).ToArray(), + } + }); + AddAssert("graph max values correct", + () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100)); + } + + [Test] + public void TestEmptyMetrics() + { + AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + { + Metrics = new BeatmapMetrics() + }); + + AddAssert("graph max values correct", + () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 0)); + } + private class GraphExposingSuccessRate : SuccessRate { public new FailRetryGraph Graph => base.Graph; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs new file mode 100644 index 0000000000..07a0bcc8d8 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Humanizer; +using NUnit.Framework; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneSimpleStatisticTable : OsuTestScene + { + private Container container; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + AutoSizeAxes = Axes.Y, + Width = 700, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333"), + }, + container = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(20) + } + } + }; + }); + + [Test] + public void TestEmpty() + { + AddStep("create with no items", + () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty()))); + } + + [Test] + public void TestManyItems( + [Values(1, 2, 3, 4, 12)] int itemCount, + [Values(1, 3, 5)] int columnCount) + { + AddStep($"create with {"item".ToQuantity(itemCount)}", () => + { + var items = Enumerable.Range(1, itemCount) + .Select(i => new SimpleStatisticItem($"Statistic #{i}") + { + Value = RNG.Next(100) + }); + + container.Add(new SimpleStatisticTable(columnCount, items)); + }); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b80b4e45ed..0100c9b210 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => + { + // Computation may have finished in a previous task. + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _)) + return existing; + + return computeDifficulty(key, beatmapInfo, rulesetInfo); + }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -245,7 +251,7 @@ namespace osu.Game.Beatmaps updateScheduler?.Dispose(); } - private readonly struct DifficultyCacheLookup : IEquatable + public readonly struct DifficultyCacheLookup : IEquatable { public readonly int BeatmapId; public readonly int RulesetId; @@ -261,7 +267,7 @@ namespace osu.Game.Beatmaps public bool Equals(DifficultyCacheLookup other) => BeatmapId == other.BeatmapId && RulesetId == other.RulesetId - && Mods.SequenceEqual(other.Mods); + && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym)); public override int GetHashCode() { diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 44ef9bcacc..c15240a4f6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats try { byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255; - - if (alpha == 0) - alpha = 255; - colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha); } catch diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6af2561c89..648e4a762b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) + if (args.Length > 3 && args[1] == "osu.ppy.sh") { switch (args[2]) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c25fb03fd0..f134db1ffe 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -25,7 +25,7 @@ using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; using osuTK; -using Key = osuTK.Input.Key; +using osuTK.Input; namespace osu.Game.Rulesets.Edit { @@ -293,7 +293,16 @@ namespace osu.Game.Rulesets.Edit public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) { - var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime); + double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance); + + double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime); + + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); + + // we don't want to exceed the actual duration and snap to a point in the future. + // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. + if (snappedEndTime > actualDuration + 1) + snappedEndTime -= beatLength; return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index c854c06031..cce631464f 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -47,6 +47,7 @@ namespace osu.Game.Rulesets.Edit /// /// Converts an unsnapped distance to a snapped distance. + /// The returned distance will always be floored (as to never exceed the provided . /// /// The time of the timing point which resides in. /// The distance to convert. diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a4a560c8e4..97cb5ca7ab 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; using osu.Game.Users; using SharpCompress.Compressors.LZMA; @@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy protected void CalculateAccuracy(ScoreInfo score) { - score.Statistics.TryGetValue(HitResult.Miss, out int countMiss); - score.Statistics.TryGetValue(HitResult.Meh, out int count50); - score.Statistics.TryGetValue(HitResult.Good, out int count100); - score.Statistics.TryGetValue(HitResult.Great, out int count300); - score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki); - score.Statistics.TryGetValue(HitResult.Ok, out int countKatu); + int countMiss = score.GetCountMiss() ?? 0; + int count50 = score.GetCount50() ?? 0; + int count100 = score.GetCount100() ?? 0; + int count300 = score.GetCount300() ?? 0; + int countGeki = score.GetCountGeki() ?? 0; + int countKatu = score.GetCountKatu() ?? 0; switch (score.Ruleset.ID) { @@ -241,12 +240,15 @@ namespace osu.Game.Scoring.Legacy } var diff = Parsing.ParseFloat(split[0]); + var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE); + var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); lastTime += diff; - if (i == 0 && diff == 0) - // osu-stable adds a zero-time frame before potentially valid negative user frames. - // we need to ignore this. + if (i < 2 && mouseX == 256 && mouseY == -500) + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; // Todo: At some point we probably want to rewind and play back the negative-time frames @@ -255,8 +257,8 @@ namespace osu.Game.Scoring.Legacy continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, - Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), - Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), + mouseX, + mouseY, (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); replay.Frames.Add(currentFrame); diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index 6731fef6f7..d92d38da45 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu const int line_end_offset = 120; smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); + smallRing.Delay(400).FadeColour(Color4.Black, 300); lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs new file mode 100644 index 0000000000..3d9ba2f225 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Represents a simple statistic item (one that only needs textual display). + /// Richer visualisations should be done with s. + /// + public abstract class SimpleStatisticItem : Container + { + /// + /// The text to display as the statistic's value. + /// + protected string Value + { + set => this.value.Text = value; + } + + private readonly OsuSpriteText value; + + /// + /// Creates a new simple statistic item. + /// + /// The name of the statistic. + protected SimpleStatisticItem(string name) + { + Name = name; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddRange(new[] + { + new OsuSpriteText + { + Text = Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14) + }, + value = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) + } + }); + } + } + + /// + /// Strongly-typed generic specialisation for . + /// + public class SimpleStatisticItem : SimpleStatisticItem + { + /// + /// The statistic's value to be displayed. + /// + public new TValue Value + { + set => base.Value = DisplayValue(value); + } + + /// + /// Used to convert to a text representation. + /// Defaults to using . + /// + protected virtual string DisplayValue(TValue value) => value.ToString(); + + public SimpleStatisticItem(string name) + : base(name) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs new file mode 100644 index 0000000000..8b503cc04e --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Represents a table with simple statistics (ones that only need textual display). + /// Richer visualisations should be done with s and s. + /// + public class SimpleStatisticTable : CompositeDrawable + { + private readonly SimpleStatisticItem[] items; + private readonly int columnCount; + + private FillFlowContainer[] columns; + + /// + /// Creates a statistic row for the supplied s. + /// + /// The number of columns to layout the into. + /// The s to display in this row. + public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) + { + if (columnCount < 1) + throw new ArgumentOutOfRangeException(nameof(columnCount)); + + this.columnCount = columnCount; + this.items = items.ToArray(); + } + + [BackgroundDependencyLoader] + private void load() + { + columns = new FillFlowContainer[columnCount]; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = createColumnDimensions().ToArray(), + Content = new[] { createColumns().ToArray() } + }; + + for (int i = 0; i < items.Length; ++i) + columns[i % columnCount].Add(items[i]); + } + + private IEnumerable createColumnDimensions() + { + for (int column = 0; column < columnCount; ++column) + { + if (column > 0) + yield return new Dimension(GridSizeMode.Absolute, 30); + + yield return new Dimension(); + } + } + + private IEnumerable createColumns() + { + for (int column = 0; column < columnCount; ++column) + { + if (column > 0) + { + yield return new Spacer + { + Alpha = items.Length > column ? 1 : 0 + }; + } + + yield return columns[column] = createColumn(); + } + } + + private FillFlowContainer createColumn() => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + }; + + private class Spacer : CompositeDrawable + { + public Spacer() + { + RelativeSizeAxes = Axes.Both; + Padding = new MarginPadding { Vertical = 4 }; + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Y, + Width = 3, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + CornerRadius = 2, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#222") + } + }; + } + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index ed98698411..485d24d024 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - new Drawable[] + new[] { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Children = new Drawable[] - { - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 9, - Width = 4, - Colour = Color4Extensions.FromHex("#00FFAA") - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - } - } - } + createHeader(item) }, new Drawable[] { @@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } + + private static Drawable createHeader(StatisticItem item) + { + if (string.IsNullOrEmpty(item.Name)) + return Empty(); + + return new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 9, + Width = 4, + Colour = Color4Extensions.FromHex("#00FFAA") + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = item.Name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + }; + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index e959ed24fc..4903983759 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of the item. + /// The name of the item. Can be to hide the item header. /// The content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 7f406331cd..c2ace6a04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -94,14 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), + Alpha = 0 }; foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) { rows.Add(new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] @@ -125,6 +126,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); + d.FadeIn(250, Easing.OutQuint); }, localCancellationSource.Token); }), localCancellationSource.Token); } diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs new file mode 100644 index 0000000000..5b368c3e8d --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Displays the unstable rate statistic for a given play. + /// + public class UnstableRate : SimpleStatisticItem + { + /// + /// Creates and computes an statistic. + /// + /// Sequence of s to calculate the unstable rate based on. + public UnstableRate(IEnumerable hitEvents) + : base("Unstable Rate") + { + var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray(); + Value = 10 * standardDeviation(timeOffsets); + } + + private static double standardDeviation(double[] timeOffsets) + { + if (timeOffsets.Length == 0) + return double.NaN; + + var mean = timeOffsets.Average(); + var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); + return Math.Sqrt(squares / timeOffsets.Length); + } + + protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2"); + } +} diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 9669a1391c..0ee52f3e48 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select private void updateMetrics() { var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; - var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) && (beatmap?.Metrics.Fails?.Any() ?? false); + var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false); if (hasRatings) { diff --git a/osu.Game/Screens/Select/Details/FailRetryGraph.cs b/osu.Game/Screens/Select/Details/FailRetryGraph.cs index 134fd0598a..7cc80acfd3 100644 --- a/osu.Game/Screens/Select/Details/FailRetryGraph.cs +++ b/osu.Game/Screens/Select/Details/FailRetryGraph.cs @@ -29,16 +29,30 @@ namespace osu.Game.Screens.Select.Details var retries = Metrics?.Retries ?? Array.Empty(); var fails = Metrics?.Fails ?? Array.Empty(); + var retriesAndFails = sumRetriesAndFails(retries, fails); - float maxValue = fails.Any() ? fails.Zip(retries, (fail, retry) => fail + retry).Max() : 0; + float maxValue = retriesAndFails.Any() ? retriesAndFails.Max() : 0; failGraph.MaxValue = maxValue; retryGraph.MaxValue = maxValue; - failGraph.Values = fails.Select(f => (float)f); - retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + Math.Clamp(fail, 0, maxValue)); + failGraph.Values = fails.Select(v => (float)v); + retryGraph.Values = retriesAndFails.Select(v => (float)v); } } + private int[] sumRetriesAndFails(int[] retries, int[] fails) + { + var result = new int[Math.Max(retries.Length, fails.Length)]; + + for (int i = 0; i < retries.Length; ++i) + result[i] = retries[i]; + + for (int i = 0; i < fails.Length; ++i) + result[i] += fails[i]; + + return result; + } + public FailRetryGraph() { Children = new[] diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 89afc729fe..39fa4f777d 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select internal static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", + @"\b(?stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -43,6 +43,7 @@ namespace osu.Game.Screens.Select break; case "dr" when parseFloatWithPoint(value, out var dr): + case "hp" when parseFloatWithPoint(value, out dr): updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); break; diff --git a/osu.Game/Skinning/LegacyColourCompatibility.cs b/osu.Game/Skinning/LegacyColourCompatibility.cs new file mode 100644 index 0000000000..b842b50426 --- /dev/null +++ b/osu.Game/Skinning/LegacyColourCompatibility.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Skinning +{ + /// + /// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only. + /// + public static class LegacyColourCompatibility + { + /// + /// Forces an alpha of 1 if a given is fully transparent. + /// + /// + /// This is equivalent to setting colour post-constructor in osu!stable. + /// + /// The to disallow zero alpha on. + /// The resultant . + public static Color4 DisallowZeroAlpha(Color4 colour) + { + if (colour.A == 0) + colour.A = 1; + return colour; + } + + /// + /// Applies a to a , doubling the alpha value into the property. + /// + /// + /// This is equivalent to setting colour in the constructor in osu!stable. + /// + /// The to apply the colour to. + /// The to apply. + /// The given . + public static T ApplyWithDoubledAlpha(T drawable, Color4 colour) + where T : Drawable + { + drawable.Alpha = colour.A; + drawable.Colour = DisallowZeroAlpha(colour); + return drawable; + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..65d5851455 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -31,10 +31,12 @@ namespace osu.Game.Skinning public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; public readonly float[] ExplosionWidth; + public readonly float[] HoldNoteLightWidth; public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; + public bool KeysUnderNotes; public LegacyManiaSkinConfiguration(int keys) { @@ -44,6 +46,7 @@ namespace osu.Game.Skinning ColumnSpacing = new float[keys - 1]; ColumnWidth = new float[keys]; ExplosionWidth = new float[keys]; + HoldNoteLightWidth = new float[keys]; ColumnLineWidth.AsSpan().Fill(2); ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 4990ca8e60..a99710ea96 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,6 +34,8 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + HoldNoteLightImage, + HoldNoteLightScale, ExplosionImage, ExplosionScale, ColumnLineColour, @@ -50,5 +52,6 @@ namespace osu.Game.Skinning Hit100, Hit50, Hit0, + KeysUnderNotes, } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index aebc229f7c..a9d88e77ad 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -97,10 +97,18 @@ namespace osu.Game.Skinning currentConfig.ShowJudgementLine = pair.Value == "1"; break; + case "KeysUnderNotes": + currentConfig.KeysUnderNotes = pair.Value == "1"; + break; + case "LightingNWidth": parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + case "LightingLWidth": + parseArrayValue(pair.Value, currentConfig.HoldNoteLightWidth); + break; + case "WidthForNoteHeightScale": float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; if (minWidth > 0) @@ -116,6 +124,7 @@ namespace osu.Game.Skinning case string _ when pair.Key.StartsWith("KeyImage"): case string _ when pair.Key.StartsWith("Hit"): case string _ when pair.Key.StartsWith("Stage"): + case string _ when pair.Key.StartsWith("Lighting"): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 02d07eee45..5caf07b554 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -173,6 +173,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + case LegacyManiaSkinConfigurationLookups.ExplosionImage: + return SkinUtils.As(getManiaImage(existing, "LightingN")); + case LegacyManiaSkinConfigurationLookups.ExplosionScale: Debug.Assert(maniaLookup.TargetColumn != null); @@ -217,6 +220,20 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: + return SkinUtils.As(getManiaImage(existing, "LightingL")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: + Debug.Assert(maniaLookup.TargetColumn != null); + + if (GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + case LegacyManiaSkinConfigurationLookups.KeyImage: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); @@ -255,6 +272,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.Hit300: case LegacyManiaSkinConfigurationLookups.Hit300g: return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); + + case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: + return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); } return null; From 3db0e7cd75d55126d45b94661a865b17a1b8babd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Sep 2020 22:34:57 +0200 Subject: [PATCH 21/32] Generalise LegacyRollingCounter --- .../Skinning/LegacyComboCounter.cs | 33 ------------ osu.Game/Skinning/LegacyRollingCounter.cs | 51 +++++++++++++++++++ 2 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 osu.Game/Skinning/LegacyRollingCounter.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index e03b30f58f..ccfabdc5fd 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Catch.UI; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -128,35 +125,5 @@ namespace osu.Game.Rulesets.Catch.Skinning lastExplosion = explosion; } - - private class LegacyRollingCounter : RollingCounter - { - private readonly ISkin skin; - - private readonly string fontName; - private readonly float fontOverlap; - - protected override bool IsRollingProportional => true; - - public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) - { - this.skin = skin; - this.fontName = fontName; - this.fontOverlap = fontOverlap; - } - - protected override double GetProportionalDuration(int currentValue, int newValue) - { - return Math.Abs(newValue - currentValue) * 75.0; - } - - protected override OsuSpriteText CreateSpriteText() - { - return new LegacySpriteText(skin, fontName) - { - Spacing = new Vector2(-fontOverlap, 0f) - }; - } - } } } diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs new file mode 100644 index 0000000000..8aa9d4e9af --- /dev/null +++ b/osu.Game/Skinning/LegacyRollingCounter.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Skinning +{ + /// + /// An integer that uses number sprites from a legacy skin. + /// + public class LegacyRollingCounter : RollingCounter + { + private readonly ISkin skin; + + private readonly string fontName; + private readonly float fontOverlap; + + protected override bool IsRollingProportional => true; + + /// + /// Creates a new . + /// + /// The from which to get counter number sprites. + /// The name of the legacy font to use. + /// + /// The numeric overlap of number sprites to use. + /// A positive number will bring the number sprites closer together, while a negative number + /// will split them apart more. + /// + public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) + { + this.skin = skin; + this.fontName = fontName; + this.fontOverlap = fontOverlap; + } + + protected override double GetProportionalDuration(int currentValue, int newValue) + { + return Math.Abs(newValue - currentValue) * 75.0; + } + + protected sealed override OsuSpriteText CreateSpriteText() => + new LegacySpriteText(skin, fontName) + { + Spacing = new Vector2(-fontOverlap, 0f) + }; + } +} From fcf3a1d13c9dcad3652062c613c741e049e9ea43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Sep 2020 22:39:06 +0200 Subject: [PATCH 22/32] Encapsulate combo display better --- .../TestSceneCatcherArea.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 13 ++----------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 13 +++++++++---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index b4f123598b..e055f08dc2 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests Schedule(() => { area.AttemptCatch(fruit); - area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); + area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); drawable.Expire(); }); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 409ea6dbc6..735d7fc300 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Catch.UI internal readonly CatcherArea CatcherArea; - private CatchComboDisplay comboDisplay => CatcherArea.ComboDisplay; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // only check the X position; handle all vertical space. base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y)); @@ -73,16 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) - { - var catchObject = (DrawableCatchHitObject)judgedObject; - CatcherArea.OnResult(catchObject, result); - - comboDisplay.OnNewResult(catchObject, result); - } + => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result) - { - comboDisplay.OnRevertResult((DrawableCatchHitObject)judgedObject, result); - } + => CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9cfb9f41d7..d3e63b0333 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI public Func> CreateDrawableRepresentation; public readonly Catcher MovableCatcher; - internal readonly CatchComboDisplay ComboDisplay; + private readonly CatchComboDisplay comboDisplay; public Container ExplodingFruitTarget { @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Children = new Drawable[] { - ComboDisplay = new CatchComboDisplay + comboDisplay = new CatchComboDisplay { RelativeSizeAxes = Axes.None, AutoSizeAxes = Axes.Both, @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI }; } - public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) { if (result.Judgement is IgnoreJudgement) return; @@ -99,8 +99,13 @@ namespace osu.Game.Rulesets.Catch.UI else MovableCatcher.Drop(); } + + comboDisplay.OnNewResult(fruit, result); } + public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) + => comboDisplay.OnRevertResult(fruit, result); + public void OnReleased(CatchAction action) { } @@ -119,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI if (state?.CatcherX != null) MovableCatcher.X = state.CatcherX.Value; - ComboDisplay.X = MovableCatcher.X; + comboDisplay.X = MovableCatcher.X; } } } From bfe332909c5312df0e1e338217463305a5a5b691 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Sep 2020 14:25:36 +0300 Subject: [PATCH 23/32] Remove "hide combo counter on break time" feature for being too complex The combo counter will be hidden at most one second after the break has started anyways, so why not just remove this feature if the way of implementing it is complicated to be merged within the legacy counter implementation. --- .../TestSceneComboCounter.cs | 15 -------------- .../Skinning/LegacyComboCounter.cs | 20 ------------------- osu.Game/Screens/Play/GameplayBeatmap.cs | 5 ----- osu.Game/Screens/Play/Player.cs | 1 - 4 files changed, 41 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 89521d616d..e79792e04a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -3,8 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; @@ -12,7 +10,6 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; @@ -21,20 +18,9 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestSceneComboCounter : CatchSkinnableTestScene { private ScoreProcessor scoreProcessor; - private GameplayBeatmap gameplayBeatmap; - private readonly Bindable isBreakTime = new BindableBool(); private Color4 judgedObjectColour = Color4.White; - [BackgroundDependencyLoader] - private void load() - { - gameplayBeatmap = new GameplayBeatmap(CreateBeatmapForSkinProvider()); - gameplayBeatmap.IsBreakTime.BindTo(isBreakTime); - Dependencies.Cache(gameplayBeatmap); - Add(gameplayBeatmap); - } - [SetUp] public void SetUp() => Schedule(() => { @@ -54,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); AddStep("perform miss", () => performJudgement(HitResult.Miss)); - AddToggleStep("toggle gameplay break", v => isBreakTime.Value = v); AddStep("randomize judged object colour", () => { judgedObjectColour = new Color4( diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index ccfabdc5fd..6a10ba5eb3 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -47,23 +44,6 @@ namespace osu.Game.Rulesets.Catch.Skinning }; } - private IBindable isBreakTime; - - [Resolved(canBeNull: true)] - private GameplayBeatmap beatmap { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - isBreakTime = beatmap?.IsBreakTime.GetBoundCopy(); - isBreakTime?.BindValueChanged(b => - { - if (b.NewValue) - this.FadeOut(400.0, Easing.OutQuint); - }); - } - public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index d7eed73275..64894544f4 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -16,11 +16,6 @@ namespace osu.Game.Screens.Play { public readonly IBeatmap PlayableBeatmap; - /// - /// Whether the gameplay is currently in a break. - /// - public IBindable IsBreakTime { get; } = new Bindable(); - public GameplayBeatmap(IBeatmap playableBeatmap) { PlayableBeatmap = playableBeatmap; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 478f88ab11..539f9227a3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -632,7 +632,6 @@ namespace osu.Game.Screens.Play // bind component bindables. Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); - gameplayBeatmap.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From f629c33dc0544e920aeb18f0de40d0e4e1ea9887 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:14:31 +0900 Subject: [PATCH 24/32] Make explosion additive to match stable --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 6a10ba5eb3..cce8a81c00 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Catch.Skinning var explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) { Alpha = 0.65f, + Blending = BlendingParameters.Additive, Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.5f), From a27a65bf03d1a2d5ac3a8ef98a41f867ef4528d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:24:26 +0900 Subject: [PATCH 25/32] Don't recreate explosion counter each increment --- .../Skinning/LegacyComboCounter.cs | 59 +++++++------------ 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index cce8a81c00..c3231e1e55 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -16,19 +16,14 @@ namespace osu.Game.Rulesets.Catch.Skinning /// public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { - private readonly ISkin skin; - - private readonly string fontName; - private readonly float fontOverlap; - private readonly LegacyRollingCounter counter; + private readonly LegacyRollingCounter explosion; + public LegacyComboCounter(ISkin skin) { - this.skin = skin; - - fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; - fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; + var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; + var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; AutoSizeAxes = Axes.Both; @@ -37,18 +32,27 @@ namespace osu.Game.Rulesets.Catch.Skinning Origin = Anchor.Centre; Scale = new Vector2(0.8f); - InternalChild = counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + InternalChildren = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Alpha = 0.65f, + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), + }, + counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, }; } public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); - private LegacyRollingCounter lastExplosion; - private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) { // There may still be existing transforms to the counter (including value change after 250ms), @@ -59,17 +63,12 @@ namespace osu.Game.Rulesets.Catch.Skinning if (combo == 0) { counter.Current.Value = 0; - if (lastExplosion != null) - lastExplosion.Current.Value = 0; + explosion.Current.Value = 0; this.FadeOut(immediate ? 0.0 : 400.0, Easing.Out); return; } - // Remove last explosion to not conflict with the upcoming one. - if (lastExplosion != null) - RemoveInternal(lastExplosion); - this.FadeIn().Delay(1000.0).FadeOut(300.0); // For simplicity, in the case of rewinding we'll just set the counter to the current combo value. @@ -86,25 +85,11 @@ namespace osu.Game.Rulesets.Catch.Skinning counter.Delay(250.0).ScaleTo(1f).ScaleTo(1.1f, 60.0).Then().ScaleTo(1f, 30.0); - var explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) - { - Alpha = 0.65f, - Blending = BlendingParameters.Additive, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.5f), - Colour = hitObjectColour ?? Color4.White, - Depth = 1f, - }; - - AddInternal(explosion); + explosion.Colour = hitObjectColour ?? Color4.White; explosion.SetCountWithoutRolling(combo); - explosion.ScaleTo(1.9f, 400.0, Easing.Out) - .FadeOut(400.0) - .Expire(true); - - lastExplosion = explosion; + explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400.0, Easing.Out) + .FadeOutFromOne(400.0); } } } From 92cda6bccb2e2ae4e5ca461b92d0fa42322726f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:27:47 +0900 Subject: [PATCH 26/32] Adjust xmldoc slightly --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index c3231e1e55..320fc9c440 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -12,7 +12,7 @@ using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning { /// - /// A combo counter implementation that visually behaves almost similar to osu!stable's combo counter. + /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { From 08d8975566b9a3474c3df1b3882e86e49fd3f649 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:35:18 +0900 Subject: [PATCH 27/32] Remove DisplayInitialCombo method for simplicity --- .../Skinning/LegacyComboCounter.cs | 12 +++++++++--- osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 2 +- osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs | 12 +----------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 320fc9c440..047d9b3602 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -50,11 +50,17 @@ namespace osu.Game.Rulesets.Catch.Skinning }; } - public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); - public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); + private int lastDisplayedCombo; - private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) + public void UpdateCombo(int combo, Color4? hitObjectColour = null) { + bool immediate = Time.Elapsed < 0; + + if (combo == lastDisplayedCombo) + return; + + lastDisplayedCombo = combo; + // There may still be existing transforms to the counter (including value change after 250ms), // finish them immediately before new transforms. counter.FinishTransforms(); diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index b53711e4ed..deb2cb99ed 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); - ComboCounter?.DisplayInitialCombo(currentCombo); + ComboCounter?.UpdateCombo(currentCombo); } public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result) diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs index 1363ed1352..cfb6879067 100644 --- a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs @@ -11,16 +11,6 @@ namespace osu.Game.Rulesets.Catch.UI /// public interface ICatchComboCounter : IDrawable { - /// - /// Updates the counter to display the provided as initial value. - /// The value should be immediately displayed without any animation. - /// - /// - /// This is required for when instantiating a combo counter in middle of accumulating combo (via skin change). - /// - /// The combo value to be displayed as initial. - void DisplayInitialCombo(int combo); - /// /// Updates the counter to animate a transition from the old combo value it had to the current provided one. /// @@ -29,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The new combo value. /// The colour of the object if hit, null on miss. - void UpdateCombo(int combo, Color4? hitObjectColour); + void UpdateCombo(int combo, Color4? hitObjectColour = null); } } From ffd4874ac0f6e98df62d7e09520e8ce46cead0b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:37:18 +0900 Subject: [PATCH 28/32] Remove unnecessary double suffixes --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 047d9b3602..5dcc532a08 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -86,16 +86,16 @@ namespace osu.Game.Rulesets.Catch.Skinning return; } - counter.ScaleTo(1.5f).ScaleTo(0.8f, 250.0, Easing.Out) + counter.ScaleTo(1.5f).ScaleTo(0.8f, 250, Easing.Out) .OnComplete(c => c.SetCountWithoutRolling(combo)); - counter.Delay(250.0).ScaleTo(1f).ScaleTo(1.1f, 60.0).Then().ScaleTo(1f, 30.0); + counter.Delay(250).ScaleTo(1f).ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30); explosion.Colour = hitObjectColour ?? Color4.White; explosion.SetCountWithoutRolling(combo); - explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400.0, Easing.Out) - .FadeOutFromOne(400.0); + explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400, Easing.Out) + .FadeOutFromOne(400); } } } From 1c58f568d61584ef5a5568b52c1eeefa9bcdf1d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:54:21 +0900 Subject: [PATCH 29/32] Simplify and reformat rewind/transform logic --- .../Skinning/LegacyComboCounter.cs | 48 ++++++++----------- .../UI/CatchComboDisplay.cs | 6 --- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 5dcc532a08..a87286da89 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -54,16 +54,14 @@ namespace osu.Game.Rulesets.Catch.Skinning public void UpdateCombo(int combo, Color4? hitObjectColour = null) { - bool immediate = Time.Elapsed < 0; - if (combo == lastDisplayedCombo) return; - lastDisplayedCombo = combo; - // There may still be existing transforms to the counter (including value change after 250ms), // finish them immediately before new transforms. - counter.FinishTransforms(); + counter.SetCountWithoutRolling(lastDisplayedCombo); + + lastDisplayedCombo = combo; // Combo fell to zero, roll down and fade out the counter. if (combo == 0) @@ -71,31 +69,27 @@ namespace osu.Game.Rulesets.Catch.Skinning counter.Current.Value = 0; explosion.Current.Value = 0; - this.FadeOut(immediate ? 0.0 : 400.0, Easing.Out); - return; + this.FadeOut(400, Easing.Out); } - - this.FadeIn().Delay(1000.0).FadeOut(300.0); - - // For simplicity, in the case of rewinding we'll just set the counter to the current combo value. - immediate |= Time.Elapsed < 0; - - if (immediate) + else { - counter.SetCountWithoutRolling(combo); - return; + this.FadeInFromZero().Delay(1000).FadeOut(300); + + counter.ScaleTo(1.5f) + .ScaleTo(0.8f, 250, Easing.Out) + .OnComplete(c => c.SetCountWithoutRolling(combo)); + + counter.Delay(250) + .ScaleTo(1f) + .ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30); + + explosion.Colour = hitObjectColour ?? Color4.White; + + explosion.SetCountWithoutRolling(combo); + explosion.ScaleTo(1.5f) + .ScaleTo(1.9f, 400, Easing.Out) + .FadeOutFromOne(400); } - - counter.ScaleTo(1.5f).ScaleTo(0.8f, 250, Easing.Out) - .OnComplete(c => c.SetCountWithoutRolling(combo)); - - counter.Delay(250).ScaleTo(1f).ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30); - - explosion.Colour = hitObjectColour ?? Color4.White; - - explosion.SetCountWithoutRolling(combo); - explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400, Easing.Out) - .FadeOutFromOne(400); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index deb2cb99ed..58a3140bb5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -50,12 +50,6 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Judgement.AffectsCombo || !result.HasResult) return; - if (result.Type == HitResult.Miss) - { - updateCombo(result.ComboAtJudgement, null); - return; - } - updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); } From 1b261f177f8ea0f4b1ffc5bf554d5588daddeac6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 13:17:53 +0900 Subject: [PATCH 30/32] Disable rewind handling --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index a87286da89..c8abc9e832 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -63,6 +63,14 @@ namespace osu.Game.Rulesets.Catch.Skinning lastDisplayedCombo = combo; + if (Time.Elapsed < 0) + { + // needs more work to make rewind somehow look good. + // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle). + Hide(); + return; + } + // Combo fell to zero, roll down and fade out the counter. if (combo == 0) { @@ -73,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Skinning } else { - this.FadeInFromZero().Delay(1000).FadeOut(300); + this.FadeInFromZero().Then().Delay(1000).FadeOut(300); counter.ScaleTo(1.5f) .ScaleTo(0.8f, 250, Easing.Out) From 7c40071b21d047d3f14e92a4d876d8d11e8fc4c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 13:32:00 +0900 Subject: [PATCH 31/32] Revert changes to SkinnableTestScene but change load order --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 33 ++++++++------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 58e0b23fab..c0db05d5c5 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -153,38 +153,29 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; - private readonly HashSet legacyFontPrefixes = new HashSet(); - public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) : base(skin, storage, audioManager, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; - - // use a direct string lookup instead of enum to avoid having to reference ruleset assemblies. - legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); - legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); - legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); } public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { + var lookup = base.GetTexture(componentName, wrapModeS, wrapModeT); + + if (lookup != null) + return lookup; + // extrapolate frames to test longer animations - if (extrapolateAnimations && isAnimationComponent(componentName, out var number) && number < 60) - return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); + if (extrapolateAnimations) + { + var match = Regex.Match(componentName, "-([0-9]*)"); - return base.GetTexture(componentName, wrapModeS, wrapModeT); - } + if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60) + return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); + } - private bool isAnimationComponent(string componentName, out int number) - { - number = 0; - - // legacy font glyph textures have the pattern "{fontPrefix}-{character}", which could be mistaken for an animation frame. - if (legacyFontPrefixes.Any(p => componentName.StartsWith($"{p}-"))) - return false; - - var match = Regex.Match(componentName, "-([0-9]*)"); - return match.Length > 0 && int.TryParse(match.Groups[1].Value, out number); + return null; } } } From 552968f65f7c5451faef6c96c144efe3bcefce31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 13:38:52 +0900 Subject: [PATCH 32/32] Remove unnecessary using --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index c0db05d5c5..a856789d96 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; using osu.Framework.Allocation;