889 Commits

Author SHA1 Message Date
17d56f8db0 now completely in sync with master 2025-12-16 20:22:38 +03:00
9637ea1df7 again**2 2025-12-16 19:51:36 +03:00
0940d61f7e try again 2025-12-16 19:47:28 +03:00
e6a6352a9c sync with master fully 2025-12-16 19:39:40 +03:00
Dean Herbert
734c6f933d Merge pull request #36020 from bdach/fail-indicator-right-sound
Fix replay fail indicator not using fail sample from beatmap skin
2025-12-17 01:15:22 +09:00
Dean Herbert
ac213c90cb Merge pull request #36023 from bdach/report-message-pm
Adjust message on successful report to match bancho
2025-12-16 23:52:31 +09:00
Bartłomiej Dach
74ca87c252 Merge pull request #36009 from frenzibyte/fix-skip-button
Fix skip overlay potentially not allowing skipping
2025-12-16 14:58:43 +01:00
Bartłomiej Dach
032912e62b Adjust message on successful report to match bancho
Addresses https://github.com/ppy/osu/discussions/36004.

Not adding localisation because the previous implementation was
`.ToString()`ing anyway.

Would have made the abuse e-mail a link but `mailto:` doesn't work with
`MessageFormatter` and I don't want to go into that right now.

The message *almost* matches stable. The "almost" is because it doesn't
mention the `/ignore` chat command. I was just going to implement the
command, but I went to check what it does, and backed away slowly
because it has like weird scoping to chat, highlights, and PMs, so
`nope.avi`.
2025-12-16 11:20:54 +01:00
Bartłomiej Dach
1e79c56240 Fix replay fail indicator not using fail sample from beatmap skin
Closes https://github.com/ppy/osu/issues/36003.

The duplicated `RulesetSkinProvidingContainer` is unfortunate but it's
either this or I start doing proxy shenanigans.
2025-12-16 09:48:10 +01:00
86f0159e65 adjust some CI settings 2025-12-15 20:32:33 +03:00
ae5a64ba81 fix most failing test cases 2025-12-15 20:29:31 +03:00
Dean Herbert
82256ae2de Merge pull request #34807 from mcendu/legacy-pp-counter
Add "legacy" pp counter
2025-12-15 20:24:04 +09:00
Dean Herbert
1142be45ec Update resources 2025-12-15 19:22:11 +09:00
Dean Herbert
dcb6d71287 Adjust constant and documentation slightly 2025-12-15 19:07:46 +09:00
Salman Alshamrani
881a35b382 Fix skip overlay potentially not allowing skipping 2025-12-15 03:54:09 -05:00
Dean Herbert
89d8b402af Merge branch 'master' into legacy-pp-counter 2025-12-15 16:55:51 +09:00
Dean Herbert
1d351002df Merge pull request #36005 from smoogipoo/update-packages
Update localisation analyser packages
2025-12-15 16:15:37 +09:00
e3a7ae30cd prevent an exception if icon is broken (probably) 2025-12-13 22:41:23 +03:00
b6f845d99c slightly change score panels and mod icons 2025-12-13 02:41:05 +03:00
Dean Herbert
2606f3a0b5 Merge pull request #35980 from smoogipoo/qp-ux-fixes
Various minor quick play UX fixes
2025-12-12 19:20:17 +09:00
Dan Balasescu
1c463aa060 Automatically accept invitation in queue screen 2025-12-12 18:26:52 +09:00
Dan Balasescu
7853abe8aa Move to queue screen when clicking notification 2025-12-12 18:13:00 +09:00
Dan Balasescu
1aff418981 Reword waiting text 2025-12-12 17:57:50 +09:00
Dean Herbert
62e92bb242 Merge pull request #35971 from smoogipoo/fix-mp-screen-leave
Forcefully leave room on multiplayer exit
2025-12-12 17:06:20 +09:00
Dan Balasescu
79151ae5b4 Remove mention of exception that doesn't exist 2025-12-12 15:46:00 +09:00
a1d6bda63e hide online status if no map is actually selected (ssv1/v2) 2025-12-12 00:06:05 +03:00
547d22a4b5 update some URLs to match instance, fix potential mp crash 2025-12-11 23:57:01 +03:00
Dan Balasescu
c17db2cdd0 Forcefully leave room on multiplayer exit 2025-12-11 20:07:44 +09:00
Dan Balasescu
bbdd70c843 Always perform leave room sequence 2025-12-11 20:07:36 +09:00
Bartłomiej Dach
4250a54245 Merge pull request #35969 from peppy/delay-loading-animation
Slightly delay song select leaderboard's loading placeholder to avoid flashing during local score retrieval
2025-12-11 11:54:27 +01:00
Bartłomiej Dach
40fdb8662e Merge pull request #35966 from peppy/fix-muting-after-gameplay-with-bad-network
Fix audio track potentially muting after gameplay with bad network
2025-12-11 10:57:45 +01:00
Dean Herbert
6ce8b0a4bc Slightly delay song select leaderboard's loading placeholder to avoid flashing during local score retrieval
Closes #35893.
2025-12-11 18:54:38 +09:00
Dean Herbert
b30047def6 Remove audio adjustments immediately on gameplay hotkey overlays
Closes #22164.
2025-12-11 17:57:07 +09:00
Bartłomiej Dach
0ffb86262f Merge pull request #35965 from peppy/debounce-seek-only-when-playing
Fix editor not seeking smoothly when paused
2025-12-11 08:54:36 +01:00
Dean Herbert
f71eb4b980 Debounce track seeks only when track is playing
This fixes the editor no longer seeking smoothly when paused.

