Compare commits
889 Commits
2025.913.0
...
old-testin
| Author | SHA1 | Date | |
|---|---|---|---|
| 17d56f8db0 | |||
| 9637ea1df7 | |||
| 0940d61f7e | |||
| e6a6352a9c | |||
|
|
734c6f933d | ||
|
|
ac213c90cb | ||
|
|
74ca87c252 | ||
|
|
032912e62b | ||
|
|
1e79c56240 | ||
| 86f0159e65 | |||
| ae5a64ba81 | |||
|
|
82256ae2de | ||
|
|
1142be45ec | ||
|
|
dcb6d71287 | ||
|
|
881a35b382 | ||
|
|
89d8b402af | ||
|
|
1d351002df | ||
| e3a7ae30cd | |||
| b6f845d99c | |||
|
|
2606f3a0b5 | ||
|
|
1c463aa060 | ||
|
|
7853abe8aa | ||
|
|
1aff418981 | ||
|
|
62e92bb242 | ||
|
|
79151ae5b4 | ||
| a1d6bda63e | |||
| 547d22a4b5 | |||
|
|
c17db2cdd0 | ||
|
|
bbdd70c843 | ||
|
|
4250a54245 | ||
|
|
40fdb8662e | ||
|
|
6ce8b0a4bc | ||
|
|
b30047def6 | ||
|
|
0ffb86262f | ||
|
|
f71eb4b980 | ||
|
|
1faf02e860 | ||
|
|
d700375e55 | ||
|
|
095a67c24e | ||
|
|
86054497d0 | ||
|
|
c4f7dee82b | ||
|
|
22825f6509 | ||
|
|
691e8bcd05 | ||
|
|
e68bab4f4b | ||
|
|
9430a62af4 | ||
| b4c530ac04 | |||
| 5af05d2479 | |||
| 43ab18ffea | |||
| 9f59259a40 | |||
| 82b3015fcc | |||
| 68f92ab57c | |||
| d76d4d9a35 | |||
|
|
bb7417c099 | ||
|
|
066e093987 | ||
|
|
56e0c3e65d | ||
|
|
89d7726903 | ||
|
|
36f1bfef07 | ||
|
|
118f07878a | ||
|
|
e144968893 | ||
|
|
d2ffea41c6 | ||
|
|
a8be9b1381 | ||
|
|
bdac75e542 | ||
|
|
5c2df50714 | ||
|
|
887d280bfa | ||
|
|
42b184f167 | ||
|
|
27737bd4e9 | ||
|
|
84db289779 | ||
|
|
4c0522b795 | ||
|
|
8bb885a0dc | ||
|
|
0b06acb29d | ||
|
|
b129837e57 | ||
|
|
07ea9fe2a4 | ||
|
|
3e221c7f61 | ||
|
|
7106a6a5e5 | ||
|
|
eaf2721f5b | ||
| 490a6fd724 | |||
|
|
59a27dad3d | ||
|
|
f73307876e | ||
|
|
4e4aa44a02 | ||
|
|
d6cd748d2a | ||
| 237e1828f8 | |||
| 3413f722f7 | |||
| 9f779dac03 | |||
| 0727c53cdc | |||
| a57ff24191 | |||
|
|
c23d6b7fd1 | ||
|
|
582ff999aa | ||
|
|
a6c001244f | ||
|
|
a96b024ac5 | ||
|
|
1c10acba76 | ||
|
|
4ae4c700ae | ||
|
|
2be50d917a | ||
|
|
35fdc6f8b9 | ||
|
|
d04029bcc7 | ||
|
|
107098314a | ||
|
|
d1d76a76ba | ||
|
|
fbac5db964 | ||
|
|
5a920d15c1 | ||
|
|
324d088d46 | ||
|
|
1db4b897eb | ||
|
|
8e2230d149 | ||
|
|
8d33c35646 | ||
|
|
c359898a75 | ||
|
|
6343bf7d29 | ||
|
|
b1e27d842b | ||
|
|
28eeb7f743 | ||
|
|
38c3167a9d | ||
|
|
66ebce8c12 | ||
|
|
fed9564b40 | ||
|
|
f595a47059 | ||
|
|
8a9f60df68 | ||
|
|
2d8b1e7152 | ||
|
|
fef8117b5c | ||
|
|
99da986e02 | ||
|
|
1c33291b3f | ||
|
|
c6cc92315c | ||
|
|
12170df80a | ||
|
|
3e4c038a37 | ||
|
|
5d76353ae4 | ||
|
|
0b3ec3f1e1 | ||
|
|
043a1c2793 | ||
|
|
ca8247c667 | ||
|
|
a5ae542502 | ||
|
|
fe5cbc4932 | ||
|
|
0a378e5efd | ||
|
|
2a7e71d7fd | ||
|
|
1d221c1a7a | ||
| bdb3418b67 | |||
|
|
82f4406c79 | ||
|
|
0b4f96efc8 | ||
|
|
92e9a36744 | ||
|
|
c6eba26a67 | ||
|
|
8f927ea7b5 | ||
|
|
a8f058141b | ||
|
|
78c6973298 | ||
|
|
d8d7c80832 | ||
|
|
ae33690632 | ||
|
|
9e2ea63e70 | ||
|
|
6bb25b2abe | ||
|
|
037743e002 | ||
|
|
6244617e5e | ||
|
|
ddfcb4d6da | ||
|
|
2660f4dcb0 | ||
|
|
5a865476ce | ||
|
|
2472c91924 | ||
|
|
db50019f31 | ||
|
|
1e43509e4a | ||
|
|
ded8aaecfd | ||
|
|
75df8e3639 | ||
|
|
0d9a50e839 | ||
|
|
7473c62949 | ||
|
|
8d30e3d852 | ||
|
|
97fdc89fe3 | ||
|
|
f6a6c9f885 | ||
|
|
26c50b874c | ||
|
|
83706b7fb6 | ||
|
|
9e3486d4e6 | ||
|
|
545b13c3fb | ||
|
|
2c9fc32756 | ||
|
|
79bfe7880a | ||
|
|
0786e619f1 | ||
|
|
c968981697 | ||
|
|
45567f19b7 | ||
|
|
f0f4e7c7a5 | ||
|
|
1d353ef637 | ||
|
|
52af905237 | ||
|
|
64668eafb9 | ||
|
|
b0762fc8ec | ||
|
|
d59e9572d2 | ||
|
|
098da946e1 | ||
|
|
510fc506fb | ||
|
|
da09ad9c46 | ||
|
|
aaff7d358f | ||
|
|
a69b2cd803 | ||
|
|
855d5dba3c | ||
|
|
9c981a52f8 | ||
|
|
96de47ac4f | ||
|
|
43834b55f2 | ||
|
|
8fb402665e | ||
|
|
e4975e8d3b | ||
|
|
dbd9f13f2d | ||
|
|
ec890cd459 | ||
|
|
33c8c4d639 | ||
|
|
83ce56b718 | ||
|
|
9d88c761d3 | ||
|
|
49eb013967 | ||
|
|
b6ccc8cae4 | ||
|
|
d0e09e5b5c | ||
|
|
8900c79758 | ||
|
|
fd652982ce | ||
|
|
a2bfb409d2 | ||
| 936640edeb | |||
| 1187d03333 | |||
|
|
26da75ecfb | ||
|
|
9f8554cc13 | ||
|
|
713b6453c0 | ||
|
|
1b3ac49f2a | ||
|
|
d8b71423b0 | ||
|
|
2c40e116e1 | ||
|
|
90e7faf271 | ||
|
|
fc74726d11 | ||
|
|
5d3997152a | ||
|
|
41b56971e5 | ||
|
|
98e7a10e1e | ||
|
|
e99b9984d0 | ||
|
|
38504fed22 | ||
|
|
13dab24d41 | ||
|
|
67530b39cf | ||
|
|
19f5e5ba7c | ||
|
|
56ce955e0c | ||
|
|
73349ab182 | ||
|
|
a6a98fc078 | ||
|
|
a8594f1c08 | ||
|
|
d3860f1630 | ||
|
|
15ee49348d | ||
|
|
908a950cd2 | ||
|
|
df79269e6f | ||
|
|
34146b8bcb | ||
|
|
08ed2844b4 | ||
|
|
871c0ebe3d | ||
|
|
6362cdb675 | ||
|
|
8e78f4dac4 | ||
|
|
fa8d303922 | ||
|
|
d465bee0ab | ||
|
|
721ba8aeba | ||
|
|
1dd026c0f0 | ||
|
|
a873f2be65 | ||
|
|
edf7a126c8 | ||
|
|
f0f33b6df4 | ||
|
|
6052ed790d | ||
|
|
107c481fb9 | ||
|
|
aba567d258 | ||
|
|
094454499c | ||
|
|
c7e1a5770d | ||
|
|
a8ac82aa1f | ||
|
|
47faf774b0 | ||
|
|
be77257ddb | ||
|
|
397041099e | ||
|
|
4b59a4657f | ||
|
|
02090bf6c4 | ||
| 3bd996ee43 | |||
| 37b9f91d42 | |||
|
|
603c77e3e9 | ||
|
|
f284864f96 | ||
| 1a5a5606dc | |||
|
|
fa1bf7bd96 | ||
|
|
ef4408a73e | ||
|
|
6f7f9802bd | ||
| 87ff1051e9 | |||
|
|
277f4268db | ||
|
|
80fbcd5fbd | ||
|
|
9c2319b989 | ||
|
|
a040143825 | ||
|
|
80474565fc | ||
| 2e4b0ff197 | |||
| 499f410c94 | |||
| 616c0d8ecd | |||
| ed889138a0 | |||
|
|
89f2c7160d | ||
|
|
f0ca079fe6 | ||
|
|
fbd83cb048 | ||
|
|
843c318ec1 | ||
|
|
19b6761697 | ||
|
|
0c341c1f3e | ||
|
|
ae5584bd88 | ||
|
|
edf08b176a | ||
| 18075bef29 | |||
| bc7780a870 | |||
| 8930b8fadb | |||
| 2d457f4305 | |||
| a784734f94 | |||
|
|
7b952b83bf | ||
|
|
214122f633 | ||
|
|
76c0bd4750 | ||
|
|
4bf3d9397f | ||
|
|
fe56ba2921 | ||
|
|
1ca4c8860b | ||
|
|
1e05613859 | ||
|
|
424ef9237f | ||
|
|
e541e917a4 | ||
|
|
7796394685 | ||
|
|
32900f563c | ||
|
|
e349a597ba | ||
|
|
8b778e8106 | ||
|
|
ce5e54c9d2 | ||
| ecfd4764e7 | |||
| f5ca5083d6 | |||
| 1d7c77d8d6 | |||
| 774e52fbd6 | |||
|
|
45e8df7af2 | ||
|
|
1c30cb8371 | ||
| a9d7a9d5d5 | |||
| f7069b1009 | |||
| c3ce5dc787 | |||
| 98076e2092 | |||
| b7d1092f90 | |||
| 08db90c278 | |||
|
|
bd4ed49c06 | ||
| b7e36164c3 | |||
|
|
1e91dde92e | ||
|
|
a593a40429 | ||
|
|
02b88de76e | ||
|
|
435cd272ea | ||
| 0f5f13858d | |||
|
|
b64abbf1f5 | ||
|
|
4265e72180 | ||
| 89a0c75156 | |||
|
|
72507b80c7 | ||
| ab7e5c94f1 | |||
| 8dc9ea4553 | |||
| dcf553c252 | |||
|
|
cb9d9734d6 | ||
|
|
5763b7dbe9 | ||
|
|
e1baa03622 | ||
|
|
4f783f8c41 | ||
|
|
4c72a60ee2 | ||
| 43f3a506ea | |||
| ab51579c27 | |||
| d8e977c05f | |||
|
|
c56c528824 | ||
|
|
013de9f85d | ||
|
|
cd6c9405fe | ||
|
|
1df640898f | ||
|
|
7b55b9e4f2 | ||
|
|
822cb9e2fb | ||
| c023767df9 | |||
|
|
680614fbee | ||
|
|
cb8ddc706f | ||
|
|
04d2ce150a | ||
|
|
eaffb89b4c | ||
|
|
650a61539b | ||
|
|
75bc934aa5 | ||
|
|
8d80e2bd2c | ||
|
|
34a3b1ba78 | ||
|
|
b354fa4472 | ||
|
|
1fbe1bd6c9 | ||
|
|
3c215f6574 | ||
|
|
8c28d26130 | ||
|
|
933fbd274d | ||
|
|
55ae7e8bb8 | ||
|
|
4a22ef88ce | ||
|
|
43ca046f9b | ||
|
|
dbefba57ce | ||
|
|
20904de276 | ||
|
|
6a6c7ad3ba | ||
|
|
fb2fe65a77 | ||
|
|
4662c5d678 | ||
|
|
243cd9c073 | ||
|
|
e8db35a5c9 | ||
|
|
d98cb9ca45 | ||
|
|
a7e4aa8b12 | ||
|
|
0f54608cee | ||
|
|
f8331e0b28 | ||
|
|
6ff2a6225d | ||
|
|
a8020dea7c | ||
|
|
88dd458394 | ||
|
|
23cb7f3b23 | ||
|
|
7da051b144 | ||
|
|
78f639d760 | ||
|
|
4ea03d0e07 | ||
|
|
4d706b12ac | ||
|
|
c44f701abe | ||
|
|
4c81d661aa | ||
|
|
f4049c7ec1 | ||
|
|
645d27bb32 | ||
|
|
73f1849365 | ||
|
|
89b443bccc | ||
|
|
be9170832c | ||
|
|
9db200ed41 | ||
|
|
1ab017d4e2 | ||
|
|
2413e98108 | ||
|
|
65fb5311ea | ||
|
|
14cdc40f0f | ||
|
|
9a393f912b | ||
|
|
a9ca4634fc | ||
|
|
bdcc0ee937 | ||
|
|
6f94b1ab6d | ||
|
|
b20a41c1e8 | ||
|
|
d0ce74063d | ||
|
|
373162df02 | ||
|
|
8e0c9281d3 | ||
|
|
25a1a1ba37 | ||
|
|
73e05e3fae | ||
|
|
2a01e3d148 | ||
|
|
cf0e5edf34 | ||
|
|
a825104688 | ||
|
|
ea1798d731 | ||
|
|
a435dfe93e | ||
|
|
5c1171f358 | ||
|
|
3fcc626e29 | ||
|
|
7ff6edeb64 | ||
|
|
657bc31539 | ||
|
|
f9f7740acb | ||
|
|
5e4dd77e64 | ||
|
|
ce96c0b037 | ||
|
|
5af9bb784b | ||
|
|
4c60df21db | ||
|
|
3c6fb14a32 | ||
|
|
3afc7b045c | ||
|
|
2f2847f1dd | ||
|
|
ee7c52465b | ||
|
|
beb977892e | ||
|
|
7203f419a2 | ||
|
|
722cfb72d8 | ||
|
|
5da132cc2f | ||
|
|
9fac96cf07 | ||
|
|
fadcb9882c | ||
|
|
0610781c6c | ||
|
|
e9260de56f | ||
|
|
2d177226fd | ||
|
|
bd912710f1 | ||
|
|
4e76bd0f24 | ||
|
|
050c10cec2 | ||
|
|
b4fd7ec10f | ||
|
|
cbe7da99ad | ||
|
|
9a965a2546 | ||
|
|
7b0121a430 | ||
|
|
627fec2e3a | ||
|
|
c779e142e6 | ||
|
|
89fffa5a1a | ||
|
|
6d597fc815 | ||
|
|
a78b456e20 | ||
|
|
9237c76942 | ||
|
|
378c64b7f8 | ||
|
|
87b66685d6 | ||
|
|
c524bf5432 | ||
|
|
a40230da4b | ||
|
|
b1a421c22b | ||
|
|
9601708087 | ||
|
|
22f11b6fa5 | ||
|
|
f95d0d214e | ||
|
|
0205cf0fb9 | ||
|
|
8b2b6517ca | ||
|
|
ce3b8bc77b | ||
|
|
98829bf857 | ||
|
|
3c37ac1718 | ||
|
|
f8769d2e44 | ||
|
|
4d66762898 | ||
|
|
e61ae7ab8a | ||
|
|
98eb29c43d | ||
|
|
bb578d254d | ||
|
|
b7c07ad0e5 | ||
|
|
08621c4cc9 | ||
|
|
be9b99f975 | ||
|
|
765b9a20b5 | ||
|
|
473fb5720c | ||
|
|
5faf791ca0 | ||
|
|
9ca47fc53a | ||
|
|
79a76ce587 | ||
|
|
72fa1553c3 | ||
|
|
afdebcf188 | ||
|
|
954061b00b | ||
|
|
b74962af92 | ||
|
|
1af462b692 | ||
|
|
0558f9f2d9 | ||
|
|
763739e877 | ||
|
|
f96be84c57 | ||
|
|
613c208362 | ||
|
|
a3c78de710 | ||
|
|
e240817087 | ||
|
|
ddb844f81b | ||
|
|
b6f4175939 | ||
|
|
819da1bc38 | ||
|
|
33e42d2809 | ||
|
|
1a49d030a0 | ||
|
|
03adae4417 | ||
|
|
5a38e1537b | ||
|
|
6ac8e59a11 | ||
|
|
a816977208 | ||
|
|
f0bfb4becc | ||
|
|
ec6ffb3403 | ||
|
|
6ded2e3de7 | ||
|
|
6c7eafde1a | ||
|
|
14d0982b6c | ||
|
|
29787360ba | ||
|
|
6b56a0611b | ||
|
|
0f1bf35bd9 | ||
|
|
b600860540 | ||
|
|
ae7ba034a7 | ||
|
|
0cb521798d | ||
|
|
1ee24521b8 | ||
|
|
efe7d92096 | ||
|
|
4f478db056 | ||
|
|
646cb71b77 | ||
|
|
ce3e3d17fa | ||
|
|
d062c99666 | ||
|
|
803737c947 | ||
|
|
44a427f4e5 | ||
|
|
85ab0d621c | ||
|
|
1591d49143 | ||
|
|
b9de7ba196 | ||
|
|
dfa8d4fe7c | ||
|
|
5eda9a0fd7 | ||
|
|
ca8fc81a7e | ||
|
|
82bcbb53dd | ||
|
|
9a089315b8 | ||
|
|
baea05a0a1 | ||
|
|
c361e6d3c2 | ||
|
|
a4a2e4e639 | ||
|
|
5fc8cde0f7 | ||
|
|
a2098b633a | ||
|
|
0a3665c43d | ||
|
|
c34b2ffc05 | ||
|
|
4ebd97b804 | ||
|
|
dcb30ed5b3 | ||
|
|
a29a5ab7e6 | ||
|
|
62599de649 | ||
|
|
4cac1781c5 | ||
|
|
fcf6d04791 | ||
|
|
c6fcba7e1c | ||
|
|
47b95de06d | ||
|
|
4e4eba2070 | ||
|
|
2ec0cbe5db | ||
|
|
0644543e28 | ||
|
|
23933452fe | ||
|
|
61547d6b38 | ||
|
|
0b84916c3e | ||
|
|
3120871298 | ||
|
|
2dbfdc3d2c | ||
|
|
0af3f15c7c | ||
|
|
594b8c1a60 | ||
|
|
0269257287 | ||
|
|
03a5aedf99 | ||
|
|
1ec6735a35 | ||
|
|
fbaf27e3db | ||
|
|
45eb34ecde | ||
|
|
10beab6ad3 | ||
|
|
70188d4fab | ||
|
|
1ce846294b | ||
|
|
4be29425a0 | ||
|
|
db3cc583e6 | ||
|
|
fec7ef7624 | ||
|
|
6e378ad5af | ||
|
|
8f8f605748 | ||
|
|
5f9ade6610 | ||
|
|
1a569e9e2a | ||
|
|
98762ce09e | ||
|
|
8f6f859c15 | ||
|
|
cf1834b080 | ||
|
|
a2bae15db1 | ||
|
|
159349a64b | ||
|
|
81a529200d | ||
|
|
6fa4a7152f | ||
|
|
508c37c27b | ||
|
|
95f86a5c0e | ||
|
|
cd62f66d79 | ||
|
|
3fbe777f33 | ||
|
|
b8ceb91a78 | ||
|
|
9f61284371 | ||
|
|
3f18a1312e | ||
|
|
17758d5934 | ||
|
|
eaebe99a5c | ||
|
|
1edbb1d586 | ||
|
|
a060ddb543 | ||
|
|
f488974d39 | ||
|
|
8e01fb70c3 | ||
|
|
a3f635588c | ||
|
|
f8d3285ab4 | ||
|
|
a73c325235 | ||
|
|
97739c39e7 | ||
|
|
b58f03bc36 | ||
|
|
d870547a18 | ||
|
|
28c846b4d9 | ||
|
|
8e36533f65 | ||
|
|
e183e6bb88 | ||
|
|
5dc44fbdf9 | ||
|
|
60544bf595 | ||
|
|
8f0b8153eb | ||
|
|
3070a0068a | ||
|
|
c5e17f5f0f | ||
|
|
8439e6ec6c | ||
|
|
9ebc5b0a35 | ||
|
|
0b00191d71 | ||
|
|
0389da4559 | ||
|
|
40c447a792 | ||
|
|
4132a7cd53 | ||
|
|
ca4c033b76 | ||
|
|
85bbe13aa9 | ||
|
|
1f2928bf2f | ||
|
|
94c7489c1c | ||
|
|
3d6a3e3eda | ||
|
|
a0f295d6fe | ||
|
|
3bb6685e75 | ||
|
|
e3459eccf2 | ||
|
|
0d68e5eeeb | ||
|
|
faad1753a4 | ||
|
|
fed04eb63b | ||
|
|
52f82c7e56 | ||
|
|
3fc3a53521 | ||
|
|
2136d95087 | ||
|
|
c58d13b802 | ||
|
|
b477790d3e | ||
|
|
e33de9b658 | ||
|
|
6eaf91d31a | ||
|
|
6da6edd1d1 | ||
|
|
6af5158bb4 | ||
|
|
79c367d208 | ||
|
|
348713d83d | ||
|
|
cab0b3451f | ||
|
|
91019dcdf7 | ||
|
|
0cc2e4eaa0 | ||
|
|
422392233b | ||
|
|
7b1e3cd537 | ||
|
|
30412ba3f2 | ||
|
|
4622d39f41 | ||
|
|
0d5c041cac | ||
|
|
ffe8b9f4c3 | ||
|
|
9ba9966078 | ||
|
|
f1ec04114f | ||
|
|
4337046680 | ||
|
|
1a522a19f1 | ||
|
|
e4c5fc1906 | ||
|
|
568ddc2d2d | ||
|
|
0ebc90462d | ||
|
|
a08f7327b1 | ||
|
|
533c61e719 | ||
|
|
64fba9470d | ||
|
|
743a94bd22 | ||
|
|
bda5737135 | ||
|
|
03bbbebbbc | ||
|
|
97c95de94a | ||
|
|
3dcbcb5f64 | ||
|
|
19718c706a | ||
|
|
4b3d2b0c57 | ||
|
|
df58dc0ca2 | ||
|
|
d08b5a72b7 | ||
|
|
b286f05c78 | ||
|
|
b3ed7717a8 | ||
|
|
bc7e02c30b | ||
|
|
557df19bf4 | ||
|
|
8a450563fd | ||
|
|
f9a226d68e | ||
|
|
dd846dcc52 | ||
|
|
71ae1d34cd | ||
|
|
dd8dfc04b3 | ||
|
|
2f6bd6605f | ||
|
|
e76d1ae619 | ||
|
|
c0a7c97185 | ||
|
|
dbd48bc3a1 | ||
|
|
a2770a8674 | ||
|
|
67041254db | ||
|
|
68d5f53cc7 | ||
|
|
a30c7f51d0 | ||
|
|
4e90a2aee2 | ||
|
|
e24df0b9d0 | ||
|
|
27ff2a7d49 | ||
|
|
3cb85f743a | ||
|
|
43878f6d33 | ||
|
|
60dc2c8876 | ||
|
|
87128453d6 | ||
|
|
ae4e015352 | ||
|
|
007de10e2b | ||
|
|
3d5dc60cfe | ||
|
|
e7076b9582 | ||
|
|
2a85f7b7c8 | ||
|
|
44a1a5ffc7 | ||
|
|
365cdfd40e | ||
|
|
aad321a0b8 | ||
|
|
1867aad1a6 | ||
|
|
37a11b5592 | ||
|
|
565a2866ea | ||
|
|
c9a6a4887c | ||
|
|
9b78187d29 | ||
|
|
0230a27de6 | ||
|
|
ccf3151172 | ||
|
|
b980183e98 | ||
|
|
b7d36cffd4 | ||
|
|
345ae29770 | ||
|
|
6bf589af88 | ||
|
|
03455ba683 | ||
|
|
1f7aa2fe5e | ||
|
|
e26e44cd45 | ||
|
|
5ab40e4b73 | ||
|
|
ee638492bf | ||
|
|
7d81ff8115 | ||
|
|
114e7f5c61 | ||
|
|
e2b9288716 | ||
|
|
919c47b918 | ||
|
|
bc4d5d07d7 | ||
|
|
4b2f3efcbd | ||
|
|
33ddb84633 | ||
|
|
f66c8d10e0 | ||
|
|
38278eaaac | ||
|
|
ac2df49c35 | ||
|
|
2edd49d2c0 | ||
|
|
a078d4bdb4 | ||
|
|
6e39e714e1 | ||
|
|
d512adb971 | ||
|
|
a3e09a1c31 | ||
|
|
e636a09e0f | ||
|
|
3ebb72a20a | ||
|
|
256483165f | ||
|
|
1a084f28ff | ||
|
|
a0ff7728b9 | ||
|
|
9860bba2f3 | ||
|
|
bb9b4a2827 | ||
|
|
057406c910 | ||
|
|
062174d874 | ||
|
|
d31df5bbc7 | ||
|
|
41698d5848 | ||
|
|
838b8d3013 | ||
|
|
2c39e1e9db | ||
|
|
18549ea7dc | ||
|
|
d76dce76ec | ||
|
|
40cbe58220 | ||
|
|
68523a637a | ||
|
|
3f5b71fdc3 | ||
|
|
573d639238 | ||
|
|
82beeec730 | ||
|
|
5336bcd71d | ||
|
|
47b6b70cae | ||
|
|
fd412618db | ||
|
|
9dc79e6f0d | ||
|
|
ba3d7392bf | ||
|
|
bed9fb0ba6 | ||
|
|
9257e62bac | ||
|
|
70f683e7fa | ||
|
|
d1cf248b9a | ||
|
|
295adf9f28 | ||
|
|
6ac4f482ee | ||
|
|
33791318fe | ||
|
|
cf20bbbf80 | ||
|
|
55ef221390 | ||
|
|
0f8d8780d3 | ||
|
|
b743506207 | ||
|
|
0a17a3c4ed | ||
|
|
da80b61f38 | ||
|
|
badeb24d56 | ||
|
|
76c3043913 | ||
|
|
c66cc5ebb1 | ||
|
|
9dc5605d95 | ||
|
|
3af4edf051 | ||
|
|
e9063dcf57 | ||
|
|
d690477776 | ||
|
|
7879e091c0 | ||
|
|
702511c918 | ||
|
|
e1ba1b45b0 | ||
|
|
8ea9e2e4bb | ||
|
|
ef88a3530a | ||
|
|
df795da070 | ||
|
|
12e29f0bcc | ||
|
|
ff54908687 | ||
|
|
3c37fb11be | ||
|
|
3176006510 | ||
|
|
183ccbf792 | ||
|
|
f1ffc1dc33 | ||
|
|
b3cfded8f2 | ||
|
|
67291c1a42 | ||
|
|
93afc83c4b | ||
|
|
57e5fe7265 | ||
|
|
e082d24bab | ||
|
|
13cb5deca3 | ||
|
|
3556d6c8c8 | ||
|
|
83dafb4ef9 | ||
|
|
9df3bd9a98 | ||
|
|
597a06ac38 | ||
|
|
a129345a82 | ||
|
|
8660578d2c | ||
|
|
bf63b6f9f0 | ||
|
|
85ea278e1d | ||
|
|
37e661b27e | ||
|
|
ff6c6083b4 | ||
|
|
aba0d2c1d3 | ||
|
|
3789010dd5 | ||
|
|
82ac42cae3 | ||
|
|
87061959ea | ||
|
|
1840363713 | ||
|
|
b91ff8a5c5 | ||
|
|
9fddce92e9 | ||
|
|
7c5278ea45 | ||
|
|
0e523d3eb7 | ||
|
|
55a4c75e76 | ||
|
|
c07dd72f6d | ||
|
|
c320c9df1c | ||
|
|
c08d88eb7f | ||
|
|
5eaf376a60 | ||
|
|
b26a1b6330 | ||
|
|
6dc3432735 | ||
|
|
e0c86b3048 | ||
|
|
7852df639a | ||
|
|
37f58e5c80 | ||
|
|
0dbee70ca9 | ||
|
|
2749184c38 | ||
|
|
0a3844d3ef | ||
|
|
c7f50f35b7 | ||
|
|
b1bc5cae87 | ||
|
|
9577472c9e | ||
|
|
55fe927af0 | ||
|
|
4ccfebe842 | ||
|
|
b7d6e76d19 | ||
|
|
e34b0659da | ||
|
|
e73e9275ba | ||
|
|
a9c021ce04 | ||
|
|
9a2513230c | ||
|
|
ccc5ca5d80 | ||
|
|
9b9e7a8f75 | ||
|
|
5b781655c1 | ||
|
|
644b797734 | ||
|
|
e5fbf62ff5 | ||
|
|
bcff6be5f6 | ||
|
|
0c68a91b4c | ||
|
|
69a0ac6c76 | ||
|
|
619a6e7321 | ||
|
|
a436372b05 | ||
|
|
449038d070 | ||
|
|
ffab8342f2 | ||
|
|
31188127ef | ||
|
|
48bad31255 | ||
|
|
1a49c81bab | ||
|
|
27db49bad3 | ||
|
|
e6dbb1020c | ||
|
|
e0c11504a2 | ||
|
|
35e1fa6660 | ||
|
|
330b61f4c0 | ||
|
|
3985596602 | ||
|
|
3786efaa5e | ||
|
|
0225c1a867 | ||
|
|
111b98ef8e | ||
|
|
22bfab95b0 | ||
|
|
a78c78ecdd | ||
|
|
84309f57c5 | ||
|
|
6a35b7237b | ||
|
|
90ac249f5e | ||
|
|
087f0565e6 | ||
|
|
038bf3fdda | ||
|
|
61c3aad537 | ||
|
|
3aad0868af | ||
|
|
973c4c8319 | ||
|
|
ac21f8b960 | ||
| e78c8fa03d | |||
|
|
0b3b6468a5 | ||
|
|
7d1c54f045 | ||
|
|
dce4132209 | ||
|
|
fa1fea02dc | ||
|
|
802e559472 | ||
|
|
dbb16fc834 | ||
|
|
eaaca60b1d | ||
|
|
803e30f50f | ||
|
|
e54779ceee | ||
|
|
83765abe34 | ||
|
|
28d36dd3bd | ||
|
|
945db7b431 | ||
|
|
56b072cfd9 | ||
|
|
ddf9d6b8c8 | ||
|
|
a75e0c3850 | ||
|
|
f2dcf2024a | ||
|
|
7ec6c8bf57 | ||
|
|
cf4d6bea72 | ||
|
|
d5ef8c8524 | ||
|
|
b783bb70e9 | ||
|
|
19e9bffc11 | ||
|
|
87023b22ea | ||
|
|
7066f3def7 | ||
|
|
699fbb1a85 | ||
|
|
5df41c08f4 | ||
|
|
c4b07413b1 | ||
|
|
6ae8a68389 | ||
|
|
642b938358 | ||
|
|
6a9aeda5d4 | ||
|
|
b982c3cd20 | ||
|
|
366f2469ef | ||
|
|
737ec8b2a8 | ||
|
|
63654ad1e0 | ||
|
|
01d9c526d9 | ||
|
|
ace74824b8 | ||
|
|
ee055ba8f5 | ||
|
|
60eaf088df | ||
|
|
4a343ceaf1 | ||
|
|
553a8601ed | ||
|
|
9314ea94b5 | ||
|
|
d22b3fb200 | ||
|
|
3165b147ee | ||
|
|
ce73dbbcc6 | ||
|
|
4f298760de | ||
|
|
2aeb80a8bd | ||
|
|
7a9d31adb6 | ||
|
|
cf7fdc0627 | ||
|
|
30f9716db9 | ||
|
|
69c90f9926 | ||
|
|
0c3ee1938e | ||
|
|
8b11be5ac0 | ||
|
|
9eadc9f68c | ||
|
|
2c88e60ed3 | ||
|
|
fd504e5641 | ||
|
|
93ed0483b6 | ||
|
|
4dd0672aa5 | ||
|
|
66eff14d2b | ||
|
|
245ade004a | ||
|
|
6cb46106fe | ||
|
|
3666e4c332 |
@@ -19,6 +19,11 @@ indent_style = space
|
|||||||
indent_size = 4
|
indent_size = 4
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
|
||||||
|
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
|
||||||
|
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
|
||||||
|
dotnet_diagnostic.CS1591.severity = none
|
||||||
|
|
||||||
#license header
|
#license header
|
||||||
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
|||||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,12 +1,6 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Help
|
- name: Help
|
||||||
url: https://github.com/ppy/osu/discussions/categories/q-a
|
url: https://t.me/jvnkosu_chat
|
||||||
about: osu! not working or performing as you'd expect? Not sure it's a bug? Check the Q&A section!
|
about: Your jvnkosu! is not working right? Please contact us using our Telegram chat
|
||||||
- name: Suggestions or feature request
|
|
||||||
url: https://github.com/ppy/osu/discussions/categories/ideas
|
|
||||||
about: Got something you think should change or be added? Search for or start a new discussion!
|
|
||||||
- name: osu!stable issues
|
|
||||||
url: https://github.com/ppy/osu-stable-issues
|
|
||||||
about: For osu!(stable) - ie. the current "live" game version, check out the dedicated repository. Note that this is for serious bug reports only, not tech support.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
name: Update osu-web mod definitions
|
name: Update osu-web mod definitions (DO NOT USE YET!!!!!)
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
# push:
|
||||||
- '*'
|
# tags:
|
||||||
|
# - '*'
|
||||||
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -1,4 +1,10 @@
|
|||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
name: Continuous Integration
|
name: Continuous Integration
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -33,7 +39,7 @@ jobs:
|
|||||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
|
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
|
||||||
|
|
||||||
- name: Dotnet code style
|
- name: Dotnet code style
|
||||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||||
|
|
||||||
- name: CodeFileSanity
|
- name: CodeFileSanity
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
notify_pending_production_deploy:
|
notify_pending_production_deploy:
|
||||||
@@ -12,7 +13,7 @@ jobs:
|
|||||||
- name: Submit pending deployment notification
|
- name: Submit pending deployment notification
|
||||||
run: |
|
run: |
|
||||||
export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME"
|
export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME"
|
||||||
export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID"
|
export URL="https://github.com/jvnkosu-dev/client/actions/runs/$GITHUB_RUN_ID"
|
||||||
export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME:
|
export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME:
|
||||||
[View Workflow Run]($URL)"
|
[View Workflow Run]($URL)"
|
||||||
export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
|
export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
|
||||||
|
|||||||
8
.github/workflows/sentry-release.yml
vendored
8
.github/workflows/sentry-release.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
|||||||
uses: getsentry/action-release@v1
|
uses: getsentry/action-release@v1
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ppy
|
SENTRY_ORG: jvnkosu
|
||||||
SENTRY_PROJECT: osu
|
SENTRY_PROJECT: client
|
||||||
SENTRY_URL: https://sentry.ppy.sh/
|
SENTRY_URL: https://satellite.jvnko.boats/
|
||||||
with:
|
with:
|
||||||
environment: production
|
environment: production
|
||||||
version: osu@${{ github.ref_name }}
|
version: jvnkosu@${{ github.ref_name }}
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
|||||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||||
|
|
||||||
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
||||||
|
- Please pick the following target branch for your pull request:
|
||||||
|
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
|
||||||
|
- `master`, otherwise.
|
||||||
- Please avoid pushing untested or incomplete code.
|
- Please avoid pushing untested or incomplete code.
|
||||||
- Please do not force-push or rebase unless we ask you to.
|
- Please do not force-push or rebase unless we ask you to.
|
||||||
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageType>Template</PackageType>
|
<PackageType>Template</PackageType>
|
||||||
<PackageId>ppy.osu.Game.Templates</PackageId>
|
<PackageId>jvnkosu.Client.Templates</PackageId>
|
||||||
<Title>osu! templates</Title>
|
<Title>osu! templates</Title>
|
||||||
<Authors>ppy Pty Ltd</Authors>
|
<Authors>ppy Pty Ltd</Authors>
|
||||||
<PackageLicenseUrl>https://github.com/ppy/osu/blob/master/LICENCE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/ppy/osu/blob/master/LICENCE</PackageLicenseUrl>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.908.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1209.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="boats.jvnko.osu.android" android:installLocation="auto">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||||
<application android:allowBackup="true"
|
<application android:allowBackup="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
internal partial class DiscordRichPresence : Component
|
internal partial class DiscordRichPresence : Component
|
||||||
{
|
{
|
||||||
private const string client_id = "1216669957799018608";
|
private const string client_id = "1440647613358800918";
|
||||||
|
|
||||||
private DiscordRpcClient client = null!;
|
private DiscordRpcClient client = null!;
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ namespace osu.Desktop
|
|||||||
}
|
}
|
||||||
|
|
||||||
// user party
|
// user party
|
||||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
|
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
|
||||||
{
|
{
|
||||||
MultiplayerRoom room = multiplayerClient.Room;
|
MultiplayerRoom room = multiplayerClient.Room;
|
||||||
|
|
||||||
|
|||||||
@@ -115,12 +115,10 @@ namespace osu.Desktop
|
|||||||
if (IsFirstRun)
|
if (IsFirstRun)
|
||||||
LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
||||||
|
|
||||||
// if (IsPackageManaged)
|
if (IsPackageManaged)
|
||||||
// return new NoActionUpdateManager();
|
return new NoActionUpdateManager();
|
||||||
|
|
||||||
// return new VelopackUpdateManager();
|
return new VelopackUpdateManager(); // yay
|
||||||
|
|
||||||
return new NoActionUpdateManager(); // for now, APIs are useless for actually downloading the releases. TODO: adapt UpdateManager for gitea
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool RestartAppWhenExited()
|
public override bool RestartAppWhenExited()
|
||||||
@@ -150,7 +148,13 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||||
if (iconStream != null)
|
if (iconStream != null)
|
||||||
host.Window.SetIconFromStream(iconStream);
|
try
|
||||||
|
{
|
||||||
|
host.Window.SetIconFromStream(iconStream);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
host.Window.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
|
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
|
||||||
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
|
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
|
||||||
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
|
if (windowsVersion.Major < 6)
|
||||||
{
|
{
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
@@ -53,13 +53,26 @@ namespace osu.Desktop
|
|||||||
// We could also better detect compatibility mode if required:
|
// We could also better detect compatibility mode if required:
|
||||||
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
||||||
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||||
"Your operating system is too old to run osu!"u8,
|
"Your operating system is too old to run this game!"u8,
|
||||||
"This version of osu! requires at least Windows 8.1 to run.\n"u8
|
"This version of the game requires at least Windows 8.1 to run reliably.\n"u8
|
||||||
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
|
+ "You may try to run it on Windows 8 or older, but it's not guaranteed to actually work.\n\n"u8
|
||||||
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
|
+ "Please upgrade your operating system or consider using an older game version.\n\n"u8
|
||||||
|
+ "If you are running a newer version of Windows, please check you don't have \"Compatibility mode\" turned on for the game's executable."u8, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_WARNING,
|
||||||
|
"Your operating system is too old to run this game!"u8,
|
||||||
|
"While the version of Windows you're using may be able to launch it, it's likely to work unreliably and crash.\n"u8
|
||||||
|
+ "Please upgrade your operating system or consider using an older game version.\n\n"u8
|
||||||
|
+ "If you are running a newer version of Windows, please check you don't have \"Compatibility mode\" turned on for the game's executable."u8, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NVIDIA profiles are based on the executable name of a process.
|
// NVIDIA profiles are based on the executable name of a process.
|
||||||
@@ -208,9 +221,10 @@ namespace osu.Desktop
|
|||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
private static void configureWindows(VelopackApp app)
|
private static void configureWindows(VelopackApp app)
|
||||||
{
|
{
|
||||||
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
// we do not want associations here, as that breaks official lazer's associations
|
||||||
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
// app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||||
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
// app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||||
|
// app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace osu.Desktop.Updater
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
IUpdateSource updateSource = new GiteaSource(@"https://gitea.jvnko.boats/jvnkosu/client", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||||
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
||||||
{
|
{
|
||||||
AllowVersionDowngrade = true
|
AllowVersionDowngrade = true
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
private static readonly UriAssociation[] uri_associations =
|
private static readonly UriAssociation[] uri_associations =
|
||||||
{
|
{
|
||||||
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
new UriAssociation(@"jvnkosu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
||||||
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
new UriAssociation(@"jvnkosump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 401 KiB |
77
osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs
Normal file
77
osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Rulesets.Catch;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Mania;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Taiko;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Benchmarks
|
||||||
|
{
|
||||||
|
public class BenchmarkDifficultyCalculation : BenchmarkTest
|
||||||
|
{
|
||||||
|
private DifficultyCalculator osuCalculator = null!;
|
||||||
|
private DifficultyCalculator taikoCalculator = null!;
|
||||||
|
private DifficultyCalculator catchCalculator = null!;
|
||||||
|
private DifficultyCalculator maniaCalculator = null!;
|
||||||
|
|
||||||
|
public override void SetUp()
|
||||||
|
{
|
||||||
|
using var resources = new DllResourceStore(typeof(TestResources).Assembly);
|
||||||
|
|
||||||
|
using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz");
|
||||||
|
using var archiveReader = new ZipArchiveReader(archive);
|
||||||
|
|
||||||
|
var osuBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Gamu) [Insane].osu");
|
||||||
|
var taikoBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (MMzz) [Oni].osu");
|
||||||
|
var catchBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Deif) [Salad].osu");
|
||||||
|
var maniaBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (ExPew) [Another].osu");
|
||||||
|
|
||||||
|
osuCalculator = new OsuRuleset().CreateDifficultyCalculator(osuBeatmap);
|
||||||
|
taikoCalculator = new TaikoRuleset().CreateDifficultyCalculator(taikoBeatmap);
|
||||||
|
catchCalculator = new CatchRuleset().CreateDifficultyCalculator(catchBeatmap);
|
||||||
|
maniaCalculator = new ManiaRuleset().CreateDifficultyCalculator(maniaBeatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatmapName)
|
||||||
|
{
|
||||||
|
using var beatmapStream = new MemoryStream();
|
||||||
|
archiveReader.GetStream(beatmapName).CopyTo(beatmapStream);
|
||||||
|
|
||||||
|
beatmapStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
using var reader = new LineBufferedReader(beatmapStream);
|
||||||
|
|
||||||
|
var decoder = Beatmaps.Formats.Decoder.GetDecoder<Beatmap>(reader);
|
||||||
|
return new FlatWorkingBeatmap(decoder.Decode(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyOsu() => osuCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyTaiko() => taikoCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyCatch() => catchCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyMania() => maniaCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyOsuHundredTimes()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
osuCalculator.Calculate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTinyDropletMissPreservesCatcherState()
|
public void TestTinyDropletMissChangesCatcherState()
|
||||||
{
|
{
|
||||||
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
||||||
{
|
{
|
||||||
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}));
|
}));
|
||||||
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
||||||
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
||||||
// catcher state and hyper dash state is preserved
|
// catcher state is changed but hyper dash state is preserved
|
||||||
checkState(CatcherAnimationState.Kiai);
|
checkState(CatcherAnimationState.Fail);
|
||||||
checkHyperDash(true);
|
checkHyperDash(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
private float halfCatcherWidth;
|
private float halfCatcherWidth;
|
||||||
|
|
||||||
public override int Version => 20250306;
|
public override int Version => 20251020;
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Audio.Track;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
@@ -51,15 +51,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if (catchAttributes.MaxCombo > 0)
|
if (catchAttributes.MaxCombo > 0)
|
||||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
value *= Math.Min(Math.Pow(score.MaxCombo, 0.35) / Math.Pow(catchAttributes.MaxCombo, 0.35), 1.0);
|
||||||
|
|
||||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||||
|
|
||||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||||
|
|
||||||
var track = new TrackVirtual(10000);
|
double clockRate = ModUtils.CalculateRateWithMods(score.Mods);
|
||||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
|
||||||
double clockRate = track.Rate;
|
|
||||||
|
|
||||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class MovementEvaluator
|
||||||
|
{
|
||||||
|
private const double direction_change_bonus = 21.0;
|
||||||
|
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
|
||||||
|
{
|
||||||
|
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||||
|
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||||
|
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||||
|
|
||||||
|
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||||
|
|
||||||
|
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||||
|
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||||
|
|
||||||
|
double edgeDashBonus = 0;
|
||||||
|
|
||||||
|
// Direction change bonus.
|
||||||
|
if (Math.Abs(catchCurrent.DistanceMoved) > 0.1)
|
||||||
|
{
|
||||||
|
if (current.Index >= 1 && Math.Abs(catchLast.DistanceMoved) > 0.1 && Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchLast.DistanceMoved))
|
||||||
|
{
|
||||||
|
double bonusFactor = Math.Min(50, Math.Abs(catchCurrent.DistanceMoved)) / 50;
|
||||||
|
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(catchLast.DistanceMoved)) / 70, 0.38);
|
||||||
|
|
||||||
|
distanceAddition += direction_change_bonus / Math.Sqrt(catchLast.StrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base bonus for every movement, giving some weight to streams.
|
||||||
|
distanceAddition += 12.5 * Math.Min(Math.Abs(catchCurrent.DistanceMoved), CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2)
|
||||||
|
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for edge dashes.
|
||||||
|
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||||
|
{
|
||||||
|
if (!catchCurrent.LastObject.HyperDash)
|
||||||
|
edgeDashBonus += 5.7;
|
||||||
|
|
||||||
|
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||||
|
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||||
|
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH offsets
|
||||||
|
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||||
|
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH)
|
||||||
|
if (current.Index >= 2 && Math.Abs(catchCurrent.ExactDistanceMoved) <= CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2
|
||||||
|
&& catchCurrent.ExactDistanceMoved == -catchLast.ExactDistanceMoved && catchLast.ExactDistanceMoved == -catchLastLast.ExactDistanceMoved
|
||||||
|
&& catchCurrent.StrainTime == catchLast.StrainTime && catchLast.StrainTime == catchLastLast.StrainTime)
|
||||||
|
distanceAddition = 0;
|
||||||
|
|
||||||
|
return distanceAddition / weightedStrainTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,15 +11,49 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
public class CatchDifficultyHitObject : DifficultyHitObject
|
public class CatchDifficultyHitObject : DifficultyHitObject
|
||||||
{
|
{
|
||||||
private const float normalized_hitobject_radius = 41.0f;
|
public const float NORMALIZED_HALF_CATCHER_WIDTH = 41.0f;
|
||||||
|
private const float absolute_player_positioning_error = 16.0f;
|
||||||
|
|
||||||
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
||||||
|
|
||||||
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of <see cref="BaseObject"/>.
|
||||||
|
/// </summary>
|
||||||
public readonly float NormalizedPosition;
|
public readonly float NormalizedPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of <see cref="LastObject"/>.
|
||||||
|
/// </summary>
|
||||||
public readonly float LastNormalizedPosition;
|
public readonly float LastNormalizedPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of the player required to catch <see cref="BaseObject"/>, assuming the player moves as little as possible.
|
||||||
|
/// </summary>
|
||||||
|
public float PlayerPosition { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of the player after catching <see cref="LastObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public float LastPlayerPosition { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized distance between <see cref="LastPlayerPosition"/> and <see cref="PlayerPosition"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||||
|
/// </remarks>
|
||||||
|
public float DistanceMoved { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized distance the player has to move from <see cref="LastPlayerPosition"/> in order to catch <see cref="BaseObject"/> at its <see cref="NormalizedPosition"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||||
|
/// </remarks>
|
||||||
|
public float ExactDistanceMoved { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,13 +63,35 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
|||||||
: base(hitObject, lastObject, clockRate, objects, index)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
float scalingFactor = NORMALIZED_HALF_CATCHER_WIDTH / halfCatcherWidth;
|
||||||
|
|
||||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||||
|
|
||||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||||
StrainTime = Math.Max(40, DeltaTime);
|
StrainTime = Math.Max(40, DeltaTime);
|
||||||
|
|
||||||
|
setMovementState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMovementState()
|
||||||
|
{
|
||||||
|
LastPlayerPosition = Index == 0 ? LastNormalizedPosition : ((CatchDifficultyHitObject)Previous(0)).PlayerPosition;
|
||||||
|
|
||||||
|
PlayerPosition = Math.Clamp(
|
||||||
|
LastPlayerPosition,
|
||||||
|
NormalizedPosition - (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error),
|
||||||
|
NormalizedPosition + (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error)
|
||||||
|
);
|
||||||
|
|
||||||
|
DistanceMoved = PlayerPosition - LastPlayerPosition;
|
||||||
|
|
||||||
|
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||||
|
ExactDistanceMoved = NormalizedPosition - LastPlayerPosition;
|
||||||
|
|
||||||
|
// After a hyperdash we ARE in the correct position. Always!
|
||||||
|
if (LastObject.HyperDash)
|
||||||
|
PlayerPosition = NormalizedPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using osu.Game.Rulesets.Catch.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@@ -11,10 +10,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
public class Movement : StrainDecaySkill
|
public class Movement : StrainDecaySkill
|
||||||
{
|
{
|
||||||
private const float absolute_player_positioning_error = 16f;
|
|
||||||
private const float normalized_hitobject_radius = 41.0f;
|
|
||||||
private const double direction_change_bonus = 21.0;
|
|
||||||
|
|
||||||
protected override double SkillMultiplier => 1;
|
protected override double SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 0.2;
|
protected override double StrainDecayBase => 0.2;
|
||||||
|
|
||||||
@@ -24,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
|
|
||||||
protected readonly float HalfCatcherWidth;
|
protected readonly float HalfCatcherWidth;
|
||||||
|
|
||||||
private float? lastPlayerPosition;
|
|
||||||
private float lastDistanceMoved;
|
|
||||||
private float lastExactDistanceMoved;
|
|
||||||
private double lastStrainTime;
|
|
||||||
private bool isInBuzzSection;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The speed multiplier applied to the player's catcher.
|
/// The speed multiplier applied to the player's catcher.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -49,80 +38,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
|
||||||
|
|
||||||
lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
|
|
||||||
|
|
||||||
float playerPosition = Math.Clamp(
|
|
||||||
lastPlayerPosition.Value,
|
|
||||||
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
|
|
||||||
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
|
|
||||||
);
|
|
||||||
|
|
||||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
|
||||||
|
|
||||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
|
||||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
|
||||||
|
|
||||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
|
||||||
|
|
||||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
|
||||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
|
||||||
|
|
||||||
double edgeDashBonus = 0;
|
|
||||||
|
|
||||||
// Direction change bonus.
|
|
||||||
if (Math.Abs(distanceMoved) > 0.1)
|
|
||||||
{
|
|
||||||
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
|
|
||||||
{
|
|
||||||
double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
|
|
||||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
|
|
||||||
|
|
||||||
distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base bonus for every movement, giving some weight to streams.
|
|
||||||
distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bonus for edge dashes.
|
|
||||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
|
||||||
{
|
|
||||||
if (!catchCurrent.LastObject.HyperDash)
|
|
||||||
edgeDashBonus += 5.7;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// After a hyperdash we ARE in the correct position. Always!
|
|
||||||
playerPosition = catchCurrent.NormalizedPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
|
||||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
|
||||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
|
||||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
|
||||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
|
||||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
|
||||||
{
|
|
||||||
if (isInBuzzSection)
|
|
||||||
distanceAddition = 0;
|
|
||||||
else
|
|
||||||
isInBuzzSection = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isInBuzzSection = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastPlayerPosition = playerPosition;
|
|
||||||
lastDistanceMoved = distanceMoved;
|
|
||||||
lastStrainTime = catchCurrent.StrainTime;
|
|
||||||
lastExactDistanceMoved = exactDistanceMoved;
|
|
||||||
|
|
||||||
return distanceAddition / weightedStrainTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
private double placementStartTime;
|
private double placementStartTime;
|
||||||
private double placementEndTime;
|
private double placementEndTime;
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||||
|
|
||||||
public BananaShowerPlacementBlueprint()
|
public BananaShowerPlacementBlueprint()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
private InputManager inputManager = null!;
|
private InputManager inputManager = null!;
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||||
|
|
||||||
public JuiceStreamPlacementBlueprint()
|
public JuiceStreamPlacementBlueprint()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@@ -224,7 +225,8 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
#region Clipboard handling
|
#region Clipboard handling
|
||||||
|
|
||||||
public override string ConvertSelectionToString()
|
public override string ConvertSelectionToString()
|
||||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime)
|
||||||
|
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
|
||||||
|
|
||||||
// 1,2,3,4 ...
|
// 1,2,3,4 ...
|
||||||
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
string format(string acronym, DifficultyBindable bindable)
|
||||||
|
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Catch.Judgements;
|
using osu.Game.Rulesets.Catch.Judgements;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@@ -53,13 +54,25 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
public override IEnumerable<string> LookupNames => lookup_names;
|
public override IEnumerable<string> LookupNames => lookup_names;
|
||||||
|
|
||||||
public BananaHitSampleInfo(int volume = 100)
|
public BananaHitSampleInfo()
|
||||||
: base(string.Empty, volume: volume)
|
: this(string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
|
public BananaHitSampleInfo(HitSampleInfo info)
|
||||||
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
|
: this(info.Name, info.Bank, info.Suffix, info.Volume, info.EditorAutoBank, info.UseBeatmapSamples)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private BananaHitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true, bool useBeatmapSamples = false)
|
||||||
|
: base(name, bank, suffix, volume, editorAutoBank, useBeatmapSamples)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
|
||||||
|
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
|
||||||
|
=> new BananaHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume),
|
||||||
|
newEditorAutoBank.GetOr(EditorAutoBank), newUseBeatmapSamples.GetOr(UseBeatmapSamples));
|
||||||
|
|
||||||
public bool Equals(BananaHitSampleInfo? other)
|
public bool Equals(BananaHitSampleInfo? other)
|
||||||
=> other != null;
|
=> other != null;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
StartTime = time,
|
StartTime = time,
|
||||||
BananaIndex = count,
|
BananaIndex = count,
|
||||||
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) }
|
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo()) }
|
||||||
});
|
});
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
|
|||||||
@@ -224,7 +224,20 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
|
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
|
||||||
}
|
}
|
||||||
|
|
||||||
// droplet doesn't affect the catcher state
|
if (result.IsHit)
|
||||||
|
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||||
|
else if (hitObject is not Banana)
|
||||||
|
CurrentState = CatcherAnimationState.Fail;
|
||||||
|
|
||||||
|
if (palpableObject.HitObject.LastInCombo)
|
||||||
|
{
|
||||||
|
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||||
|
Explode();
|
||||||
|
else
|
||||||
|
Drop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// droplet doesn't affect hyperdash state
|
||||||
if (hitObject is TinyDroplet) return;
|
if (hitObject is TinyDroplet) return;
|
||||||
|
|
||||||
// if a hyper fruit was already handled this frame, just go where it says to go.
|
// if a hyper fruit was already handled this frame, just go where it says to go.
|
||||||
@@ -244,19 +257,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
else
|
else
|
||||||
SetHyperDashState();
|
SetHyperDashState();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.IsHit)
|
|
||||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
|
||||||
else if (!(hitObject is Banana))
|
|
||||||
CurrentState = CatcherAnimationState.Fail;
|
|
||||||
|
|
||||||
if (palpableObject.HitObject.LastInCombo)
|
|
||||||
{
|
|
||||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
|
||||||
Explode();
|
|
||||||
else
|
|
||||||
Drop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnRevertResult(JudgementResult result)
|
public void OnRevertResult(JudgementResult result)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<PropertyGroup Label="Nuget">
|
<PropertyGroup Label="Nuget">
|
||||||
<Title>osu!catch (ruleset)</Title>
|
<Title>osu!catch (ruleset)</Title>
|
||||||
<PackageId>ppy.osu.Game.Rulesets.Catch</PackageId>
|
<PackageId>jvnkosu.Client.Rulesets.Catch</PackageId>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@@ -36,21 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestPlaceBeforeCurrentTimeDownwards()
|
public void TestPlaceBeforeCurrentTimeDownwards()
|
||||||
{
|
{
|
||||||
|
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
|
||||||
AddStep("move mouse before current time", () =>
|
AddStep("move mouse before current time", () =>
|
||||||
{
|
|
||||||
var column = this.ChildrenOfType<Column>().Single();
|
|
||||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
|
||||||
|
|
||||||
AddAssert("note start time < 0", () => getNote().StartTime < 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestPlaceAfterCurrentTimeDownwards()
|
|
||||||
{
|
|
||||||
AddStep("move mouse after current time", () =>
|
|
||||||
{
|
{
|
||||||
var column = this.ChildrenOfType<Column>().Single();
|
var column = this.ChildrenOfType<Column>().Single();
|
||||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
|
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
|
||||||
@@ -58,7 +47,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
|
|
||||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
AddAssert("note start time > 0", () => getNote().StartTime > 0);
|
AddAssert("note start time < 200", () => getNote().StartTime < 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlaceAfterCurrentTimeDownwards()
|
||||||
|
{
|
||||||
|
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
|
||||||
|
AddStep("move mouse after current time", () =>
|
||||||
|
{
|
||||||
|
var column = this.ChildrenOfType<Column>().Single();
|
||||||
|
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(300));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("note start time > 200", () => getNote().StartTime > 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||||
|
|||||||
@@ -18,15 +18,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
public void TestNormalSelection()
|
public void TestNormalSelection()
|
||||||
{
|
{
|
||||||
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
|
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
|
||||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
|
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, [(5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1)]));
|
||||||
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
|
|
||||||
));
|
|
||||||
|
|
||||||
addReset();
|
addReset();
|
||||||
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
|
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
|
||||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
|
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, [(42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1)]));
|
||||||
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
|
|
||||||
));
|
|
||||||
|
|
||||||
addReset();
|
addReset();
|
||||||
AddStep("add notes to row", () =>
|
AddStep("add notes to row", () =>
|
||||||
@@ -41,15 +37,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
EditorBeatmap.AddRange(new[] { second, third, forth });
|
EditorBeatmap.AddRange(new[] { second, third, forth });
|
||||||
});
|
});
|
||||||
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
|
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
|
||||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
|
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, [(11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3)]));
|
||||||
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
|
|
||||||
));
|
|
||||||
|
|
||||||
addReset();
|
addReset();
|
||||||
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
|
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
|
||||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
|
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, [(96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1)]));
|
||||||
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
|
}
|
||||||
));
|
|
||||||
|
[Test]
|
||||||
|
public void TestRoundingToNearestMillisecondApplied()
|
||||||
|
{
|
||||||
|
AddStep("resnap note to have fractional coordinates",
|
||||||
|
() => EditorBeatmap.HitObjects.OfType<ManiaHitObject>().Single(ho => ho.StartTime == 85_373 && ho.Column == 1).StartTime = 85_373.125);
|
||||||
|
addStepClickLink("01:25:373 (85373|1)");
|
||||||
|
AddAssert("selected note", () => checkSnapAndSelectColumn(85_373.125, [(85_373.125, 1)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
|
|
||||||
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
||||||
|
|
||||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
|
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(double, int)>? columnPairs = null)
|
||||||
{
|
{
|
||||||
bool checkColumns = columnPairs != null
|
bool checkColumns = columnPairs != null
|
||||||
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
|
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
[TestCase("mania-samples")]
|
[TestCase("mania-samples")]
|
||||||
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
||||||
[TestCase("slider-convert-samples")]
|
[TestCase("slider-convert-samples")]
|
||||||
|
[TestCase("spinner-convert-samples")]
|
||||||
public void Test(string name) => base.Test(name);
|
public void Test(string name) => base.Test(name);
|
||||||
|
|
||||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
Mode: 0
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
0,300,4,0,2,100,1,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
444,320,1000,5,2,0:0:0:0:
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"Mappings": [{
|
||||||
|
"StartTime": 1000.0,
|
||||||
|
"Objects": [{
|
||||||
|
"StartTime": 1000.0,
|
||||||
|
"EndTime": 8000.0,
|
||||||
|
"Column": 0,
|
||||||
|
"PlaySlidingSamples": false,
|
||||||
|
"NodeSamples": [
|
||||||
|
["Gameplay/soft-hitnormal"],
|
||||||
|
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
|
||||||
|
],
|
||||||
|
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
Mode: 0
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:5
|
||||||
|
CircleSize:5
|
||||||
|
OverallDifficulty:5
|
||||||
|
ApproachRate:5
|
||||||
|
SliderMultiplier:1.4
|
||||||
|
SliderTickRate:1
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
0,500,4,2,0,100,1,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
256,192,1000,8,4,8000,0:2:0:0:
|
||||||
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
AssertBeatmapLookup(expected_sample);
|
AssertBeatmapLookup(expected_sample);
|
||||||
AssertNoLookup(unwanted_sample);
|
AssertNoLookup(unwanted_sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestConvertHitObjectCustomSampleBank()
|
||||||
|
{
|
||||||
|
const string beatmap_sample = "normal-hitwhistle2";
|
||||||
|
const string user_skin_sample = "normal-hitnormal";
|
||||||
|
|
||||||
|
SetupSkins(beatmap_sample, user_skin_sample);
|
||||||
|
|
||||||
|
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
|
||||||
|
|
||||||
|
AssertBeatmapLookup(beatmap_sample);
|
||||||
|
AssertUserLookup(user_skin_sample);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
Duration = endTime - HitObject.StartTime,
|
Duration = endTime - HitObject.StartTime,
|
||||||
Column = column,
|
Column = column,
|
||||||
Samples = HitObject.Samples,
|
Samples = HitObject.Samples,
|
||||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
NodeSamples =
|
||||||
|
[
|
||||||
|
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
|
||||||
|
HitObject.Samples
|
||||||
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public class IndividualStrainEvaluator
|
||||||
|
{
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
|
double startTime = maniaCurrent.StartTime;
|
||||||
|
double endTime = maniaCurrent.EndTime;
|
||||||
|
|
||||||
|
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||||
|
|
||||||
|
// We award a bonus if this note starts and ends before the end of another hold note.
|
||||||
|
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||||
|
{
|
||||||
|
if (maniaPrevious is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||||
|
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||||
|
{
|
||||||
|
holdFactor = 1.25;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 2.0 * holdFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public class OverallStrainEvaluator
|
||||||
|
{
|
||||||
|
private const double release_threshold = 30;
|
||||||
|
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
|
double startTime = maniaCurrent.StartTime;
|
||||||
|
double endTime = maniaCurrent.EndTime;
|
||||||
|
bool isOverlapping = false;
|
||||||
|
|
||||||
|
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||||
|
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||||
|
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||||
|
|
||||||
|
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||||
|
{
|
||||||
|
if (maniaPrevious is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||||
|
isOverlapping |= Precision.DefinitelyBigger(maniaPrevious.EndTime, startTime, 1) &&
|
||||||
|
Precision.DefinitelyBigger(endTime, maniaPrevious.EndTime, 1) &&
|
||||||
|
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1);
|
||||||
|
|
||||||
|
// We give a slight bonus to everything if something is held meanwhile
|
||||||
|
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||||
|
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||||
|
holdFactor = 1.25;
|
||||||
|
|
||||||
|
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - maniaPrevious.EndTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||||
|
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||||
|
// holdAddition
|
||||||
|
// ^
|
||||||
|
// 1.0 + - - - - - -+-----------
|
||||||
|
// | /
|
||||||
|
// 0.5 + - - - - -/ Sigmoid Curve
|
||||||
|
// | /|
|
||||||
|
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||||
|
// release_threshold
|
||||||
|
if (isOverlapping)
|
||||||
|
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||||
|
|
||||||
|
return (1 + holdAddition) * holdFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||||
|
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||||
|
|
||||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||||
|
|
||||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
List<DifficultyHitObject>[] perColumnObjects = new List<DifficultyHitObject>[totalColumns];
|
||||||
|
|
||||||
|
for (int column = 0; column < totalColumns; column++)
|
||||||
|
perColumnObjects[column] = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
for (int i = 1; i < sortedObjects.Length; i++)
|
for (int i = 1; i < sortedObjects.Length; i++)
|
||||||
objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));
|
{
|
||||||
|
var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count);
|
||||||
|
objects.Add(currentObject);
|
||||||
|
perColumnObjects[currentObject.Column].Add(currentObject);
|
||||||
|
}
|
||||||
|
|
||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
||||||
|
|
||||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
private readonly List<DifficultyHitObject>[] perColumnObjects;
|
||||||
|
|
||||||
|
private readonly int columnIndex;
|
||||||
|
|
||||||
|
public readonly int Column;
|
||||||
|
|
||||||
|
// The hit object earlier in time than this note in each column
|
||||||
|
public readonly ManiaDifficultyHitObject?[] PreviousHitObjects;
|
||||||
|
|
||||||
|
public readonly double ColumnStrainTime;
|
||||||
|
|
||||||
|
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, List<DifficultyHitObject>[] perColumnObjects, int index)
|
||||||
: base(hitObject, lastObject, clockRate, objects, index)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
|
int totalColumns = perColumnObjects.Length;
|
||||||
|
this.perColumnObjects = perColumnObjects;
|
||||||
|
Column = BaseObject.Column;
|
||||||
|
columnIndex = perColumnObjects[Column].Count;
|
||||||
|
PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns];
|
||||||
|
ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime;
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1];
|
||||||
|
|
||||||
|
for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++)
|
||||||
|
PreviousHitObjects[i] = prevNote.PreviousHitObjects[i];
|
||||||
|
|
||||||
|
// intentionally depends on processing order to match live.
|
||||||
|
PreviousHitObjects[prevNote.Column] = prevNote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The previous object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="backwardsIndex">The number of notes to go back.</param>
|
||||||
|
/// <returns>The object in this column <paramref name="backwardsIndex"/> notes back, or null if this is the first note in the column.</returns>
|
||||||
|
public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex)
|
||||||
|
{
|
||||||
|
int index = columnIndex - (backwardsIndex + 1);
|
||||||
|
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="forwardsIndex">The number of notes to go forward.</param>
|
||||||
|
/// <returns>The object in this column <paramref name="forwardsIndex"/> notes forward, or null if this is the last note in the column.</returns>
|
||||||
|
public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex)
|
||||||
|
{
|
||||||
|
int index = columnIndex + (forwardsIndex + 1);
|
||||||
|
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
using osu.Game.Rulesets.Mania.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
@@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
private const double individual_decay_base = 0.125;
|
private const double individual_decay_base = 0.125;
|
||||||
private const double overall_decay_base = 0.30;
|
private const double overall_decay_base = 0.30;
|
||||||
private const double release_threshold = 30;
|
|
||||||
|
|
||||||
protected override double SkillMultiplier => 1;
|
protected override double SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 1;
|
protected override double StrainDecayBase => 1;
|
||||||
|
|
||||||
private readonly double[] startTimes;
|
|
||||||
private readonly double[] endTimes;
|
|
||||||
private readonly double[] individualStrains;
|
private readonly double[] individualStrains;
|
||||||
|
private double highestIndividualStrain;
|
||||||
private double individualStrain;
|
|
||||||
private double overallStrain;
|
private double overallStrain;
|
||||||
|
|
||||||
public Strain(Mod[] mods, int totalColumns)
|
public Strain(Mod[] mods, int totalColumns)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
startTimes = new double[totalColumns];
|
|
||||||
endTimes = new double[totalColumns];
|
|
||||||
individualStrains = new double[totalColumns];
|
individualStrains = new double[totalColumns];
|
||||||
overallStrain = 1;
|
overallStrain = 1;
|
||||||
}
|
}
|
||||||
@@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
double startTime = maniaCurrent.StartTime;
|
|
||||||
double endTime = maniaCurrent.EndTime;
|
|
||||||
int column = maniaCurrent.BaseObject.Column;
|
|
||||||
bool isOverlapping = false;
|
|
||||||
|
|
||||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base);
|
||||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current);
|
||||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
|
||||||
|
|
||||||
for (int i = 0; i < endTimes.Length; ++i)
|
// Take the hardest individualStrain for notes that happen at the same time (in a chord).
|
||||||
{
|
// This is to ensure the order in which the notes are processed does not affect the resultant total strain.
|
||||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column];
|
||||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
|
|
||||||
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
|
|
||||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
|
|
||||||
|
|
||||||
// We give a slight bonus to everything if something is held meanwhile
|
overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base);
|
||||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
|
overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current);
|
||||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
|
|
||||||
holdFactor = 1.25;
|
|
||||||
|
|
||||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
|
||||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
|
||||||
// holdAddition
|
|
||||||
// ^
|
|
||||||
// 1.0 + - - - - - -+-----------
|
|
||||||
// | /
|
|
||||||
// 0.5 + - - - - -/ Sigmoid Curve
|
|
||||||
// | /|
|
|
||||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
|
||||||
// release_threshold
|
|
||||||
if (isOverlapping)
|
|
||||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
|
||||||
|
|
||||||
// Decay and increase individualStrains in own column
|
|
||||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
|
||||||
individualStrains[column] += 2.0 * holdFactor;
|
|
||||||
|
|
||||||
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
|
|
||||||
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
|
|
||||||
|
|
||||||
// Decay and increase overallStrain
|
|
||||||
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
|
|
||||||
overallStrain += (1 + holdAddition) * holdFactor;
|
|
||||||
|
|
||||||
// Update startTimes and endTimes arrays
|
|
||||||
startTimes[column] = startTime;
|
|
||||||
endTimes[column] = endTime;
|
|
||||||
|
|
||||||
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||||
return individualStrain + overallStrain - CurrentStrain;
|
return highestIndividualStrain + overallStrain - CurrentStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)
|
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) =>
|
||||||
=> applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||||
|
|
||||||
private double applyDecay(double value, double deltaTime, double decayBase)
|
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||||
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||||
|
|
||||||
public HoldNotePlacementBlueprint()
|
public HoldNotePlacementBlueprint()
|
||||||
: base(new HoldNote())
|
: base(new HoldNote())
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
@@ -54,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
};
|
};
|
||||||
|
|
||||||
public override string ConvertSelectionToString()
|
public override string ConvertSelectionToString()
|
||||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime)
|
||||||
|
.Select(h => FormattableString.Invariant($"{Math.Round(h.StartTime)}|{h.Column}")));
|
||||||
|
|
||||||
// 123|0,456|1,789|2 ...
|
// 123|0,456|1,789|2 ...
|
||||||
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
|
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
|
||||||
@@ -73,10 +76,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
if (split.Length != 2)
|
if (split.Length != 2)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
|
if (!int.TryParse(split[0], out int time) || !int.TryParse(split[1], out int column))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => Precision.AlmostEquals(h.StartTime, time, 0.5) && h.Column == column);
|
||||||
|
|
||||||
if (current == null)
|
if (current == null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => Name;
|
public override string Acronym => Name;
|
||||||
public abstract int KeyCount { get; }
|
public abstract int KeyCount { get; }
|
||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
public override double ScoreMultiplier => 0.9;
|
public override double ScoreMultiplier => 1;
|
||||||
public override bool Ranked => UsesDefaultConfiguration;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
typeof(ManiaModFadeIn)
|
typeof(ManiaModFadeIn)
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
public override bool Ranked => false;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,20 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
public class ManiaModDifficultyAdjust : ModDifficultyAdjust
|
public class ManiaModDifficultyAdjust : ModDifficultyAdjust
|
||||||
{
|
{
|
||||||
|
public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
|
||||||
|
{
|
||||||
|
Precision = 0.1f,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
// Use larger extended limits for mania to include OD values that occur with EZ or HR enabled
|
||||||
|
#if !DEBUG
|
||||||
|
ExtendedMaxValue = 15,
|
||||||
|
ExtendedMinValue = -15,
|
||||||
|
#else
|
||||||
|
ExtendedMinValue = -250,
|
||||||
|
ExtendedMaxValue = 50,
|
||||||
|
#endif
|
||||||
|
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
|
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
public override bool Ranked => false;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
|
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
|
|||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
|
||||||
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||||
|
|
||||||
|
double mostCommonBeatLengthBefore = beatmap.GetMostCommonBeatLength();
|
||||||
|
|
||||||
var newObjects = new List<ManiaHitObject>();
|
var newObjects = new List<ManiaHitObject>();
|
||||||
|
|
||||||
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
|
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
|
||||||
@@ -48,6 +51,17 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
}
|
}
|
||||||
|
|
||||||
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
|
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
|
||||||
|
|
||||||
|
double mostCommonBeatLengthAfter = beatmap.GetMostCommonBeatLength();
|
||||||
|
|
||||||
|
// the process of removing hold notes can result in shortening the beatmap's play time,
|
||||||
|
// and therefore, as a side effect, changing the most common BPM, which will change scroll speed.
|
||||||
|
// to compensate for this, apply a multiplier to effect points in order to maintain the beatmap's original intended scroll speed.
|
||||||
|
if (!Precision.AlmostEquals(mostCommonBeatLengthBefore, mostCommonBeatLengthAfter))
|
||||||
|
{
|
||||||
|
foreach (var effectPoint in beatmap.ControlPointInfo.EffectPoints)
|
||||||
|
effectPoint.ScrollSpeed *= mostCommonBeatLengthBefore / mostCommonBeatLengthAfter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => "1K";
|
public override string Acronym => "1K";
|
||||||
public override IconUsage? Icon => OsuIcon.ModOneKey;
|
public override IconUsage? Icon => OsuIcon.ModOneKey;
|
||||||
public override LocalisableString Description => @"Play with one key.";
|
public override LocalisableString Description => @"Play with one key.";
|
||||||
public override bool Ranked => false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => "10K";
|
public override string Acronym => "10K";
|
||||||
public override IconUsage? Icon => OsuIcon.ModTenKeys;
|
public override IconUsage? Icon => OsuIcon.ModTenKeys;
|
||||||
public override LocalisableString Description => @"Play with ten keys.";
|
public override LocalisableString Description => @"Play with ten keys.";
|
||||||
public override bool Ranked => false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => "2K";
|
public override string Acronym => "2K";
|
||||||
public override IconUsage? Icon => OsuIcon.ModTwoKeys;
|
public override IconUsage? Icon => OsuIcon.ModTwoKeys;
|
||||||
public override LocalisableString Description => @"Play with two keys.";
|
public override LocalisableString Description => @"Play with two keys.";
|
||||||
public override bool Ranked => false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => "3K";
|
public override string Acronym => "3K";
|
||||||
public override IconUsage? Icon => OsuIcon.ModThreeKeys;
|
public override IconUsage? Icon => OsuIcon.ModThreeKeys;
|
||||||
public override LocalisableString Description => @"Play with three keys.";
|
public override LocalisableString Description => @"Play with three keys.";
|
||||||
public override bool Ranked => false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
|
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
|
||||||
{
|
{
|
||||||
public override LocalisableString Description => "Notes are flipped horizontally.";
|
public override LocalisableString Description => "Notes are flipped horizontally.";
|
||||||
public override bool Ranked => UsesDefaultConfiguration;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
|
|
||||||
private Drawable noteAnimation = null!;
|
private Drawable noteAnimation = null!;
|
||||||
|
|
||||||
private float? minimumColumnWidth;
|
private float? widthForNoteHeightScale;
|
||||||
|
|
||||||
public LegacyNotePiece()
|
public LegacyNotePiece()
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||||
{
|
{
|
||||||
minimumColumnWidth = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
|
widthForNoteHeightScale = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale))?.Value;
|
||||||
|
|
||||||
InternalChild = directionContainer = new Container
|
InternalChild = directionContainer = new Container
|
||||||
{
|
{
|
||||||
@@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
|
|
||||||
if (texture != null)
|
if (texture != null)
|
||||||
{
|
{
|
||||||
// The height is scaled to the minimum column width, if provided.
|
float noteHeight = widthForNoteHeightScale ?? DrawWidth;
|
||||||
float minimumWidth = minimumColumnWidth ?? DrawWidth;
|
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, noteHeight), texture.DisplayWidth);
|
||||||
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
private readonly Lazy<bool> hasKeyTexture;
|
private readonly Lazy<bool> hasKeyTexture;
|
||||||
|
|
||||||
private readonly ManiaBeatmap beatmap;
|
private readonly ManiaBeatmap beatmap;
|
||||||
|
private readonly bool isBeatmapConverted;
|
||||||
|
|
||||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||||
: base(skin)
|
: base(skin)
|
||||||
{
|
{
|
||||||
this.beatmap = (ManiaBeatmap)beatmap;
|
this.beatmap = (ManiaBeatmap)beatmap;
|
||||||
|
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||||
|
|
||||||
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
||||||
hasKeyTexture = new Lazy<bool>(() =>
|
hasKeyTexture = new Lazy<bool>(() =>
|
||||||
@@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
|
|
||||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||||
{
|
{
|
||||||
// layered hit sounds never play in mania
|
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
|
||||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
|
||||||
return new SampleVirtual();
|
return new SampleVirtual();
|
||||||
|
|
||||||
return base.GetSample(sampleInfo);
|
return base.GetSample(sampleInfo);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<PropertyGroup Label="Nuget">
|
<PropertyGroup Label="Nuget">
|
||||||
<Title>osu!mania (ruleset)</Title>
|
<Title>osu!mania (ruleset)</Title>
|
||||||
<PackageId>ppy.osu.Game.Rulesets.Mania</PackageId>
|
<PackageId>jvnkosu.Client.Rulesets.Mania</PackageId>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -245,13 +245,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddAssert("grid spacing is distance to slider tail", () =>
|
AddAssert("grid spacing is distance to slider tail", () =>
|
||||||
{
|
{
|
||||||
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
|
||||||
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
|
||||||
});
|
});
|
||||||
AddAssert("grid rotation points to slider tail", () =>
|
AddAssert("grid rotation points to slider tail", () =>
|
||||||
{
|
{
|
||||||
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
@@ -280,9 +280,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddAssert("grid spacing and rotation unchanged", () =>
|
AddAssert("grid spacing and rotation unchanged", () =>
|
||||||
{
|
{
|
||||||
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
|
||||||
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
|
||||||
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@@ -22,7 +23,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
|
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
|
||||||
{
|
{
|
||||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||||
|
{
|
||||||
|
var beatmap = new TestBeatmap(ruleset, false);
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
|
||||||
|
return beatmap;
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTouchInputPlaceHitCircleDirectly()
|
public void TestTouchInputPlaceHitCircleDirectly()
|
||||||
|
|||||||
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||||
|
|
||||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
|
||||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
[TestCase(0.13841532030395723d, 2, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(9.6779746353001634d, 239, "diffcalc-test")]
|
[TestCase(9.6491691624112761d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
|
[TestCase(1.756936832498702d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
|
[TestCase(0.57771197086735004d, 4, "very-fast-slider")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
|
||||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 644 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneGameplayCursorSizeChange : PlayerTestScene
|
||||||
|
{
|
||||||
|
private const float initial_cursor_size = 1f;
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager? skins { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
if (skins != null) skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
base.SetUpSteps();
|
||||||
|
|
||||||
|
AddStep("Set gameplay cursor size: 1", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, initial_cursor_size));
|
||||||
|
AddStep("resume player", () => Player.GameplayClockContainer.Start());
|
||||||
|
AddUntilStep("clock running", () => Player.GameplayClockContainer.IsRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPausedChangeCursorSize()
|
||||||
|
{
|
||||||
|
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||||
|
AddStep("move cursor to top left", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft));
|
||||||
|
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||||
|
AddStep("move cursor to top right", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight));
|
||||||
|
AddStep("press escape", () => InputManager.Key(Key.Escape));
|
||||||
|
|
||||||
|
AddSliderStep("cursor size", 0.1f, 2f, 1f, v => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
@@ -10,6 +11,7 @@ using osu.Framework.Input.States;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Testing.Input;
|
using osu.Framework.Testing.Input;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
@@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
foreach (var smokeContainer in smokeContainers)
|
foreach (var smokeContainer in smokeContainers)
|
||||||
{
|
{
|
||||||
if (smokeContainer.Children.Count != 0)
|
if (smokeContainer.Children.OfType<SkinnableDrawable>().Any())
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
[FlakyTest]
|
||||||
public void TestRewind()
|
public void TestRewind()
|
||||||
{
|
{
|
||||||
AddStep("set manual clock", () => manualClock = new ManualClock
|
AddStep("set manual clock", () => manualClock = new ManualClock
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
public static class AimEvaluator
|
public static class AimEvaluator
|
||||||
{
|
{
|
||||||
private const double wide_angle_multiplier = 1.5;
|
private const double wide_angle_multiplier = 1.5;
|
||||||
private const double acute_angle_multiplier = 2.6;
|
private const double acute_angle_multiplier = 2.55;
|
||||||
private const double slider_multiplier = 1.35;
|
private const double slider_multiplier = 1.35;
|
||||||
private const double velocity_change_multiplier = 0.75;
|
private const double velocity_change_multiplier = 0.75;
|
||||||
private const double wiggle_multiplier = 1.02;
|
private const double wiggle_multiplier = 1.02;
|
||||||
@@ -34,12 +34,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||||
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||||
|
var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2);
|
||||||
|
|
||||||
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
||||||
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
||||||
|
|
||||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||||
@@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
}
|
}
|
||||||
|
|
||||||
// As above, do the same for the previous hitobject.
|
// As above, do the same for the previous hitobject.
|
||||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance)
|
if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||||
{
|
{
|
||||||
@@ -69,59 +70,77 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
|
|
||||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||||
|
|
||||||
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||||
{
|
{
|
||||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
double currAngle = osuCurrObj.Angle.Value;
|
||||||
|
double lastAngle = osuLastObj.Angle.Value;
|
||||||
|
|
||||||
|
// Rewarding angles, take the smaller velocity as base.
|
||||||
|
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||||
|
|
||||||
|
if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
|
||||||
{
|
{
|
||||||
double currAngle = osuCurrObj.Angle.Value;
|
|
||||||
double lastAngle = osuLastObj.Angle.Value;
|
|
||||||
|
|
||||||
// Rewarding angles, take the smaller velocity as base.
|
|
||||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
|
||||||
|
|
||||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
|
||||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||||
|
|
||||||
// Penalize angle repetition.
|
// Penalize angle repetition.
|
||||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
|
||||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||||
|
|
||||||
// Apply full wide angle bonus for distance more than one diameter
|
|
||||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
|
||||||
|
|
||||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||||
acuteAngleBonus *= angleBonus *
|
acuteAngleBonus *= angleBonus *
|
||||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
|
||||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
||||||
wiggleBonus = angleBonus
|
// Penalize angle repetition.
|
||||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
|
||||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
// Apply full wide angle bonus for distance more than one diameter
|
||||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
|
||||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||||
|
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||||
|
wiggleBonus = angleBonus
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||||
|
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||||
|
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||||
|
|
||||||
|
if (osuLast2Obj != null)
|
||||||
|
{
|
||||||
|
// If objects just go back and forth through a middle point - don't give as much wide bonus
|
||||||
|
// Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object
|
||||||
|
var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject;
|
||||||
|
var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject;
|
||||||
|
|
||||||
|
float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length;
|
||||||
|
|
||||||
|
if (distance < 1)
|
||||||
|
{
|
||||||
|
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||||
{
|
{
|
||||||
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
||||||
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
|
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.AdjustedDeltaTime;
|
||||||
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
|
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||||
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
|
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
||||||
|
|
||||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
|
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));
|
||||||
|
|
||||||
velocityChangeBonus = overlapVelocityBuff * distRatio;
|
velocityChangeBonus = overlapVelocityBuff * distRatio;
|
||||||
|
|
||||||
// Penalize for rhythm changes.
|
// Penalize for rhythm changes.
|
||||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
|
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (osuLastObj.BaseObject is Slider)
|
if (osuLastObj.BaseObject is Slider)
|
||||||
@@ -131,9 +150,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
}
|
}
|
||||||
|
|
||||||
aimStrain += wiggleBonus * wiggle_multiplier;
|
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||||
|
aimStrain += velocityChangeBonus * velocity_change_multiplier;
|
||||||
|
|
||||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
||||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
||||||
|
|
||||||
|
// Apply high circle size bonus
|
||||||
|
aimStrain *= osuCurrObj.SmallCircleBonus;
|
||||||
|
|
||||||
// Add in additional slider velocity bonus.
|
// Add in additional slider velocity bonus.
|
||||||
if (withSliderTravelDistance)
|
if (withSliderTravelDistance)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||||
|
|
||||||
cumulativeStrainTime += lastObj.StrainTime;
|
cumulativeStrainTime += lastObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
if (!(currentObj.BaseObject is Spinner))
|
if (!(currentObj.BaseObject is Spinner))
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
if (osuCurrent.BaseObject is Slider osuSlider)
|
if (osuCurrent.BaseObject is Slider osuSlider)
|
||||||
{
|
{
|
||||||
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
||||||
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
|
double pixelTravelDistance = osuCurrent.LazyTravelDistance / scalingFactor;
|
||||||
|
|
||||||
// Reward sliders based on velocity.
|
// Reward sliders based on velocity.
|
||||||
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
|
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
{
|
{
|
||||||
private const int history_time_max = 5 * 1000; // 5 seconds
|
private const int history_time_max = 5 * 1000; // 5 seconds
|
||||||
private const int history_objects_max = 32;
|
private const int history_objects_max = 32;
|
||||||
private const double rhythm_overall_multiplier = 0.95;
|
private const double rhythm_overall_multiplier = 1.0;
|
||||||
private const double rhythm_ratio_multiplier = 12.0;
|
private const double rhythm_ratio_multiplier = 15.0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||||
@@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
if (current.BaseObject is Spinner)
|
if (current.BaseObject is Spinner)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
var currentOsuObject = (OsuDifficultyHitObject)current;
|
||||||
|
|
||||||
double rhythmComplexitySum = 0;
|
double rhythmComplexitySum = 0;
|
||||||
|
|
||||||
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
|
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
|
||||||
@@ -62,22 +64,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
|
|
||||||
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
||||||
|
|
||||||
double currDelta = currObj.StrainTime;
|
// Use custom cap value to ensure that that at this point delta time is actually zero
|
||||||
double prevDelta = prevObj.StrainTime;
|
double currDelta = Math.Max(currObj.DeltaTime, 1e-7);
|
||||||
double lastDelta = lastObj.StrainTime;
|
double prevDelta = Math.Max(prevObj.DeltaTime, 1e-7);
|
||||||
|
double lastDelta = Math.Max(lastObj.DeltaTime, 1e-7);
|
||||||
|
|
||||||
// calculate how much current delta difference deserves a rhythm bonus
|
// calculate how much current delta difference deserves a rhythm bonus
|
||||||
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||||
double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta);
|
double deltaDifference = Math.Max(prevDelta, currDelta) / Math.Min(prevDelta, currDelta);
|
||||||
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2));
|
|
||||||
|
// Take only the fractional part of the value since we're only interested in punishing multiples
|
||||||
|
double deltaDifferenceFraction = deltaDifference - Math.Truncate(deltaDifference);
|
||||||
|
|
||||||
|
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, DifficultyCalculationUtils.SmoothstepBellCurve(deltaDifferenceFraction));
|
||||||
|
|
||||||
// reduce ratio bonus if delta difference is too big
|
// reduce ratio bonus if delta difference is too big
|
||||||
double fraction = Math.Max(prevDelta / currDelta, currDelta / prevDelta);
|
double differenceMultiplier = Math.Clamp(2.0 - deltaDifference / 8.0, 0.0, 1.0);
|
||||||
double fractionMultiplier = Math.Clamp(2.0 - fraction / 8.0, 0.0, 1.0);
|
|
||||||
|
|
||||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
||||||
|
|
||||||
double effectiveRatio = windowPenalty * currRatio * fractionMultiplier;
|
double effectiveRatio = windowPenalty * currRatio * differenceMultiplier;
|
||||||
|
|
||||||
if (firstDeltaSwitch)
|
if (firstDeltaSwitch)
|
||||||
{
|
{
|
||||||
@@ -170,7 +176,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
prevObj = currObj;
|
prevObj = currObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
double rhythmDifficulty = Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
|
rhythmDifficulty *= 1 - currentOsuObject.GetDoubletapness((OsuDifficultyHitObject)current.Next(0));
|
||||||
|
|
||||||
|
return rhythmDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Island : IEquatable<Island>
|
private class Island : IEquatable<Island>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
private const double distance_multiplier = 0.9;
|
private const double distance_multiplier = 0.8;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates the difficulty of tapping the current object, based on:
|
/// Evaluates the difficulty of tapping the current object, based on:
|
||||||
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||||
|
|
||||||
double strainTime = osuCurrObj.StrainTime;
|
double strainTime = osuCurrObj.AdjustedDeltaTime;
|
||||||
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
||||||
|
|
||||||
// Cap deltatime to the OD 300 hitwindow.
|
// Cap deltatime to the OD 300 hitwindow.
|
||||||
@@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||||
|
|
||||||
|
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
|
||||||
|
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
|
||||||
|
|
||||||
if (mods.OfType<OsuModAutopilot>().Any())
|
if (mods.OfType<OsuModAutopilot>().Any())
|
||||||
distanceBonus = 0;
|
distanceBonus = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,37 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("slider_factor")]
|
[JsonProperty("slider_factor")]
|
||||||
public double SliderFactor { get; set; }
|
public double SliderFactor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes how much of <see cref="AimDifficultStrainCount"/> is contributed to by hitcircles or sliders
|
||||||
|
/// A value closer to 0.0 indicates most of <see cref="AimDifficultStrainCount"/> is contributed by hitcircles
|
||||||
|
/// A value closer to Infinity indicates most of <see cref="AimDifficultStrainCount"/> is contributed by sliders
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("aim_top_weighted_slider_factor")]
|
||||||
|
public double AimTopWeightedSliderFactor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes how much of <see cref="SpeedDifficultStrainCount"/> is contributed to by hitcircles or sliders
|
||||||
|
/// A value closer to 0.0 indicates most of <see cref="SpeedDifficultStrainCount"/> is contributed by hitcircles
|
||||||
|
/// A value closer to Infinity indicates most of <see cref="SpeedDifficultStrainCount"/> is contributed by sliders
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("speed_top_weighted_slider_factor")]
|
||||||
|
public double SpeedTopWeightedSliderFactor { get; set; }
|
||||||
|
|
||||||
[JsonProperty("aim_difficult_strain_count")]
|
[JsonProperty("aim_difficult_strain_count")]
|
||||||
public double AimDifficultStrainCount { get; set; }
|
public double AimDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty("speed_difficult_strain_count")]
|
[JsonProperty("speed_difficult_strain_count")]
|
||||||
public double SpeedDifficultStrainCount { get; set; }
|
public double SpeedDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("nested_score_per_object")]
|
||||||
|
public double NestedScorePerObject { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("legacy_score_base_multiplier")]
|
||||||
|
public double LegacyScoreBaseMultiplier { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("maximum_legacy_combo_score")]
|
||||||
|
public double MaximumLegacyComboScore { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -97,6 +122,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||||
|
yield return (ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR, AimTopWeightedSliderFactor);
|
||||||
|
yield return (ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR, SpeedTopWeightedSliderFactor);
|
||||||
|
yield return (ATTRIB_ID_NESTED_SCORE_PER_OBJECT, NestedScorePerObject);
|
||||||
|
yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier);
|
||||||
|
yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@@ -112,6 +142,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||||
|
AimTopWeightedSliderFactor = values[ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR];
|
||||||
|
SpeedTopWeightedSliderFactor = values[ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR];
|
||||||
|
NestedScorePerObject = values[ATTRIB_ID_NESTED_SCORE_PER_OBJECT];
|
||||||
|
LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER];
|
||||||
|
MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE];
|
||||||
DrainRate = onlineInfo.DrainRate;
|
DrainRate = onlineInfo.DrainRate;
|
||||||
HitCircleCount = onlineInfo.CircleCount;
|
HitCircleCount = onlineInfo.CircleCount;
|
||||||
SliderCount = onlineInfo.SliderCount;
|
SliderCount = onlineInfo.SliderCount;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -13,69 +11,103 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 0.0675;
|
private const double star_rating_multiplier = 0.0265;
|
||||||
|
|
||||||
public override int Version => 20250306;
|
public override int Version => 20251020;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate)
|
||||||
|
{
|
||||||
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
|
||||||
|
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double CalculateRateAdjustedOverallDifficulty(double overallDifficulty, double clockRate)
|
||||||
|
{
|
||||||
|
HitWindows hitWindows = new OsuHitWindows();
|
||||||
|
hitWindows.SetDifficulty(overallDifficulty);
|
||||||
|
|
||||||
|
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
|
||||||
|
return (79.5 - hitWindowGreat) / 6;
|
||||||
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new OsuDifficultyAttributes { Mods = mods };
|
return new OsuDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
|
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
|
||||||
double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier;
|
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
||||||
double aimDifficultyStrainCount = aim.CountTopWeightedStrains();
|
var speed = skills.OfType<Speed>().Single();
|
||||||
|
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
||||||
|
|
||||||
|
double speedNotes = speed.RelevantNoteCount();
|
||||||
|
|
||||||
|
double aimDifficultStrainCount = aim.CountTopWeightedStrains();
|
||||||
|
double speedDifficultStrainCount = speed.CountTopWeightedStrains();
|
||||||
|
|
||||||
|
double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders();
|
||||||
|
double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains();
|
||||||
|
|
||||||
|
double aimTopWeightedSliderFactor = aimNoSlidersTopWeightedSliderCount / Math.Max(1, aimNoSlidersDifficultStrainCount - aimNoSlidersTopWeightedSliderCount);
|
||||||
|
|
||||||
|
double speedTopWeightedSliderCount = speed.CountTopWeightedSliders();
|
||||||
|
double speedTopWeightedSliderFactor = speedTopWeightedSliderCount / Math.Max(1, speedDifficultStrainCount - speedTopWeightedSliderCount);
|
||||||
|
|
||||||
double difficultSliders = aim.GetDifficultSliders();
|
double difficultSliders = aim.GetDifficultSliders();
|
||||||
|
|
||||||
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
double approachRate = CalculateRateAdjustedApproachRate(beatmap.Difficulty.ApproachRate, clockRate);
|
||||||
double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier;
|
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate);
|
||||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
|
||||||
|
|
||||||
var speed = skills.OfType<Speed>().Single();
|
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||||
double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier;
|
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||||
double speedNotes = speed.RelevantNoteCount();
|
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||||
double speedDifficultyStrainCount = speed.CountTopWeightedStrains();
|
|
||||||
|
|
||||||
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
int totalHits = beatmap.HitObjects.Count;
|
||||||
double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier;
|
|
||||||
|
|
||||||
if (mods.Any(m => m is OsuModTouchDevice))
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
{
|
|
||||||
aimRating = Math.Pow(aimRating, 0.8);
|
|
||||||
flashlightRating = Math.Pow(flashlightRating, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mods.Any(h => h is OsuModRelax))
|
double aimDifficultyValue = aim.DifficultyValue();
|
||||||
{
|
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
|
||||||
aimRating *= 0.9;
|
double speedDifficultyValue = speed.DifficultyValue();
|
||||||
speedRating = 0.0;
|
|
||||||
flashlightRating *= 0.7;
|
double mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
|
||||||
}
|
double sliderFactor = aimDifficultyValue > 0 ? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue) : 1;
|
||||||
else if (mods.Any(h => h is OsuModAutopilot))
|
|
||||||
{
|
var osuRatingCalculator = new OsuRatingCalculator(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor);
|
||||||
speedRating *= 0.5;
|
|
||||||
aimRating = 0.0;
|
double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
|
||||||
flashlightRating *= 0.4;
|
double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
|
||||||
}
|
|
||||||
|
double flashlightRating = 0.0;
|
||||||
|
|
||||||
|
if (flashlight is not null)
|
||||||
|
flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
|
||||||
|
|
||||||
|
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
|
||||||
|
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
|
||||||
|
|
||||||
|
var simulator = new OsuLegacyScoreSimulator();
|
||||||
|
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
|
||||||
|
|
||||||
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||||
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||||
double baseFlashlightPerformance = 0.0;
|
double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
||||||
|
|
||||||
if (mods.Any(h => h is OsuModFlashlight))
|
|
||||||
baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
|
||||||
|
|
||||||
double basePerformance =
|
double basePerformance =
|
||||||
Math.Pow(
|
Math.Pow(
|
||||||
@@ -84,15 +116,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||||
);
|
);
|
||||||
|
|
||||||
double starRating = basePerformance > 0.00001
|
double starRating = calculateStarRating(basePerformance);
|
||||||
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
double drainRate = beatmap.Difficulty.DrainRate;
|
|
||||||
|
|
||||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
|
||||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
|
||||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
|
||||||
|
|
||||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
@@ -104,18 +128,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedNoteCount = speedNotes,
|
SpeedNoteCount = speedNotes,
|
||||||
FlashlightDifficulty = flashlightRating,
|
FlashlightDifficulty = flashlightRating,
|
||||||
SliderFactor = sliderFactor,
|
SliderFactor = sliderFactor,
|
||||||
AimDifficultStrainCount = aimDifficultyStrainCount,
|
AimDifficultStrainCount = aimDifficultStrainCount,
|
||||||
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
SpeedDifficultStrainCount = speedDifficultStrainCount,
|
||||||
|
AimTopWeightedSliderFactor = aimTopWeightedSliderFactor,
|
||||||
|
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
|
||||||
DrainRate = drainRate,
|
DrainRate = drainRate,
|
||||||
MaxCombo = beatmap.GetMaxCombo(),
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
HitCircleCount = hitCirclesCount,
|
HitCircleCount = hitCircleCount,
|
||||||
SliderCount = sliderCount,
|
SliderCount = sliderCount,
|
||||||
SpinnerCount = spinnerCount,
|
SpinnerCount = spinnerCount,
|
||||||
|
NestedScorePerObject = sliderNestedScorePerObject,
|
||||||
|
LegacyScoreBaseMultiplier = legacyScoreBaseMultiplier,
|
||||||
|
MaximumLegacyComboScore = scoreAttributes.ComboScore
|
||||||
};
|
};
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue)
|
||||||
|
{
|
||||||
|
double aimValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue));
|
||||||
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(speedDifficultyValue));
|
||||||
|
|
||||||
|
double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
|
||||||
|
|
||||||
|
return calculateStarRating(totalValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateStarRating(double basePerformance)
|
||||||
|
{
|
||||||
|
if (basePerformance <= 0.00001)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4);
|
||||||
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
@@ -124,8 +171,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
||||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null;
|
objects.Add(new OsuDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate, objects, objects.Count));
|
||||||
objects.Add(new OsuDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], lastLast, clockRate, objects, objects.Count));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return objects;
|
return objects;
|
||||||
@@ -154,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
new OsuModEasy(),
|
new OsuModEasy(),
|
||||||
new OsuModHardRock(),
|
new OsuModHardRock(),
|
||||||
new OsuModFlashlight(),
|
new OsuModFlashlight(),
|
||||||
new MultiMod(new OsuModFlashlight(), new OsuModHidden())
|
new OsuModHidden(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
200
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs
Normal file
200
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
|
{
|
||||||
|
public class OsuLegacyScoreMissCalculator
|
||||||
|
{
|
||||||
|
private readonly ScoreInfo score;
|
||||||
|
private readonly OsuDifficultyAttributes attributes;
|
||||||
|
|
||||||
|
public OsuLegacyScoreMissCalculator(ScoreInfo scoreInfo, OsuDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
score = scoreInfo;
|
||||||
|
this.attributes = attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Calculate()
|
||||||
|
{
|
||||||
|
if (attributes.MaxCombo == 0 || score.LegacyTotalScore == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double scoreV1Multiplier = attributes.LegacyScoreBaseMultiplier * getLegacyScoreMultiplier();
|
||||||
|
double relevantComboPerObject = calculateRelevantScoreComboPerObject();
|
||||||
|
|
||||||
|
double maximumMissCount = calculateMaximumComboBasedMissCount();
|
||||||
|
|
||||||
|
double scoreObtainedDuringMaxCombo = calculateScoreAtCombo(score.MaxCombo, relevantComboPerObject, scoreV1Multiplier);
|
||||||
|
double remainingScore = score.LegacyTotalScore.Value - scoreObtainedDuringMaxCombo;
|
||||||
|
|
||||||
|
if (remainingScore <= 0)
|
||||||
|
return maximumMissCount;
|
||||||
|
|
||||||
|
double remainingCombo = attributes.MaxCombo - score.MaxCombo;
|
||||||
|
double expectedRemainingScore = calculateScoreAtCombo(remainingCombo, relevantComboPerObject, scoreV1Multiplier);
|
||||||
|
|
||||||
|
double scoreBasedMissCount = expectedRemainingScore / remainingScore;
|
||||||
|
|
||||||
|
// If there's less then one miss detected - let combo-based miss count decide if this is FC or not
|
||||||
|
scoreBasedMissCount = Math.Max(scoreBasedMissCount, 1);
|
||||||
|
|
||||||
|
// Cap result by very harsh version of combo-based miss count
|
||||||
|
return Math.Min(scoreBasedMissCount, maximumMissCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the amount of score that would be achieved at a given combo.
|
||||||
|
/// </summary>
|
||||||
|
private double calculateScoreAtCombo(double combo, double relevantComboPerObject, double scoreV1Multiplier)
|
||||||
|
{
|
||||||
|
int countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
|
int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
|
int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
|
int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
|
int totalHits = countGreat + countOk + countMeh + countMiss;
|
||||||
|
|
||||||
|
double estimatedObjects = combo / relevantComboPerObject - 1;
|
||||||
|
|
||||||
|
// The combo portion of ScoreV1 follows arithmetic progression
|
||||||
|
// Therefore, we calculate the combo portion of score using the combo per object and our current combo.
|
||||||
|
double comboScore = relevantComboPerObject > 0 ? (2 * (relevantComboPerObject - 1) + (estimatedObjects - 1) * relevantComboPerObject) * estimatedObjects / 2 : 0;
|
||||||
|
|
||||||
|
// We then apply the accuracy and ScoreV1 multipliers to the resulting score.
|
||||||
|
comboScore *= score.Accuracy * 300 / 25 * scoreV1Multiplier;
|
||||||
|
|
||||||
|
double objectsHit = (totalHits - countMiss) * combo / attributes.MaxCombo;
|
||||||
|
|
||||||
|
// Score also has a non-combo portion we need to create the final score value.
|
||||||
|
double nonComboScore = (300 + attributes.NestedScorePerObject) * score.Accuracy * objectsHit;
|
||||||
|
|
||||||
|
return comboScore + nonComboScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the relevant combo per object for legacy score.
|
||||||
|
/// This assumes a uniform distribution for circles and sliders.
|
||||||
|
/// This handles cases where objects (such as buzz sliders) do not fit a normal arithmetic progression model.
|
||||||
|
/// </summary>
|
||||||
|
private double calculateRelevantScoreComboPerObject()
|
||||||
|
{
|
||||||
|
double comboScore = attributes.MaximumLegacyComboScore;
|
||||||
|
|
||||||
|
// We then reverse apply the ScoreV1 multipliers to get the raw value.
|
||||||
|
comboScore /= 300.0 / 25.0 * attributes.LegacyScoreBaseMultiplier;
|
||||||
|
|
||||||
|
// Reverse the arithmetic progression to work out the amount of combo per object based on the score.
|
||||||
|
double result = (attributes.MaxCombo - 2) * attributes.MaxCombo;
|
||||||
|
result /= Math.Max(attributes.MaxCombo + 2 * (comboScore - 1), 1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This function is a harsher version of current combo-based miss count, used to provide reasonable value for cases where score-based miss count can't do this.
|
||||||
|
/// </summary>
|
||||||
|
private double calculateMaximumComboBasedMissCount()
|
||||||
|
{
|
||||||
|
int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
|
if (attributes.SliderCount <= 0)
|
||||||
|
return countMiss;
|
||||||
|
|
||||||
|
int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
|
int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
|
|
||||||
|
int totalImperfectHits = countOk + countMeh + countMiss;
|
||||||
|
|
||||||
|
double missCount = 0;
|
||||||
|
|
||||||
|
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||||
|
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||||
|
|
||||||
|
if (score.MaxCombo < fullComboThreshold)
|
||||||
|
missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5);
|
||||||
|
|
||||||
|
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||||
|
missCount = Math.Min(missCount, totalImperfectHits);
|
||||||
|
|
||||||
|
// Every slider has *at least* 2 combo attributed in classic mechanics.
|
||||||
|
// If they broke on a slider with a tick, then this still works since they would have lost at least 2 combo (the tick and the end)
|
||||||
|
// Using this as a max means a score that loses 1 combo on a map can't possibly have been a slider break.
|
||||||
|
// It must have been a slider end.
|
||||||
|
int maxPossibleSliderBreaks = Math.Min(attributes.SliderCount, (attributes.MaxCombo - score.MaxCombo) / 2);
|
||||||
|
|
||||||
|
int scoreMissCount = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
|
double sliderBreaks = missCount - scoreMissCount;
|
||||||
|
|
||||||
|
if (sliderBreaks > maxPossibleSliderBreaks)
|
||||||
|
missCount = scoreMissCount + maxPossibleSliderBreaks;
|
||||||
|
|
||||||
|
return missCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Logic copied from <see cref="OsuLegacyScoreSimulator.GetLegacyScoreMultiplier"/>.
|
||||||
|
/// </remarks>
|
||||||
|
private double getLegacyScoreMultiplier()
|
||||||
|
{
|
||||||
|
bool scoreV2 = score.Mods.Any(m => m is ModScoreV2);
|
||||||
|
|
||||||
|
double multiplier = 1.0;
|
||||||
|
|
||||||
|
foreach (var mod in score.Mods)
|
||||||
|
{
|
||||||
|
switch (mod)
|
||||||
|
{
|
||||||
|
case OsuModNoFail:
|
||||||
|
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModEasy:
|
||||||
|
multiplier *= 0.5;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModHalfTime:
|
||||||
|
case OsuModDaycore:
|
||||||
|
multiplier *= 0.3;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModHidden:
|
||||||
|
multiplier *= 1.06;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModHardRock:
|
||||||
|
multiplier *= scoreV2 ? 1.10 : 1.06;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModDoubleTime:
|
||||||
|
case OsuModNightcore:
|
||||||
|
multiplier *= scoreV2 ? 1.20 : 1.12;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModFlashlight:
|
||||||
|
multiplier *= 1.12;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModSpunOut:
|
||||||
|
multiplier *= 0.9;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuModRelax:
|
||||||
|
case OsuModAutopilot:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("speed_deviation")]
|
[JsonProperty("speed_deviation")]
|
||||||
public double? SpeedDeviation { get; set; }
|
public double? SpeedDeviation { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("combo_based_estimated_miss_count")]
|
||||||
|
public double ComboBasedEstimatedMissCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("score_based_estimated_miss_count")]
|
||||||
|
public double? ScoreBasedEstimatedMissCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("aim_estimated_slider_breaks")]
|
||||||
|
public double AimEstimatedSliderBreaks { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("speed_estimated_slider_breaks")]
|
||||||
|
public double SpeedEstimatedSliderBreaks { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
{
|
{
|
||||||
foreach (var attribute in base.GetAttributesForDisplay())
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
|||||||
@@ -4,25 +4,25 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Audio.Track;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||||
{
|
{
|
||||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
|
|
||||||
private bool usingClassicSliderAccuracy;
|
private bool usingClassicSliderAccuracy;
|
||||||
|
private bool usingScoreV2;
|
||||||
|
|
||||||
private double accuracy;
|
private double accuracy;
|
||||||
private int scoreMaxCombo;
|
private int scoreMaxCombo;
|
||||||
@@ -55,6 +55,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
private double? speedDeviation;
|
private double? speedDeviation;
|
||||||
|
|
||||||
|
private double aimEstimatedSliderBreaks;
|
||||||
|
private double speedEstimatedSliderBreaks;
|
||||||
|
|
||||||
public OsuPerformanceCalculator()
|
public OsuPerformanceCalculator()
|
||||||
: base(new OsuRuleset())
|
: base(new OsuRuleset())
|
||||||
{
|
{
|
||||||
@@ -65,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
||||||
|
|
||||||
usingClassicSliderAccuracy = score.Mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);
|
usingClassicSliderAccuracy = score.Mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);
|
||||||
|
usingScoreV2 = score.Mods.Any(m => m is ModScoreV2);
|
||||||
|
|
||||||
accuracy = score.Accuracy;
|
accuracy = score.Accuracy;
|
||||||
scoreMaxCombo = score.MaxCombo;
|
scoreMaxCombo = score.MaxCombo;
|
||||||
@@ -80,9 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||||
|
|
||||||
var track = new TrackVirtual(10000);
|
clockRate = ModUtils.CalculateRateWithMods(score.Mods);
|
||||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
|
||||||
clockRate = track.Rate;
|
|
||||||
|
|
||||||
HitWindows hitWindows = new OsuHitWindows();
|
HitWindows hitWindows = new OsuHitWindows();
|
||||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||||
@@ -91,35 +93,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||||
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||||
|
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
|
||||||
|
overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate);
|
||||||
|
|
||||||
overallDifficulty = (79.5 - greatHitWindow) / 6;
|
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
|
||||||
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
|
double? scoreBasedEstimatedMissCount = null;
|
||||||
|
|
||||||
if (osuAttributes.SliderCount > 0)
|
if (usingClassicSliderAccuracy && score.LegacyTotalScore != null)
|
||||||
{
|
{
|
||||||
if (usingClassicSliderAccuracy)
|
var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes);
|
||||||
{
|
scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate();
|
||||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
|
||||||
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
|
||||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
|
|
||||||
|
|
||||||
if (scoreMaxCombo < fullComboThreshold)
|
effectiveMissCount = scoreBasedEstimatedMissCount.Value;
|
||||||
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
}
|
||||||
|
else
|
||||||
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
{
|
||||||
effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
|
// Use combo-based miss count if this isn't a legacy score
|
||||||
}
|
effectiveMissCount = comboBasedEstimatedMissCount;
|
||||||
else
|
|
||||||
{
|
|
||||||
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
|
|
||||||
|
|
||||||
if (scoreMaxCombo < fullComboThreshold)
|
|
||||||
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
|
||||||
|
|
||||||
// Combine regular misses with tick misses since tick misses break combo as well
|
|
||||||
effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
||||||
@@ -135,10 +125,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
if (score.Mods.Any(h => h is OsuModRelax))
|
if (score.Mods.Any(h => h is OsuModRelax))
|
||||||
{
|
{
|
||||||
// https://www.desmos.com/calculator/bc9eybdthb
|
// https://www.desmos.com/calculator/vspzsop6td
|
||||||
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
||||||
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
||||||
double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0);
|
double okMultiplier = 0.75 * Math.Max(0.0, overallDifficulty > 0.0 ? 1 - overallDifficulty / 13.33 : 1.0);
|
||||||
double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0);
|
double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0);
|
||||||
|
|
||||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||||
@@ -167,6 +157,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Accuracy = accuracyValue,
|
Accuracy = accuracyValue,
|
||||||
Flashlight = flashlightValue,
|
Flashlight = flashlightValue,
|
||||||
EffectiveMissCount = effectiveMissCount,
|
EffectiveMissCount = effectiveMissCount,
|
||||||
|
ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount,
|
||||||
|
ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount,
|
||||||
|
AimEstimatedSliderBreaks = aimEstimatedSliderBreaks,
|
||||||
|
SpeedEstimatedSliderBreaks = speedEstimatedSliderBreaks,
|
||||||
SpeedDeviation = speedDeviation,
|
SpeedDeviation = speedDeviation,
|
||||||
Total = totalValue
|
Total = totalValue
|
||||||
};
|
};
|
||||||
@@ -207,30 +201,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
aimValue *= lengthBonus;
|
aimValue *= lengthBonus;
|
||||||
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
{
|
||||||
|
aimEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.AimTopWeightedSliderFactor, attributes);
|
||||||
|
|
||||||
double approachRateFactor = 0.0;
|
double relevantMissCount = Math.Min(effectiveMissCount + aimEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
||||||
if (approachRate > 10.33)
|
|
||||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
|
||||||
else if (approachRate < 8.0)
|
|
||||||
approachRateFactor = 0.05 * (8.0 - approachRate);
|
|
||||||
|
|
||||||
if (score.Mods.Any(h => h is OsuModRelax))
|
aimValue *= calculateMissPenalty(relevantMissCount, attributes.AimDifficultStrainCount);
|
||||||
approachRateFactor = 0.0;
|
}
|
||||||
|
|
||||||
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
|
||||||
|
|
||||||
|
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
|
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
|
||||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
else if (score.Mods.Any(m => m is OsuModTraceable))
|
||||||
{
|
{
|
||||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate, sliderFactor: attributes.SliderFactor);
|
||||||
aimValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aimValue *= accuracy;
|
aimValue *= accuracy;
|
||||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
||||||
aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
|
||||||
|
|
||||||
return aimValue;
|
return aimValue;
|
||||||
}
|
}
|
||||||
@@ -247,26 +234,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
speedValue *= lengthBonus;
|
speedValue *= lengthBonus;
|
||||||
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
{
|
||||||
|
speedEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.SpeedTopWeightedSliderFactor, attributes);
|
||||||
|
|
||||||
double approachRateFactor = 0.0;
|
double relevantMissCount = Math.Min(effectiveMissCount + speedEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
||||||
if (approachRate > 10.33)
|
|
||||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
|
||||||
|
|
||||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
speedValue *= calculateMissPenalty(relevantMissCount, attributes.SpeedDifficultStrainCount);
|
||||||
approachRateFactor = 0.0;
|
}
|
||||||
|
|
||||||
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
|
||||||
|
|
||||||
|
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
{
|
{
|
||||||
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||||
speedValue *= 1.12;
|
speedValue *= 1.12;
|
||||||
}
|
}
|
||||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
else if (score.Mods.Any(m => m is OsuModTraceable))
|
||||||
{
|
{
|
||||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
speedValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
|
||||||
speedValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||||
@@ -280,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||||
|
|
||||||
// Scale the speed value with accuracy and OD.
|
// Scale the speed value with accuracy and OD.
|
||||||
speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
speedValue *= Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
||||||
|
|
||||||
return speedValue;
|
return speedValue;
|
||||||
}
|
}
|
||||||
@@ -293,7 +277,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||||
double betterAccuracyPercentage;
|
double betterAccuracyPercentage;
|
||||||
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
||||||
if (!usingClassicSliderAccuracy)
|
if (!usingClassicSliderAccuracy || usingScoreV2)
|
||||||
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||||
|
|
||||||
if (amountHitObjectsWithAccuracy > 0)
|
if (amountHitObjectsWithAccuracy > 0)
|
||||||
@@ -316,7 +300,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
accuracyValue *= 1.14;
|
accuracyValue *= 1.14;
|
||||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||||
accuracyValue *= 1.08;
|
{
|
||||||
|
// Decrease bonus for AR > 10
|
||||||
|
accuracyValue *= 1 + 0.08 * DifficultyCalculationUtils.ReverseLerp(approachRate, 11.5, 10);
|
||||||
|
}
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is OsuModFlashlight))
|
if (score.Mods.Any(m => m is OsuModFlashlight))
|
||||||
accuracyValue *= 1.02;
|
accuracyValue *= 1.02;
|
||||||
@@ -337,18 +324,73 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
flashlightValue *= getComboScalingFactor(attributes);
|
flashlightValue *= getComboScalingFactor(attributes);
|
||||||
|
|
||||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
||||||
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
|
|
||||||
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
|
|
||||||
|
|
||||||
// Scale the flashlight value with accuracy _slightly_.
|
// Scale the flashlight value with accuracy _slightly_.
|
||||||
flashlightValue *= 0.5 + accuracy / 2.0;
|
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||||
// It is important to also consider accuracy difficulty when doing that.
|
|
||||||
flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
|
||||||
|
|
||||||
return flashlightValue;
|
return flashlightValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (attributes.SliderCount <= 0)
|
||||||
|
return countMiss;
|
||||||
|
|
||||||
|
double missCount = countMiss;
|
||||||
|
|
||||||
|
if (usingClassicSliderAccuracy)
|
||||||
|
{
|
||||||
|
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||||
|
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||||
|
|
||||||
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
|
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
|
|
||||||
|
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||||
|
missCount = Math.Min(missCount, totalImperfectHits);
|
||||||
|
|
||||||
|
// Every slider has *at least* 2 combo attributed in classic mechanics.
|
||||||
|
// If they broke on a slider with a tick, then this still works since they would have lost at least 2 combo (the tick and the end)
|
||||||
|
// Using this as a max means a score that loses 1 combo on a map can't possibly have been a slider break.
|
||||||
|
// It must have been a slider end.
|
||||||
|
int maxPossibleSliderBreaks = Math.Min(attributes.SliderCount, (attributes.MaxCombo - scoreMaxCombo) / 2);
|
||||||
|
|
||||||
|
double sliderBreaks = missCount - countMiss;
|
||||||
|
|
||||||
|
if (sliderBreaks > maxPossibleSliderBreaks)
|
||||||
|
missCount = countMiss + maxPossibleSliderBreaks;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
|
||||||
|
|
||||||
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
|
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
|
|
||||||
|
// Combine regular misses with tick misses since tick misses break combo as well
|
||||||
|
missCount = Math.Min(missCount, countSliderTickMiss + countMiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
return missCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateEstimatedSliderBreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (!usingClassicSliderAccuracy || countOk == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double missedComboPercent = 1.0 - (double)scoreMaxCombo / attributes.MaxCombo;
|
||||||
|
double estimatedSliderBreaks = Math.Min(countOk, effectiveMissCount * topWeightedSliderFactor);
|
||||||
|
|
||||||
|
// Scores with more Oks are more likely to have slider breaks.
|
||||||
|
double okAdjustment = ((countOk - estimatedSliderBreaks) + 0.5) / countOk;
|
||||||
|
|
||||||
|
// There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred.
|
||||||
|
estimatedSliderBreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2);
|
||||||
|
|
||||||
|
return estimatedSliderBreaks * okAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
||||||
/// Treats all speed notes as hit circles.
|
/// Treats all speed notes as hit circles.
|
||||||
@@ -368,7 +410,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
||||||
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
|
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
|
||||||
|
|
||||||
return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
return calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -377,45 +419,45 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
/// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
|
/// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
|
||||||
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
|
private double? calculateDeviation(double relevantCountGreat, double relevantCountOk, double relevantCountMeh)
|
||||||
{
|
{
|
||||||
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
|
// The sample proportion of successful hits.
|
||||||
|
double n = Math.Max(1, relevantCountGreat + relevantCountOk);
|
||||||
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
||||||
// the number of greats on circles divided by the number of circles, and then add one
|
|
||||||
// to the number of circles as a bias correction.
|
|
||||||
double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
|
|
||||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
|
||||||
|
|
||||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
|
||||||
double p = relevantCountGreat / n;
|
double p = relevantCountGreat / n;
|
||||||
|
|
||||||
// We can be 99% confident that p is at least this value.
|
// 99% critical value for the normal distribution (one-tailed).
|
||||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
const double z = 2.32634787404;
|
||||||
|
|
||||||
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
|
// We can be 99% confident that the population proportion is at least this value.
|
||||||
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
double pLowerBound = Math.Min(p, (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4));
|
||||||
double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
|
||||||
|
|
||||||
double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2))
|
double deviation;
|
||||||
/ (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
|
|
||||||
|
|
||||||
deviation *= Math.Sqrt(1 - randomValue);
|
// Tested max precision for the deviation calculation.
|
||||||
|
if (pLowerBound > 0.01)
|
||||||
|
{
|
||||||
|
// Compute deviation assuming greats and oks are normally distributed.
|
||||||
|
deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||||
|
|
||||||
// Value deviation approach as greatCount approaches 0
|
// Subtract the deviation provided by tails that land outside the ok hit window from the deviation computed above.
|
||||||
double limitValue = okHitWindow / Math.Sqrt(3);
|
// This is equivalent to calculating the deviation of a normal distribution truncated at +-okHitWindow.
|
||||||
|
double okHitWindowTailAmount = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2))
|
||||||
|
/ (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
|
||||||
|
|
||||||
// If precision is not enough to compute true deviation - use limit value
|
deviation *= Math.Sqrt(1 - okHitWindowTailAmount);
|
||||||
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
|
}
|
||||||
deviation = limitValue;
|
else
|
||||||
|
{
|
||||||
|
// A tested limit value for the case of a score only containing oks.
|
||||||
|
deviation = okHitWindow / Math.Sqrt(3);
|
||||||
|
}
|
||||||
|
|
||||||
// Then compute the variance for mehs.
|
// Compute and add the variance for mehs, assuming that they are uniformly distributed.
|
||||||
double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3;
|
double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3;
|
||||||
|
|
||||||
// Find the total deviation.
|
|
||||||
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||||
|
|
||||||
return deviation;
|
return deviation;
|
||||||
|
|||||||
213
osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs
Normal file
213
osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
|
{
|
||||||
|
public class OsuRatingCalculator
|
||||||
|
{
|
||||||
|
private const double difficulty_multiplier = 0.0675;
|
||||||
|
|
||||||
|
private readonly Mod[] mods;
|
||||||
|
private readonly int totalHits;
|
||||||
|
private readonly double approachRate;
|
||||||
|
private readonly double overallDifficulty;
|
||||||
|
private readonly double mechanicalDifficultyRating;
|
||||||
|
private readonly double sliderFactor;
|
||||||
|
|
||||||
|
public OsuRatingCalculator(Mod[] mods, int totalHits, double approachRate, double overallDifficulty, double mechanicalDifficultyRating, double sliderFactor)
|
||||||
|
{
|
||||||
|
this.mods = mods;
|
||||||
|
this.totalHits = totalHits;
|
||||||
|
this.approachRate = approachRate;
|
||||||
|
this.overallDifficulty = overallDifficulty;
|
||||||
|
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
|
||||||
|
this.sliderFactor = sliderFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeAimRating(double aimDifficultyValue)
|
||||||
|
{
|
||||||
|
if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double aimRating = CalculateDifficultyRating(aimDifficultyValue);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
|
aimRating = Math.Pow(aimRating, 0.8);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModRelax))
|
||||||
|
aimRating *= 0.9;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModMagnetised))
|
||||||
|
{
|
||||||
|
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||||
|
aimRating *= 1.0 - magnetisedStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratingMultiplier = 1.0;
|
||||||
|
|
||||||
|
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
|
|
||||||
|
double approachRateFactor = 0.0;
|
||||||
|
if (approachRate > 10.33)
|
||||||
|
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||||
|
else if (approachRate < 8.0)
|
||||||
|
approachRateFactor = 0.05 * (8.0 - approachRate);
|
||||||
|
|
||||||
|
if (mods.Any(h => h is OsuModRelax))
|
||||||
|
approachRateFactor = 0.0;
|
||||||
|
|
||||||
|
ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
|
{
|
||||||
|
double visibilityFactor = calculateAimVisibilityFactor(approachRate);
|
||||||
|
ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor, sliderFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||||
|
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||||
|
|
||||||
|
return aimRating * Math.Cbrt(ratingMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeSpeedRating(double speedDifficultyValue)
|
||||||
|
{
|
||||||
|
if (mods.Any(m => m is OsuModRelax))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double speedRating = CalculateDifficultyRating(speedDifficultyValue);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
speedRating *= 0.5;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModMagnetised))
|
||||||
|
{
|
||||||
|
// reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
|
||||||
|
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||||
|
speedRating *= 1.0 - magnetisedStrength * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratingMultiplier = 1.0;
|
||||||
|
|
||||||
|
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
|
|
||||||
|
double approachRateFactor = 0.0;
|
||||||
|
if (approachRate > 10.33)
|
||||||
|
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
approachRateFactor = 0.0;
|
||||||
|
|
||||||
|
ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
|
{
|
||||||
|
double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
|
||||||
|
ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
|
||||||
|
|
||||||
|
return speedRating * Math.Cbrt(ratingMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeFlashlightRating(double flashlightDifficultyValue)
|
||||||
|
{
|
||||||
|
if (!mods.Any(m => m is OsuModFlashlight))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
|
flashlightRating = Math.Pow(flashlightRating, 0.8);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModRelax))
|
||||||
|
flashlightRating *= 0.7;
|
||||||
|
else if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
flashlightRating *= 0.4;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModMagnetised))
|
||||||
|
{
|
||||||
|
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||||
|
flashlightRating *= 1.0 - magnetisedStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModDeflate))
|
||||||
|
{
|
||||||
|
float deflateInitialScale = mods.OfType<OsuModDeflate>().First().StartScale.Value;
|
||||||
|
flashlightRating *= Math.Clamp(DifficultyCalculationUtils.ReverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratingMultiplier = 1.0;
|
||||||
|
|
||||||
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||||
|
ratingMultiplier *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
|
||||||
|
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
|
||||||
|
|
||||||
|
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||||
|
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||||
|
|
||||||
|
return flashlightRating * Math.Sqrt(ratingMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateAimVisibilityFactor(double approachRate)
|
||||||
|
{
|
||||||
|
const double ar_factor_end_point = 11.5;
|
||||||
|
|
||||||
|
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
|
||||||
|
double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
|
||||||
|
|
||||||
|
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateSpeedVisibilityFactor(double approachRate)
|
||||||
|
{
|
||||||
|
const double ar_factor_end_point = 11.5;
|
||||||
|
|
||||||
|
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
|
||||||
|
double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
|
||||||
|
|
||||||
|
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a visibility bonus that is applicable to Hidden and Traceable.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1, double sliderFactor = 1)
|
||||||
|
{
|
||||||
|
// NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
|
||||||
|
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
|
||||||
|
|
||||||
|
// Start from normal curve, rewarding lower AR up to AR7
|
||||||
|
// TC forcefully requires a lower reading bonus for now as it's post-applied in PP which makes it multiplicative with the regular AR bonuses
|
||||||
|
// This means it has an advantage over HD, so we decrease the multiplier to compensate
|
||||||
|
// This should be removed once we're able to apply TC bonuses in SR (depends on real-time difficulty calculations being possible)
|
||||||
|
double readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) * (12.0 - Math.Max(approachRate, 7));
|
||||||
|
|
||||||
|
readingBonus *= visibilityFactor;
|
||||||
|
|
||||||
|
// We want to reward slideraim on low AR less
|
||||||
|
double sliderVisibilityFactor = Math.Pow(sliderFactor, 3);
|
||||||
|
|
||||||
|
// For AR up to 0 - reduce reward for very low ARs when object is visible
|
||||||
|
if (approachRate < 7)
|
||||||
|
readingBonus += (isAlwaysPartiallyVisible ? 0.02 : 0.045) * (7.0 - Math.Max(approachRate, 0)) * sliderVisibilityFactor;
|
||||||
|
|
||||||
|
// Starting from AR0 - cap values so they won't grow to infinity
|
||||||
|
if (approachRate < 0)
|
||||||
|
readingBonus += (isAlwaysPartiallyVisible ? 0.01 : 0.1) * (1 - Math.Pow(1.5, approachRate)) * sliderVisibilityFactor;
|
||||||
|
|
||||||
|
return readingBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,11 +28,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
||||||
|
|
||||||
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
||||||
|
protected new OsuHitObject LastObject => (OsuHitObject)base.LastObject;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
/// <see cref="DifficultyHitObject.DeltaTime"/> capped to a minimum of <see cref="MIN_DELTA_TIME"/>ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly double StrainTime;
|
public readonly double AdjustedDeltaTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||||
@@ -75,6 +76,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double TravelTime { get; private set; }
|
public double TravelTime { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the cursor at the point of completion of this <see cref="OsuDifficultyHitObject"/> if it is a <see cref="Slider"/>
|
||||||
|
/// and was hit with as few movements as possible.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2? LazyEndPosition { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The distance travelled by the cursor upon completion of this <see cref="OsuDifficultyHitObject"/> if it is a <see cref="Slider"/>
|
||||||
|
/// and was hit with as few movements as possible.
|
||||||
|
/// </summary>
|
||||||
|
public double LazyTravelDistance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time taken by the cursor upon completion of this <see cref="OsuDifficultyHitObject"/> if it is a <see cref="Slider"/>
|
||||||
|
/// and was hit with as few movements as possible.
|
||||||
|
/// </summary>
|
||||||
|
public double LazyTravelTime { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
|
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
|
||||||
/// Calculated as the angle between the circles (current-2, current-1, current).
|
/// Calculated as the angle between the circles (current-2, current-1, current).
|
||||||
@@ -86,17 +105,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double HitWindowGreat { get; private set; }
|
public double HitWindowGreat { get; private set; }
|
||||||
|
|
||||||
private readonly OsuHitObject? lastLastObject;
|
/// <summary>
|
||||||
private readonly OsuHitObject lastObject;
|
/// Selective bonus for maps with higher circle size.
|
||||||
|
/// </summary>
|
||||||
|
public double SmallCircleBonus { get; private set; }
|
||||||
|
|
||||||
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
private readonly OsuDifficultyHitObject? lastLastDifficultyObject;
|
||||||
|
private readonly OsuDifficultyHitObject? lastDifficultyObject;
|
||||||
|
|
||||||
|
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||||
: base(hitObject, lastObject, clockRate, objects, index)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
this.lastLastObject = lastLastObject as OsuHitObject;
|
lastLastDifficultyObject = index > 1 ? (OsuDifficultyHitObject)objects[index - 2] : null;
|
||||||
this.lastObject = (OsuHitObject)lastObject;
|
lastDifficultyObject = index > 0 ? (OsuDifficultyHitObject)objects[index - 1] : null;
|
||||||
|
|
||||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||||
StrainTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
AdjustedDeltaTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
||||||
|
|
||||||
|
SmallCircleBonus = Math.Max(1.0, 1.0 + (30 - BaseObject.Radius) / 40);
|
||||||
|
|
||||||
if (BaseObject is Slider sliderObject)
|
if (BaseObject is Slider sliderObject)
|
||||||
{
|
{
|
||||||
@@ -107,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeSliderCursorPosition();
|
||||||
setDistances(clockRate);
|
setDistances(clockRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,35 +188,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
if (BaseObject is Slider currentSlider)
|
if (BaseObject is Slider currentSlider)
|
||||||
{
|
{
|
||||||
computeSliderCursorPosition(currentSlider);
|
|
||||||
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||||
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
TravelDistance = LazyTravelDistance * Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
||||||
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
TravelTime = Math.Max(LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||||
if (BaseObject is Spinner || lastObject is Spinner)
|
if (BaseObject is Spinner || LastObject is Spinner)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
|
float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
|
||||||
|
|
||||||
if (BaseObject.Radius < 30)
|
Vector2 lastCursorPosition = lastDifficultyObject != null ? getEndCursorPosition(lastDifficultyObject) : LastObject.StackedPosition;
|
||||||
{
|
|
||||||
float smallCircleBonus = Math.Min(30 - (float)BaseObject.Radius, 5) / 50;
|
|
||||||
scalingFactor *= 1 + smallCircleBonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
|
|
||||||
|
|
||||||
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||||
MinimumJumpTime = StrainTime;
|
MinimumJumpTime = AdjustedDeltaTime;
|
||||||
MinimumJumpDistance = LazyJumpDistance;
|
MinimumJumpDistance = LazyJumpDistance;
|
||||||
|
|
||||||
if (lastObject is Slider lastSlider)
|
if (LastObject is Slider lastSlider && lastDifficultyObject != null)
|
||||||
{
|
{
|
||||||
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
double lastTravelTime = Math.Max(lastDifficultyObject.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, MIN_DELTA_TIME);
|
MinimumJumpTime = Math.Max(AdjustedDeltaTime - lastTravelTime, MIN_DELTA_TIME);
|
||||||
|
|
||||||
//
|
//
|
||||||
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
||||||
@@ -217,11 +237,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
|
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastLastObject != null && !(lastLastObject is Spinner))
|
if (lastLastDifficultyObject != null && lastLastDifficultyObject.BaseObject is not Spinner)
|
||||||
{
|
{
|
||||||
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
|
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastDifficultyObject);
|
||||||
|
|
||||||
Vector2 v1 = lastLastCursorPosition - lastObject.StackedPosition;
|
Vector2 v1 = lastLastCursorPosition - LastObject.StackedPosition;
|
||||||
Vector2 v2 = BaseObject.StackedPosition - lastCursorPosition;
|
Vector2 v2 = BaseObject.StackedPosition - lastCursorPosition;
|
||||||
|
|
||||||
float dot = Vector2.Dot(v1, v2);
|
float dot = Vector2.Dot(v1, v2);
|
||||||
@@ -231,9 +251,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeSliderCursorPosition(Slider slider)
|
private void computeSliderCursorPosition()
|
||||||
{
|
{
|
||||||
if (slider.LazyEndPosition != null)
|
if (BaseObject is not Slider slider)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (LazyEndPosition != null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// TODO: This commented version is actually correct by the new lazer implementation, but intentionally held back from
|
// TODO: This commented version is actually correct by the new lazer implementation, but intentionally held back from
|
||||||
@@ -280,15 +303,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
nestedObjects = reordered;
|
nestedObjects = reordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
slider.LazyTravelTime = trackingEndTime - slider.StartTime;
|
LazyTravelTime = trackingEndTime - slider.StartTime;
|
||||||
|
|
||||||
double endTimeMin = slider.LazyTravelTime / slider.SpanDuration;
|
double endTimeMin = LazyTravelTime / slider.SpanDuration;
|
||||||
if (endTimeMin % 2 >= 1)
|
if (endTimeMin % 2 >= 1)
|
||||||
endTimeMin = 1 - endTimeMin % 1;
|
endTimeMin = 1 - endTimeMin % 1;
|
||||||
else
|
else
|
||||||
endTimeMin %= 1;
|
endTimeMin %= 1;
|
||||||
|
|
||||||
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
|
LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
|
||||||
|
|
||||||
Vector2 currCursorPosition = slider.StackedPosition;
|
Vector2 currCursorPosition = slider.StackedPosition;
|
||||||
|
|
||||||
@@ -310,7 +333,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
// There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement.
|
// There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement.
|
||||||
// For sliders that are circular, the lazy end position may actually be farther away than the sliders true end.
|
// For sliders that are circular, the lazy end position may actually be farther away than the sliders true end.
|
||||||
// This code is designed to prevent buffing situations where lazy end is actually a less efficient movement.
|
// This code is designed to prevent buffing situations where lazy end is actually a less efficient movement.
|
||||||
Vector2 lazyMovement = Vector2.Subtract((Vector2)slider.LazyEndPosition, currCursorPosition);
|
Vector2 lazyMovement = Vector2.Subtract((Vector2)LazyEndPosition, currCursorPosition);
|
||||||
|
|
||||||
if (lazyMovement.Length < currMovement.Length)
|
if (lazyMovement.Length < currMovement.Length)
|
||||||
currMovement = lazyMovement;
|
currMovement = lazyMovement;
|
||||||
@@ -328,25 +351,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
// this finds the positional delta from the required radius and the current position, and updates the currCursorPosition accordingly, as well as rewarding distance.
|
// this finds the positional delta from the required radius and the current position, and updates the currCursorPosition accordingly, as well as rewarding distance.
|
||||||
currCursorPosition = Vector2.Add(currCursorPosition, Vector2.Multiply(currMovement, (float)((currMovementLength - requiredMovement) / currMovementLength)));
|
currCursorPosition = Vector2.Add(currCursorPosition, Vector2.Multiply(currMovement, (float)((currMovementLength - requiredMovement) / currMovementLength)));
|
||||||
currMovementLength *= (currMovementLength - requiredMovement) / currMovementLength;
|
currMovementLength *= (currMovementLength - requiredMovement) / currMovementLength;
|
||||||
slider.LazyTravelDistance += (float)currMovementLength;
|
LazyTravelDistance += currMovementLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == nestedObjects.Count - 1)
|
if (i == nestedObjects.Count - 1)
|
||||||
slider.LazyEndPosition = currCursorPosition;
|
LazyEndPosition = currCursorPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 getEndCursorPosition(OsuHitObject hitObject)
|
private Vector2 getEndCursorPosition(OsuDifficultyHitObject difficultyHitObject)
|
||||||
{
|
{
|
||||||
Vector2 pos = hitObject.StackedPosition;
|
return difficultyHitObject.LazyEndPosition ?? difficultyHitObject.BaseObject.StackedPosition;
|
||||||
|
|
||||||
if (hitObject is Slider slider)
|
|
||||||
{
|
|
||||||
computeSliderCursorPosition(slider);
|
|
||||||
pos = slider.LazyEndPosition ?? pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
|
||||||
private double skillMultiplier => 25.6;
|
private double skillMultiplier => 26;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
|
|
||||||
private readonly List<double> sliderStrains = new List<double>();
|
private readonly List<double> sliderStrains = new List<double>();
|
||||||
@@ -41,9 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
||||||
|
|
||||||
if (current.BaseObject is Slider)
|
if (current.BaseObject is Slider)
|
||||||
{
|
|
||||||
sliderStrains.Add(currentStrain);
|
sliderStrains.Add(currentStrain);
|
||||||
}
|
|
||||||
|
|
||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
@@ -54,10 +53,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double maxSliderStrain = sliderStrains.Max();
|
double maxSliderStrain = sliderStrains.Max();
|
||||||
|
|
||||||
if (maxSliderStrain == 0)
|
if (maxSliderStrain == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@@ -15,12 +18,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : OsuStrainSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private double skillMultiplier => 1.46;
|
private double skillMultiplier => 1.47;
|
||||||
private double strainDecayBase => 0.3;
|
private double strainDecayBase => 0.3;
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
private double currentRhythm;
|
private double currentRhythm;
|
||||||
|
|
||||||
|
private readonly List<double> sliderStrains = new List<double>();
|
||||||
|
|
||||||
protected override int ReducedSectionCount => 5;
|
protected override int ReducedSectionCount => 5;
|
||||||
|
|
||||||
public Speed(Mod[] mods)
|
public Speed(Mod[] mods)
|
||||||
@@ -34,13 +39,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
||||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
|
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
|
||||||
|
|
||||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||||
|
|
||||||
double totalStrain = currentStrain * currentRhythm;
|
double totalStrain = currentStrain * currentRhythm;
|
||||||
|
|
||||||
|
if (current.BaseObject is Slider)
|
||||||
|
sliderStrains.Add(totalStrain);
|
||||||
|
|
||||||
return totalStrain;
|
return totalStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,5 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs
Normal file
101
osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
|
||||||
|
{
|
||||||
|
public static class LegacyScoreUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the average amount of score per object that is caused by nested judgements such as slider-ticks and spinners.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalculateNestedScorePerObject(IBeatmap beatmap, int objectCount)
|
||||||
|
{
|
||||||
|
const double big_tick_score = 30;
|
||||||
|
const double small_tick_score = 10;
|
||||||
|
|
||||||
|
var sliders = beatmap.HitObjects.OfType<Slider>().ToArray();
|
||||||
|
|
||||||
|
// 1 for head, 1 for tail
|
||||||
|
int amountOfBigTicks = sliders.Length * 2;
|
||||||
|
|
||||||
|
// Add slider repeats
|
||||||
|
amountOfBigTicks += sliders.Select(s => s.RepeatCount).Sum();
|
||||||
|
|
||||||
|
int amountOfSmallTicks = sliders.Select(s => s.NestedHitObjects.Count(nho => nho is SliderTick)).Sum();
|
||||||
|
|
||||||
|
double sliderScore = amountOfBigTicks * big_tick_score + amountOfSmallTicks * small_tick_score;
|
||||||
|
|
||||||
|
double spinnerScore = 0;
|
||||||
|
|
||||||
|
foreach (var spinner in beatmap.HitObjects.OfType<Spinner>())
|
||||||
|
{
|
||||||
|
spinnerScore += calculateSpinnerScore(spinner);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (sliderScore + spinnerScore) / objectCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Logic borrowed from <see cref="OsuLegacyScoreSimulator.simulateHit"/> for basic score calculations.
|
||||||
|
/// </remarks>
|
||||||
|
private static double calculateSpinnerScore(Spinner spinner)
|
||||||
|
{
|
||||||
|
const int spin_score = 100;
|
||||||
|
const int bonus_spin_score = 1000;
|
||||||
|
|
||||||
|
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
||||||
|
// We'll redo the calculations to match osu-stable here...
|
||||||
|
const double maximum_rotations_per_second = 477.0 / 60;
|
||||||
|
|
||||||
|
// Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score.
|
||||||
|
// As we're primarily concerned with computing the maximum theoretical final score,
|
||||||
|
// this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1.
|
||||||
|
const double minimum_rotations_per_second = 3;
|
||||||
|
|
||||||
|
double secondsDuration = spinner.Duration / 1000;
|
||||||
|
|
||||||
|
// The total amount of half spins possible for the entire spinner.
|
||||||
|
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
||||||
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second);
|
||||||
|
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
||||||
|
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
||||||
|
|
||||||
|
long score = 0;
|
||||||
|
|
||||||
|
int fullSpins = (totalHalfSpinsPossible / 2);
|
||||||
|
|
||||||
|
// Normal spin score
|
||||||
|
score += spin_score * fullSpins;
|
||||||
|
|
||||||
|
int bonusSpins = (totalHalfSpinsPossible - halfSpinsRequiredBeforeBonus) / 2;
|
||||||
|
|
||||||
|
// Reduce amount of bonus spins because we want to represent the more average case, rather than the best one.
|
||||||
|
bonusSpins = Math.Max(0, bonusSpins - fullSpins / 2);
|
||||||
|
|
||||||
|
score += bonus_spin_score * bonusSpins;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CalculateDifficultyPeppyStars(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
int objectCount = beatmap.HitObjects.Count;
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (objectCount > 0)
|
||||||
|
{
|
||||||
|
int breakLength = beatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(beatmap.HitObjects[^1].StartTime) - (int)Math.Round(beatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LegacyRulesetExtensions.CalculateDifficultyPeppyStars(beatmap.Difficulty, objectCount, drainLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
osu.Game.Rulesets.Osu/Difficulty/Utils/OsuStrainUtils.cs
Normal file
26
osu.Game.Rulesets.Osu/Difficulty/Utils/OsuStrainUtils.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Game.Rulesets.Difficulty.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
|
||||||
|
{
|
||||||
|
public static class OsuStrainUtils
|
||||||
|
{
|
||||||
|
public static double CountTopWeightedSliders(IReadOnlyCollection<double> sliderStrains, double difficultyValue)
|
||||||
|
{
|
||||||
|
if (sliderStrains.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double consistentTopStrain = difficultyValue / 10; // What would the top strain be if all strain values were identical
|
||||||
|
|
||||||
|
if (consistentTopStrain == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||||
|
return sliderStrains.Sum(s => DifficultyCalculationUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
this.gridToolboxGroup = gridToolboxGroup;
|
this.gridToolboxGroup = gridToolboxGroup;
|
||||||
originalOrigin = gridToolboxGroup.StartPosition.Value;
|
originalOrigin = gridToolboxGroup.StartPosition.Value;
|
||||||
originalSpacing = gridToolboxGroup.Spacing.Value;
|
originalSpacing = gridToolboxGroup.GridLineSpacing.Value;
|
||||||
originalRotation = gridToolboxGroup.GridLinesRotation.Value;
|
originalRotation = gridToolboxGroup.GridLinesRotation.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
// Reset the grid to the default values.
|
// Reset the grid to the default values.
|
||||||
gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default;
|
gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default;
|
||||||
gridToolboxGroup.Spacing.Value = gridToolboxGroup.Spacing.Default;
|
gridToolboxGroup.GridLineSpacing.Value = gridToolboxGroup.GridLineSpacing.Default;
|
||||||
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||||
gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default;
|
gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default;
|
||||||
EndPlacement(true);
|
EndPlacement(true);
|
||||||
@@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
// Default to the original spacing and rotation if the distance is too small.
|
// Default to the original spacing and rotation if the distance is too small.
|
||||||
if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2)
|
if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2)
|
||||||
{
|
{
|
||||||
gridToolboxGroup.Spacing.Value = originalSpacing;
|
gridToolboxGroup.GridLineSpacing.Value = originalSpacing;
|
||||||
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||||
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
private void resetGridState()
|
private void resetGridState()
|
||||||
{
|
{
|
||||||
gridToolboxGroup.StartPosition.Value = originalOrigin;
|
gridToolboxGroup.StartPosition.Value = originalOrigin;
|
||||||
gridToolboxGroup.Spacing.Value = originalSpacing;
|
gridToolboxGroup.GridLineSpacing.Value = originalSpacing;
|
||||||
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||||
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,9 +469,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
|
Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition);
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
// Snapping inherited B-spline control points to nearby objects would be unintuitive, because snapping them does not equate to snapping the interpolated slider path.
|
||||||
|
bool shouldSnapToNearbyObjects = dragPathTypes[draggedControlPointIndex] is not null ||
|
||||||
|
dragPathTypes[..draggedControlPointIndex].LastOrDefault(t => t is not null)?.Type != SplineType.BSpline;
|
||||||
|
|
||||||
|
SnapResult result = null;
|
||||||
|
if (shouldSnapToNearbyObjects)
|
||||||
|
result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime);
|
||||||
|
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
|
||||||
|
result = gridSnapResult;
|
||||||
|
result ??= new SnapResult(newControlPointPosition, oldStartTime);
|
||||||
|
|
||||||
|
Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||||
|
|
||||||
for (int i = 0; i < controlPoints.Count; ++i)
|
for (int i = 0; i < controlPoints.Count; ++i)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement;
|
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || HitObject.Path.HasValidLengthForPlacement);
|
||||||
|
|
||||||
public SliderPlacementBlueprint()
|
public SliderPlacementBlueprint()
|
||||||
: base(new Slider())
|
: base(new Slider())
|
||||||
|
|||||||
@@ -626,10 +626,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
|
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
|
||||||
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
|
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
|
||||||
|
|
||||||
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
|
protected override Vector2[] ScreenSpaceAdditionalNodes => getScreenSpaceControlPointNodes().Prepend(
|
||||||
{
|
|
||||||
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
|
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
|
||||||
};
|
).ToArray();
|
||||||
|
|
||||||
|
private IEnumerable<Vector2> getScreenSpaceControlPointNodes()
|
||||||
|
{
|
||||||
|
// Returns the positions of control points that produce visible kinks on the slider's path
|
||||||
|
// This excludes inherited control points from Bezier, B-Spline, Perfect, and Catmull curves
|
||||||
|
if (DrawableObject.SliderBody == null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
PathType? currentPathType = null;
|
||||||
|
|
||||||
|
// Skip the last control point because its always either not on the slider path or exactly on the slider end
|
||||||
|
for (int i = 0; i < DrawableObject.HitObject.Path.ControlPoints.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var controlPoint = DrawableObject.HitObject.Path.ControlPoints[i];
|
||||||
|
|
||||||
|
if (controlPoint.Type is not null)
|
||||||
|
currentPathType = controlPoint.Type;
|
||||||
|
|
||||||
|
// Skip the first control point because it is already covered by the slider head
|
||||||
|
if (i == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (controlPoint.Type is null && currentPathType != PathType.LINEAR)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var screenSpacePosition = DrawableObject.SliderBody.ToScreenSpace(DrawableObject.SliderBody.PathOffset + controlPoint.Position);
|
||||||
|
yield return screenSpacePosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ using System;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
@@ -42,25 +44,31 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private readonly BindableInt displayTolerance = new BindableInt(90)
|
private readonly BindableInt displayTolerance = new BindableInt(90)
|
||||||
{
|
{
|
||||||
MinValue = 5,
|
MinValue = 5,
|
||||||
MaxValue = 100
|
MaxValue = 100,
|
||||||
|
Precision = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly BindableInt displayCornerThreshold = new BindableInt(40)
|
private readonly BindableInt displayCornerThreshold = new BindableInt(40)
|
||||||
{
|
{
|
||||||
MinValue = 5,
|
MinValue = 5,
|
||||||
MaxValue = 100
|
MaxValue = 100,
|
||||||
|
Precision = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
|
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
|
||||||
{
|
{
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 100
|
MaxValue = 100,
|
||||||
|
Precision = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
private ExpandableSlider<int> toleranceSlider = null!;
|
private ExpandableSlider<int> toleranceSlider = null!;
|
||||||
private ExpandableSlider<int> cornerThresholdSlider = null!;
|
private ExpandableSlider<int> cornerThresholdSlider = null!;
|
||||||
private ExpandableSlider<int> circleThresholdSlider = null!;
|
private ExpandableSlider<int> circleThresholdSlider = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IExpandingContainer? expandingContainer { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@@ -68,15 +76,18 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
toleranceSlider = new ExpandableSlider<int>
|
toleranceSlider = new ExpandableSlider<int>
|
||||||
{
|
{
|
||||||
Current = displayTolerance
|
Current = displayTolerance,
|
||||||
|
ExpandedLabelText = "Control point spacing",
|
||||||
},
|
},
|
||||||
cornerThresholdSlider = new ExpandableSlider<int>
|
cornerThresholdSlider = new ExpandableSlider<int>
|
||||||
{
|
{
|
||||||
Current = displayCornerThreshold
|
Current = displayCornerThreshold,
|
||||||
|
ExpandedLabelText = "Corner bias",
|
||||||
},
|
},
|
||||||
circleThresholdSlider = new ExpandableSlider<int>
|
circleThresholdSlider = new ExpandableSlider<int>
|
||||||
{
|
{
|
||||||
Current = displayCircleThreshold
|
Current = displayCircleThreshold,
|
||||||
|
ExpandedLabelText = "Perfect curve bias"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -88,24 +99,18 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
displayTolerance.BindValueChanged(tolerance =>
|
displayTolerance.BindValueChanged(tolerance =>
|
||||||
{
|
{
|
||||||
toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}";
|
toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}";
|
||||||
toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}";
|
|
||||||
|
|
||||||
Tolerance.Value = displayToInternalTolerance(tolerance.NewValue);
|
Tolerance.Value = displayToInternalTolerance(tolerance.NewValue);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
displayCornerThreshold.BindValueChanged(threshold =>
|
displayCornerThreshold.BindValueChanged(threshold =>
|
||||||
{
|
{
|
||||||
cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}";
|
cornerThresholdSlider.ContractedLabelText = $"C. B.: {threshold.NewValue:N0}";
|
||||||
cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}";
|
|
||||||
|
|
||||||
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
|
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
displayCircleThreshold.BindValueChanged(threshold =>
|
displayCircleThreshold.BindValueChanged(threshold =>
|
||||||
{
|
{
|
||||||
circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}";
|
circleThresholdSlider.ContractedLabelText = $"P. C. B.: {threshold.NewValue:N0}";
|
||||||
circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}";
|
|
||||||
|
|
||||||
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
|
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@@ -119,6 +124,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
|
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||||
|
{
|
||||||
|
Spacing = v.NewValue ? new Vector2(5) : new Vector2(15);
|
||||||
|
}, true);
|
||||||
|
|
||||||
float displayToInternalTolerance(float v) => v / 50f;
|
float displayToInternalTolerance(float v) => v / 50f;
|
||||||
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
|
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = 0f,
|
MinValue = 0f,
|
||||||
MaxValue = OsuPlayfield.BASE_SIZE.X,
|
MaxValue = OsuPlayfield.BASE_SIZE.X,
|
||||||
Precision = 0.01f,
|
Precision = 0.1f,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,17 +48,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = 0f,
|
MinValue = 0f,
|
||||||
MaxValue = OsuPlayfield.BASE_SIZE.Y,
|
MaxValue = OsuPlayfield.BASE_SIZE.Y,
|
||||||
Precision = 0.01f,
|
Precision = 0.1f,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The spacing between grid lines.
|
/// The spacing between grid lines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BindableFloat Spacing { get; } = new BindableFloat(4f)
|
public BindableFloat GridLineSpacing { get; } = new BindableFloat(4f)
|
||||||
{
|
{
|
||||||
MinValue = 4f,
|
MinValue = 4f,
|
||||||
MaxValue = 256f,
|
MaxValue = 256f,
|
||||||
Precision = 0.01f,
|
Precision = 0.1f,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = -180f,
|
MinValue = -180f,
|
||||||
MaxValue = 180f,
|
MaxValue = 180f,
|
||||||
Precision = 0.01f,
|
Precision = 0.1f,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
float dist = Vector2.Distance(point1, point2);
|
float dist = Vector2.Distance(point1, point2);
|
||||||
while (dist >= max_automatic_spacing)
|
while (dist >= max_automatic_spacing)
|
||||||
dist /= 2;
|
dist /= 2;
|
||||||
Spacing.Value = dist;
|
GridLineSpacing.Value = dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@@ -127,21 +127,25 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
Current = StartPositionX,
|
Current = StartPositionX,
|
||||||
KeyboardStep = 1,
|
KeyboardStep = 1,
|
||||||
|
ExpandedLabelText = "X offset",
|
||||||
},
|
},
|
||||||
startPositionYSlider = new ExpandableSlider<float>
|
startPositionYSlider = new ExpandableSlider<float>
|
||||||
{
|
{
|
||||||
Current = StartPositionY,
|
Current = StartPositionY,
|
||||||
KeyboardStep = 1,
|
KeyboardStep = 1,
|
||||||
|
ExpandedLabelText = "Y offset",
|
||||||
},
|
},
|
||||||
spacingSlider = new ExpandableSlider<float>
|
spacingSlider = new ExpandableSlider<float>
|
||||||
{
|
{
|
||||||
Current = Spacing,
|
Current = GridLineSpacing,
|
||||||
KeyboardStep = 1,
|
KeyboardStep = 1,
|
||||||
|
ExpandedLabelText = "Spacing",
|
||||||
},
|
},
|
||||||
gridLinesRotationSlider = new ExpandableSlider<float>
|
gridLinesRotationSlider = new ExpandableSlider<float>
|
||||||
{
|
{
|
||||||
Current = GridLinesRotation,
|
Current = GridLinesRotation,
|
||||||
KeyboardStep = 1,
|
KeyboardStep = 1,
|
||||||
|
ExpandedLabelText = "Rotation",
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
{
|
{
|
||||||
@@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Spacing.Value = editorBeatmap.GridSize;
|
GridLineSpacing.Value = editorBeatmap.GridSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@@ -182,14 +186,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
StartPositionX.BindValueChanged(x =>
|
StartPositionX.BindValueChanged(x =>
|
||||||
{
|
{
|
||||||
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
|
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
|
||||||
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}";
|
|
||||||
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
StartPositionY.BindValueChanged(y =>
|
StartPositionY.BindValueChanged(y =>
|
||||||
{
|
{
|
||||||
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
|
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
|
||||||
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}";
|
|
||||||
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@@ -199,10 +201,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
StartPositionY.Value = pos.NewValue.Y;
|
StartPositionY.Value = pos.NewValue.Y;
|
||||||
});
|
});
|
||||||
|
|
||||||
Spacing.BindValueChanged(spacing =>
|
GridLineSpacing.BindValueChanged(spacing =>
|
||||||
{
|
{
|
||||||
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
|
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
|
||||||
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
|
|
||||||
SpacingVector.Value = new Vector2(spacing.NewValue);
|
SpacingVector.Value = new Vector2(spacing.NewValue);
|
||||||
editorBeatmap.GridSize = (int)spacing.NewValue;
|
editorBeatmap.GridSize = (int)spacing.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
@@ -210,7 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
GridLinesRotation.BindValueChanged(rotation =>
|
GridLinesRotation.BindValueChanged(rotation =>
|
||||||
{
|
{
|
||||||
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
|
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
|
||||||
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
GridType.BindValueChanged(v =>
|
GridType.BindValueChanged(v =>
|
||||||
@@ -239,6 +239,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||||
|
|
||||||
|
Spacing = v.NewValue ? new Vector2(5) : new Vector2(15);
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.EditorCycleGridSpacing:
|
case GlobalAction.EditorCycleGridSpacing:
|
||||||
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
GridLineSpacing.Value = GridLineSpacing.Value * 2 >= max_automatic_spacing ? GridLineSpacing.Value / 8 : GridLineSpacing.Value * 2;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.EditorCycleGridType:
|
case GlobalAction.EditorCycleGridType:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@@ -142,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
case PositionSnapGridType.Triangle:
|
case PositionSnapGridType.Triangle:
|
||||||
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
|
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
|
||||||
|
|
||||||
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
|
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.GridLineSpacing);
|
||||||
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
|
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
|
||||||
|
|
||||||
positionSnapGrid = triangularPositionSnapGrid;
|
positionSnapGrid = triangularPositionSnapGrid;
|
||||||
@@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
case PositionSnapGridType.Circle:
|
case PositionSnapGridType.Circle:
|
||||||
var circularPositionSnapGrid = new CircularPositionSnapGrid();
|
var circularPositionSnapGrid = new CircularPositionSnapGrid();
|
||||||
|
|
||||||
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
|
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.GridLineSpacing);
|
||||||
|
|
||||||
positionSnapGrid = circularPositionSnapGrid;
|
positionSnapGrid = circularPositionSnapGrid;
|
||||||
break;
|
break;
|
||||||
@@ -171,7 +172,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
=> new OsuBlueprintContainer(this);
|
=> new OsuBlueprintContainer(this);
|
||||||
|
|
||||||
public override string ConvertSelectionToString()
|
public override string ConvertSelectionToString()
|
||||||
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime)
|
||||||
|
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
|
||||||
|
|
||||||
// 1,2,3,4 ...
|
// 1,2,3,4 ...
|
||||||
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
var hitObjects = selectedMovableObjects;
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects, true);
|
||||||
|
|
||||||
Vector2 delta = Vector2.Zero;
|
Vector2 delta = Vector2.Zero;
|
||||||
|
|
||||||
|
|||||||
@@ -81,12 +81,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
originalConvexHull = GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
|
||||||
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
|
|
||||||
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
|
|
||||||
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
|
||||||
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
{
|
{
|
||||||
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
|
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys, true);
|
||||||
|
|
||||||
Vector2 delta = Vector2.Zero;
|
Vector2 delta = Vector2.Zero;
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||||
LastAcceptedAction = null;
|
LastAcceptedAction = null;
|
||||||
|
|
||||||
|
if (LastAcceptedAction != null && gameplayClock.IsRewinding)
|
||||||
|
LastAcceptedAction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract bool CheckValidNewAction(OsuAction action);
|
protected abstract bool CheckValidNewAction(OsuAction action);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override LocalisableString Description => @"Don't use the same key twice in a row!";
|
public override LocalisableString Description => @"Don't use the same key twice in a row!";
|
||||||
public override IconUsage? Icon => OsuIcon.ModAlternate;
|
public override IconUsage? Icon => OsuIcon.ModAlternate;
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
|
||||||
|
public override bool Ranked => true;
|
||||||
|
|
||||||
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
|
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
|
||||||
|
|
||||||
|
#if !DEBUG
|
||||||
|
private string username = "Autoplay";
|
||||||
|
#else
|
||||||
|
private string username = "Chicony";
|
||||||
|
#endif
|
||||||
|
|
||||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = username });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,19 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
|
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||||
{
|
{
|
||||||
|
|
||||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||||
{
|
{
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
|
#if !DEBUG
|
||||||
ExtendedMaxValue = 11,
|
ExtendedMaxValue = 11,
|
||||||
|
#else
|
||||||
|
ExtendedMinValue = -250,
|
||||||
|
ExtendedMaxValue = 13,
|
||||||
|
#endif
|
||||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,8 +38,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
|
#if !DEBUG
|
||||||
ExtendedMinValue = -10,
|
ExtendedMinValue = -10,
|
||||||
ExtendedMaxValue = 11,
|
ExtendedMaxValue = 11,
|
||||||
|
#else
|
||||||
|
ExtendedMinValue = -250,
|
||||||
|
ExtendedMaxValue = 13,
|
||||||
|
#endif
|
||||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +62,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
string format(string acronym, DifficultyBindable bindable)
|
||||||
|
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModSingleTap;
|
public override IconUsage? Icon => OsuIcon.ModSingleTap;
|
||||||
public override LocalisableString Description => @"You must only use one key!";
|
public override LocalisableString Description => @"You must only use one key!";
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
|
||||||
|
public override bool Ranked => true;
|
||||||
|
|
||||||
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
|
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
||||||
public override double ScoreMultiplier => 0.9;
|
public override double ScoreMultiplier => 0.9;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
|
||||||
public override bool Ranked => UsesDefaultConfiguration;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
|
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public class OsuModTouchDevice : ModTouchDevice
|
public class OsuModTouchDevice : ModTouchDevice
|
||||||
{
|
{
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModBloom) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModBloom) }).ToArray();
|
||||||
public override bool Ranked => UsesDefaultConfiguration;
|
public override bool Ranked => true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user