Closes https://github.com/ppy/osu/issues/35963.
2025-12-11 16:00:13 +09:00
Dan Balasescu
1faf02e860 Update localisation analyser packages 2025-12-11 13:53:09 +09:00
Dean Herbert
d700375e55 Merge pull request #35843 from SollyBunny/master
Make tracked leaderboard score yellow again
2025-12-11 13:50:59 +09:00
Bartłomiej Dach
095a67c24e Fix dragging volume meter to adjust volume closing overlays if mouse is released outside of overlay content (#35940)
* Add failing test

* Fix dragging volume meter to adjust volume closing overlays if mouse is released outside of overlay content

Fixes https://osu.ppy.sh/community/forums/topics/2159553.
2025-12-11 13:47:08 +09:00
Bartłomiej Dach
86054497d0 Disable save replay on fail overlay when spectating (#35942)
"Closes" https://github.com/ppy/osu/issues/35920.

The button can't easily work anyway since it's not guaranteed that the
spectating user has all of the frames of the replay (think entering
spectate midway through a play).

This matches the results screen in spectator too.
2025-12-11 13:42:15 +09:00
Bartłomiej Dach
c4f7dee82b Fix skin editor sometimes dropping anchor/origin specification on paste (#35957)
* Add failing test for copy->paste not being idempotent

* Ensure all elements on default skins use fixed anchors

`UsesFixedAnchor` defaults to false, i.e. closest anchors. Combined with
manual anchor / origin specs on some drawables, this would get default
skins into impossible states wherein a drawable would use "closest
anchor" but also explicitly specify anchor / origin that aren't closest,
which horribly fails on attempting to copy and paste.

Frankly shocked this has gone unnoticed for this long, and I regret not
vetoing this "feature" more every time I see its tentacles spread to
produce breakage of levels yet unseen.

Does this commit contain major levels of suck? For sure. Do I have any
better ideas that wouldn't consist of a multi-day rewrite or deletion of
this "feature"? No.

* Fix skin editor always applying closest anchor / origin on paste regardless of whether the component uses fixed anchor

Self-explanatory. Should close https://github.com/ppy/osu/issues/29111
along with previous commit.
2025-12-11 13:40:48 +09:00
Bartłomiej Dach
22825f6509 Merge pull request #35958 from bdach/fix-storyboard-samples-not-playing-in-editor
Fix storyboard samples not playing in editor
2025-12-10 14:33:30 +01:00
Bartłomiej Dach
691e8bcd05 Fix storyboard samples not playing in editor
Closes https://github.com/ppy/osu/issues/35954.
2025-12-10 11:20:50 +01:00
Bartłomiej Dach
e68bab4f4b Merge pull request #35802 from Xiragi/gameplay-cursor-size-change
Fix cursor trail detaching from cursor when adjusting cursor scale
2025-12-10 07:34:08 +01:00
Bartłomiej Dach
9430a62af4 Merge pull request #35925 from rrex971/storyboard-alpha-overshoot-handling
Adjust alpha handling for values exceeding 1 for storyboard sprite transforms to match stable behavior.
2025-12-10 07:26:50 +01:00
b4c530ac04 add a very safe check for IApplicableFailExit mods 2025-12-09 23:59:35 +03:00
5af05d2479 show a message if we successfully migrate db to new place 2025-12-09 23:52:57 +03:00
43ab18ffea hide quit+replay button in most cases where replay can't be saved 2025-12-09 23:15:25 +03:00
9f59259a40 update volume meter design a bit more 2025-12-09 22:45:15 +03:00
82b3015fcc and again? 2025-12-09 21:35:24 +03:00
68f92ab57c messed things up again 2025-12-09 20:24:09 +03:00
d76d4d9a35 okay, i messed things up 2025-12-09 20:20:07 +03:00
Bartłomiej Dach
bb7417c099 Filter out more exceptions from being sent to sentry
More or less covers the first page of client sentry issues sorted by
volume, all of which is pretty much useless for anything because it's
client-specific-failure noise.
2025-12-09 20:19:16 +03:00
Dan Balasescu
066e093987 Adjust vote-to-skip to be explicit about states 2025-12-09 20:19:16 +03:00
Dan Balasescu
56e0c3e65d Fix potentially unsafe quick play event handling 2025-12-09 20:19:16 +03:00
Natelytle
89d7726903 Rank swap mod 2025-12-09 20:19:16 +03:00
Dan Balasescu
36f1bfef07 Fix incorrect quick play download progress 2025-12-09 20:19:15 +03:00
Dan Balasescu
118f07878a Allow score panel to animate 2025-12-09 20:19:15 +03:00
Dan Balasescu
e144968893 Remove quick play round results scroll animation 2025-12-09 20:19:15 +03:00
Dan Balasescu
d2ffea41c6 Consider abandon time for user placements 2025-12-09 20:19:15 +03:00
Dan Balasescu
a8be9b1381 Make quick play chat retain focus after posting 2025-12-09 20:19:15 +03:00
Dean Herbert
bdac75e542 Merge pull request #31141 from DanielPower/screen-scaling-tablet-output
Scale tablet output size when UI Scaling mode is "Everything"
2025-12-10 01:40:46 +09:00
Dean Herbert
5c2df50714 Add test coverage of weird storyboard sprite behaviour 2025-12-09 19:53:02 +09:00
Bartłomiej Dach
887d280bfa Merge branch 'master' into gameplay-cursor-size-change 2025-12-09 11:51:06 +01:00
Dean Herbert
42b184f167 Update framework 2025-12-09 19:20:29 +09:00
Dean Herbert
27737bd4e9 Merge pull request #35938 from bdach/less-sentry
Filter out more exceptions from being sent to sentry
2025-12-09 19:10:57 +09:00
Dean Herbert
84db289779 Use modulus instead of previous solution to match stable more closely 2025-12-09 18:57:49 +09:00
Dean Herbert
4c0522b795 Update comment and fix formatting 2025-12-09 18:00:32 +09:00
Bartłomiej Dach
8bb885a0dc Filter out more exceptions from being sent to sentry
More or less covers the first page of client sentry issues sorted by
volume, all of which is pretty much useless for anything because it's
client-specific-failure noise.
2025-12-09 09:54:46 +01:00
Bartłomiej Dach
0b06acb29d Merge pull request #35909 from smoogipoo/fix-vote-to-skip
Adjust vote-to-skip messaging flow to be explicit about states
2025-12-09 08:59:53 +01:00
Bartłomiej Dach
b129837e57 Merge pull request #35918 from smoogipoo/qp-fix-unsafe-schedules
Fix potentially unsafe quick play event handling
2025-12-09 08:32:56 +01:00
Bartłomiej Dach
07ea9fe2a4 Merge branch 'master' into screen-scaling-tablet-output 2025-12-09 08:05:20 +01:00
Dean Herbert
3e221c7f61 Merge pull request #35906 from Natelytle/jank-tames-raiko
Rank the taiko swap mod
2025-12-09 15:57:02 +09:00
Dean Herbert
7106a6a5e5 Merge pull request #35897 from smoogipoo/qp-fix-download-progress
Fix incorrect quick play download progress
2025-12-09 15:53:15 +09:00
Dean Herbert
eaf2721f5b Merge pull request #35912 from smoogipoo/qp-remove-results-scroll
Remove quick play results scroll animation
2025-12-09 15:52:14 +09:00
490a6fd724 fix out of range exception in changelog overlay 2025-12-08 17:34:27 +03:00
Dean Herbert
59a27dad3d Merge pull request #35923 from smoogipoo/qp-abandoned-at
Consider abandon time for user placements
2025-12-08 19:27:56 +09:00
rrex971
f73307876e Also apply alpha logic to StoryboardAnimation sprites too. 2025-12-08 02:19:29 +05:30
rrex971
4e4aa44a02 Override sprite update method to handle alpha values > 1 like stable.
If alpha exceeds 1 during a sprite's alpha transform like in a FadeTo(), it will set it to 0 mimicking stable's behavior.
2025-12-08 02:17:35 +05:30
Dan Balasescu
d6cd748d2a Consider abandon time for user placements 2025-12-07 23:26:40 +09:00
237e1828f8 add option for playing miss sound on any combo break, make...
exit & restart game options in fail condition mods mutually exclusive
2025-12-07 13:14:38 +03:00
3413f722f7 make volume meter use argon counter (it looks cool) 2025-12-06 23:52:20 +03:00
9f779dac03 forgot to disable christmas intro after testing 2025-12-06 21:57:44 +03:00
0727c53cdc bump to 2025.12.05-lazer 2025-12-06 21:52:53 +03:00
a57ff24191 bump to 2025.1203.0-tachyon, add no intro option, slightly change seasonal bg code 2025-12-06 21:51:48 +03:00
Dan Balasescu
c23d6b7fd1 Fix potentially unsafe quick play event handling 2025-12-07 02:11:26 +09:00
Dean Herbert
582ff999aa Merge pull request #35913 from smoogipoo/qp-chat-hold-focus
Make quick play chat retain focus after posting
2025-12-06 20:20:42 +09:00
Chirag Mahesh
a6c001244f Redundant string interpolation 2025-12-06 11:18:18 +00:00
Dan Balasescu
a96b024ac5 Make quick play chat retain focus after posting 2025-12-06 17:50:58 +09:00
Dan Balasescu
1c10acba76 Allow score panel to animate 2025-12-06 17:40:17 +09:00
Dan Balasescu
4ae4c700ae Remove quick play round results scroll animation 2025-12-06 17:39:54 +09:00
Dan Balasescu
2be50d917a Adjust vote-to-skip to be explicit about states 2025-12-06 13:23:16 +09:00
Natelytle
35fdc6f8b9 Rank swap mod 2025-12-05 22:51:00 -05:00
Dan Balasescu
d04029bcc7 Fix incorrect quick play download progress 2025-12-06 03:24:17 +09:00
Chirag Mahesh
107098314a Move and refactor TestSceneGameplayCursorSizeChange into Ruleset Osu Tests 2025-12-05 17:23:31 +00:00
Chirag Mahesh
d1d76a76ba Refactor trail scale update logic into a dedicated method 2025-12-05 17:23:29 +00:00
Dean Herbert
fbac5db964 Update framework 2025-12-05 22:28:55 +09:00
Dean Herbert
5a920d15c1 Merge pull request #35875 from bdach/always-bind-virtual-modifier
Do not distinguish between left/right modifiers when assigning new key combinations
2025-12-05 22:02:50 +09:00
Dean Herbert
324d088d46 Merge pull request #35878 from frenzibyte/vote-to-skip-design-2
Update multiplayer vote-to-skip button design
2025-12-05 21:40:16 +09:00
Dean Herbert
1db4b897eb Update tests to match new behaviour 2025-12-05 21:27:11 +09:00
Bartłomiej Dach
8e2230d149 Add xmldoc to confusing field
I don't have any better ideas at this time.
2025-12-05 13:05:51 +01:00
Dean Herbert
8d33c35646 Update framework 2025-12-05 20:50:07 +09:00
Dean Herbert
c359898a75 Merge pull request #35890 from bdach/disallow-placing-objects-before-first-timing-point
Disallow placing objects before first timing point
2025-12-05 20:49:44 +09:00
Bartłomiej Dach
6343bf7d29 Privatise setter 2025-12-05 12:35:26 +01:00
Bartłomiej Dach
b1e27d842b Ensure skip counter doesn't overflow the button 2025-12-05 12:34:18 +01:00
Bartłomiej Dach
28eeb7f743 Merge pull request #35889 from smoogipoo/fix-quick-play-kick-message
Fix "kicked" users not being marked as quit
2025-12-05 10:24:30 +01:00
Bartłomiej Dach
38c3167a9d Merge pull request #35888 from smoogipoo/fix-quick-play-crash
Fix quick play crash when presenting random selection
2025-12-05 09:54:20 +01:00
Bartłomiej Dach
66ebce8c12 Fix failing tests after disallowing object placements before first timing point 2025-12-05 09:52:32 +01:00
Dan Balasescu
fed9564b40 Fix "kicked" users not being marked as quit 2025-12-05 17:26:55 +09:00
Dan Balasescu
f595a47059 Fix quick play crash when presenting random selection 2025-12-05 16:59:23 +09:00
Dan Balasescu
8a9f60df68 Add failing test 2025-12-05 16:59:23 +09:00
Salman Alshamrani
2d8b1e7152 Make button brighter on hover 2025-12-04 14:00:39 -05:00
Salman Alshamrani
fef8117b5c Add test coverage for players leaving during intro 2025-12-04 13:38:30 -05:00
Salman Alshamrani
99da986e02 Implement redesigned multiplayer vote-to-skip button 2025-12-04 13:38:30 -05:00
Salman Alshamrani
1c33291b3f Adjust skip button colour and add triangles 2025-12-04 13:38:30 -05:00
Bartłomiej Dach
c6cc92315c Add basic colour indication as to when placements are valid
Unsure about this one, but I find the preceding commit to be very
lacking in explaining to the user why the editor don't work. Shining
some things red may help aid understanding.
2025-12-04 14:37:56 +01:00
Bartłomiej Dach
12170df80a Disallow placing hit objects before first timing point
Because they can break stable. See
https://github.com/ppy/osu/issues/31591#issuecomment-3575270120 for
detailed rationale.
2025-12-04 14:15:52 +01:00
Bartłomiej Dach
3e4c038a37 Do not distinguish between left/right modifiers when assigning new key combinations
Addresses https://github.com/ppy/osu/discussions/35851.

And no I'm not making it "you have to press both modifiers for it to
become any of the two" because that's ultra weird.
2025-12-04 12:09:27 +01:00
Dean Herbert
5d76353ae4 Merge pull request #35874 from bdach/fix-skinnable-welcome
Fix welcome intro text not being looked up from user skin for supporters
2025-12-04 19:36:11 +09:00
Bartłomiej Dach
0b3ec3f1e1 Fix changing beatmap during hold-to-reveal-background delay turning off blur (#35867)
Closes https://github.com/ppy/osu/issues/35864.
2025-12-04 19:23:32 +09:00
Bartłomiej Dach
043a1c2793 Disable quick retry binding in solo spectator (#35873)
Closes https://github.com/ppy/osu/issues/35870? For some definition of
"closes", I guess?

Why would you ever do this, unless on purpose just to break stuff? Don't
answer that.

A side effect of setting this flag is that the hold-to-exit menu button
that's there on devices that support touch will slightly change
behaviour to the behaviour multiplayer play has:

	e3ea38a366/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs (L67)
	8d9245c1d4/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs (L79-L82)

but upon thinking about it for three minutes I decided I don't care and
it's probably fine because all of this was already racking up to fifteen
minutes that I shouldn't have had to spend on any of this.

Notably this shouldn't affect the actual spectated user retrying,
because all of that is handled elsewhere via

	2f90bb4d67/osu.Game/Screens/Spectate/SpectatorScreen.cs (L138-L154)
2025-12-04 19:17:44 +09:00
Bartłomiej Dach
ca8247c667 Fix welcome intro skin not being looked up from user skin for supporters
Closes https://github.com/ppy/osu/issues/35833.
2025-12-04 10:43:18 +01:00
Bartłomiej Dach
a5ae542502 Merge pull request #35872 from peppy/locus-winners-part-2
Add remaining two locus winners as bundled beatmaps
2025-12-04 09:40:37 +01:00
Dean Herbert
fe5cbc4932 Add remaining two locus winners as bundled beatmaps 2025-12-04 14:20:04 +09:00
Bartłomiej Dach
0a378e5efd Merge pull request #35835 from Hiviexd/trim-timestamp-when-pasting
Trim editor timestamp when pasting into `TimeInfoContainer`
2025-12-03 14:56:52 +01:00
Solly
2a7e71d7fd Merge branch 'master' into master 2025-12-03 09:15:21 +00:00
Dean Herbert
1d221c1a7a Merge pull request #35852 from frenzibyte/support-ipad-resizing
Allow window resizing on iPadOS
2025-12-03 13:11:19 +09:00
bdb3418b67 fix useless DB versioning in release builds
Idk why, but in my first commit here I just commented out the lines
restricting DB schema suffixes to debug builds only, and before all that
mess there was a "TODO: fix".
I'm only doing this for sake of tools like BeatmapExporter and to not
clog up disk space when newer schema versions arrive.
This should work well with existing installations (hopefully)
2025-12-02 20:15:00 +03:00
Salman Alshamrani
82f4406c79 Allow resizing osu! on iPadOS 2025-11-30 05:14:02 -05:00
SollyBunny
0b4f96efc8 Make tracked leaderboard score yellow again 2025-11-29 03:23:13 +00:00
Vanni
92e9a36744 Force exit to menu on quick play disonnection (#35793) 2025-11-28 11:25:43 +09:00
Hivie
c6eba26a67 trim timestamp when pasting into TimeInfoContainer 2025-11-28 01:40:07 +01:00
Bartłomiej Dach
8f927ea7b5 Fix Beatmap.GetMostCommonBeatLength() potentially returning a beat length smaller or larger than the actual limits (#35827)
Closes https://github.com/ppy/osu/issues/35807.

The reason this closes the aforementioned issue is as follows:

Taking https://osu.ppy.sh/beatmapsets/1236180#osu/4650477 as the
example, we have:

```
minBeatLength = 342.857142857143
maxBeatLength = 419.58041958042003
mostCommonBeatLength = 342.85700000000003
```

Note that `mostCommonBeatLength < minBeatLength` here.

Taking the inverse of that to compute BPM, we get

```
minBpm = 174.99999999999991
maxBpm = 142.99999999999986
mostCommonBpm = 175.00007291669704
```

which without DT present doesn't do anything bad, but when DT is
engaged (and thus BPM is multiplied by 1.5), midpoint rounding causes
the min BPM to become 262, and the most common BPM to become 263.
2025-11-28 08:25:47 +09:00
Bartłomiej Dach
a8f058141b Fix several issues with editor timestamps for objects with fractional start times in osu!mania (#35829)
* Fix mania editor timestamp generation being culture-dependent

Mostly closes https://github.com/ppy/osu/issues/35809.

* Add failing test for notes with fractions

* Round note time when copying out timestamp & apply half-millisecond tolerance when parsing

Closes the rest of https://github.com/ppy/osu/issues/35809.

One issue here was that while the timestamp generation would allow
fractional object timestamps to be output, the parsing (via
`selection_regex`) would *reject* fractional timestamps, therefore
making lazer incompatible even with itself.

The other is that rounding is probably fine to do anyway for
interoperability with stable. I'd hope nobody actually *needs*
sub-millisecond precision but I'm ready to be proven wrong by some
aspire jokester.

* Specify invariant culture when writing out combo indices to editor timestamp in other rulesets

Pretty sure this is just a much-of-muchness because it's integers but
might as well if I'm spending time here already.
2025-11-28 08:21:13 +09:00
Chirag Mahesh
78c6973298 move cursorScale persistance into OsuCursor and use skinnableCursorScale 2025-11-27 16:39:51 +00:00
Chirag Mahesh
d8d7c80832 Persist cursorScale on skin refresh 2025-11-27 16:09:50 +00:00
Chirag Mahesh
ae33690632 Implement CursorTrail Scaling
- Add CursorScale property to CursorTrail and adjust for scaling
2025-11-27 16:09:50 +00:00
Chirag Mahesh
9e2ea63e70 Revert Changes to Trail Position Calculation
- Revert changes to CursorTrail.cs made during 79bfe7880a
2025-11-27 16:09:49 +00:00
Bartłomiej Dach
6bb25b2abe Fix gameplay leaderboard tracked player not using team colour (#35826)
* Demonstrate colour problem in test

* Fix gameplay leaderboard tracked player not using team colour

Closes https://github.com/ppy/osu/issues/35806.
2025-11-27 21:22:06 +09:00
Bartłomiej Dach
037743e002 Add context menu shortcut to watch local replays from song select (#35823)
Addresses https://github.com/ppy/osu/discussions/35811 I guess.

Will only work for local leaderboards for now but maybe good enough for
what is essentially a 5 minute job?

Can be made to work with online leaderboards too I guess if need be.
2025-11-27 21:10:46 +09:00
Bartłomiej Dach
6244617e5e Attempt to prevent main menu osu! logo being triggered by media keys (#35825)
Maybe addresses https://github.com/ppy/osu/discussions/35813. I can't
reproduce on macOS, may be a $USER_OS idiosyncrasy.
2025-11-27 20:58:47 +09:00
Bartłomiej Dach
ddfcb4d6da Merge pull request #35821 from smoogipoo/qp-adjust-pool-selector
Display quick play pool name as sub-heading in selector
2025-11-27 12:16:02 +01:00
Bartłomiej Dach
2660f4dcb0 Merge pull request #35822 from smoogipoo/remove-unnecessary-code
Remove now-unnecessary timestamp updates
2025-11-27 12:15:38 +01:00
Dan Balasescu
5a865476ce Remove now-unnecessary timestamp updates
Since #35820, this is now handled when messages are added and removed.
2025-11-27 18:42:48 +09:00
Bartłomiej Dach
2472c91924 Merge pull request #35820 from smoogipoo/fix-chat-background-alt-2
Fix chat lines flipping colours at maximum history
2025-11-27 10:37:52 +01:00
Dan Balasescu
db50019f31 Display quick play pool name as sub-heading 2025-11-27 18:25:32 +09:00
Bartłomiej Dach
1e43509e4a Fix formatting 2025-11-27 09:37:40 +01:00
Dan Balasescu
ded8aaecfd Fix chat lines flipping colours at maximum history 2025-11-27 15:42:47 +09:00
Dan Balasescu
75df8e3639 Add failing tests 2025-11-27 15:42:40 +09:00
maarvin
0d9a50e839 Quickplay: Update top level layout to match designs (#35791)
* Adjust top level matchmaking screen layout

* Adjust colours in StageDisplay

* Fix flipped animation in StageDisplay

* Adjust colours in CurrentRoundDisplay

* Fade out stage segments as they approach the left screen border

* Remove redundant `OfType<T>()` call

* Soften banner shadow

Co-authored-by: marvin <minetoblend@gmail.com>

---------

Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
2025-11-26 22:11:40 +09:00
Bartłomiej Dach
7473c62949 Merge pull request #35803 from peppy/show-self-in-online-users
Show self in online users list
2025-11-25 13:05:35 +01:00
Bartłomiej Dach
8d30e3d852 Merge pull request #35800 from peppy/fix-audio-dim-results
Fix quick retry/exit overlay volume dimming potentially sticking at results
2025-11-25 12:57:51 +01:00
Bartłomiej Dach
97fdc89fe3 Merge pull request #35804 from Hiviexd/verify/update-taiko-drain-thresholds
Update drain thresholds in osu!taiko verify check
2025-11-25 10:59:38 +01:00
Dean Herbert
f6a6c9f885 Fix failing test 2025-11-25 18:48:03 +09:00
Hiviexd
26c50b874c update osu!taiko drain thresholds
see change in 25169ccbe6
2025-11-25 09:31:41 +01:00
Bartłomiej Dach
83706b7fb6 Merge pull request #35771 from Joehuu/fix-copy-toast-not-showing
Fix some copy link actions/buttons not showing copied toast
2025-11-25 08:50:46 +01:00
Bartłomiej Dach
9e3486d4e6 Merge pull request #35757 from peppy/settings-wank
Adjust settings buttons and general section to feel better
2025-11-25 08:46:00 +01:00
Dean Herbert
545b13c3fb Show self in online users
I don't see a reason to hide self. I kinda expect to be able to see that
I'm online.
2025-11-25 16:45:52 +09:00
Dean Herbert
2c9fc32756 Assert that player suspension is final 2025-11-25 16:39:39 +09:00
Chirag Mahesh
79bfe7880a Move LocalSpacePosition calculation until the time of render
Would address #35734
2025-11-25 07:37:23 +00:00
Bartłomiej Dach
0786e619f1 Leave note about lack of toast for posterity 2025-11-25 07:52:38 +01:00
Dean Herbert
c968981697 Fix quick retry/exit overlay volume dimming potentially sticking at results
Closes #35737.
2025-11-25 15:51:47 +09:00
Bartłomiej Dach
45567f19b7 Fix test not compiling 2025-11-25 07:46:29 +01:00
Dean Herbert
f0f4e7c7a5 Update resources 2025-11-25 14:50:25 +09:00
Joseph Madamba
1d353ef637 Revert showing toast on editor timestamp clipboard 2025-11-24 11:06:01 -08:00
Dean Herbert
52af905237 Hide full installation section on non-desktop platforms 2025-11-25 01:06:29 +09:00
Dean Herbert
64668eafb9 Adjust some more visual metrics to feel better 2025-11-25 01:05:31 +09:00
Dean Herbert
b0762fc8ec Reduce abstractions of rounded button 2025-11-25 00:55:42 +09:00
Dean Herbert
d59e9572d2 Add missing padding around countdown settings button 2025-11-25 00:55:22 +09:00
Bartłomiej Dach
098da946e1 Merge pull request #35763 from stanriders/real-map-difficulty-settings
Use actual mod-adjusted map difficulty settings in the `SongBar`
2025-11-24 14:48:44 +01:00
Dean Herbert
510fc506fb Merge pull request #35786 from bdach/bypass-debounce-local-lbs
Bypass 300ms debounce when requesting local leaderboards in song select
2025-11-24 22:29:33 +09:00
Dean Herbert
da09ad9c46 Merge pull request #35785 from bdach/everybody-be-hover-fighting
Fix hover fighting when a `SettingsToolboxGroup`'s child handles hover
2025-11-24 22:29:00 +09:00
Dean Herbert
aaff7d358f Merge pull request #35787 from bdach/revert-group-expansion
Revert "Expand group that current selection resides in when moving mouse to left side of song select"
2025-11-24 21:59:13 +09:00
Bartłomiej Dach
a69b2cd803 Revert "Expand group that current selection resides in when moving mouse to left side of song select"
Reverts https://github.com/ppy/osu/pull/35184 as per
https://github.com/ppy/osu/discussions/35683#discussioncomment-15034835.
2025-11-24 13:38:53 +01:00
Bartłomiej Dach
855d5dba3c Bypass 300ms debounce when requesting local leaderboards in song select
RFC. Would probably close https://github.com/ppy/osu/issues/35773.
2025-11-24 13:20:39 +01:00
Bartłomiej Dach
9c981a52f8 Fix test failures
This is dodgy as hell but `ShortName` is completely derived from
`OnlineID` anyway so there should be no valid reason to ever attempt to
serialise it anyway.
2025-11-24 12:52:57 +01:00
Bartłomiej Dach
96de47ac4f Fix hover fighting when a SettingsToolboxGroup's child handles hover
Addresses https://github.com/ppy/osu/discussions/35772.
2025-11-24 12:46:30 +01:00
Dean Herbert
43834b55f2 Merge pull request #35784 from bdach/purge-private-channels-on-user-change
Clear chat state when local user changes
2025-11-24 20:28:29 +09:00
Arpa
8fb402665e Merge pull request #35698 from ArpaDeveloper/master
Fix editor test play autoplay / quick play toggles being usable while pause or resume overlays were showing
2025-11-24 12:08:50 +01:00
Bartłomiej Dach
e4975e8d3b Remove unnecessary cast 2025-11-24 12:00:33 +01:00
Dean Herbert
dbd9f13f2d Merge pull request #35783 from bdach/form-slider-bar-double-click-to-reset
Add double-click-nub-to-reset function to form slider bars
2025-11-24 19:54:33 +09:00
Bartłomiej Dach
ec890cd459 Clear chat state when local user changes
Closes https://github.com/ppy/osu/issues/35081.
2025-11-24 11:41:52 +01:00
Bartłomiej Dach
33c8c4d639 Add failing test 2025-11-24 11:33:24 +01:00
StanR
83ce56b718 Use APIRuleset instead of a blank RulesetInfo 2025-11-24 15:11:47 +05:00
Bartłomiej Dach
9d88c761d3 Add double-click-nub-to-reset function to form slider bars
See https://github.com/ppy/osu/pull/35742#issuecomment-3561517030.
2025-11-24 10:32:42 +01:00
Joseph Madamba
49eb013967 Fix some copy link actions/buttons not showing copied toast 2025-11-22 17:13:52 -08:00
Joseph Madamba
b6ccc8cae4 Replace local osd and clipboard method with existing game method 2025-11-22 17:13:52 -08:00
Joseph Madamba
d0e09e5b5c Fix one remaining case of "copy link" not using existing localisation 2025-11-22 17:04:40 -08:00
StanR
8900c79758 Set TournamentBeatmap's IBeatmapInfo.Ruleset to a dummy ruleset.
This is being queried by the https://github.com/ppy/osu/blob/master/osu.Game.Rulesets.Mania/ManiaRuleset.cs#L442 but since we don't actually draw column count anywhere nor are we supposed to be running converts in tournaments it should be safe to populate it with nothing.
2025-11-22 03:35:10 +05:00
StanR
fd652982ce Add ruleset tests 2025-11-22 03:29:39 +05:00
StanR
a2bfb409d2 Use actual mod-adjusted map difficulty settings in the SongBar 2025-11-22 03:16:36 +05:00
936640edeb update some configs for iOS 2025-11-21 19:04:31 +03:00
1187d03333 prepare repo for github ci/cd (mostly) 2025-11-21 18:59:41 +03:00
Dean Herbert
26da75ecfb Merge pull request #35704 from minetoblend/feature/quickplay-random-panel-design-pass 2025-11-21 23:18:11 +09:00
Dean Herbert
9f8554cc13 Merge branch 'master' into feature/quickplay-random-panel-design-pass 2025-11-21 21:51:31 +09:00
Dean Herbert
713b6453c0 Merge pull request #35758 from peppy/update-framework
Update framework
2025-11-21 21:42:53 +09:00
Bartłomiej Dach
1b3ac49f2a Merge branch 'cursor-path-smooth' into update-framework 2025-11-21 12:44:19 +01:00
Dean Herbert
d8b71423b0 Update framework 2025-11-21 19:33:38 +09:00
Dean Herbert
2c40e116e1 Merge pull request #35751 from bdach/seek-differently
Debounce continuous track seeks to at most one every 200ms
2025-11-21 17:59:57 +09:00
marvin
90e7faf271 Replace PowEasingFunction with CubicBezieEasingFunction 2025-11-21 09:57:14 +01:00
Dean Herbert
fc74726d11 Ensure the skip overlay shows when someone votes to skip 2025-11-21 17:25:40 +09:00
Bartłomiej Dach
5d3997152a Merge pull request #35755 from smoogipoo/qp-fix-expired-items
Fix quick play showing expired playlist items
2025-11-21 09:25:37 +01:00
Dean Herbert
41b56971e5 Merge pull request #35350 from Loreos7/rename-delete-button
Restore original `delete` button name
2025-11-21 17:14:37 +09:00
Dean Herbert
98e7a10e1e Rename localised string 2025-11-21 17:13:44 +09:00
Dean Herbert
e99b9984d0 Merge branch 'master' into rename-delete-button 2025-11-21 17:11:24 +09:00
Dean Herbert
38504fed22 Merge pull request #35754 from minetoblend/feature/matchmaking-bg
Quickplay: Update background image to match designs
2025-11-21 17:07:38 +09:00
Dean Herbert
13dab24d41 Adjust to 200 ms debounce
This [matches
stable](52f3f75ed7/osu!/Audio/AudioEngine.cs#L1295)
and feels somewhat better.
2025-11-21 16:56:58 +09:00
Bartłomiej Dach
67530b39cf Merge pull request #35750 from bdach/created-filter
Add `created` alias for `submitted` song select filter
2025-11-21 08:46:55 +01:00
Dean Herbert
19f5e5ba7c Merge pull request #35742 from bdach/eternal-war-against-sliders
Use new sliders-with-text-input in editor toolboxes
2025-11-21 16:46:06 +09:00
Dean Herbert
56ce955e0c Move export logs to quick actions (to sit with report issue button) 2025-11-21 16:40:38 +09:00
Dean Herbert
73349ab182 Move quick actions to top 2025-11-21 16:35:10 +09:00
Dean Herbert
a6a98fc078 Only show update settings if the game can be updated 2025-11-21 16:35:09 +09:00
Dean Herbert
a8594f1c08 Move installation settings into own subsection 2025-11-21 16:35:09 +09:00
Dan Balasescu
d3860f1630 Fix quick play showing expired playlist items 2025-11-21 16:27:48 +09:00
Dan Balasescu
15ee49348d Add failing test 2025-11-21 16:27:48 +09:00
Dean Herbert
908a950cd2 Move quick action settings into own subsection 2025-11-21 16:25:04 +09:00
Dean Herbert
df79269e6f Adjust tablet settings layout to feel a touch nicer 2025-11-21 16:24:10 +09:00
Dean Herbert
34146b8bcb Update rounded button to be less rounded
Intended to match the rest of the UI which is less rounded these days.
See inline comment for reason for not matching `FormControl` corner
radius just yet.
2025-11-21 16:23:53 +09:00
Dean Herbert
08ed2844b4 Merge pull request #35673 from bdach/report-issue-button
Add "Report an issue" button to general settings
2025-11-21 15:55:17 +09:00
Dan Balasescu
871c0ebe3d Rename to GameplayItem + adjust documentation 2025-11-21 15:34:58 +09:00
marvin
6362cdb675 Replace MatchmakingRoomState.CandidateType with MatchmakingRoomState.FinalItem 2025-11-21 15:34:41 +09:00
Dean Herbert
8e78f4dac4 Adjust button colour and don't show warning 2025-11-21 15:33:14 +09:00
Dean Herbert
fa8d303922 Update framework 2025-11-21 14:54:25 +09:00
Dean Herbert
d465bee0ab Merge pull request #31057 from rikimasan/rikimasan/rank-alternate-mod
Rank the Alternate and Single Tap mods
2025-11-21 14:19:42 +09:00
Dean Herbert
721ba8aeba Merge branch 'master' into rikimasan/rank-alternate-mod 2025-11-21 14:00:18 +09:00
Dean Herbert
1dd026c0f0 Fix everything crashing 2025-11-21 13:58:21 +09:00
Dean Herbert
a873f2be65 Merge pull request #35740 from bdach/dont-nuke-all-channels-in-tourney-client
Avoid nuking logged in user's joined channels on showing match chat in tournament client
2025-11-21 10:20:47 +09:00
marvin
edf7a126c8 Use single drawable for background 2025-11-21 01:32:11 +01:00
Bartłomiej Dach
f0f33b6df4 Adjust precisions to be less weird
In a perfect world you could specify different precisions for the slider
and the text box but let's start here and see if we get complaints
first.
2025-11-20 12:36:05 +01:00
Bartłomiej Dach
6052ed790d Debounce continuous track seeks to at most one every 500ms
See https://github.com/ppy/osu/pull/35677#issuecomment-3555903209.
2025-11-20 12:23:31 +01:00
Marvin Schürz
107c481fb9 Use new background in all matchmaking test scenes 2025-11-20 12:03:42 +01:00
Marvin Schürz
aba567d258 Add background screen 2025-11-20 12:01:27 +01:00
Bartłomiej Dach
094454499c Add created alias for submitted song select filter
Symmetrical change to https://github.com/ppy/osu-web/pull/12561 (can
probably wait until that one is reviewed to be legitimate).
2025-11-20 11:51:28 +01:00
Dean Herbert
c7e1a5770d Adjust code structure slightly to simplify logic 2025-11-20 18:22:16 +09:00
Dean Herbert
a8ac82aa1f Fix test failure due to channel not being joined 2025-11-20 18:19:30 +09:00
Bartłomiej Dach
47faf774b0 Fix tests 2025-11-20 10:12:43 +01:00
Bartłomiej Dach
be77257ddb Do not overwrite website state of 'hide online presence' toggle (#35741)
Closes https://github.com/ppy/osu/issues/35735.
2025-11-20 11:10:12 +09:00
Bartłomiej Dach
397041099e Adjust element spacing in editor toolboxes 2025-11-19 13:38:34 +01:00
Bartłomiej Dach
4b59a4657f Use new sliders-with-text-input in editor toolboxes
Addresses https://github.com/ppy/osu/discussions/35732.

And yes, I renamed "perfect curve threshold" to "bias" so that the text
can fit. Sue me.
2025-11-19 13:22:20 +01:00
marvin
02090bf6c4 Resolve candidateItem in RollAndDisplayFinalBeatmap instead of PresentRolledBeatmap 2025-11-19 13:15:53 +01:00
3bd996ee43 synchronize with github (tag 2025.1119.0-tachyon) 2025-11-19 15:13:25 +03:00
37b9f91d42 make discord rich presence work 2025-11-19 14:03:57 +03:00
Bartłomiej Dach
603c77e3e9 Avoid nuking logged in user's joined channels on showing match chat in tournament client
Closes https://github.com/ppy/osu/issues/35721.

I worry that straight up removing the nuke and not adding any channel
leave calls in exchange is going to leave tourney client users
with the *inverse* problem of being joined into a gorillion channels
from multiplayer matches they broadcasted, so this attempts to strike a
reasonable balance.
2025-11-19 11:50:02 +01:00
Bartłomiej Dach
f284864f96 Merge pull request #35691 from Kawaritai/fix/window-sizing-dropdown
Add window sizes in dropdown menu options
2025-11-19 10:55:24 +01:00
1a5a5606dc don't log that we're running an unofficial build 2025-11-19 12:02:27 +03:00
Bartłomiej Dach
fa1bf7bd96 Merge branch 'master' into fix/window-sizing-dropdown 2025-11-19 09:52:31 +01:00
Bartłomiej Dach
ef4408a73e Fix song select crashing when selecting random beatmap and changing star rating filter simultaneously (#35730)
Closes https://github.com/ppy/osu/issues/35728.
2025-11-19 16:29:55 +09:00
Kawaritai
6f7f9802bd Change windowed resolutions filtering. Add comment about borders logic. 2025-11-19 09:18:07 +11:00
87ff1051e9 set up sentry (glitchtip) logging properly 2025-11-18 23:27:57 +03:00
marvin
277f4268db Remove BeatmapSelectGrid.RevealRandomItem method 2025-11-18 19:33:59 +01:00
Bartłomiej Dach
80fbcd5fbd Move application of scaling to tablet output area to scaling container
It's the safest place for it to be there, really.
2025-11-18 14:49:01 +01:00
Bartłomiej Dach
9c2319b989 Use existing bindables instead of refetching 2025-11-18 14:40:47 +01:00
Bartłomiej Dach
a040143825 Merge branch 'master' into screen-scaling-tablet-output 2025-11-18 14:24:03 +01:00
Bartłomiej Dach
80474565fc Merge pull request #35726 from peppy/update-framework
Update framework
2025-11-18 14:19:47 +01:00
2e4b0ff197 make score cards for failed scores look better 2025-11-18 15:33:37 +03:00
499f410c94 slightly changed warnings for old windows versions 2025-11-18 14:44:34 +03:00
616c0d8ecd make key counters show proper readable key names 2025-11-18 14:24:05 +03:00
ed889138a0 break countdown now uses argon-style counter 2025-11-18 13:23:44 +03:00
Dean Herbert
89f2c7160d Update framework 2025-11-18 18:45:38 +09:00
Urantij
f0ca079fe6 Fix cursor incorrectly flashing red after a rewind in replays with Alternate mod active (#35725)
* Fix red cursor with alt mod when rewind

* Change rewind detection in input blocking
2025-11-18 09:52:37 +01:00
Bartłomiej Dach
fbd83cb048 Update framework 2025-11-18 09:50:42 +01:00
Bartłomiej Dach
843c318ec1 Merge branch 'master' into fix/window-sizing-dropdown 2025-11-18 09:50:35 +01:00
Bartłomiej Dach
19b6761697 Clarify target branch requirements in CONTRIBUTING.md
Because it appears to be a point of confusion to new contributors
(https://github.com/ppy/osu/pull/35725#issuecomment-3545734262).
2025-11-18 09:39:06 +01:00
Kawaritai
0c341c1f3e Clamp sizing 2025-11-18 14:38:34 +11:00
Kawaritai
ae5584bd88 Center window within usable bounds 2025-11-18 14:10:18 +11:00
Dean Herbert
edf08b176a Merge pull request #35718 from bdach/smoke-pooling
Add pooling support to smoke segments
2025-11-18 11:37:13 +09:00
18075bef29 display score retractions in recent activity 2025-11-17 21:28:03 +03:00
bc7780a870 updated the first players cutoff date for user profiles 2025-11-17 20:55:42 +03:00
8930b8fadb synchronize with github (tag 2025.1105.0) 2025-11-17 20:41:55 +03:00
2d457f4305 use online status of beatmap instead of mapset 2025-11-17 20:14:23 +03:00
a784734f94 fix up a couple strings 2025-11-17 20:10:54 +03:00
Bartłomiej Dach
7b952b83bf Fix test 2025-11-17 13:55:53 +01:00
Bartłomiej Dach
214122f633 Fix bad localisation reuse in pause overlay (#35717)
Closes https://github.com/ppy/osu-resources/issues/393.

Matches break overlay:

5dc44fbdf9/osu.Game/Screens/Play/Break/BreakInfo.cs (L48)
2025-11-17 20:01:27 +09:00
Bartłomiej Dach
76c0bd4750 Add pooling support to smoke segments
- Closes https://github.com/ppy/osu/issues/35703
- Supersedes / closes https://github.com/ppy/osu/pull/35711

Can test using something dumb like

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 11b3b5c71d..e21d8389ef 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -8,6 +8,7 @@
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using JetBrains.Annotations;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Bindables;
@@ -540,6 +541,10 @@ protected override void ParseConfigurationStream(Stream stream)
                 case "Menu/fountain-star":
                     componentName = "star2";
                     break;
+
+                case "cursor-smoke":
+                    Thread.Sleep(500);
+                    break;
             }

             Texture? texture = null;
2025-11-17 11:58:28 +01:00
Bartłomiej Dach
4bf3d9397f Merge pull request #35714 from smoogipoo/fix-preview-track-owners
Fix various screens not registering themselves as `IPreviewTrackOwner`
2025-11-17 08:52:43 +01:00
marvin
fe56ba2921 Turn MatchmakingCandidateType into top level declaration 2025-11-17 07:46:05 +01:00
marvin
1ca4c8860b Add slight wiggle when random card reveals beatmap 2025-11-17 07:38:04 +01:00
marvin
1e05613859 Combine random card reveal & panel roll animation into the same event 2025-11-17 07:37:56 +01:00
marvin
424ef9237f Move result animation & sample implementation into selection panels 2025-11-17 07:35:11 +01:00
marvin
e541e917a4 Change order of tests 2025-11-17 07:32:27 +01:00
marvin
7796394685 Play roll animation when revealing random beatmap 2025-11-17 07:31:46 +01:00
marvin
32900f563c Roll dice on click 2025-11-17 07:30:15 +01:00
marvin
e349a597ba Use dice icon for MatchmakingSelectPanelRandom 2025-11-17 07:29:27 +01:00
maarvin
8b778e8106 Split quickplay beatmap & "random" panel into separate classes (V2) (#35701)
* Load all beatmaps in bulk for SubScreenBeatmapSelect

* Fix tests no longer working due to drawable changes

* Remove test that no longer makes sense

* Split matchmaking panel into subclasses for each panel type

* Adjust tests to match new structure

* Add `ConfigureAwait`

* Display loading spinner while beatmaps are being fetched

* Fix test failure

* Load playlist items directly in `LoadComplete`

* Convert `MatchmakingSelectPanel` card content classes into nested classes

* Wait for panels to be loaded before operating on them

* Add ConfigureAwait()

---------

Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
2025-11-17 14:11:07 +09:00
Dan Balasescu
ce5e54c9d2 Fix various screens not registering themselves as IPreviewTrackOwner 2025-11-17 13:34:02 +09:00
ecfd4764e7 fixed a blunder 2025-11-16 22:29:25 +03:00
f5ca5083d6 implement pp for legacy song select 2025-11-16 21:21:12 +03:00
1d7c77d8d6 add performance points to selectv2 beatmap info wedge 2025-11-16 20:49:27 +03:00
774e52fbd6 slight result screen score panel redesign 2025-11-16 18:52:04 +03:00
Dean Herbert
45e8df7af2 Merge pull request #35702 from nekodex/matchmaking-random-reveal-sfx
Add SFX to the matchmaking roulette random reveal
2025-11-16 21:23:57 +09:00
Dean Herbert
1c30cb8371 Update resources 2025-11-16 20:22:21 +09:00
a9d7a9d5d5 score panel is now tinted, plus some other changes 2025-11-16 02:48:41 +03:00
f7069b1009 minor fixes for some mods 2025-11-16 02:47:38 +03:00
c3ce5dc787 add argon-style longest combo counter 2025-11-15 19:24:49 +03:00
98076e2092 add skinnable online status and star rating components 2025-11-15 19:03:14 +03:00
b7d1092f90 Merge branch 'master' of https://gitea.jvnko.boats/jvnkosu/client 2025-11-15 17:02:04 +03:00
08db90c278 some minor hud changes
- argon-style cps counter
- keybinds in key counters (for now, only default)
2025-11-15 16:45:14 +03:00
Bartłomiej Dach
bd4ed49c06 Fix several issues with incorrect sample playback (#35685)
* Add failing test coverage for layered hit samples not playing in mania when beatmap is converted

Adding the `osu.Game.Rulesets.Osu` reference to the mania test project
is required so that `HitObjectSampleTest` base logic doesn't die on

f0aeeeea96/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs (L88-L91)

* Fix layered hit sounds not playing on converted beatmaps in mania

Compare
f9e58b4864/osu!/GameplayElements/HitObjects/HitObject.cs#L476-L477.

In case of converted beatmaps, the last condition there
(`BeatmapManager.Current.PlayMode != PlayModes.OsuMania`) fails,
and thus layered hitsounds are allowed to play.

* Add failing test coverage for mania beatmap conversion assigning wrong samples to spinners

* Fix mania beatmap conversion assigning wrong samples to spinners

A spinner is never `IHasRepeats`. It was a dead condition, leading to
the hitobject generating fallback `NodeSamples`, which in particular
feature a silent tail which stable doesn't do.

Noticeably, stable also appears to force the head of the generated hold
note to have no addition sounds:

f9e58b4864/osu!/GameplayElements/HitObjects/Mania/SpinnerMania.cs#L86-L89

* Add failing test coverage for file hit sample not falling back to plain samples if file missing

* Allow `FileHitSampleInfo` to fall back to standard samples if the file is not found (or not allowed to be looked up)

I'm honestly not 100% as to how closely this matches stable because I
reached the point wherein I'd rather not look at stable code anymore, so
as long as this passes tests I'm fine to wait for someone else to report
new breakage.

* Use alternative workaround for lack of osu! ruleset assembly in mania test project

* Fix encode stability test failures
2025-11-15 16:19:08 +09:00
b7e36164c3 update issue templates 2025-11-14 22:22:43 +01:00
Kawaritai
1e91dde92e Separate bindables and centering logic for windowed resolution changes. 2025-11-15 05:43:22 +11:00
Dean Herbert
a593a40429 Merge pull request #35682 from bdach/nom-nom-tasty-exceptions 2025-11-14 19:36:09 +09:00
Jamie Taylor
02b88de76e Add SFX to the matchmaking roulette random reveal 2025-11-14 19:20:56 +09:00
Kawaritai
435cd272ea Separate fullscreen/windowed dropdowns. Center window on size change. 2025-11-14 09:48:32 +11:00
0f5f13858d exit game option for fail condition mods (SD, PF, AC) 2025-11-13 21:41:09 +03:00
Bartłomiej Dach
b64abbf1f5 Alleviate song select post-filter update thread hitches by caching a model-to-carousel-item mapping (#35628) 2025-11-13 23:21:14 +09:00
Bartłomiej Dach
4265e72180 Improve loading time of collection grouping mode (#35693)
Supersedes / closes https://github.com/ppy/osu/pull/35687.

Implements idea from
https://github.com/ppy/osu/pull/35687#issuecomment-3520613982, except
without the additional record, because there's no need for it.

Co-authored-by: WitherFlower <maxime.barniaudy@gmail.com>
2025-11-13 14:10:24 +09:00
89a0c75156 all user-playable mods are now always ranked 2025-11-12 20:53:07 +03:00
Kawaritai
72507b80c7 Add window sizes in dropdown menu options 2025-11-12 06:51:55 +11:00
ab7e5c94f1 make autoupdates work, at last 2025-11-11 19:11:36 +03:00
8dc9ea4553 add startup disclaimer 2025-11-11 18:14:38 +03:00
dcf553c252 Revert "Remove disclaimer screen completely"
This reverts commit bd0e2b4dde.
2025-11-11 16:45:04 +03:00
Bartłomiej Dach
cb9d9734d6 Move realm collection writes off of update thread (#35681)
Probably closes https://github.com/ppy/osu/issues/35650.

Realm slow, episode 23894. I can't reproduce freezes as big as the video
in the issue is showing but 'realm slow' is 99% the culprit, because
affected user's database is not small.
2025-11-11 20:29:39 +09:00
Bartłomiej Dach
5763b7dbe9 Fix skin layout deserialisation eating exceptions without logging
Because I just wasted 30 minutes trying to debug why a skin provided by
a user in an issue thread was failing to deserialise, only to realise
halfway through that the deserialisation error I was seeing was *from
the fallback path and thus a complete red herring*.
2025-11-11 10:24:30 +01:00
Dean Herbert
e1baa03622 Update framework 2025-11-11 18:00:15 +09:00
Bartłomiej Dach
4f783f8c41 Fix attempting to select beatmap which was just externally edited in song select crashing (#35676)
Closes https://github.com/ppy/osu/issues/35651.

The reproduction steps provided in the issue are too complex even. In my
testing all you need to do is go into editor, replace the background via
external editing, and exit out to song select; you'll immediately see
loss of selection on the carousel, the set panel still using the old
background, and eventually a crash when you attempt to re-select any of
the difficulties of the edited set.

`HandleItemsChanged()` - an optimisation aiming to reduce the number
of redundant re-filters due to minor changes to realm models that aren't
visible to the user anyway - ignoring changes to `BeatmapInfo.ID` after
re-entering song select post-external edit meant that song select would
retain stale beatmap models that no longer existed in the realm
database, thus failing refetch attempts via `GetWorkingBeatmap()` or

	8f6f859c15/osu.Game/Screens/SelectV2/FooterButtonOptions.cs (L56-L57)
2025-11-11 14:20:42 +09:00
Bartłomiej Dach
4c72a60ee2 Delay seeking the current track when dragging now playing overlay progress bar until commit (#35677)
RFC. Written to address
https://osu.ppy.sh/community/forums/topics/2150023.

Few other things we might want to happen here:

- pause the track when starting the drag
- figure out what to do when a drag is held while the track changes in
  the background (which was impossible to happen before this)

but I want to see the reaction to this first.
2025-11-11 14:18:40 +09:00
43f3a506ea new icon + default logo color 2025-11-11 00:04:18 +03:00
ab51579c27 added quit w/ replay button to pause menu; minor visual changes for ranks 2025-11-10 22:39:10 +03:00
d8e977c05f minor changes to mod scoring, all mods are ranked now
Probably all user-playable mods are ranked by default now,
Mania key mods were reverted to 1.0x score multiplier
2025-11-10 18:52:07 +03:00
Bartłomiej Dach
c56c528824 Add button for reporting issues to general settings
Clicking the button opens the browser, on the "new topic" page inside
the help forum. Web can now correctly read the build number of the
client since https://github.com/ppy/osu-web/pull/12478 so I see
no reason not to.

Minimal effort implementation. Stemmed from discussion in
https://discord.com/channels/90072389919997952/299846395031060480/1437368033734561792.

Not really interested in putting more effort into this at this point, if
this is not considered acceptable then just close the PR and this can be
revisited more properly at a later date.
2025-11-10 11:28:15 +01:00
复予
013de9f85d Add circular progress display to back-to-top button (#35625)
* Show circular progress on ScrollBackButton of OverlayScrollContainer

* Adjust standardization of position progress
2025-11-10 18:08:00 +09:00
Bartłomiej Dach
cd6c9405fe Fix legacy skin drum roll head circle being underneath ticks (#35647)
Closes https://github.com/ppy/osu/issues/35321.
2025-11-10 15:43:59 +09:00
Loreos7
1df640898f Use proper string key 2025-11-09 17:48:19 +03:00
Andrei Zavatski
7b55b9e4f2 Change path thickness to 1px
Looks better with the new path rendering
2025-11-09 02:07:13 +03:00
Dean Herbert
822cb9e2fb Merge pull request #35643 from diquoks/localisation/wasapi
Localise `WASAPI` setting
2025-11-09 02:16:37 +09:00
c023767df9 bump to 2025.1031.0-tachyon + misc changes for debug builds 2025-11-08 16:26:06 +03:00
Bartłomiej Dach
680614fbee Fix messages from blocked users being visible in public channels (#35645)
* Add failing test coverage for blocking users not removing their messages from public channels

* Fix messages from blocked users being visible in public channels

Closes https://github.com/ppy/osu/issues/35633.

It appears that the expectation from web here is that messages from
blocked users should be excised client-side. Compare:

12dd504255/resources/js/chat/conversation-view.tsx (L104)

This implementation won't *restore* the messages after a block and
unblock, but I kind of... don't care if I'm honest with you? Making that
happen will result in a bunch of complications for no reason, so I'm
fine waiting for anyone to complain about it.
2025-11-07 23:12:12 +09:00
Dean Herbert
cb8ddc706f Merge pull request #35435 from nekodex/matchmaking-jumpy-jump
Add SFX for 'jumping' in quick play
2025-11-07 22:23:35 +09:00
Denis Titovets
04d2ce150a Localise WASAPI setting 2025-11-07 14:46:40 +03:00
Bartłomiej Dach
eaffb89b4c Merge pull request #35638 from smoogipoo/qp-beatmap-panel-mods
Display mods in quick play beatmap cards
2025-11-07 12:34:41 +01:00
Bartłomiej Dach
650a61539b Merge branch 'master' into qp-beatmap-panel-mods 2025-11-07 11:13:54 +01:00
Bartłomiej Dach
75bc934aa5 Merge pull request #35637 from smoogipoo/qp-random-selection
Add support for selecting a "random" quick play item
2025-11-07 11:13:31 +01:00
Dan Balasescu
8d80e2bd2c Adjust guard to be based on current stage 2025-11-07 18:35:46 +09:00
Dan Balasescu
34a3b1ba78 Display mods in quick play beatmap cards 2025-11-07 17:59:02 +09:00
Dan Balasescu
b354fa4472 Implement random beatmap card 2025-11-07 15:30:07 +09:00
Dan Balasescu
1fbe1bd6c9 Fix selected item callback being lost 2025-11-07 15:30:06 +09:00
Bartłomiej Dach
3c215f6574 Fix retro skin changing when creating copy for skin editor (#35630)
RFC, lowest effort solution for https://github.com/ppy/osu/issues/34979.

The `SkinImporter` conditional *is* hella ugly, but anything less ugly
will require taking a hammer to structures. Maybe passing version via
the import flow, maybe even trying to make the `EnsureMutableSkin()`
flow somehow attempt to read the `skin.ini` that's in resources. No
idea.

Properties from `skin.ini` that were defaults or that lazer can't
(won't ever?) understand snipped.
2025-11-07 12:01:11 +09:00
Dan Balasescu
8c28d26130 Document -1 as a special "random" playlist item 2025-11-07 00:23:58 +09:00
Bartłomiej Dach
933fbd274d Fix incorrect handling of user verification failure response (#35629)
`VerificationFailureResponse.RequiredSessionVerificationMethod` not
being nullable means that if it was missing in the verification
response, it would not be `null` but default to `TimedOneTimePassword`
instead, therefore showing TOTP-related error messages to users that
never enabled it rather than the user-facing message they were supposed
to.

Most easily tested on a local full-stack environment with

```diff
diff --git a/app/Libraries/SessionVerification/MailState.php b/app/Libraries/SessionVerification/MailState.php
index 305a2794ec0..3c2d15f335b 100644
--- a/app/Libraries/SessionVerification/MailState.php
+++ b/app/Libraries/SessionVerification/MailState.php
@@ -14,7 +14,7 @@ use Carbon\CarbonImmutable;

 class MailState
 {
-    private const KEY_VALID_DURATION = 600;
+    private const KEY_VALID_DURATION = 10;

     public readonly CarbonImmutable $expiresAt;
     public readonly string $key;
```

applied so that you don't have to wait 10 minutes to trigger the
failure.
2025-11-06 23:21:26 +09:00
Giovanni D.
55ae7e8bb8 Fix timing of beatmap break overlay (#35566)
Issue was bisected to [this commit](6f1664f0a6)

This change in the commit outlined is what caused the issue:
```diff
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
  Clock = DrawableRuleset.FrameStableClock,
  ProcessCustomClock = false,
- Breaks = working.Beatmap.Breaks
+ BreakTracker = breakTracker,
},
```

`BreakTracker` always initializes breaks as `new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION);` leaving room at the end to account for the fade before resuming gameplay.

Because of this, changing the `BreakOverlay` to use a `BreakTracker` instead of the original beatmap breaks caused each break to be  `BREAK_FADE_DURATION` shorter than it was originally - which in this case is 325ms - leading to the discrepancy between the background fadeout and the overlay fadeout.

Since the current behavior is 'correct', aligning the overlay with the rest of the beatmap such as background fadeout, I changed the timing to account for the shorter duration instead of revert the overlay initialization.
2025-11-06 14:01:00 +01:00
Bartłomiej Dach
4a22ef88ce Adjust global rank colour tiers
See https://github.com/ppy/osu-web/pull/12522.
2025-11-06 13:14:25 +01:00
Bartłomiej Dach
43ca046f9b Merge branch 'master' into matchmaking-jumpy-jump 2025-11-06 13:06:16 +01:00
Bartłomiej Dach
dbefba57ce Fix pressing Enter on song select with IME active advancing to gameplay instead of confirming choice (#35619)
Closes https://github.com/ppy/osu/issues/35568.
2025-11-06 16:06:52 +09:00
Dean Herbert
20904de276 Update resources 2025-11-05 22:47:21 +09:00
Loreos7
6a6c7ad3ba Move Delete... button to CommonStrings 2025-11-05 15:56:07 +03:00
Bartłomiej Dach
fb2fe65a77 Merge pull request #35611 from stanriders/clamp-notification-avatar
Clamp notification avatar width
2025-11-05 10:42:10 +01:00
Bartłomiej Dach
4662c5d678 Merge pull request #35606 from smoogipoo/qp-history-link
Add history footer button to quick play rooms
2025-11-05 10:22:36 +01:00
Bartłomiej Dach
243cd9c073 Merge pull request #35542 from smoogipoo/mp-vote-to-skip
Implement vote-to-skip in multiplayer
2025-11-05 10:06:59 +01:00
Dan Balasescu
e8db35a5c9 Merge branch 'master' into mp-vote-to-skip 2025-11-05 16:53:44 +09:00
Dan Balasescu
d98cb9ca45 Correctly link to room history 2025-11-05 16:42:32 +09:00
StanR
a7e4aa8b12 Clamp notification avatar width 2025-11-04 21:27:07 +05:00
Bartłomiej Dach
0f54608cee Merge pull request #35575 from smoogipoo/qp-player-download-progress
Add download progress bars to quick play users
2025-11-04 14:54:03 +01:00
Bartłomiej Dach
f8331e0b28 Apply one more missed rename 2025-11-04 12:56:03 +01:00
Bartłomiej Dach
6ff2a6225d Merge branch 'master' into qp-player-download-progress 2025-11-04 12:52:01 +01:00
Bartłomiej Dach
a8020dea7c Bring back size spec in a better way 2025-11-04 12:51:53 +01:00
Dan Balasescu
88dd458394 Apply suggestions from review
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-11-04 11:37:12 +09:00
Dan Balasescu
23cb7f3b23 Add download progress bars to quick play users 2025-11-04 11:37:12 +09:00
Dan Balasescu
7da051b144 Add test 2025-11-04 11:37:07 +09:00
Dan Balasescu
78f639d760 Attempt to clean up chat size definition 2025-11-04 11:29:51 +09:00
Dan Balasescu
4ea03d0e07 Add history footer button to quick play rooms 2025-11-04 11:28:08 +09:00
Dan Balasescu
4d706b12ac Fix missing disposal 2025-11-04 11:09:02 +09:00
Dan Balasescu
c44f701abe Also update text when users leave 2025-11-04 11:09:02 +09:00
Dan Balasescu
4c81d661aa Bypass vote for auto-skip
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-11-04 11:08:57 +09:00
Dan Balasescu
f4049c7ec1 Suffix introp methods with "Intro" 2025-11-04 11:05:41 +09:00
Bartłomiej Dach
645d27bb32 Add tiered colours for global rank (#35597)
* Add new API property backing for tiered rank

* Slightly refactor `ProfileValueDisplay` for direct access to things that will need direct access

* Extract separate component for global rank display

* Add tiered colours for global rank
2025-11-04 10:47:33 +09:00
Bartłomiej Dach
73f1849365 Fix signalr connector connection failure logging eating exception stack trace (#35598)
As seen in
https://discord.com/channels/188630481301012481/1097318920991559880/1434899538123952128,
wherein precisely zero useful detail can be gleaned (and nothing is
reported to sentry either).
2025-11-04 09:46:09 +09:00
复予
89b443bccc Add GitHub link button to the wiki overlay header (#35595)
* Add Github link button to wiki overlay header

* Localize jump link string

* Mark ILinkHandler dependency as nullable

* Make the button actually look like it does on the website

* Use existing web string instead of inventing a new one

* Bind value change callback more reliably

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-11-03 13:29:46 +01:00
Bartłomiej Dach
be9170832c Merge pull request #35583 from Marvefect/maniaKeymodePP
Add PP breakdown to osu!mania profiles
2025-11-03 12:09:23 +01:00
Bartłomiej Dach
9db200ed41 Merge pull request #35585 from smoogipoo/qp-notification-join-on-click
Fix quick play notification not setting "accepted" state
2025-11-03 09:50:32 +01:00
Dan Balasescu
1ab017d4e2 Fix quick play notification not setting "accepted" state 2025-11-02 12:44:01 +09:00
Dan Balasescu
2413e98108 Fix file and class name mismatch 2025-11-02 11:58:09 +09:00
Marvefect
65fb5311ea Removed unneccesary blank space, reran dotnet format 2025-11-02 02:27:39 +03:00
Marvefect
14cdc40f0f Added Tooltip 2025-11-02 02:04:48 +03:00
Dean Herbert
9a393f912b Merge pull request #35545 from bdach/switch-active-carousel-group
Switch active carousel group if current selection no longer exists in the previous group
2025-11-01 18:42:25 +09:00
Dan Balasescu
a9ca4634fc Resolve CI inspections 2025-10-31 21:48:24 +09:00
Dan Balasescu
bdcc0ee937 Apply suggestions from review 2025-10-31 21:42:29 +09:00
Dan Balasescu
6f94b1ab6d Move property reset into GameplayStarted() 2025-10-31 21:40:40 +09:00
Dan Balasescu
b20a41c1e8 Add simple multiplayer skip overlay 2025-10-31 21:39:41 +09:00
Dan Balasescu
d0ce74063d Skip full intro length 2025-10-31 21:39:41 +09:00
Dan Balasescu
373162df02 Add support for vote-to-skip in multiplayer 2025-10-31 21:39:41 +09:00
Dean Herbert
8e0c9281d3 Merge pull request #35543 from bdach/fix-thing
Fix bad performance when moving mouse to left side of song select forcibly expands group with current selection
2025-10-31 18:35:53 +09:00
Dean Herbert
25a1a1ba37 Merge pull request #35484 from glacc/show-hud-while-editing-skin-layout
Always show HUD while editing skin layout
2025-10-31 08:48:59 +09:00
Bartłomiej Dach
73e05e3fae Switch active carousel group if current selection no longer exists in the previous group
This was primarily written to fix
https://github.com/ppy/osu/issues/35538, but also incidentally targets
some other scenarios, such as:

- When switching from artist filtering to title filtering, selection
  sometimes would stay at the group under which the selection's artist
  was filed, rather than moving to the group under which the selection's
  title is filed (in other words, the group that *the selection is
  currently under*).

- When simply assigning a beatmap to a collection such that it would
  be moved out of the current group, the selection will now follow to
  the new collection's group rather than staying at its previous
  position.

  Whether this is desired is highly likely to be extremely situational,
  but I don't want to introduce complications unless it's absolutely
  necessary.

This has a significant performance overhead because
`CheckModelEquality()` isn't free, but it doesn't seem horrible in
profiling.
2025-10-30 14:54:10 +01:00
Bartłomiej Dach
2a01e3d148 Add failing test case 2025-10-30 14:54:08 +01:00
Jamie Taylor
cf0e5edf34 Rework player jump feedback 2025-10-30 22:51:55 +09:00
Jamie Taylor
a825104688 Add test scene for player jump spamming 2025-10-30 21:34:42 +09:00
Bartłomiej Dach
ea1798d731 Fix bad performance when moving mouse to left side of song select forcibly expands group with current selection
Calling `HandleItemActivated()` rather than its intended 'parent method'
of `Activate()` meant that selection state was not correctly
invalidated:

	819da1bc38/osu.Game/Graphics/Carousel/Carousel.cs (L157)

which in turn meant that carousel item Y positions would not be
recalculated correctly after the group was expanded, which meant that
the items would become

- visible,
- stuck to the bottom of the expanded group,
- one on top of another.

Which is not something that's going to perform well.

Certified OOP moment.
2025-10-30 13:25:38 +01:00
Dan Balasescu
a435dfe93e Add interop models 2025-10-30 19:04:49 +09:00
Bartłomiej Dach
5c1171f358 Merge pull request #35537 from smoogipoo/qp-fix-view-beatmap
Fix quick play "view beatmap" showing incorrect difficulty
2025-10-30 11:04:46 +01:00
Bartłomiej Dach
3fcc626e29 Merge pull request #35511 from smoogipoo/qp-fix-empty-sequence
Fix potential sources of empty sequence errors
2025-10-30 10:49:12 +01:00
Dan Balasescu
7ff6edeb64 Fix quick play "view beatmap" showing incorrect difficulty 2025-10-30 15:27:28 +09:00
Dan Balasescu
657bc31539 Fix potential sources of empty sequence errors 2025-10-29 23:06:28 +09:00
Dan Balasescu
f9f7740acb Add failing test 2025-10-29 23:06:28 +09:00
Bartłomiej Dach
5e4dd77e64 Merge branch 'master' into show-hud-while-editing-skin-layout 2025-10-29 14:27:28 +01:00
Bartłomiej Dach
ce96c0b037 Merge extremely similar setting-enforcing flows in skin editor 2025-10-29 14:24:18 +01:00
Dean Herbert
5af9bb784b Merge pull request #35495 from Joehuu/fix-drawable-date-update
Fix `DrawableDate` not updating
2025-10-29 20:17:15 +09:00
Bartłomiej Dach
4c60df21db Fix DrawableDate not updating
Co-authored-by: Dean Herbert <pe@ppy.sh>
2025-10-29 11:51:31 +01:00
Bartłomiej Dach
3c6fb14a32 Merge pull request #35501 from peppy/more-quick-play-notification-improvements
More quick play notification improvements
2025-10-29 11:33:34 +01:00
Dan Balasescu
3afc7b045c Remove redundant default value 2025-10-29 17:27:33 +09:00
Bartłomiej Dach
2f2847f1dd Merge pull request #35498 from smoogipoo/qp-add-helpers
Add quick play helpers to add users/rounds
2025-10-29 09:13:03 +01:00
Dean Herbert
ee7c52465b Allow queue completion notification to show even during gameplay 2025-10-29 16:58:18 +09:00
Dean Herbert
beb977892e Use better iconography and colour for queue completion notification 2025-10-29 16:58:17 +09:00
Bartłomiej Dach
7203f419a2 Merge branch 'master' into qp-add-helpers 2025-10-29 08:13:51 +01:00
Dan Balasescu
722cfb72d8 Replace indexers with GetOrAdd()
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-10-29 16:07:46 +09:00
Bartłomiej Dach
5da132cc2f Merge pull request #35482 from smoogipoo/qp-fix-initial-placement-display
Ensure to never display "0th" placement
2025-10-29 07:47:35 +01:00
Dean Herbert
9fac96cf07 Merge pull request #35499 from smoogipoo/qp-fix-beatmap-nullref
Fix potential quick play crash if beatmap lookup fails
2025-10-29 15:43:31 +09:00
Jamie Taylor
fadcb9882c Merge branch 'master' into matchmaking-jumpy-jump 2025-10-29 15:34:50 +09:00
Bartłomiej Dach
0610781c6c Merge pull request #35483 from smoogipoo/qp-fix-results-no-scores-crash
Fix quick play results screen crash when no one plays
2025-10-29 07:30:59 +01:00
Dan Balasescu
e9260de56f Fix potential nullref if beatmap lookup fails 2025-10-29 15:15:36 +09:00
Dan Balasescu
2d177226fd Add failing test 2025-10-29 15:08:40 +09:00
Dan Balasescu
bd912710f1 Add quick play helpers to add users/rounds 2025-10-29 14:49:22 +09:00
Dean Herbert
4e76bd0f24 Play sound when match is available even when queueing in background (#35496) 2025-10-29 13:58:20 +09:00
Bartłomiej Dach
050c10cec2 Ensure all invocations of spectator server hub methods have their errors observed (#35488)
Fell out when attempting
https://github.com/ppy/osu-server-spectator/pull/346.

Functionally, if a true non-`HubException` is produced via an invocation
of a spectator server hub method, this doesn't really do much - the
error will still log as 'unobserved' due to the default handler, it will
still show up on sentry, etc. The only difference is that it'll get
handled via the continuation installed in `FireAndForget()` rather than
the `TaskScheduler.UnobservedTaskException` event.

The only real case where this is relevant is when the server throws
`HubException`s, which will now instead bubble up to a more
human-readable form. Which is relevant to the aforementioned PR because
that one makes any hub method potentially throw a `HubException` if the
client version is too old.

Obviously this does nothing for the existing old clients.
2025-10-29 12:18:23 +09:00
De4n
b4fd7ec10f Add a keycounter that has been actually used in Triangles skin (#35491) 2025-10-29 12:18:00 +09:00
Bartłomiej Dach
cbe7da99ad Fix screen footer overlay content being pushed to right during fade-out (#35481)
* Apply some renames & drawable names for visualiser

Optional but really helps me make heads of tails as to what anything is
here.

Like really, multiple variations of `footerContent` inside a
`ScreenFooter` class, with zero elaboration that it's really content to
do with *overlays*...

* Fix screen footer overlay content being pushed to right during fade-out

- Closes https://github.com/ppy/osu/issues/35203
- Supersedes / closes https://github.com/ppy/osu/pull/35468
2025-10-29 12:14:37 +09:00
Joseph Madamba
9a965a2546 Add failing drawable date seconds update test 2025-10-28 19:39:07 -07:00
Dan Balasescu
7b0121a430 Fix quick play results screen when no one plays 2025-10-29 11:18:25 +09:00
Dan Balasescu
627fec2e3a Add failing test case 2025-10-29 11:18:25 +09:00
Glacc
c779e142e6 Code quality fix. 2025-10-28 23:04:09 +08:00
Glacc
89fffa5a1a Code quality fix. 2025-10-28 22:54:07 +08:00
Glacc
6d597fc815 Null check for configVisibilityMode. 2025-10-28 21:51:21 +08:00
Glacc
a78b456e20 Revert value after closing editor. 2025-10-28 21:42:34 +08:00
Glacc
9237c76942 And make HUD visibility mode lease when Skin Layout Editor is visible. 2025-10-28 21:38:28 +08:00
Glacc
378c64b7f8 Only set HUD visibility mode to non-Never when skin layout editor is visible by saving and restoring HUD visibility mode setting. 2025-10-28 21:21:07 +08:00
Glacc
87b66685d6 Always show HUD while editing skin layout. 2025-10-28 19:42:47 +08:00
Dan Balasescu
c524bf5432 Make MachmakingUser.Placement nullable 2025-10-28 20:39:09 +09:00
Dan Balasescu
a40230da4b Ensure to never display "0th" placement 2025-10-28 19:35:15 +09:00
Dean Herbert
b1a421c22b Merge pull request #35470 from smoogipoo/qp-show-quit-users 2025-10-28 19:26:10 +09:00
Dean Herbert
9601708087 Fix quit text on avatar only mode, fix avatar fade 2025-10-28 18:29:05 +09:00
Dean Herbert
22f11b6fa5 Update test in line with new quit panel behaviour 2025-10-28 16:30:31 +09:00
Bartłomiej Dach
f95d0d214e Merge pull request #35474 from peppy/fix-wasapi-setting-text
Fix WASAPI settings notice text not displaying on startup
2025-10-28 08:23:03 +01:00
Dean Herbert
0205cf0fb9 Render frame buffers at a higher resolution to fix blurry for now 2025-10-28 15:43:25 +09:00
Dean Herbert
8b2b6517ca Fix regression of avatar animation 2025-10-28 15:41:40 +09:00
Dean Herbert
ce3b8bc77b Update framework 2025-10-28 15:15:35 +09:00
Dean Herbert
98829bf857 Merge pull request #35249 from dnfd1/mania-difficultychange-limits
Adjust extended OD limits for mania difficulty change mod to reflect HR and EZ values
2025-10-28 15:05:26 +09:00
Dan Balasescu
3c37ac1718 Fix clipped outline
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-10-27 23:19:28 +09:00
Dean Herbert
f8769d2e44 Fix WASAPI settings notice text not displaying on startup 2025-10-27 22:59:02 +09:00
Dean Herbert
4d66762898 Merge pull request #35471 from bdach/single-result-reselected
Fix single filtered selection not being reselected after being filtered away
2025-10-27 22:11:26 +09:00
Bartłomiej Dach
e61ae7ab8a Fix single filtered selection not being reselected after being filtered away
Closes https://github.com/ppy/osu/issues/35003.

Bit dodgy to use `CurrentSelectionItem` for this. Ideally I would use
the global `Beatmap.IsDefault`, but I kind of don't want to violate the
rule that `BeatmapCarousel` shouldn't have direct access to the global
beatmap. And this seems to work, so... maybe fine to use until it
doesn't?
2025-10-27 11:13:48 +01:00
Bartłomiej Dach
98eb29c43d Add failing test 2025-10-27 11:13:47 +01:00
Dan Balasescu
bb578d254d Mark panels as quit instead of removing 2025-10-27 19:07:09 +09:00
Dan Balasescu
b7c07ad0e5 Add support for marking panels as quit 2025-10-27 19:07:09 +09:00
Dan Balasescu
08621c4cc9 Refactor panel structure 2025-10-27 19:07:05 +09:00
Bartłomiej Dach
be9b99f975 Merge pull request #35467 from smoogipoo/qp-discord-presence
Adjust Discord rich presence for quick play
2025-10-27 09:11:20 +01:00
Dan Balasescu
765b9a20b5 Hide quick play room name in Discord rich presence 2025-10-27 12:13:15 +09:00
Dan Balasescu
473fb5720c Disable Discord invites to quick play rooms 2025-10-27 12:11:11 +09:00
Dean Herbert
5faf791ca0 Merge pull request #35445 from peppy/experimental-wasapi-user-toggle
Add settings toggle for experimental BASS initialisation mode
2025-10-25 20:44:53 +09:00
Dean Herbert
9ca47fc53a Update framework 2025-10-25 19:41:28 +09:00
Dean Herbert
79a76ce587 Update AudioDevicesSettings.cs
Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
2025-10-25 17:35:48 +09:00
Dean Herbert
72fa1553c3 Add settings toggle for experimental BASS initialisation mode 2025-10-25 13:46:49 +09:00
Andrei Zavatski
afdebcf188 Make CursorPathContainer a smooth path 2025-10-25 01:50:27 +03:00
Dean Herbert
954061b00b Merge pull request #35432 from smoogipoo/improve-qp-chat
Several improvements to quick play chat input
2025-10-25 00:50:11 +09:00
Dean Herbert
b74962af92 Merge pull request #35422 from bdach/favourites-grouping
Implement grouping by favourites
2025-10-24 23:24:48 +09:00
Dean Herbert
1af462b692 Add very simple countdown timer for quick play stages (#35433) 2025-10-24 23:07:04 +09:00
Jamie Taylor
0558f9f2d9 Add SFX for 'jumping' in quickplay 2025-10-24 22:42:28 +09:00
Dean Herbert
763739e877 Merge branch 'master' into favourites-grouping 2025-10-24 22:39:14 +09:00
Dan Balasescu
f96be84c57 Fix tests 2025-10-24 22:23:05 +09:00
Dan Balasescu
613c208362 Fix partially offscreen quick play chat context menu 2025-10-24 21:45:56 +09:00
Dan Balasescu
a3c78de710 Move context menu from channel to chat overlay 2025-10-24 21:45:56 +09:00
Dan Balasescu
e240817087 Move quick play chat entirely to screen footer 2025-10-24 21:45:56 +09:00
Dean Herbert
ddb844f81b Merge pull request #35429 from bdach/scrolling-song-select-wedge-text
Scroll song select title wedge text if it overflows
2025-10-24 21:40:03 +09:00
Dean Herbert
b6f4175939 Merge pull request #35431 from bdach/instant-scroll-carousel-post-filter
SongSelectV2: Scroll to selection instantly after a filter
2025-10-24 21:27:25 +09:00
Bartłomiej Dach
819da1bc38 SongSelectV2: Scroll to selection instantly after a filter
Closes https://github.com/ppy/osu/issues/33379.

Pretty sure this matches song select V1. The two call sites where the
old one does instant scrolls are:

	30412ba3f2/osu.Game/Screens/Select/BeatmapCarousel.cs (L672)

which happens just after a filter, and

	30412ba3f2/osu.Game/Screens/Select/BeatmapCarousel.cs (L683)

which is a bit more difficult to pin down, but generally appears to
happen on changes to the visible items, which on `SongSelectV2` triggers
a re-filter anyway.
2025-10-24 13:24:47 +02:00
Bartłomiej Dach
33e42d2809 Fix code quality 2025-10-24 12:11:38 +02:00
Bartłomiej Dach
1a49d030a0 Fix marquee container not updating scrolling state if its content changes size
This is actually possible in current usages if you e.g. toggle "use
original metadata" on/off which will change the width of the underlying
sprite texts. Or by setting window size. Pick your poison.
2025-10-24 11:08:01 +02:00
Bartłomiej Dach
03adae4417 Scroll song select title wedge text if it overflows
Instead of truncating.

Addresses https://github.com/ppy/osu/discussions/35404.

The one "tiny" problem is that the "click to search" functionality of
these texts is maybe a bit worse now, because the clickable target is
now the full width of the wedge rather than autosized to the text.
Salvaging this is *maybe* possible, but *definitely* annoying, so I'd
rather not frontload it.
2025-10-24 11:07:06 +02:00
Dean Herbert
5a38e1537b Merge pull request #35428 from bdach/star-rating-grouping-limit
Use single group for beatmaps of 15 stars and above
2025-10-24 18:00:57 +09:00
Dean Herbert
6ac8e59a11 Merge pull request #35426 from bdach/collection-grouping-order
Fix song select collection group order not matching other collection lists when certain characters are used
2025-10-24 17:53:28 +09:00
Bartłomiej Dach
a816977208 Use single group for beatmaps of above 15 stars
Addresses https://github.com/ppy/osu/discussions/35201.
2025-10-24 10:23:36 +02:00
Bartłomiej Dach
f0bfb4becc Assert expected behaviour in test 2025-10-24 10:23:36 +02:00
Dean Herbert
ec6ffb3403 Merge pull request #35420 from smoogipoo/qp-handle-beatmap-present
Disable presenting beatmaps during quick play
2025-10-24 16:59:45 +09:00
Bartłomiej Dach
6ded2e3de7 Fix song select collection group order not matching other collection lists when certain characters are used
Addresses https://github.com/ppy/osu/discussions/35391.

See inline commentary for explanation.
2025-10-24 09:51:34 +02:00
Dean Herbert
6c7eafde1a Merge pull request #35395 from bdach/culture-info-is-the-bane-of-my-existence
Allow `NumberFormattingExtensions.ToStandardFormattedString()` to accept culture
2025-10-24 16:46:37 +09:00
Bartłomiej Dach
14d0982b6c Implement grouping by favourites
- Closes https://github.com/ppy/osu/issues/34494.
- Supersedes / closes https://github.com/ppy/osu/pull/34744.
2025-10-24 08:46:34 +02:00
Bartłomiej Dach
29787360ba Change BeatmapCarouselFilterGrouping constructor params to required init properties 2025-10-24 08:42:45 +02:00
Bartłomiej Dach
6b56a0611b Refetch list of user favourites on every change to favourites
Is this lazy? Sure it is. Friends and blocks do the same thing, though,
and I'm not overthinking this any more than I already have.

Being smarter here would likely mean being more invasive with respect to
listening in on all outgoing API requests and silently updating
favourites on that basis. Which is "smart" but also complicated.
2025-10-24 08:41:52 +02:00
Bartłomiej Dach
0f1bf35bd9 Add favourite beatmap set tracking to LocalUserInfo 2025-10-24 08:40:50 +02:00
Bartłomiej Dach
b600860540 Implement request & response for fetching logged in user's favourite beatmap sets 2025-10-24 08:36:13 +02:00
Bartłomiej Dach
ae7ba034a7 Always use current culture in ToStandardFormattedString() 2025-10-24 08:26:01 +02:00
Dean Herbert
0cb521798d Merge pull request #35398 from bdach/song-select-working-beatmap-pressure
Attempt to improve performance of beatmap carousel when not grouped by sets
2025-10-24 15:22:41 +09:00
Dan Balasescu
1ee24521b8 Disable presenting beatmaps during matchmaking 2025-10-24 15:15:46 +09:00
Dean Herbert
efe7d92096 Merge branch 'master' into song-select-working-beatmap-pressure 2025-10-24 14:36:59 +09:00
Dean Herbert
4f478db056 Merge pull request #35400 from bdach/leaderboard-sometimes-incorrectly-partial
Fix solo leaderboard sometimes not showing user position while it technically could
2025-10-24 14:36:14 +09:00
Dean Herbert
646cb71b77 Merge pull request #35415 from bdach/local-user-state-refactor
Extract all pieces of local user-related state to `APIAccess` subcomponent
2025-10-24 14:32:43 +09:00
Dean Herbert
ce3e3d17fa Merge pull request #35393 from smoogipoo/fix-qp-availability
Make quick play redownload locally modified beatmaps
2025-10-24 14:28:03 +09:00
Dean Herbert
d062c99666 Merge branch 'master' into fix-qp-availability 2025-10-24 14:27:45 +09:00
Dean Herbert
803737c947 Use more legible collection initialisation 2025-10-24 14:22:10 +09:00
Dean Herbert
44a427f4e5 Merge pull request #35399 from smoogipoo/qp-view-beatmap
Fix quick play "view beatmap" not showing beatmap overlay
2025-10-24 14:21:49 +09:00
Dean Herbert
85ab0d621c Merge pull request #35397 from smoogipoo/qp-match-ended-counter
Fix round counter showing on match end
2025-10-24 14:17:09 +09:00
Dean Herbert
1591d49143 Merge pull request #35401 from bdach/custom-sample-set-model-support
Adjust gameplay sample models to support custom sample sets
2025-10-23 18:36:17 +09:00
Dean Herbert
b9de7ba196 Merge pull request #35367 from smoogipoo/qp-idle-track
Preview next song in quick play
2025-10-23 18:23:20 +09:00
Bartłomiej Dach
dfa8d4fe7c Fix blocks not being correctly cleared on logout
See, this refactor is where omissions like this that normally would pass
unnoticed stop passing unnoticed.
2025-10-23 10:54:52 +02:00
Bartłomiej Dach
5eda9a0fd7 Extract all pieces of local user-related state to APIAccess subcomponent
Something I've asked to be done for a long time. Relevant because I've
complained about this on every addition of a new piece of user-local
state: friends, blocks, and now favourite beatmaps.

It's just so messy managing all this inside `APIAccess` next to
everything else, IMO.
2025-10-23 10:53:58 +02:00
Bartłomiej Dach
ca8fc81a7e Fix solo leaderboard sometimes not showing user position while it technically could
The "partial" leaderboard logic in `SoloGameplayLeaderboardProvider`
always assumed the online fetch would request 50 scores, which is no
longer the case after https://github.com/ppy/osu/pull/33100.
2025-10-23 08:53:29 +02:00
Bartłomiej Dach
82bcbb53dd Fix taiko sample models not passing everything forward as they should 2025-10-23 08:48:39 +02:00
Dan Balasescu
9a089315b8 Don't block input on mouse down 2025-10-22 22:22:44 +09:00
Bartłomiej Dach
baea05a0a1 Adjust test to cover better 2025-10-22 14:58:13 +02:00
Bartłomiej Dach
c361e6d3c2 Adjust gameplay sample models to support custom sample sets
This is a set of model changes which is supposed to facilitate support
for custom sample sets to the beatmap editor that is on par with stable.

It is the minimal set of changes. Because of this, it can probably be
considered "ugly" or however else you want to put it - but before you
say that, I want to try and pre-empt that criticism by explaining where
the problems lie.

Problem #1: duality in sample models
---

There is currently a weird duality of what a `HitObject`'s samples will
be.

- If an object has just been placed in the editor, and not saved /
  decoded yet, it will use `HitSampleInfo`.

- If an object has already been encoded to the beatmap at least once, it
  will use `ConvertHitObjectParser.LegacyHitSampleInfo`.

As long as that state of affairs remains, `HitSampleInfo` must be able
to represent anything that `LegacyHitSampleInfo` can, if feature parity
is to be achieved.

Problem 2: The 0 & 1 sample banks
---

Custom sample banks of 2 and above are a pretty clean affair. They map to
a suffix on the sample filename, and said samples are allowed to be
looked up from the beatmap skin. `Suffix` already exists in
`HitSampleInfo`.

However, the 1 custom sample bank is evil. It uses *non-suffixed*
samples, *allows lookups from the beatmap skins*, contrary to no bank /
bank 0, which *also* uses non-suffixed samples, but *doesn't* allow them
to be looked up from the beatmap skin.

This is why `HitSampleInfo.UseBeatmapSamples` has been called to
existence - without it there is no way to represent the ability of using
or not using the beatmap skin assets.

As has been stated previously in discussions about this feature, it's
both a *mapping* and a *skinning* concern.

There are many things you could do about either of these problems, but I
am pretty sure tackling either one is going to take *many* more lines of
code than this commit does. Which is why this is the starting point of
negotiation.
2025-10-22 14:58:08 +02:00
Bartłomiej Dach
a4a2e4e639 Add test coverage of expected behaviour of sample suffix 2025-10-22 14:30:47 +02:00
Dan Balasescu
5fc8cde0f7 Fix AllowSelection blocking all input 2025-10-22 19:55:43 +09:00
Dan Balasescu
a2098b633a Fix quick play "view beatmap" not showing beatmap overlay 2025-10-22 19:29:51 +09:00
Dan Balasescu
0a3665c43d Fix round counter showing on match end 2025-10-22 19:01:02 +09:00
Bartłomiej Dach
c34b2ffc05 Fix performance overhead when computing spacing between standalone panels
The equality check that was supposed to replace the read of
`CurrentSelectionItem` showed up as a hotspot in profiling.

The selection updating code looks a little stupid after this, but all in
the name of performance...
2025-10-22 11:46:34 +02:00
Bartłomiej Dach
4ebd97b804 Slightly delay retrieval of working beatmaps in song select panels
Beatmap panels can be visible for very brief instants.
`PanelSetBackground` has a backstop to prevent expensive background
loads which is based on the position of the panel relative to centre of
screen.

However, retrieving the working beatmap that *precedes* any of that
expensive background load logic, is *also* expensive, and *always* runs
even if a panel is visible on screen for only a brief second. Therefore,
by moving some of that background load delay towards delaying retrieving
the working beatmap, we can save on doing even more work, which has
beneficial implications for performance.
2025-10-22 11:00:20 +02:00
Dean Herbert
dcb30ed5b3 Add pool names to quick play pool selector (#35394) 2025-10-22 17:54:49 +09:00
Bartłomiej Dach
a29a5ab7e6 Allow NumberFormattingExtensions.ToStandardFormattedString() to accept culture
I had previously made it invariant in
https://github.com/ppy/osu/pull/32837, and in another instance of past
me being an asshole, I can't actually find the reasoning for this at
this time.

That said, you'd be excused for thinking "why does this matter"? Well,
this will fix https://github.com/ppy/osu/issues/35381, because that
failure only occurs when the user's culture is set to one that doesn't
use a decimal point (.) but rather a decimal comma (,). This messes with
framework, which uses the *current* culture to check for decimal
separator rather than invariant:

d3226a7842/osu.Framework/Graphics/UserInterface/TextBox.cs (L106-L111)

An alternative would be to change framework instead to always accept the
invariant decimal separator.

God I hate this culture crap.
2025-10-22 10:31:12 +02:00
Dan Balasescu
62599de649 Fix intermittent footer test failures
See:
https://github.com/ppy/osu/pull/35367/checks?check_run_id=53269836615

Can be reproed via a `Thread.Sleep(1000)` in a `TestScreenOne` BDL load.
Code here is similar to `OsuGameTestScene.PushAndConfirm()`.
2025-10-22 16:40:20 +09:00
Dan Balasescu
4cac1781c5 Use interface instead 2025-10-22 16:02:46 +09:00
Dan Balasescu
fcf6d04791 Download using preferred video mode 2025-10-22 15:34:40 +09:00
Dan Balasescu
c6fcba7e1c Fix quick play not redownloading modified beatmaps 2025-10-22 15:34:29 +09:00
Dean Herbert
47b95de06d Merge pull request #35365 from bdach/bump-diffcalc-versions
Bump difficulty calculator versions
2025-10-22 15:12:56 +09:00
Dean Herbert
4e4eba2070 Merge pull request #35388 from Hoopsier/master
Accuracy to pause
2025-10-22 14:31:23 +09:00
Dean Herbert
2ec0cbe5db Allow localisation of accuracy display on pause screen 2025-10-22 13:56:38 +09:00
Hoopsy
0644543e28 Accuracy to pause and fail 2025-10-22 00:29:55 +03:00
Dan Balasescu
23933452fe Preserve last beatmap until selection is made 2025-10-21 20:00:21 +09:00
Dean Herbert
61547d6b38 Merge pull request #35377 from peppy/update-framework
Update framework
2025-10-21 18:58:42 +09:00
Dean Herbert
0b84916c3e Update framework 2025-10-21 17:24:33 +09:00
Dean Herbert
3120871298 Merge pull request #35366 from bdach/carousel-collapsing-broke
Fix beatmap set not expanding post-filter if grouping was turned off after manually collapsing active group
2025-10-20 20:22:56 +09:00
Dan Balasescu
2dbfdc3d2c Preview next song in quick play 2025-10-20 17:02:35 +09:00
Bartłomiej Dach
0af3f15c7c Merge pull request #35313 from smoogipoo/subscreen-footer
Make `ScreenFooter` support subscreens
2025-10-20 10:00:36 +02:00
Bartłomiej Dach
594b8c1a60 Fix beatmap set not expanding post-filter if grouping was turned off after manually collapsing active group
Closes https://github.com/ppy/osu/issues/35339.
2025-10-20 09:30:02 +02:00
Bartłomiej Dach
0269257287 Add failing test 2025-10-20 09:23:06 +02:00
Bartłomiej Dach
03a5aedf99 Bump difficulty calculator versions
To trigger client-side recalculations of star ratings.

Should have been done in https://github.com/ppy/osu/pull/35029.

Probably closes https://github.com/ppy/osu/issues/35357.
2025-10-20 08:46:30 +02:00
Loreos7
1ec6735a35 Restore original delete button name 2025-10-18 19:17:08 +03:00
Dean Herbert
fbaf27e3db Merge pull request #35342 from peppy/revert-framework
Revert framework bump to fix crashes for some users
2025-10-19 00:39:43 +09:00
Dean Herbert
45eb34ecde Merge pull request #35335 from peppy/fix-quick-play-user-panels
Fix quick play player panels being hard to see against bright user backgrounds
2025-10-19 00:39:37 +09:00
Dean Herbert
10beab6ad3 Revert framework bump to fix crashes for some users 2025-10-18 23:53:45 +09:00
Dean Herbert
70188d4fab Fix quick play player panels being hard to see against bright user backgrounds 2025-10-18 23:18:57 +09:00
Dean Herbert
1ce846294b Update framework 2025-10-18 19:08:07 +09:00
Dan Balasescu
4be29425a0 Use new implementation in ScreenTestScene 2025-10-17 19:01:28 +09:00
Dan Balasescu
db3cc583e6 Isolate footer behaviour to ScreenStackFooter, support subscreens 2025-10-17 19:01:28 +09:00
Bartłomiej Dach
fec7ef7624 Merge pull request #35316 from kennyaja/round_control_points
Round slider control points to integer positions (instead of truncating them)
2025-10-17 11:46:52 +02:00
Bartłomiej Dach
6e378ad5af Update relevant test with new expectations 2025-10-17 08:58:09 +02:00
Bartłomiej Dach
8f8f605748 Use MathF instead of casting 2025-10-17 08:44:58 +02:00
Bartłomiej Dach
5f9ade6610 Fix code quality 2025-10-17 08:44:30 +02:00
Bartłomiej Dach
1a569e9e2a Merge pull request #35317 from peppy/locus-bundle
Add locus 2025 winners to bundled download beatmaps list
2025-10-16 20:16:40 +02:00
Dean Herbert
98762ce09e Add locus 2025 winners to bundled download beatmaps list 2025-10-17 02:03:52 +09:00
Alban Cabannes-Michel
8f6f859c15 SSV2 : Replace "Mark as Played" with "Remove from Played" if map is already played (#35287)
* SSV2 : Replace Mark as Played with Remove from Played if already played

* Remove checks of BeatmapInfo.LastPlayed for DateTimeOffset.MinValue

* Make FooterButtonOptions use a RealmLive<BeatmapInfo> and act on review comments

* FIXUP: Detach BeatmapInfo before passing it to FooterButtonOptions.Popover

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-10-16 14:59:16 +02:00
kennyaja
cf1834b080 Also round slider control points to integer positions if it has less than 2 segments 2025-10-16 19:25:43 +07:00
Bartłomiej Dach
a2bae15db1 Fix Hold Off mod changing scroll speed in rare scenarios (#35265)
Reported (in a rather confusing manner) in
https://discord.com/channels/188630481301012481/1097318920991559880/1426084740783538268.

The relevant bit here is the following logic:

	32c60bfb36/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs (L111-L118)

which mania enables.
2025-10-16 21:09:52 +09:00
Dean Herbert
159349a64b Merge pull request #35312 from smoogipoo/fix-mp-exit
Fix screen event misusages in multplayer/matchmaking
2025-10-16 18:19:01 +09:00
Dan Balasescu
81a529200d Add footer tests involving subscreens 2025-10-16 17:38:51 +09:00
Dan Balasescu
6fa4a7152f Refactor resuming / exiting events 2025-10-16 17:05:52 +09:00
Dan Balasescu
508c37c27b Fix missing entering / suspending events 2025-10-16 17:05:52 +09:00
Dan Balasescu
95f86a5c0e Fix matchmaking calling OnExiting() twice
Once prior to the confirmation dialog being displayed, then a second
time after the exit is confirmed.
2025-10-16 17:05:46 +09:00
Dan Balasescu
cd62f66d79 Fix multiplayer calling OnExiting() twice
Once via explicit call to `OnExiting()`, and the second time via
`.Exit()`.
2025-10-16 17:02:20 +09:00
kennyaja
3fbe777f33 Round instead of truncating slider control points to integer positions 2025-10-16 14:44:44 +07:00
Dean Herbert
b8ceb91a78 Merge pull request #35236 from tadatomix/song-select/coloured-group-ranked-status
Colour Ranked Status panel to the related status
2025-10-15 16:56:45 +09:00
Dean Herbert
9f61284371 Merge pull request #35300 from bdach/show-leaderboard-in-solo-spectator
Show leaderboard in solo spectator
2025-10-14 22:55:55 +09:00
Dean Herbert
3f18a1312e Merge pull request #35263 from bdach/user-tag-not-equals-filter
Fix not-equals user, artist, and title tag filters not working
2025-10-14 22:29:09 +09:00
Bartłomiej Dach
17758d5934 Merge pull request #34845 from diquoks/localisation/binding-settings
Localise "back" button in `BindingSettings`
2025-10-14 13:00:26 +02:00
Bartłomiej Dach
eaebe99a5c Merge pull request #35273 from diquoks/localisation/play
Localise `Break` & `PlayerSettings` on `Play` screen
2025-10-14 12:57:45 +02:00
Bartłomiej Dach
1edbb1d586 Show leaderboard in solo spectator
Closes https://github.com/ppy/osu/issues/35293.

The removed comment here says that "there's no guarantee" that
`LeaderboardManager` has the correct scores:

	a060ddb543/osu.Game/Screens/Play/SpectatorPlayer.cs (L22-L25)

but at least now, that's not correct because this is ensured via

	a060ddb543/osu.Game/Screens/Play/PlayerLoader.cs (L280-L286)

through which the solo spectator player is pushed:

	a060ddb543/osu.Game/Screens/Play/SoloSpectatorScreen.cs (L239)

The empty leaderboard provider is still valid however in the case of
multiplayer spectator, because there we don't really ever want to be
showing any leaderboards on the individual player instances.
2025-10-14 11:50:55 +02:00
Dean Herbert
a060ddb543 Merge pull request #35262 from bdach/song-select-move-realm-fetches-off-thread
Move realm refetches of beatmap in song select wedges off of update thread
2025-10-13 22:57:54 +09:00
Dean Herbert
f488974d39 Improve design of quick play endgame results (#35267) 2025-10-13 22:07:21 +09:00
Bartłomiej Dach
8e01fb70c3 Fix artist/title keyword filters not working properly with not-equals operator
Closes https://github.com/ppy/osu/issues/35264.
2025-10-13 11:39:59 +02:00
Bartłomiej Dach
a3f635588c Fix exclusion filters filtering out empty strings 2025-10-13 11:39:38 +02:00
Bartłomiej Dach
f8d3285ab4 Add failing test coverage for artist text filters also not working correct 2025-10-13 11:22:29 +02:00
Bartłomiej Dach
a73c325235 Fix code quality 2025-10-13 10:19:19 +02:00
Bartłomiej Dach
97739c39e7 Attempt to ensure RealmAccess waits for the async read before disposing itself 2025-10-13 09:27:21 +02:00
Denis Titovets
b58f03bc36 Move PlayerSettingsStrings & revert "Creator" back to "Mapper" 2025-10-13 10:13:41 +03:00
Denis Titovets
d870547a18 Localise Back button on settings' sidebar 2025-10-13 10:02:13 +03:00
James Wilson
28c846b4d9 Reading bonus hotfix for Traceable mod (#35266)
* Pass slider factor to visibility bonus correctly for TC

* Decrease reading bonuses for TC
2025-10-13 13:34:57 +09:00
Dean Herbert
8e36533f65 Quick play forward design work (#35253)
* Remove unnecessary information from matchmaking beatmap panel

* Move avatar overlay inside card for better layout

* Allow higher jumping when jumping in succession

* Exclude player panel avatars from masking

* Adjust player panel animations a bit further

* Add avatar-only display mode

* Fix round warmup test not working

* Remove dead test scenes

* Fix edge case where users are added to not-yet-loaded card

* Decouple `PlayerPanel` from `UserPanel`

* Fix remaining test failure (and rename test to match new naming)
2025-10-13 13:28:09 +09:00
Denis Titovets
e183e6bb88 Make edits based on reviews 2025-10-11 11:03:53 +03:00
Denis Titovets
5dc44fbdf9 Add some localisation to Play screen 2025-10-11 11:03:40 +03:00
Bartłomiej Dach
60544bf595 Merge branch 'master' into song-select/coloured-group-ranked-status 2025-10-10 12:37:10 +02:00
Bartłomiej Dach
8f0b8153eb Adjust test
- Don't hardcode numerical enum bounds
- Exclude `Approved` as it doesn't show in real contexts
2025-10-10 12:34:51 +02:00
Bartłomiej Dach
3070a0068a Change graveyard colour yet again
Editorial decision. The "brighter" colour was still too dark to be able
to even see any semblance of shade, or the triangles in the background.
2025-10-10 12:34:28 +02:00
Bartłomiej Dach
c5e17f5f0f Actually throw error instead of weirdly using white 2025-10-10 12:31:56 +02:00
Bartłomiej Dach
8439e6ec6c Remove strange comments 2025-10-10 12:30:57 +02:00
Bartłomiej Dach
9ebc5b0a35 Merge pull request #35255 from dnfd1/rotate
Fix default origin in skin editor when rotating multiple objects
2025-10-10 12:19:53 +02:00
Bartłomiej Dach
0b00191d71 Check for cancellation EVEN MORE aggressively 2025-10-10 12:06:57 +02:00
Bartłomiej Dach
0389da4559 Attempt to abort realm accesses on disposal 2025-10-10 10:30:31 +02:00
Bartłomiej Dach
40c447a792 Fix not-equals user tag filters not working
Omission / oversight from https://github.com/ppy/osu/pull/34568.

Addresses https://github.com/ppy/osu/discussions/35260.
2025-10-10 09:56:39 +02:00
Bartłomiej Dach
4132a7cd53 Add failing test coverage for not-equals user tag filter 2025-10-10 09:55:59 +02:00
Bartłomiej Dach
ca4c033b76 Remove redundant refresh calls 2025-10-10 09:20:23 +02:00
Bartłomiej Dach
85bbe13aa9 Move realm refetches of beatmap in song select wedges off of update thread
From local testing on release build (such that online beatmaps are
accessible) with a large database it seems that maybe this'll help with
recurrent complaints of 'stutters'.

Co-authored-by: Dean Herbert <pe@ppy.sh>
2025-10-10 08:51:12 +02:00
Dean Herbert
1f2928bf2f Merge pull request #35252 from bdach/song-select-present-sometimes-fails
Fix wrong beatmap shown when presenting a beatmap from results screen
2025-10-10 14:54:50 +09:00
Dean Herbert
94c7489c1c Add some logging when FindWithRefresh triggers a slow realm refresh 2025-10-10 13:36:28 +09:00
tadatomix
3d6a3e3eda Remove unused OsuColor variable 2025-10-10 02:34:09 +03:00
tadatomix
a0f295d6fe Make Graveyard panel colour even brighter 2025-10-10 02:32:07 +03:00
dnfd1
3bb6685e75 Merge branch 'ppy:master' into rotate 2025-10-09 08:52:21 -07:00
dnfd1
e3459eccf2 convert origin of rotation to screen space for selections multiple objects in skin editor 2025-10-09 08:36:01 -07:00
dnfd1
0d68e5eeeb add inline comment to explain larger limits 2025-10-09 05:21:08 -07:00
dnfd1
faad1753a4 Round OD limits to -15 and 15 2025-10-09 05:13:17 -07:00
dnfd1
fed04eb63b Merge branch 'ppy:master' into mania-difficultychange-limits 2025-10-09 05:10:50 -07:00
Dean Herbert
52f82c7e56 Merge pull request #35240 from bdach/song-select-preserve-selection-on-update-attempt-3
Fix song select V2 not preserving selection after an update operation
2025-10-09 17:30:28 +09:00
Bartłomiej Dach
3fc3a53521 Fix weird xmldoc issue
Rider was fine with it...
2025-10-09 10:17:55 +02:00
Dean Herbert
2136d95087 Merge pull request #35251 from smoogipoo/fix-broken-tests
Fix test failures during individual runs
2025-10-09 17:09:01 +09:00
Dean Herbert
c58d13b802 Merge pull request #35250 from smoogipoo/fix-test-leaks
Fix test scene leaks through RealmRulesetStore/RealmAccess
2025-10-09 17:08:07 +09:00
Bartłomiej Dach
b477790d3e Fix wrong beatmap shown when presenting a beatmap from results screen
- Closes https://github.com/ppy/osu/issues/35023.
- Supersedes / closes https://github.com/ppy/osu/pull/35107.
2025-10-09 09:52:22 +02:00
Bartłomiej Dach
e33de9b658 Add test demonstrating failure scenario 2025-10-09 09:46:32 +02:00
Dan Balasescu
6eaf91d31a Fix test failures during individual runs 2025-10-09 16:34:55 +09:00
Bartłomiej Dach
6da6edd1d1 Fix shift-clicking not working on extra size beatmap cards
Was broken due to double assignment to `Action`.
2025-10-09 08:59:26 +02:00
Dan Balasescu
6af5158bb4 Fix undisposed Realm subscription 2025-10-09 15:56:34 +09:00
Dan Balasescu
79c367d208 Fix test scene leaks through RealmRulesetStore/RealmAccess 2025-10-09 15:28:15 +09:00
dnfd1
348713d83d Allow OD to be overrided 2025-10-08 21:27:49 -07:00
dnfd1
cab0b3451f Override OD setting to set extended limits for mania EZ and HR 2025-10-08 21:26:45 -07:00
Dean Herbert
91019dcdf7 Merge pull request #35241 from bdach/song-select-crash-on-missing-group
Fix carousel sometimes crashing when attempting to select next random set
2025-10-09 06:20:22 +09:00
De4n
0cc2e4eaa0 Change the pool of available Ranked Statuses
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-10-08 20:09:06 +03:00
Dean Herbert
422392233b Update framework 2025-10-08 23:18:35 +09:00
Bartłomiej Dach
7b1e3cd537 Fix carousel sometimes crashing when attempting to select next random set
I'm not exactly sure on the reproduction scenario here, but I have had
switching ruleset with converts disabled crash on me a few times
today. It appears to happen sometimes when after the switch the expanded
group no longer exists in the set mapping, because a filter just ran and
removed that group from set of possible groups because there'd be no
beatmaps under it.

I tried to manufacture a quick test but it's not a quick one to test
because filtering intereference is required to reproduce, I think.
I will try again on request but I mostly just want to get this fix out
ASAP before I finish up for the day.
2025-10-08 15:13:00 +02:00
Bartłomiej Dach
30412ba3f2 Fix song select V2 not preserving selection after an update operation
Because the detached store exists and has a chance to actually
semi-reliably intercept a beatmap update operation, I decided to try
this. It still uses a bit of a heuristic in that it checks for
transactions that delete and insert one beatmap each, but probably the
best effort thus far?

Notably old song select that was already doing the same thing locally to
itself got a bit broken by this, but with some tweaking that *looks* to
be more or less harmless I managed to get it unbroken. I'm not too
concerned about old song select, mind, mostly just want to keep it
*vaguely* working if I can help it.
2025-10-08 14:42:26 +02:00
Dean Herbert
4622d39f41 Merge pull request #35233 from diquoks/quick-fix/sfx-mute-on-restart
Mute SFX when holding restart beatmap bind
2025-10-08 21:39:07 +09:00
Dean Herbert
0d5c041cac Merge pull request #35234 from diquoks/quick-fix/spinner-rpm-layering
Fix `spinner-rpm` being layered above `spinner-spin` / `spinner-clear`
2025-10-08 21:21:21 +09:00
Dean Herbert
ffe8b9f4c3 Merge pull request #35239 from bdach/song-select-weirdness
Fix current beatmap set being incorrectly expanded after collapsing group with current selection
2025-10-08 21:19:08 +09:00
Bartłomiej Dach
9ba9966078 Fix current beatmap set being incorrectly expanded after collapsing group with current selection
Noticed in passing. See preceding commit for failure scenario.

Regressed in https://github.com/ppy/osu/pull/35163.
2025-10-08 13:42:43 +02:00
Bartłomiej Dach
f1ec04114f Add failing test 2025-10-08 13:42:31 +02:00
Bartłomiej Dach
4337046680 Add test assets covering correct layering of spinner SPM metre 2025-10-08 12:39:48 +02:00
Bartłomiej Dach
1a522a19f1 Disallow zero-length sliders from specifying a non-zero number of repeats (#35220) 2025-10-08 16:01:04 +09:00
tadatomix
e4c5fc1906 Make Graveyard panel brighter 2025-10-07 23:29:34 +03:00
Denis Titovets
568ddc2d2d Fix spinner-rpm layering 2025-10-07 19:01:50 +03:00
Denis Titovets
0ebc90462d Mute SFX when holding restart beatmap bind 2025-10-07 18:42:42 +03:00
Dean Herbert
a08f7327b1 Merge pull request #35029 from ppy/pp-dev
Q3 SR & PP release
2025-10-07 19:07:58 +09:00
Dean Herbert
533c61e719 Merge pull request #35216 from diquoks/quick-fix/mods-deselection-diff
Fix mods deselection difference
2025-10-07 18:38:40 +09:00
Dean Herbert
64fba9470d Invert conditional to read a bit better 2025-10-07 18:07:58 +09:00
Dean Herbert
743a94bd22 Re-remove duplicate error functions 2025-10-07 16:30:40 +09:00
Dean Herbert
bda5737135 Merge branch 'master' into pp-dev 2025-10-07 16:11:54 +09:00
tadatomix
03bbbebbbc Fix chevron being too bright in most cases 2025-10-07 01:21:31 +03:00
tadatomix
97c95de94a Add a test for the new group 2025-10-07 01:21:01 +03:00
tadatomix
3dcbcb5f64 Make initial work to create a ranked status style 2025-10-07 01:06:46 +03:00
Dean Herbert
19718c706a Merge pull request #35221 from bdach/adjust-tag-threshold
Adjust display tag threshold to match web
2025-10-06 20:57:26 +09:00
Bartłomiej Dach
4b3d2b0c57 Merge pull request #35222 from peppy/fix-left-hover-not-always
Fix hovering left area in song select not always activating reset action
2025-10-06 12:17:56 +02:00
Dean Herbert
df58dc0ca2 Fix hovering left area in song select not always activating reset action 2025-10-06 18:23:33 +09:00
Dean Herbert
d08b5a72b7 Add failing test scene covering song select left hover action not always activating 2025-10-06 18:21:10 +09:00
Bartłomiej Dach
b286f05c78 Adjust display tag threshold to match web
Follow-up to https://github.com/ppy/osu-web/pull/12381 - tags will glow
green starting from 5 votes rather than 10 to match the new threshold
web-side.
2025-10-06 11:09:54 +02:00
Dean Herbert
b3ed7717a8 Merge pull request #35185 from nekodex/matchmaking-more-sfx
Yet more matchmaking SFX work
2025-10-06 18:04:22 +09:00
Dean Herbert
bc7e02c30b Delay round increment sound to match animation better 2025-10-06 18:03:53 +09:00
Dean Herbert
557df19bf4 Merge pull request #35184 from bdach/song-select-reselect-group-for-selection-when-moving-left
Expand group that current selection resides in when moving mouse to left side of song select
2025-10-06 17:47:20 +09:00
Dean Herbert
8a450563fd Merge branch 'master' into song-select-reselect-group-for-selection-when-moving-left 2025-10-06 16:57:57 +09:00
Dean Herbert
f9a226d68e Merge pull request #35189 from bdach/catcher-fail-state-on-tiny-droplet-miss
Fix missing tiny droplets not changing catcher animation state to fail
2025-10-06 16:42:10 +09:00
Dean Herbert
dd846dcc52 Merge pull request #35176 from bdach/local-modified-beatmap-regressions
Fix a few issues regarding incorrect treatment of locally-modified beatmaps
2025-10-06 16:22:09 +09:00
Dean Herbert
71ae1d34cd Merge pull request #35178 from bdach/song-select-loses-selection-if-convert
Fix selection being changed on re-entering song select when a converted beatmap is selected
2025-10-06 15:53:42 +09:00
Bartłomiej Dach
dd8dfc04b3 Merge pull request #35199 from peppy/editor-timing-panel-current-row-visibility
Adjust colouring to make current row in timing visualisation more obvious
2025-10-06 08:39:05 +02:00
Dean Herbert
2f6bd6605f Update resources 2025-10-06 14:49:42 +09:00
Dean Herbert
e76d1ae619 Merge pull request #35188 from bdach/song-select-do-not-flash-wrong-leaderboard
Fix wrong leaderboard flashing briefly when quickly changing beatmaps
2025-10-06 14:43:33 +09:00
Dean Herbert
c0a7c97185 Merge pull request #35179 from bdach/song-select-scroll-closest-expanded-thing-into-view
Attempt to scroll carousel to nearest expanded panel when the current selection is filtered out
2025-10-06 14:42:54 +09:00
Denis Titovets
dbd48bc3a1 Fix mods deselection difference 2025-10-06 01:58:49 +03:00
Dean Herbert
a2770a8674 Adjust colouring to make current row in timing visualisation more obvious 2025-10-04 16:13:48 +09:00
Bartłomiej Dach
67041254db Fix missing tiny droplets not changing catcher animation state to fail
Addresses https://github.com/ppy/osu/discussions/35182.

The source as it is would have you believe that this is correct and
intentional but I'm not so sure about that. For one thing, I
cross-checked stable, and sure, missing tiny droplets does change the
state to fail over there. For another, the guard in master at

	5e642cbce7/osu.Game.Rulesets.Catch/UI/Catcher.cs (L250)

is very suspicious, given that it is dead in cause of tiny droplets
because of a preceding guard:

	5e642cbce7/osu.Game.Rulesets.Catch/UI/Catcher.cs (L227-L228)

Looking into blame, the tiny droplet guard originates at
e7f1f0f38b, wherein it looks to have been
aimed at handling *hyperdash* state specifically. Later, the logic has
been moved around and around like five times; at
7069cef9ce, the catcher animation logic
was added *below* the hyperdash-aimed guard without the comment being
updated in any way; 5a5c956ced moved the
logic from `CatcherArea` to `Catcher`, while simultaneously changing
the inline comment to no longer mention hyperdashing; and finally,
1d669cf65e added specific testing of tiny
droplets not changing catcher animation state, which I wager to be
back-engineered from the implementation as-it-was rather than
supported by any actual ground truth.

For additional reference:

- catcher animation logic in stable is at 67795dba3c/osu!/GameModes/Play/Rulesets/Fruits/RulesetFruits.cs#L411-L463
- hyperdash application logic in stable is at 67795dba3c/osu!/GameModes/Play/Rulesets/Fruits/RulesetFruits.cs#L165-L171
2025-10-03 14:52:33 +02:00
Bartłomiej Dach
68d5f53cc7 Adjust test to exercise actual desired behaviour 2025-10-03 14:39:48 +02:00
Bartłomiej Dach
a30c7f51d0 Fix wrong leaderboard flashing briefly when quickly changing beatmaps
Closes https://github.com/ppy/osu/issues/33743.
2025-10-03 12:35:58 +02:00
Jamie Taylor
4e90a2aee2 Code style fixes 2025-10-03 18:04:55 +09:00
Jamie Taylor
e24df0b9d0 Change 'quick play' button to use the 'daily' sample for now 2025-10-03 17:17:23 +09:00
Jamie Taylor
27ff2a7d49 Add SFX for round transitions 2025-10-03 17:16:45 +09:00
Jamie Taylor
3cb85f743a Rework StageDisplay SFX 2025-10-03 17:16:21 +09:00
Jamie Taylor
43878f6d33 Add SFX for enqueueing and also a looping 'waiting' SFX when in certain matchmaking states 2025-10-03 17:10:52 +09:00
Jamie Taylor
60dc2c8876 Add ducking effect on match found 2025-10-03 17:05:00 +09:00
Bartłomiej Dach
87128453d6 Expand group that current selection resides in when moving mouse to left side of song select
Closes https://github.com/ppy/osu/issues/33557.
2025-10-03 09:27:05 +02:00
Bartłomiej Dach
ae4e015352 Add failing test scene 2025-10-03 09:24:37 +02:00
Bartłomiej Dach
007de10e2b Attempt to scroll carousel to nearest expanded panel when the current selection is filtered out
Addresses https://github.com/ppy/osu/issues/33443, maybe.

I considered adding tests but they'd likely be janky and take a long
time to write, so decided against until there's a demand for it.
2025-10-02 14:28:39 +02:00
Bartłomiej Dach
3d5dc60cfe Fix selection being changed on re-entering song select when a converted beatmap is selected
Closes https://github.com/ppy/osu/issues/34062.

The root cause of the issue is that `OnEntering()` calls
`onArrivingAtScreen()`, which calls `ensureGlobalBeatmapValid()`, which
would call `checkBeatmapValidForSelection()` with a `FilterCriteria`
instance retrieved from the `FilterControl`.

The problem with that is at the time that this call chain is happening,
`FilterControl` is not yet loaded, which means in particular that it has
not bound itself to the config bindable, as that happens on
`LoadComplete()`:

	bff07010d1/osu.Game/Screens/SelectV2/FilterControl.cs (L198)

To resolve this, retrieve the bindable in `SongSelect` itself, which
ensures it is valid for reading at the time the above call chain
happens.
2025-10-02 13:27:43 +02:00
Bartłomiej Dach
e7076b9582 Add failing test case 2025-10-02 13:24:24 +02:00
Bartłomiej Dach
2a85f7b7c8 Do not attempt to retrieve score submission tokens for locally-modified beatmaps
Because it is 99% sure that doing so will fail and spam the user with
"this beatmap doesn't match the online version" notifications, and
because the map status is "locally modified", they should be pretty
aware of that already. This fixes the primary mode of the failure that
https://github.com/ppy/osu/pull/35173 was attempting to hack around.

This will have regressed somewhere around the time that BSS went live,
because at that point the editor stopped resetting online IDs for
beatmaps that got locally modified, making the `beatmapId <= 0` guards
no longer prevent attempts of submission.
2025-10-02 09:16:17 +02:00
Bartłomiej Dach
44a1a5ffc7 Add failing test coverage for no score submission attempt on known locally modified beatmap 2025-10-02 09:16:17 +02:00
Bartłomiej Dach
365cdfd40e Do not overwrite "locally modified" beatmap set status when performing online lookups in song select
A relatively recent regression. It's maybe not a huge one, in that it
probably doesn't matter all that much, but it is somewhat important to
keep the "locally modified" status of the set for as long as possible.

One reason for that is that keeping the "locally modified" status will
pull up a dialog when the user attempts to update the beatmap, warning
them that they will lose their local changes - this dialog will not show
if the online lookup flow is allowed to overwrite the map status with
something else.
2025-10-02 09:16:15 +02:00
Bartłomiej Dach
aad321a0b8 Fix clicking the osu! logo when in the multiplayer submenu opening solo play instead (#35175) 2025-10-02 16:03:46 +09:00
Bartłomiej Dach
1867aad1a6 Merge branch 'master' into screen-scaling-tablet-output 2025-10-02 08:14:15 +02:00
Dean Herbert
37a11b5592 Merge pull request #35154 from smoogipoo/matchmaking-events
Add structure and support for jumping in quick play rooms
2025-10-02 13:57:00 +09:00
Dean Herbert
565a2866ea Merge pull request #35169 from bdach/song-select-enter-on-filtered-out-carousel-should-start-map
Fix pressing Enter not starting current global beatmap if carousel is fully filtered out
2025-10-01 23:44:01 +09:00
Dean Herbert
c9a6a4887c Merge pull request #35167 from bdach/carousel-panels-consistent-update-button
Use consistent ordering of update button on carousel beatmap panels
2025-10-01 21:59:03 +09:00
Bartłomiej Dach
9b78187d29 Fix pressing Enter not starting current global beatmap if carousel is fully filtered out
Closes https://github.com/ppy/osu/issues/34693.
2025-10-01 14:31:48 +02:00
Bartłomiej Dach
0230a27de6 Add failing test case 2025-10-01 14:17:16 +02:00
Bartłomiej Dach
ccf3151172 Use consistent ordering of update button on carousel beatmap panels
Closes https://github.com/ppy/osu/issues/34810.

The reason why I touched it in this direction and not the other is only
because the standalone panel positioning of the button was touched last
in 92ed964627, thus I changed the set
panel to match that.
2025-10-01 13:49:54 +02:00
Bartłomiej Dach
b980183e98 Merge pull request #35126 from Joehuu/song-select-silver-terminology
Use silver S/SS terminology when grouping by rank/grade in song select
2025-10-01 13:30:50 +02:00
Jinkku
b7d36cffd4 Refactor spritesheet-based icons to be single-file based (#34976)
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Co-authored-by: Dean Herbert <pe@ppy.sh>
2025-10-01 13:30:06 +02:00
Dean Herbert
345ae29770 Merge pull request #35163 from bdach/carousel-stop-re-expanding-group
Do not forcibly re-expand carousel groups on refilters if the user manually collapsed them
2025-10-01 16:33:59 +09:00
Bartłomiej Dach
6bf589af88 Use slightly safer method of converting to string 2025-10-01 08:50:39 +02:00
Bartłomiej Dach
03455ba683 Merge branch 'master' into song-select-silver-terminology 2025-10-01 08:32:07 +02:00
Dean Herbert
1f7aa2fe5e Merge pull request #35158 from bdach/fix-carousel-teleports-on-delete
Fix song select carousel sometimes teleporting on beatmap set deletion
2025-10-01 15:16:26 +09:00
Dean Herbert
e26e44cd45 Merge pull request #35159 from bdach/carousel-target-vertical-center
Add half-height-of-selected-panel adjustment to carousel scroll target
2025-10-01 15:00:14 +09:00
Dean Herbert
5ab40e4b73 Merge pull request #35160 from bdach/carousel-not-switching-beatmap-on-ruleset-change
Fix song select not changing global beatmap correctly when switching rulesets
2025-10-01 13:47:08 +09:00
Joseph Madamba
ee638492bf Remove now unused description attributes from ScoreRank 2025-09-30 10:55:47 -07:00
Joseph Madamba
7d81ff8115 Fix jank GetRankLetter() method 2025-09-30 10:52:55 -07:00
Joseph Madamba
114e7f5c61 Rename GetRankName to GetRankLetter 2025-09-30 10:50:34 -07:00
Dean Herbert
e2b9288716 Merge pull request #35161 from bdach/song-select-refetch-online-on-reenter
Forcibly refetch online beatmap content on re-entering song select
2025-09-30 23:20:38 +09:00
Dean Herbert
919c47b918 Merge pull request #35111 from smoogipoo/quick-play-keybinds
Add keybinds to matchmaking queue screen
2025-09-30 23:19:33 +09:00
Bartłomiej Dach
bc4d5d07d7 Do not forcibly re-expand carousel groups on refilters if the user manually collapsed them
RFC. Closes https://github.com/ppy/osu/issues/35091.
2025-09-30 15:24:48 +02:00
Bartłomiej Dach
4b2f3efcbd Add tests covering desired UX 2025-09-30 15:24:35 +02:00
Bartłomiej Dach
33ddb84633 Forcibly refetch online beatmap content on re-entering song select
Closes https://github.com/ppy/osu/issues/34546.
2025-09-30 13:58:10 +02:00
Bartłomiej Dach
f66c8d10e0 Fix song select not changing global beatmap correctly when switching rulesets
Closes https://github.com/ppy/osu/issues/35113.

Regressed in dfed564bda - setting
`Carousel.CurrentSelection` was not all that
`requestRecommendedSelection()` was doing there...

A potential point of discussion is whether this global beatmap switch
should be debounced or instant. I'm not sure I have a particularly
well-formed opinion on that. One argument in favour of not debouncing is
that if you look closely at the left side of the screen while the
debounce is in progress, you can still sort of see the broken behaviour
happen - it just doesn't stay there forever.

Thankfully `ensureGlobalBeatmapValid()` being called in every scenario
on screen suspension prevented this bug from being any worse than it is
right now.
2025-09-30 13:34:26 +02:00
Bartłomiej Dach
38278eaaac Merge pull request #35157 from peppy/update-framework
Update framework
2025-09-30 13:25:39 +02:00
Bartłomiej Dach
ac2df49c35 Demonstrate failure in test 2025-09-30 13:19:03 +02:00
Bartłomiej Dach
2edd49d2c0 Add half-height-of-selected-panel adjustment to carousel scroll target
Intended to address https://github.com/ppy/osu/issues/35147, maybe?

The old carousel would target the vertical center of the active panel
when scrolling:

	b9e1b6969e/osu.Game/Screens/Select/BeatmapCarousel.cs (L948)

This was not in place in the new carousel, weirdly, which was targeting
the top-left corner of the selected panel.
2025-09-30 12:57:24 +02:00
Dean Herbert
a078d4bdb4 Merge pull request #35149 from smoogipoo/qp-fix-nullref
Fix nullref when users leave quick-play rooms
2025-09-30 19:23:05 +09:00
Dean Herbert
6e39e714e1 Refactor to simplify sample handling 2025-09-30 19:22:16 +09:00
Dean Herbert
d512adb971 Merge branch 'master' into quick-play-keybinds 2025-09-30 19:07:29 +09:00
Bartłomiej Dach
a3e09a1c31 Fix song select carousel sometimes teleporting on beatmap set deletion
Closes https://github.com/ppy/osu/issues/35010.

The issue here does not reproduce consistently, and is more or less
random in presentation. That said, using a large enough realm database
more or less ensures that the issue will present itself (in testing on a
large realm db, the failure rate is around ~50%).

This actually regressed in https://github.com/ppy/osu/pull/34842. The
core failure in this case is here:

	fd412618db/osu.Game/Screens/SelectV2/BeatmapCarousel.cs (L161)

The `CheckModelEquality()` call above is comparing two `BeatmapInfo`s,
but a84c364e44 changed the
`BeatmapInfo`-comparing path of `CheckModelEquality()` to use
`GroupedBeatmap` instead. Due to this, `CheckModelEquality()` falls back
to reference equality comparison for `BeatmapInfo`s. When that reference
comparison fails, the carousel stops detecting that the current
selection was deleted from under it correctly, and therefore the
proximity-based selection logic never runs.

Due to the human-obvious mechanism of failure and relatively easy
manual reproduction I've decided not to try and add tests for this,
as they are likely to take a long time to write due to the mechanism
of failure being incorrect use of reference equality specifically. That
said, I can try on request.
2025-09-30 11:41:47 +02:00
Dan Balasescu
e636a09e0f Add ability to jump in quick play 2025-09-30 18:38:30 +09:00
Dan Balasescu
3ebb72a20a Add avatar action request/event models 2025-09-30 18:32:04 +09:00
Dean Herbert
256483165f Update framework 2025-09-30 18:10:34 +09:00
Dean Herbert
1a084f28ff Merge pull request #35056 from OliBomby/anchor-snap
Allow snapping slider control points to nearby objects in the editor
2025-09-30 18:01:02 +09:00
Dean Herbert
a0ff7728b9 Merge pull request #35057 from OliBomby/snap-to-anchor
Allow snapping to nearby visible slider control points in the editor
2025-09-30 18:00:31 +09:00
Dean Herbert
9860bba2f3 Merge branch 'master' into qp-fix-nullref 2025-09-30 17:46:53 +09:00
Dean Herbert
bb9b4a2827 Merge pull request #35151 from bdach/maybe-fix-bss-clicking
Ensure submission progress sample is stopped when transitioning into a final state
2025-09-30 17:29:21 +09:00
Bartłomiej Dach
057406c910 Simplify logic further 2025-09-30 08:32:53 +02:00
Bartłomiej Dach
062174d874 Merge pull request #35049 from AeroKoder/fix-scaling-sliders
Fix certain sliders incorrectly registering as a horizontal/vertical only slider.
2025-09-30 15:17:05 +09:00
Dan Balasescu
d31df5bbc7 Make quick play chat not hold focus 2025-09-30 12:35:33 +09:00
AeroKoder
41698d5848 Updated moveSelectionInBounds in OsuSelectionScaleHandler to match the one in OsuSelectionHandler 2025-09-29 09:27:01 -07:00
Dean Herbert
838b8d3013 Merge pull request #35145 from tadatomix/song-select/coloured-group-ranks
Colour Rank Achieved panels to the related rank
2025-09-29 23:40:28 +09:00
Bartłomiej Dach
2c39e1e9db Ensure submission progress sample is stopped when transitioning into a final state
Probably closes https://github.com/ppy/osu/issues/35138. I'm not sure. I
only got the issue to reproduce once, on dev, using a very large
archive that was uploading really slowly, and then never again.

The working theory is that basically handling of `progressSampleChannel`
is quite dodgy and it could possibly, in circumstances unknown, be
allowed to play forevermore after transitioning to failed / canceled
state. Success state does not get this treatment because it has special
logic to set progress to 1.
2025-09-29 15:30:45 +02:00
OliBomby
18549ea7dc Remove all linq calls from getScreenSpaceControlPointNodes 2025-09-29 15:07:03 +02:00
OliBomby
d76dce76ec dont snap inherited bspline type control points to nearby objects 2025-09-29 14:44:12 +02:00
OliBomby
40cbe58220 Revert inline method for code abstraction 2025-09-29 14:23:19 +02:00
Bartłomiej Dach
68523a637a Merge pull request #35128 from peppy/matchmaking-design-work
Switch to using more standardised beatmap cards in quick play
2025-09-29 20:15:10 +09:00
Dean Herbert
3f5b71fdc3 Always explicitly assign the action for beatmap cards 2025-09-29 17:31:45 +09:00
Dan Balasescu
573d639238 Fix nullref when users leave quick-play rooms 2025-09-29 16:56:48 +09:00
Dan Balasescu
82beeec730 Add failing test 2025-09-29 16:55:54 +09:00
Bartłomiej Dach
5336bcd71d Merge pull request #34934 from Valerus9/fixtestdouble
Fix TestDouble failing on systems where the decimal separator isn't a dot
2025-09-29 16:46:32 +09:00
Bartłomiej Dach
47b6b70cae Avoid duplicating rank name colour constants 2025-09-29 09:44:05 +02:00
Bartłomiej Dach
fd412618db Adjust initial pool size for group rank displays
11 is excessive. There can ever be at most 9 of these panels, ever,
because there are at most 9 possible letter grades at this time (F, D,
C, B, A, S, S+, SS, SS+).
2025-09-29 09:42:36 +02:00
Bartłomiej Dach
9dc79e6f0d Avoid passing the same thing three times 2025-09-29 09:42:33 +02:00
Bartłomiej Dach
ba3d7392bf Merge pull request #35120 from LumpBloom7/fix-toolbox-tooltip
Fix composition tool tooltip not changing text when enabled
2025-09-29 16:04:15 +09:00
Bartłomiej Dach
bed9fb0ba6 Merge pull request #35044 from Joehuu/old-badge-alignment
Match profile badge centre alignment with web
2025-09-29 15:57:47 +09:00
Bartłomiej Dach
9257e62bac Merge branch 'master' into fixtestdouble 2025-09-29 08:43:49 +02:00
Bartłomiej Dach
70f683e7fa Use slightly nicer way of invarianting rate adjust setting value 2025-09-29 08:39:01 +02:00
Bartłomiej Dach
d1cf248b9a Remove redundant double string invariance 2025-09-29 08:33:18 +02:00
Bartłomiej Dach
295adf9f28 Add actual test coverage for relevant failure 2025-09-29 08:32:09 +02:00
tadatomix
6ac4f482ee Add a new test for Rank Achieved panels 2025-09-28 23:10:32 +03:00
tadatomix
33791318fe Call a new panel style, when Rank Achieved grouping is picked 2025-09-28 22:56:32 +03:00
tadatomix
cf20bbbf80 Add a new Rank Achieved panel to BeatmapCarousel.cs 2025-09-28 22:55:22 +03:00
tadatomix
55ef221390 Add a separate panel for RankAchieved group 2025-09-28 22:49:45 +03:00
Dean Herbert
0f8d8780d3 Adjust stage display animation to linger for longer 2025-09-26 18:26:11 +09:00
Dean Herbert
b743506207 Keep panel backgrounds loaded 2025-09-26 18:26:11 +09:00
Dean Herbert
0a17a3c4ed Use BeatmapCard for matchmaking beatmap display 2025-09-26 18:09:05 +09:00
Dean Herbert
da80b61f38 Allow visibly disabling the "go to beatmap" button
Easiest way to make this work without rewriting the layout logic.

I think it makes sense to have the button still exist there but not be
usable on certain screens.
2025-09-26 18:08:47 +09:00
Dean Herbert
badeb24d56 Change beatmap in selection panels to always be non-null 2025-09-26 16:53:22 +09:00
Dean Herbert
76c3043913 Improve selection animation border isolation
I'm not final on the design, just wanted to split it out into an actual
border element rather than an "underlay".
2025-09-26 16:48:56 +09:00
Derrick Timmermans
c66cc5ebb1 Handle disabled text in composition tool button 2025-09-26 09:28:04 +02:00
Dean Herbert
9dc5605d95 Scale active stage larger 2025-09-26 16:15:37 +09:00
Dean Herbert
3af4edf051 Remove pointless TestSceneMatchmakingScreenStack
Should always be using `TestSceneMatchmakingScreen` right?
2025-09-26 15:48:29 +09:00
Dean Herbert
e9063dcf57 Simplify flash layer 2025-09-26 15:48:29 +09:00
Dean Herbert
d690477776 Add context menu to show beatmap details 2025-09-26 14:52:23 +09:00
Dean Herbert
7879e091c0 Simplify avatar handling in beatmap selection panels 2025-09-26 14:39:20 +09:00
Dean Herbert
702511c918 More matchmaking renames and spacing adjusts 2025-09-26 14:22:55 +09:00
Dean Herbert
e1ba1b45b0 Adjust matchmaking naming, namespaces, xmldoc (#35123)
* Adjust matchmaking naming, namespaces, xmldoc

* Change partial filenames to use `.` instead of `_` separator
2025-09-26 12:17:28 +09:00
Joseph Madamba
8ea9e2e4bb Change non-localisable sh/xh to correct terminology 2025-09-25 15:45:12 -07:00
Joseph Madamba
ef88a3530a Use silver S/SS terminology when grouping by rank/grade in song select 2025-09-25 14:18:41 -07:00
Derrick Timmermans
df795da070 Fix composition tool tooltip not changing text when enabled 2025-09-25 10:44:05 +02:00
Dean Herbert
12e29f0bcc Attempt to fix flaky TestGameplaySettingsDoesNotExpandWhenSkinOverlayPresent
See https://github.com/ppy/osu/pull/35118/checks?check_run_id=51202910556.
2025-09-25 15:56:48 +09:00
Dean Herbert
ff54908687 Matchmaking stage display / screen layout design improvements (#35118) 2025-09-25 15:16:22 +09:00
Dan Balasescu
3c37fb11be Add sounds 2025-09-24 19:47:13 +09:00
Dan Balasescu
3176006510 Add select keybind to queue screen buttons 2025-09-24 19:46:54 +09:00
Dan Balasescu
183ccbf792 Add left/right keybinds to pool selector 2025-09-24 19:46:54 +09:00
Dean Herbert
f1ffc1dc33 Merge pull request #35109 from smoogipoo/matchmaking-fix-chat
Fix matchmaking chat not working
2025-09-24 18:59:24 +09:00
Dan Balasescu
b3cfded8f2 Fix matchmaking chat not working 2025-09-24 18:58:01 +09:00
Dean Herbert
67291c1a42 Fix match-found not playing due to incorrect case in path 2025-09-24 18:55:02 +09:00
Dean Herbert
93afc83c4b Merge pull request #35105 from smoogipoo/improve-mm-testing
Various improvements to matchmaking testability
2025-09-24 16:26:43 +09:00
Andrei Zavatski
57e5fe7265 Improve FailRetryDisplay performance (#35101) 2025-09-24 15:53:16 +09:00
Dean Herbert
e082d24bab Merge pull request #35106 from smoogipoo/matchmaking-fix-player-list
Fix players positioning on next matchmaking round
2025-09-24 15:38:17 +09:00
Dan Balasescu
13cb5deca3 Fix players positioning on next matchmaking round 2025-09-24 14:44:27 +09:00
Dan Balasescu
3556d6c8c8 Reduce number of picks shown 2025-09-24 14:28:04 +09:00
Dan Balasescu
83dafb4ef9 Fix incorrect default room types 2025-09-24 14:28:04 +09:00
Dan Balasescu
9df3bd9a98 Add stage-changing helper, use in test scenes 2025-09-24 14:28:01 +09:00
Dan Balasescu
597a06ac38 Set initial matchmaking room state 2025-09-24 13:42:55 +09:00
Dean Herbert
a129345a82 Merge pull request #35098 from smoogipoo/fix-intermittent-test
Attempt to fix intermittent tests
2025-09-24 03:00:31 +09:00
Dean Herbert
8660578d2c Merge pull request #35053 from smoogipoo/matchmaking-player-list-modes
Add display styles to matchmaking player list
2025-09-24 02:59:57 +09:00
Dean Herbert
bf63b6f9f0 Simplify lookup method to appease inspection
See https://github.com/ppy/osu/pull/35100/files.
2025-09-23 22:55:43 +09:00
Dean Herbert
85ea278e1d Merge pull request #35100 from smoogipoo/matchmaking-match-start-sample
Play gameplay start sample in matchmaking
2025-09-23 22:54:31 +09:00
Dan Balasescu
37e661b27e Fix intermittent collection dropdown tests 2025-09-23 18:23:41 +09:00
Dan Balasescu
ff6c6083b4 Mark spinner rewind test as flaky 2025-09-23 18:18:07 +09:00
Dan Balasescu
aba0d2c1d3 Play gameplay start sample in matchmaking 2025-09-23 17:52:33 +09:00
Dan Balasescu
3789010dd5 Attempt to fix intermittent test 2025-09-23 15:09:45 +09:00
Dan Balasescu
82ac42cae3 Replace nested ternaries with ifs 2025-09-23 13:45:10 +09:00
Dean Herbert
87061959ea Fix failing screen test 2025-09-23 12:49:34 +09:00
Dean Herbert
1840363713 Add one more temoprary workaround for rider failings 2025-09-21 13:09:27 +09:00
qinvvv
b91ff8a5c5 Fix osu!mania legacy skin WidthForNoteHeightScale not being used (#35050)
* Add osu!mania legacy skin widthForNoteHeightScale

* Ensure WidthForNoteHeightScale correctly defaults to MinimumColumnWidth
2025-09-19 10:53:25 +09:00
Olivier Schipper
9fddce92e9 Reword inline comments 2025-09-19 01:22:35 +02:00
Olivier Schipper
7c5278ea45 Fix incorrect skip count 2025-09-19 01:09:42 +02:00
Olivier Schipper
0e523d3eb7 Allow snapping to visible slider control points 2025-09-19 00:30:31 +02:00
Olivier Schipper
55a4c75e76 Allow slider control points to snap to nearby objects
and a bit of code cleanup to reduce code duplication with the slider head anchor snapping
2025-09-18 21:26:49 +02:00
Dean Herbert
c07dd72f6d Merge pull request #35037 from bdach/totp
Add client-side support for TOTP authentication
2025-09-18 23:50:07 +09:00
Dean Herbert
c320c9df1c Merge branch 'master' into totp 2025-09-18 22:53:20 +09:00
Dan Balasescu
c08d88eb7f Adjust namespaces 2025-09-18 16:28:25 +09:00
Dan Balasescu
5eaf376a60 Decrease scale of panels 2025-09-18 16:28:25 +09:00
Dan Balasescu
b26a1b6330 Add display style to PlayerPanelList 2025-09-18 16:28:25 +09:00
AeroKoder
6dc3432735 Fix certain slider shapes incorrectly registering as a horizontal/vertical only slider. 2025-09-17 15:34:18 -07:00
Joseph Madamba
e0c86b3048 Match profile badge centre alignment with web 2025-09-16 20:37:44 -07:00
Givy120
7852df639a Use DeltaTime in RhythmEvaluator to increase stability (#32790)
* Update RhythmEvaluator.cs

* Rename `StrainTime` into `AdjustedDeltaTime`

---------

Co-authored-by: StanR <hi@stanr.info>
2025-09-16 13:31:14 +00:00
Bartłomiej Dach
37f58e5c80 Add client-side support for TOTP authentication
Closes https://github.com/ppy/osu/issues/34972.
2025-09-16 10:36:34 +09:00
Dean Herbert
0dbee70ca9 Merge pull request #35022 from smoogipoo/matchmaking-fix-errors
Fix errors in gameplay stage of matchmaking
2025-09-15 22:25:58 +09:00
James Wilson
2749184c38 Remove databasing of MechanicalDifficulty and ReadingDifficulty attributes (#35028)
* Remove databasing of `MechanicalDifficulty` and `ReadingDifficulty` attributes

* Update attribute IDs
2025-09-15 13:46:00 +03:00
James Wilson
0a3844d3ef Update tests (#35026) 2025-09-15 12:46:27 +03:00
Eloise
c7f50f35b7 osu!taiko final balancing before deploy (#34962)
* Change maximum UR estimation + buff rhythm

* Penalty for classic ezhd

* Buff mono bonus to counterbalance logic fix

* New miss penalty + slightly nerf length bonus

* Adjust rhythm values

* Adjust penalty and buff high SR acc

* Exclude HDFL from hidden reading penalties

* Make comment a lil nicer

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-09-15 09:43:26 +01:00
Du Yijie
b1bc5cae87 Merge branch 'master' into legacy-pp-counter 2025-09-15 14:41:45 +08:00
Dan Balasescu
9577472c9e Fix errors in gameplay stage of matchmaking 2025-09-15 12:49:42 +09:00
Dean Herbert
55fe927af0 Merge pull request #34985 from nekodex/matchmaking-sfx
More matchmaking SFX work
2025-09-14 14:38:10 +09:00
Dean Herbert
4ccfebe842 Update resources 2025-09-14 14:15:16 +09:00
Dean Herbert
b7d6e76d19 Merge pull request #35000 from bdach/lazer-judgement-counter-misalign 2025-09-13 22:29:58 +09:00
Valerus9
e34b0659da Fix testtooltip failure 2025-09-13 06:44:29 +02:00
Bartłomiej Dach
e73e9275ba Fix argon judgement counter looking misaligned with wireframe off
Closes https://github.com/ppy/osu/issues/34959.

`ArgonCounterTextComponent` is pretty terrible and prevents being able
to fix the issue easily. The core issue is that this is the first
instance of the component's usage where the label text can be longer
than the counter in the X dimension, so the total width of any counter
is equal to max(label width, counter width), and the label will be
aligned to the left of that width, while the counter will be aligned to
the right of that width.

The fix sort of relies on the fact that I don't expect *any* consumer of
`ArgonCounterTextComponent` that meaningfully uses the wireframe digits
to want the non-wireframe digits to be aligned to the *left* rather than
the right. It's not what I'd expect any segmented display to work.
(There are usages that specify `TopLeft` anchor, but they usually
display the same number of wireframe and non-wireframe digits, so for
them it doesn't really matter if the digits are left-aligned to the
wireframes or not.)
2025-09-13 11:11:45 +09:00
Bartłomiej Dach
a9c021ce04 Demonstrate failure in test 2025-09-13 10:50:33 +09:00
Jamie Taylor
9a2513230c Add SFX for stage progression feedback 2025-09-12 18:27:16 +09:00
Jamie Taylor
ccc5ca5d80 Rework matchmaking cloud SFX 2025-09-12 18:25:08 +09:00
Jamie Taylor
9b9e7a8f75 Refactor selection roulette SFX logic 2025-09-12 18:23:02 +09:00
Dean Herbert
5b781655c1 Merge pull request #34815 from smoogipoo/matchmaking
Add matchmaking
2025-09-11 18:17:10 +09:00
Dean Herbert
644b797734 Add temporary workaround for rider bug (attempt 2) 2025-09-11 16:21:55 +09:00
Dean Herbert
e5fbf62ff5 Revert "Add temporary workaround for rider bug"
This reverts commit bcff6be5f6.
2025-09-11 16:21:40 +09:00
Dean Herbert
bcff6be5f6 Add temporary workaround for rider bug 2025-09-11 16:21:03 +09:00
Dean Herbert
0c68a91b4c Fix main menu key tests 2025-09-11 16:09:17 +09:00
Dean Herbert
69a0ac6c76 For tachyon release 2025-09-11 16:00:41 +09:00
Dean Herbert
619a6e7321 Merge branch 'master' into matchmaking 2025-09-11 15:11:01 +09:00
Dan Balasescu
a436372b05 Allow exit during matchmaking intro (#137) 2025-09-09 18:09:43 +09:00
Dan Balasescu
449038d070 Split main menu buttons into multiplayer section (#136)
Co-authored-by: Dean Herbert <pe@ppy.sh>
2025-09-08 13:54:06 +09:00
Dan Balasescu
ffab8342f2 Merge branch 'master' into pp-dev 2025-09-07 19:22:08 +09:00
Dan Balasescu
31188127ef Transmit availability with ready state
Regressed with https://github.com/ppy/osu-server-spectator/pull/311.

As it turns out, the method not just resets ready states but also the
beatmap availabilities. So we have to send it again here.

I have a feeling this is also broken in standard multiplayer in some way
or another.
2025-09-07 16:17:28 +09:00
Dan Balasescu
48bad31255 Pessimistically set the beatmap in all stages (#135) 2025-09-07 16:17:28 +09:00
Dan Balasescu
1a49c81bab Fix matchmaking being permanently sound-ducked (#134) 2025-09-07 16:17:28 +09:00
Dan Balasescu
27db49bad3 Fix results screen crash from missing users (#133) 2025-09-07 16:17:28 +09:00
Jamie Taylor
e6dbb1020c Add some audio feedback to the matchmaking flow (#132) 2025-09-07 16:17:28 +09:00
Dan Balasescu
e0c11504a2 Query for available pools for selection (#131) 2025-09-07 16:17:27 +09:00
Dan Balasescu
35e1fa6660 Fix test failure 2025-09-07 16:17:27 +09:00
Dan Balasescu
330b61f4c0 Add ruleset selector (#130) 2025-09-07 16:17:27 +09:00
Dan Balasescu
3985596602 Join matchmaking rooms with password 2025-09-07 16:17:27 +09:00
Dan Balasescu
3786efaa5e Separate IMatchmakingClient and IMultiplayerClient
Co-authored-by: Dean Herbert <pe@ppy.sh>
2025-09-07 16:17:27 +09:00
Dan Balasescu
0225c1a867 Fix event handler leak 2025-09-07 16:17:27 +09:00
Dan Balasescu
111b98ef8e Add matchmaking 2025-09-07 16:17:27 +09:00
Valerus9
22bfab95b0 Fix testdouble failure. 2025-09-06 06:43:13 +02:00
James Wilson
a78c78ecdd Update difficulty calculation tests for osu ruleset (#34828) 2025-09-02 13:19:34 +03:00
StanR
84309f57c5 Reduce rhythm difficulty if current object is doubletappable (#34877)
* Reduce rhythm difficulty if current object is doubletappable

* Buff rhythm multiplier
2025-09-02 10:22:12 +01:00
James Wilson
6a35b7237b Prevent Taiko difficulty crash if a map only contains 0-strains (#34829)
* Prevent Taiko difficulty crash if a map only contains 0-strains

* Add second check for safety

This is accessing a different array of strains. I'd rather be safe than sorry.

* Add guard in PP too

* Make `MarginOfError` a const
2025-08-31 12:04:56 +00:00
James Wilson
90ac249f5e Move SpunOut penalty back to PP (#34838)
This isn't a super common mod compared to every other one on the list, it's probably not worth the storage (and memory in case of stable) implications. We can look at revisiting this once we have actual spinner difficulty considerations
2025-08-31 12:32:28 +05:00
Jay Lawton
087f0565e6 Implement deltatimenormaliser into rhythm grouping logic (#33403)
* additions

* review fixes

* Formatting

* comments + review

* fix

* fix renaming and namespace

* balancing + round

---------

Co-authored-by: tsunyoku <tsunyoku@gmail.com>
Co-authored-by: StanR <hi@stanr.info>
2025-08-28 13:19:13 +00:00
Daniel Power
038bf3fdda "Conform to aspect ratio" uses scaled area 2025-08-27 20:11:28 -02:30
Daniel Power
61c3aad537 Fix conflict 2025-08-27 20:00:56 -02:30
Daniel Power
3aad0868af Remove duplicated declarations 2025-08-27 19:57:58 -02:30
Daniel Power
973c4c8319 Merge branch 'master' of github.com:ppy/osu into screen-scaling-tablet-output 2025-08-27 19:46:18 -02:30
Du Yijie
ac21f8b960 Implement "legacy" pp counter
There is no pp counter in osu!(stable). However, a "legacy" pp counter
allows skinners to more easily fit a pp counter into their skin's theme.
2025-08-27 11:07:53 +08:00
e78c8fa03d (NOT STABLE!!) Added custom mode using music from MainMenu 2025-08-23 22:54:22 +03:00
Daniel Power
0b3b6468a5 Reflect tablet output area changes in osu-framework 2025-08-16 23:02:28 -02:30
Daniel Power
7d1c54f045 Merge branch 'master' of github.com:ppy/osu into screen-scaling-tablet-output 2025-08-16 22:53:41 -02:30
Givikap120
dce4132209 Nerf Low AR HD bonus for slideraim (#34215)
* Refactor slider factor calculation

* Nerf low AR HD bonus for slideraim

* finish merge

* Fixes

* Fix comment

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-08-09 19:40:37 +00:00
James Wilson
fa1fea02dc Fix edge case that estimates sliderbreaks in impossible scenarios (#34544)
* Test theory crafting

* Place in more appropriate place

* fix a bit better

* Move things around

* Reduce diff

---------

Co-authored-by: StanR <hi@stanr.info>
2025-08-07 19:13:00 +01:00
StanR
802e559472 Add DF flashlight rating reduction (#34081)
* Add DF flashlight rating reduction

* Use reverse lerp
2025-08-06 17:10:00 +01:00
Eloise
dbb16fc834 osu!taiko reduce multiplier for hidden on lazer (#34089)
* Reduce multiplier for hidden on lazer

* Refactor

* Quality

* The space
2025-07-30 21:48:45 +02:00
Eloise
eaaca60b1d osu!taiko new acc pp formula + rhythm difficulty penalty (#34188)
* New acc curve

* Penalise rhythm difficulty based on unstable rate

* Rename mono acc stuff for more clarity

* Fix nullable

* Rename stuff

* Get actual estimation for SS unstable rate

* Double space my bad

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-07-29 18:03:13 +00:00
Eloise
803e30f50f osu!taiko consistency factor changes using object strains (#34327)
* Calculate consistency factor from object strains

* Use `totalDifficultHits` in performance calc

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-07-28 13:58:54 +00:00
Jay Lawton
e54779ceee Fix colour penalties being bypassed via repeated ratio variance (#33641)
* fix a lil bit of colour

* review comments

* fix empty initialiser
2025-07-27 14:31:46 +00:00
StanR
83765abe34 Make visibility-based bonuses be additive to ratingMultiplier instead of multiplicative (#34367)
* Make visibility-based bonuses be additive to `ratingMultiplier` instead of multiplicative

* Slightly buff low AR HD, slightly nerf low AR TC
2025-07-25 19:51:30 +00:00
James Wilson
28d36dd3bd Move rating calculations to OsuRatingCalculator (#33265)
* Move rating calculations to `OsuRatingCalculator`

* Use `CalculateDifficultyRating`
2025-07-25 18:47:21 +03:00
James Wilson
945db7b431 Fix backwards logic on visibility bonus (#34369)
Co-authored-by: StanR <hi@stanr.info>
2025-07-25 06:50:23 +00:00
Givikap120
56b072cfd9 remove high CS bonus from slider bonus (#34214)
Co-authored-by: StanR <hi@stanr.info>
2025-07-24 15:41:36 +00:00
Jay Lawton
ddf9d6b8c8 ensure monolengthbonus applies to new strain contribution only (#33635)
* stamina fix

* review changes

* fix naming

---------

Co-authored-by: StanR <hi@stanr.info>
2025-07-22 09:37:51 +00:00
Givikap120
a75e0c3850 Refactor AR and OD calculations in osu! pp calculation (#34065)
* Add AR and OD calculation functions

* use created functions in perfcalc
2025-07-08 18:37:41 +03:00
Dan Balasescu
f2dcf2024a Merge branch 'master' into pp-dev 2025-07-08 20:58:22 +09:00
Dean Herbert
7ec6c8bf57 Merge branch 'master' into pp-dev 2025-07-07 17:56:44 +09:00
Natelytle
cf4d6bea72 Implement difficulty evaluators in the osu! mania ruleset (#33411)
* stuff

* Implement evaluators

* Typo

* Fixes

* clarifying comment

* Fix CalculateInitialStrain

* Remove debug line

* Small code quality fix

* Address comments, slight code quality fixes

* Change comment for clarity

---------

Co-authored-by: StanR <hi@stanr.info>
2025-06-27 23:46:52 +01:00
Natelytle
d5ef8c8524 Replace error functions in DifficultyCalculationUtils with good-enough approximations (#33717)
* Reimplement error functions

* Fix bug with adjustment for negative values

* Formatting

---------

Co-authored-by: tsunyoku <tsunyoku@gmail.com>
2025-06-18 13:14:01 +00:00
Givikap120
b783bb70e9 Optimize rhythm evaluation by replacing curve (#33423)
* Update RhythmEvaluator.cs

* add smoothstep bell curve

* Update osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs

Co-authored-by: StanR <castl@inbox.ru>

* Update osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs

Co-authored-by: StanR <castl@inbox.ru>

* Rename variables

---------

Co-authored-by: StanR <castl@inbox.ru>
Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-13 13:02:31 +05:00
James Wilson
19e9bffc11 Q2 osu! PP rebalance (#33640)
* Rebalance aim and speed

* Rebalance star rating

* Attempt further speed balancing

* More balancing

* More balancing

* Buff aim a bit

* More speed balancing

* Global rebalance

* Speed balancing

* Global rebalancing

* More speed balancing

* Buff aim

* MORE BALANCING

* Revert "Rebalance star rating"

This reverts commit f48c7445e12174c65b74edfef863cb3ae3cc29ff.
2025-06-11 19:46:46 +03:00
StanR
87023b22ea Remove wide/wiggle angle bonus rhythm requirements (#31409)
* Remove aim angle bonuses angle restrictions

* Remove unrelated change

* Only apply acute bonus for similar rhythms

* Cleanup

* Fix incorrect multiplication order

* Remove unrelated wide bonus change

* Remove redundant check

* Award less wide/wiggle bonus for sliders

* Balancing

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-10 11:20:55 +00:00
Eloise
7066f3def7 osu!taiko changes to length bonus using consistency factor (#33582)
* Implement new formulas for length bonus

* Add comment(s)

* Fix up HDFL thing
2025-06-10 11:31:11 +01:00
StanR
699fbb1a85 Decouple velocity change bonus from wide angle bonus (#33541)
* Decouple velocity change bonus from wide angle bonus

* Replace sin with smoothstep

* Set multiplier back to 0.75

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-09 11:02:49 +00:00
Eloise
5df41c08f4 osu!taiko new miss penalty using consistency factor (#33409)
* New formulas for effective miss count and penalty

* More elaborate comments

* More comment stuff
2025-06-08 22:47:26 +00:00
Natelytle
c4b07413b1 Refactor and re-comment osu! standard deviation calculations (#33218)
* Refactor

* Fix typo

* Prevent double.PositiveInfinity from occuring

* Fix leftover code branch

* Fix some idiot putting Math.Max instead of Math.Min

* Address NaN values

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-08 22:41:13 +00:00
Eloise
6ae8a68389 osu!taiko simplify pp summing and make performance attributes accurate (#33500)
* Change pp summing and adjust multipliers

* Add back convert consideration for hidden

* And the other one whoops

---------

Co-authored-by: StanR <hi@stanr.info>
2025-06-08 08:48:28 +00:00
Wulpey
642b938358 Reduce combo scaling for osu!catch (#33417)
* Reduce combo scaling for osu!catch

This is a conservative reduction, a middle point between the current
scaling and the CSR proposals.

* Reduce osu!catch combo scaling further

0.45 makes little difference so let's reduce it a bit more.

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-07 14:17:46 +00:00
Eloise
6a9aeda5d4 Remove multipliers nerfing ez (#33415) 2025-06-06 16:46:33 +03:00
Eloise
b982c3cd20 Remove stamina skill buff from strain length bonus (#33380)
Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-03 14:47:42 +01:00
Givikap120
366f2469ef Fix incorrect limit for sliderbreak estimation (#33110)
* fix incorrect clamp

* Add inline comment to explain `possibleBreaks` calculation

* move limit to aim and speed functions

* fix negative okMehAdjustment

* fix cases where lazer effective misscount gets reduced

* Simplify scope of changes

* Correct variable name

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-06-03 09:29:16 +01:00
Bartłomiej Dach
737ec8b2a8 Merge branch 'master' into pp-dev 2025-05-31 19:55:56 +02:00
Givikap120
63654ad1e0 Replace HD acc scaling adjust with reverse lerp util (#33271) 2025-05-28 13:59:23 +01:00
Givikap120
01d9c526d9 Rebalance HD bonus (#33237)
* initial commit

* changed HD curve

* removed AR variable

* update for new rework

* nerf HD acc bonus for AR>10

* add another HD nerf for AR>10

* Update OsuDifficultyCalculator.cs

* fix speed part being missing

* Update OsuDifficultyCalculator.cs

* rework to difficulty-based high AR nerf

* move TC back to perfcalc

* fix nvicka

* fix comment

* use utils function instead of manual one

* Clean up

* Use "visibility" term instead

* Store `mechanicalDifficultyRating` field

* Rename `isFullyHidden` to `isAlwaysPartiallyVisible` and clarify intent

* Remove redundant comment

* Add `calculateDifficultyRating` method

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-05-26 13:16:48 +03:00
Jay Lawton
ace74824b8 Add a consistency factor to osu!taiko diffcalc (#33233)
* add consistency attribute

* write attributes to json for serialisation

* comment change

* fix json, add mechanical difficulty

* write new attributes to database

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-05-23 11:57:37 +00:00
Givikap120
ee055ba8f5 Add spinners support to combo based estimated misscount (#33170)
* add spinner support

* Make `CalculateSpinnerScore` private & clarify comments

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-05-22 22:27:16 +00:00
StanR
60eaf088df Buff precision difficulty rating in osu! (#28877)
* Buff precision difficulty rating in osu!

* Fix position repetition calculation

* Fix aim evaluator crashing, move small circle bonus calculation, adjust the curve slightly

* Refactor

* Fix code quality

* Semicolon

* Apply small circle bonus to speed too

* Fix formatting

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-05-22 09:11:51 +01:00
James Wilson
4a343ceaf1 Move Ruleset and DifficultyCalculator allocations to global setup (#33220) 2025-05-21 11:25:20 +00:00
James Wilson
553a8601ed Add AimEstimatedSliderBreaks and SpeedEstimatedSliderBreaks performance attributes (#33181) 2025-05-18 12:50:29 +00:00
Givikap120
9314ea94b5 Change effective misscount to be based on legacy score and combo at the same time (#33066)
* implement stuff

* fix basic issues

* rework calculations

* sanity check

* don't use score based misscount if no scorev1 present

* Update OsuPerformanceCalculator.cs

* update misscount diff attribute names

* add raw score misscount attribute

* introduce more reasonable high bound for misscount

* code quality changes

* Fix osu!catch SR buzz slider detection (#32412)

* Use `normalized_hitobject_radius` during osu!catch buzz slider detection

Currently the algorithm considers some buzz sliders as standstills when
in reality they require movement. This happens because `HalfCatcherWidth`
isn't normalized while `exactDistanceMoved` is, leading to an inaccurate
comparison.

`normalized_hitobject_radius` is the normalized value of `HalfCatcherWidth`
and replacing one with the other fixes the problem.

* Rename `normalized_hitobject_radius` to `normalized_half_catcher_width`

The current name is confusing because hit objects have no radius in the
context of osu!catch difficulty calculation. The new name conveys the
actual purpose of the value.

* Only set `normalized_half_catcher_width` in `CatchDifficultyHitObject`

Prevents potential bugs if the value were to be changed in one of the
classes but not in both.

* Use `CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH` directly

Requested during code review.

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>

* Move osu!catch movement diffcalc to an evaluator (#32655)

* Move osu!catch movement state into `CatchDifficultyHitObject`

In order to port `Movement` to an evaluator, the state has to be either
moved elsewhere or calculated inside the evaluator. The latter requires
backtracking for every hit object, which in the worst case is continued
until the beginning of the map is reached. Limiting backtracking can
lead to difficulty value changes.

Thus, the first option was chosen for its simplicity.

* Move osu!catch movement difficulty calculation to an evaluator

Makes the code more in line with the other game modes.

* Add documentation for `CatchDifficultyHitObject` fields

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>

* Move all score-independent bonuses into star rating (#31351)

* basis refactor to allow for more complex SR calculations

* move all possible bonuses into star rating

* decrease star rating scaling to account for overall gains

* add extra FL guard for safety

* move star rating multiplier into a constant

* Reorganise some things

* Add HD and SO to difficulty adjustment mods

* Move non-legacy mod multipliers back to PP

* Some merge fixes

* Fix application of flashlight rating multiplier

* Fix Hidden bonuses being applied when Blinds mod is in use

* Move part of speed OD scaling into difficulty

* Move length bonus back to PP

* Remove blinds special case

* Revert star rating multiplier decrease

* More balancing

---------

Co-authored-by: StanR <hi@stanr.info>

* Add diffcalc considerations for Magnetised mod (#33004)

* Add diffcalc considerations for Magnetised mod

* Make speed reduction scale with power too

* cleaning up

* Update OsuPerformanceCalculator.cs

* Update OsuPerformanceCalculator.cs

* add new check to avoid overestimation

* fix code style

* fix nvicka

* add database attributes

* Refactor

* Rename `Working` to `WorkingBeatmap`

* Remove redundant condition

* Remove useless variable

* Remove `get` wording

* Rename `calculateScoreAtCombo`

* Remove redundant operator

* Add comments to explain how score-based miss count derivations work

* Remove redundant `decimal` calculations

* use static method to improve performance

* move stuff around for readability

* move logic into helper class

* fix the bug

* Delete OsuLegacyScoreProcessor.cs

* Delete ILegacyScoreProcessor.cs

* revert static method for multiplier

* use only basic combo score attribute

* Clean-up

* Remove unused param

* Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs

Co-authored-by: StanR <castl@inbox.ru>

* rename variables

* Add `LegacyScoreUtils`

* Add fail safe

* Move `countMiss`

* Better explain `CalculateRelevantScoreComboPerObject`

* Add `OsuLegacyScoreMissCalculator`

* Move `CalculateScoreAtCombo` and `CalculateRelevantScoreComboPerObject`

* Remove unused variables

* Move `GetLegacyScoreMultiplier`

* Add `estimated` wording

---------

Co-authored-by: wulpine <wulpine@proton.me>
Co-authored-by: James Wilson <tsunyoku@gmail.com>
Co-authored-by: StanR <hi@stanr.info>
Co-authored-by: StanR <castl@inbox.ru>
2025-05-16 23:38:12 +00:00
James Wilson
d22b3fb200 Remove track usage in difficulty and performance calculations (#33132) 2025-05-14 12:37:08 +00:00
KermitNuggies
3165b147ee Use proportion of difficult sliders to better estimate sliderbreaks on classic accuracy scores (#31234)
* scale misscount by proportion of difficult sliders

* cap sliderbreak count at count100 + count50

* use countMiss instead of effectiveMissCount as the base for sliderbreaks

* make code inspector happy + cleanup

* refactor to remove unnecesary calculation and need for new tuple

* scale sliderbreaks with combo

* use aimNoSliders for sliderbreak factor

* code cleanup

* make inspect code happy

* use diffcalcutils

* fix errors (oops)

* scaling changes

* fix div by zeros

* Fix compilation error

* Add online attributes for new difficulty attributes

* Formatting

* Rebase fixes

* Make `CountTopWeightedSliders` to remove weird protected `SliderStrains` list

* Prevent top weighted slider factor from being Infinity

---------

Co-authored-by: tsunyoku <tsunyoku@gmail.com>
2025-05-12 14:05:07 +01:00
StanR
ce73dbbcc6 Add diffcalc considerations for Magnetised mod (#33004)
* Add diffcalc considerations for Magnetised mod

* Make speed reduction scale with power too
2025-05-01 11:52:43 +01:00
Nathan Corbett
4f298760de Use sliders in acc pp if scorev2 is enabled (#32634)
Co-authored-by: StanR <hi@stanr.info>
2025-04-27 11:57:51 +00:00
James Wilson
2aeb80a8bd Move all score-independent bonuses into star rating (#31351)
* basis refactor to allow for more complex SR calculations

* move all possible bonuses into star rating

* decrease star rating scaling to account for overall gains

* add extra FL guard for safety

* move star rating multiplier into a constant

* Reorganise some things

* Add HD and SO to difficulty adjustment mods

* Move non-legacy mod multipliers back to PP

* Some merge fixes

* Fix application of flashlight rating multiplier

* Fix Hidden bonuses being applied when Blinds mod is in use

* Move part of speed OD scaling into difficulty

* Move length bonus back to PP

* Remove blinds special case

* Revert star rating multiplier decrease

* More balancing

---------

Co-authored-by: StanR <hi@stanr.info>
2025-04-27 12:30:05 +01:00
wulpine
7a9d31adb6 Move osu!catch movement diffcalc to an evaluator (#32655)
* Move osu!catch movement state into `CatchDifficultyHitObject`

In order to port `Movement` to an evaluator, the state has to be either
moved elsewhere or calculated inside the evaluator. The latter requires
backtracking for every hit object, which in the worst case is continued
until the beginning of the map is reached. Limiting backtracking can
lead to difficulty value changes.

Thus, the first option was chosen for its simplicity.

* Move osu!catch movement difficulty calculation to an evaluator

Makes the code more in line with the other game modes.

* Add documentation for `CatchDifficultyHitObject` fields

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-04-10 15:47:11 +00:00
StanR
cf7fdc0627 Move difficulty calculation fields from Slider to OsuDifficultyHitObject (#32410)
* Move difficulty calculation fields from `Slider` to `OsuDifficultyHitObject`

* Remove redundant check

* Use `LastObject` where possible

* Update tests

* Make `LazyTravelDistance` `double`

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-04-09 13:37:26 +00:00
StanR
30f9716db9 Reduce RX Ok multiplier (#32434) 2025-04-09 12:48:18 +00:00
James Wilson
69c90f9926 Use Precision.AlmostEquals to compare deviation lower bound (#32694) 2025-04-06 09:36:18 +01:00
wulpine
0c3ee1938e Fix osu!catch SR buzz slider detection (#32412)
* Use `normalized_hitobject_radius` during osu!catch buzz slider detection

Currently the algorithm considers some buzz sliders as standstills when
in reality they require movement. This happens because `HalfCatcherWidth`
isn't normalized while `exactDistanceMoved` is, leading to an inaccurate
comparison.

`normalized_hitobject_radius` is the normalized value of `HalfCatcherWidth`
and replacing one with the other fixes the problem.

* Rename `normalized_hitobject_radius` to `normalized_half_catcher_width`

The current name is confusing because hit objects have no radius in the
context of osu!catch difficulty calculation. The new name conveys the
actual purpose of the value.

* Only set `normalized_half_catcher_width` in `CatchDifficultyHitObject`

Prevents potential bugs if the value were to be changed in one of the
classes but not in both.

* Use `CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH` directly

Requested during code review.

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-03-24 20:04:40 +03:00
Jay Lawton
8b11be5ac0 osu!taiko skills refactor (#32426)
Co-authored-by: James Wilson <tsunyoku@gmail.com>
2025-03-24 10:07:23 +00:00
Dan Balasescu
9eadc9f68c Merge branch 'master' into pp-dev 2025-03-24 10:49:46 +09:00
StanR
2c88e60ed3 Add difficulty calculation benchmarks (#32542) 2025-03-23 21:08:41 +00:00
Daniel Power
fd504e5641 Minor cleanup 2024-12-16 23:17:22 -03:30
Daniel Power
93ed0483b6 Fix TestSceneTabletSettings 2024-12-16 22:59:04 -03:30
Daniel Power
4dd0672aa5 Address screen scale positioning 2024-12-16 21:04:08 -03:30
Daniel Power
66eff14d2b Initial proof of concept for tablet output scaling 2024-12-16 01:17:35 -03:30
Kian Masri
245ade004a new: rank Taiko single tap 2024-12-11 09:47:17 -07:00
Kian Masri
6cb46106fe new: also the single tap mod, it's the same thing 2024-12-10 10:04:36 -07:00
Kian Masri
3666e4c332 new: rank the alternate mode 2024-12-10 09:50:48 -07:00
646 changed files with 19177 additions and 3740 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
try
{
host.Window.SetIconFromStream(iconStream); host.Window.SetIconFromStream(iconStream);
}
catch
{
}
host.Window.Title = Name; host.Window.Title = Name;
} }

View File

@@ -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());
} }
} }
} }

View File

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

View File

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

View 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();
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"],
}]
}]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,64 +32,23 @@ 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,8 +70,6 @@ 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 currAngle = osuCurrObj.Angle.Value;
@@ -79,20 +78,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Rewarding angles, take the smaller velocity as base. // Rewarding angles, take the smaller velocity as base.
double angleBonus = Math.Min(currVelocity, prevVelocity); double angleBonus = Math.Min(currVelocity, prevVelocity);
wideAngleBonus = calcWideAngleBonus(currAngle); if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
{
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);
}
wideAngleBonus = calcWideAngleBonus(currAngle);
// Penalize angle repetition.
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
// Apply full wide angle bonus for distance more than one diameter
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
// https://www.desmos.com/calculator/dp0v0nvowc // https://www.desmos.com/calculator/dp0v0nvowc
@@ -103,25 +108,39 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter) * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8) * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)); * 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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),
}; };
} }
} }

View 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;
}
}
}

View File

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

View File

@@ -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);
// In classic scores there can't be more misses than a sum of all non-perfect judgements
effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
} }
else else
{ {
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped; // Use combo-based miss count if this isn't a legacy score
effectiveMissCount = comboBasedEstimatedMissCount;
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;
// 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));
// Subtract the deviation provided by tails that land outside the ok hit window from the deviation computed above.
// 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))); / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
deviation *= Math.Sqrt(1 - randomValue); deviation *= Math.Sqrt(1 - okHitWindowTailAmount);
}
else
{
// A tested limit value for the case of a score only containing oks.
deviation = okHitWindow / Math.Sqrt(3);
}
// Value deviation approach as greatCount approaches 0 // Compute and add the variance for mehs, assuming that they are uniformly distributed.
double limitValue = okHitWindow / Math.Sqrt(3);
// If precision is not enough to compute true deviation - use limit value
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
deviation = limitValue;
// Then compute the variance for mehs.
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;

View 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;
}
}

View File

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

View File

@@ -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());
} }
} }

View File

@@ -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());
} }
} }

View 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);
}
}
}

View 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));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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