1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:23:20 +08:00

Compare commits

...

205 Commits

Author SHA1 Message Date
Jay Lawton
fbc3000765
Merge 544ba25743 into f09d8f097a 2024-12-03 14:17:55 +00:00
Jay Lawton
544ba25743 move norm and bellcurve into diffcalcutils 2024-12-04 00:17:40 +10:00
Dan Balasescu
f09d8f097a
Merge pull request #30953 from peppy/notification-while-chedcking-for-updates
Show an ongoing operation when checking for updates
2024-12-03 17:27:10 +09:00
Dan Balasescu
be05f2a1c2
Merge pull request #30929 from timschumi/rate-change-ready
Account for rate changing mods when disabling the "Ready" button
2024-12-03 16:30:16 +09:00
Dan Balasescu
6ff1dec7b2
Add tests 2024-12-03 15:45:58 +09:00
Dean Herbert
457957d3b8
Refactor check-update flow to better handle unobserved exceptions 2024-12-03 14:23:10 +09:00
Dean Herbert
2ceb3f6f85
Show an ongoing operation when checking for updates
Addresses https://github.com/ppy/osu/discussions/30950.
2024-12-03 13:43:20 +09:00
Dean Herbert
ce4aac4184
Merge pull request #30917 from bdach/fix-incorrect-taiko-legacy-combo
Fix strong drum rolls being counted for double the combo in legacy scoring attributes
2024-12-02 20:08:49 -08:00
Tim Schumacher
e920cfa187 Move rate-changing TODO to a common place in CalculateRateWithMods 2024-12-02 23:49:51 +01:00
Dean Herbert
ce8e4120b7
Merge pull request #30947 from bdach/undesirable-deselect-on-control-click
Do not deselect objects when control-clicking without hitting anything
2024-12-02 07:17:27 -08:00
Bartłomiej Dach
b505ecc7ba
Do not deselect objects when control-clicking without hitting anything
As per feedback in
https://discord.com/channels/90072389919997952/1259818301517725707/1310270647187935284.
2024-12-02 13:51:43 +01:00
Bartłomiej Dach
b14dde937d
Add failing test case 2024-12-02 13:51:41 +01:00
Bartłomiej Dach
6c0ccc5ebe
Merge pull request #30863 from frenzibyte/improve-back-button-display
Delay back button appearance when performing a quick restart
2024-12-02 11:49:45 +01:00
Bartłomiej Dach
52b8753a12
Merge pull request #30749 from Sheppsu/multi-spectator-settings-sidebar
Add player settings to multi spectator screen
2024-12-02 11:34:57 +01:00
Dean Herbert
5b2558cec2
Merge pull request #28473 from bdach/beatmap-info-purge
Move unnecessary properties from `BeatmapInfo` / realm to `IBeatmap`
2024-12-02 16:19:12 +09:00
Dean Herbert
23522b02d8
Use local instead of field for local only usage 2024-12-01 19:53:57 +09:00
Dean Herbert
6afe083ec9
Fix settings showing up during gameplay 2024-12-01 18:44:26 +09:00
Dean Herbert
ddac71628d
Merge branch 'master' into multi-spectator-settings-sidebar 2024-12-01 18:33:46 +09:00
Tim Schumacher
164b809c89 Document ready button enable state with some comments 2024-11-30 23:02:22 +01:00
Salman Alshamrani
1d610a0f1b
Merge pull request #30928 from peppy/stop-logging-backwards-seek
Stop loudly logging backwards seek bug to sentry
2024-11-30 16:35:35 -05:00
Tim Schumacher
f4e155bfa6 Account for rate changing mods when disabling the "Ready" button 2024-11-30 16:01:32 +01:00
Dean Herbert
1e2e364cd3
Stop loudly logging backwards seek bug to sentry
Several users have reported stutters when this happens. It's potentially
from the error report overhead. We now know that this is a BASS level
issue anyway, so having this logging is not helpful.
2024-11-30 21:01:22 +09:00
Bartłomiej Dach
e92aa36f47
Merge pull request #30918 from peppy/expose-more-migration-helper-methods
Expose more migration helper methods
2024-11-29 14:55:45 +01:00
Dean Herbert
f56b2b9aef
Merge pull request #30793 from bdach/close-playlists
Add ability to close playlists within grace period after creation
2024-11-29 22:37:32 +09:00
Dean Herbert
a719693d10
Fix one remaining method call 2024-11-29 21:21:05 +09:00
Dean Herbert
0e1b62ef85
Expose more migration helper methods
For use in https://github.com/ppy/osu-queue-score-statistics/pull/305.

Some of these might look a bit odd, but I personally still prefer having
them all in one central location.
2024-11-29 21:10:30 +09:00
Dean Herbert
58efed4ebe
Merge pull request #30915 from bdach/extension-checks
Centralise supported file extensions to one helper class
2024-11-29 20:55:28 +09:00
Bartłomiej Dach
3cfa455369
Fix strong drum rolls being counted for double the combo in legacy scoring attributes 2024-11-29 10:54:32 +01:00
Bartłomiej Dach
5f092811cb
Use helper in one more place 2024-11-29 09:22:29 +01:00
Bartłomiej Dach
5a9127dfc6
Accidentally a word 2024-11-29 08:46:08 +01:00
Bartłomiej Dach
110e4fbb30
Centralise supported file extensions to one helper class
As proposed in
https://github.com/ppy/osu-server-beatmap-submission/pull/5#discussion_r1861680837.
2024-11-29 08:42:45 +01:00
Salman Alshamrani
b71bccc42d
Merge pull request #30914 from peppy/dev-footer-pass
Simplify the dev footer display
2024-11-29 02:14:30 -05:00
Dean Herbert
b697ddc6db
Simplify the dev footer display 2024-11-29 15:32:35 +09:00
Dean Herbert
ca32720cbd
Merge pull request #30904 from frenzibyte/save-android
Work around Android CI workflow errors
2024-11-29 14:54:52 +09:00
Dean Herbert
276c37bcf7
Update framework 2024-11-29 13:47:56 +09:00
Dean Herbert
a67f7ea3fb
Merge branch 'master' into save-android 2024-11-29 13:46:53 +09:00
Salman Alshamrani
f664e97496
Merge pull request #30906 from Hiviexd/daily-challenge-tier-thresholds
Adjust daily challenge tier thresholds to match expectations
2024-11-28 18:36:31 -05:00
Salman Alshamrani
51bcde67aa Remove no longer required comment 2024-11-28 17:55:15 -05:00
Salman Alshamrani
078d62fe09 Fix weird default in test scene 2024-11-28 17:54:03 -05:00
Salman Alshamrani
a8db35ac45
Merge branch 'master' into daily-challenge-tier-thresholds 2024-11-28 17:46:55 -05:00
Salman Alshamrani
932afcde01 Make editor make sense 2024-11-28 17:43:32 -05:00
Bartłomiej Dach
c93c549b05
Fix ready button not disabling on playlist close 2024-11-28 14:17:31 +01:00
Bartłomiej Dach
9926ffd326
Make button a little narrower 2024-11-28 14:06:12 +01:00
Bartłomiej Dach
ac2c4e81c7
Use switch 2024-11-28 14:04:39 +01:00
Bartłomiej Dach
2e6f43a75d
Merge branch 'master' into close-playlists 2024-11-28 14:01:36 +01:00
Bartłomiej Dach
d0e80ce982
Merge pull request #30895 from peppy/watch-replay-reliability
Fix watch replay button sometimes not loading the replay on first click
2024-11-28 13:16:40 +01:00
Hiviexd
6ed21229b7 update test 2024-11-28 12:49:48 +01:00
Hiviexd
66093872e8 Adjust daily challenge tier thresholds to match expectations 2024-11-28 12:49:30 +01:00
Bartłomiej Dach
98a156ae2d
Merge pull request #30874 from peppy/chat-order
Sort public chat channels alphabetically, private channels based on recent messages
2024-11-28 12:46:14 +01:00
Dan Balasescu
1575eed5ba
Merge pull request #30893 from peppy/realm-perf-improvements
Improve realm update performance
2024-11-28 19:08:22 +09:00
Dan Balasescu
077719903b
Merge pull request #30905 from peppy/fix-multiple-offset-applications
Clear previous `LastLocalUserScore` when returning to song select
2024-11-28 18:58:13 +09:00
Bartłomiej Dach
d218c19799
Merge pull request #30852 from frenzibyte/fix-daily-challenge-leaderboard
Fix daily challenge results screen fetching scores beginning from the user's highest
2024-11-28 10:21:52 +01:00
Dean Herbert
c26c84ba45
Add test coverage governing new behaviour 2024-11-28 18:03:19 +09:00
Dean Herbert
ced8dda1a2
Clear previous LastLocalUserScore when returning to song select
This seems like the lowest friction way of fixing
https://github.com/ppy/osu/issues/30885.

We could also only null this on application, but this feels worse
because

- It would require local handling (potentially complex) in
  `BeatmapOffsetControl` if we want to continue displaying the graph and
button after clicking it.
- It would make the session static very specific in usage and
  potentially make future usage not possible due to being nulled in only
a very specific scenario.

One might argue that it would be nice to have this non-null until the
next play, but if such a usage comes up I'd propose we rename this
session static and add a new one with that purpose.
2024-11-28 18:01:28 +09:00
Bartłomiej Dach
0d491e3159
Merge branch 'master' into fix-daily-challenge-leaderboard 2024-11-28 09:41:24 +01:00
Dan Balasescu
5d7aafaab3
Merge pull request #30894 from HenintsoaSky/star-fountains-toggle-setting
Add a toggle for star fountains during gameplay
2024-11-28 17:35:11 +09:00
Bartłomiej Dach
4314f9c0a9
Remove unused accessors 2024-11-28 09:22:08 +01:00
Dean Herbert
70eee8882a
Remove unnecessary null check 2024-11-28 15:42:37 +09:00
Salman Alshamrani
19e396f878 Fix android workflow not installing .NET 8 version 2024-11-27 23:46:21 -05:00
Dean Herbert
e0fdcaf523
Merge pull request #30848 from Joehuu/dicord-fix-beatmap-button-visibility
Fix discord "view beatmap" button being shown when editing and hide identifiable information is set
2024-11-28 13:29:09 +09:00
Salman Alshamrani
24c0799680 Move beatmap ID lookup to UesrActivity 2024-11-27 16:54:51 -05:00
Salman Alshamrani
f792b6de00 Fix comment 2024-11-27 06:07:10 -05:00
Salman Alshamrani
4ae3ccfe48 Make BackButtonVisibility in game class private 2024-11-27 06:05:02 -05:00
Dean Herbert
0f73941808
Combine new implementation back into the old one and use everywhere 2024-11-27 17:47:42 +09:00
Dean Herbert
7fdf13911b
Adjust the colour of non-pinned settings groups' headers to be more legible 2024-11-27 17:47:27 +09:00
Dean Herbert
782ce24ca6
Move player settings out of right flow 2024-11-27 17:09:15 +09:00
Dean Herbert
9c707ed341
Rename class and fix padding considerations 2024-11-27 16:47:54 +09:00
Dean Herbert
5ce55e9cb4
Merge branch 'master' into multi-spectator-settings-sidebar 2024-11-27 16:35:05 +09:00
HenintsoaSky
c3ac6d7fe5
Delete changes.patch
oops
2024-11-27 10:22:30 +03:00
Dean Herbert
4fcc76270a
Ensure events are unbound on disposal as a safety 2024-11-27 15:46:55 +09:00
Dean Herbert
5260a401d4
Use RealmLive in SaveFailedScoreButton
This also optimises the manager classes to better support `Live` usage
where the managed object is already in a good state (ie. doesn't require
re-fetching).
2024-11-27 15:25:42 +09:00
Salman Alshamrani
dfbccc2144 Knock some sense into the playlists results screen implementation
As we're moving towards using the `/playlist/<id>/scores/<id>` endpoint,
the existing playlists results screen classes needed some restructuring.
2024-11-27 01:20:43 -05:00
Dean Herbert
aa3d3a6344
Remove unnecessary local subscription in BeatmapCarousel
Not sure why I left this around during the refactor. This is 100%
handled by the `DetachedBeatmapStore`.

Removing this subscription reduces overheads by a huge amount for users
with large beatmap databases. My hypothesis is that subscriptions are
more expensive based on **the number of results matching**. This one
matches almost every beatmap so removing it is a large win.
2024-11-27 14:24:57 +09:00
HenintsoaSky
a477bb7bfe Renaming of 'StarFountainEnabled' 2024-11-27 07:38:33 +03:00
Dean Herbert
573aaf6637
Merge pull request #27128 from frenzibyte/user-statistics-provider
Introduce `UserStatisticsProvider` component and add support for respecting selected ruleset
2024-11-27 13:13:47 +09:00
Salman Alshamrani
9083daf363 Fix epic code failure
I wasn't feeling well last night.
2024-11-26 20:04:36 -05:00
HenintsoaSky
16d8b11385 A toggle for star fountains 2024-11-27 00:53:22 +03:00
Salman Alshamrani
3e1b4f4ac5 Rename AllowBackButton to AllowUserExit and rewrite visibility flow structure
Co-authored-by: Dean Herbert <pe@ppy.sh>
2024-11-26 16:52:39 -05:00
HenintsoaSky
80a66085a9 rename and remove again 2024-11-27 00:41:02 +03:00
HenintsoaSky
460471e73f Rename of the setting 2024-11-27 00:27:22 +03:00
HenintsoaSky
df74a177ae Add option to disable star fountain in gameplay 2024-11-27 00:13:32 +03:00
Joseph Madamba
a1a63608dd
Merge pull request #30865 from ItsShamed/xxx-add-localisation-support-for-menu-tip
Add localisation support for menu tip
2024-11-26 12:54:17 -08:00
Joseph Madamba
9173a74552
Merge branch 'master' into xxx-add-localisation-support-for-menu-tip 2024-11-26 12:13:08 -08:00
Joseph Madamba
f04862ea74
Edit one more word not using british english 2024-11-26 12:11:29 -08:00
Bartłomiej Dach
359cb71dd9
Merge pull request #30884 from peppy/fix-spinner-legacy-new-order-
Fix classic skin spinner's middle pieces displaying in the wrong order
2024-11-26 14:57:42 +01:00
Bartłomiej Dach
33c2eb1af7
Merge pull request #30881 from peppy/fix-editor-state-leaking
Fix hitobjects' samples getting in bad state when changing selection between objects
2024-11-26 13:54:50 +01:00
Bartłomiej Dach
3e373ae85e
Merge pull request #30868 from peppy/ur-perf-fix
Improve performance of UR calculations
2024-11-26 13:54:17 +01:00
Dan Balasescu
bd1f978138
Empty commit to fix CI 2024-11-26 21:35:24 +09:00
Bartłomiej Dach
c69d36dc96
Remove leftover [Solo] attribute 2024-11-26 12:40:49 +01:00
Bartłomiej Dach
46d1f00590
Fix Beatmap.Countdown not being copied on conversion 2024-11-26 11:39:03 +01:00
Bartłomiej Dach
cf905d0f5c
Merge branch 'master' into beatmap-info-purge 2024-11-26 10:21:16 +01:00
Dean Herbert
312336de24
Fix classic skin spinner's middle pieces displaying in the wrong order
Closes https://github.com/ppy/osu/issues/30873.
See [stable
reference](3ea48705eb/osu!/GameplayElements/HitObjects/Osu/SpinnerOsu.cs#L148-L158).
2024-11-26 18:12:28 +09:00
tsrk.
41c309fb72
chore(MenuTip): update text according to recent changes
Signed-off-by: tsrk. <tsrk@tsrk.me>
2024-11-26 09:35:18 +01:00
Dean Herbert
3ecb3b674d
Don't reset state when changing from one selection to another in the editor
This was causing state pollution in the new selection. I can't see why
this needs to happen when a selection changes to another.

This fixes https://github.com/ppy/osu/issues/30839 and also the same
issue happening for the new combo toggle.

Tests all seem to pass, and I can't immediately find anything broken,
but YMMV.
2024-11-26 17:33:41 +09:00
Dean Herbert
e0199386a3
Add failing test case showing changing selection in editor affects samples 2024-11-26 17:33:41 +09:00
Dan Balasescu
d3d111de7d
Merge pull request #30832 from peppy/mania-precise-scroll-speed
Allow setting osu!mania scroll speed to single decimal precision
2024-11-26 17:30:56 +09:00
Dan Balasescu
943837e3b5
Merge pull request #30878 from peppy/config-pause-hold-thing-of-course
Add setting to allow hold-for-pause to still exist
2024-11-26 16:35:35 +09:00
Salman Alshamrani
42c68ba43e Add inline comment 2024-11-26 01:28:58 -05:00
Salman Alshamrani
b76460f100 Schedule the thing
Queuing up requests on change to `api.LocalUser` is bad because the API
state is updated after `LocalUser` is updated, therefore we have to
schhhhhedullllllllleeeeeeeeeeeeeeee.
2024-11-26 01:26:44 -05:00
Dean Herbert
e3ea38a366
Add setting to allow hold-for-pause to still exist
Users have asked for this multiple times since last release.

Not sure on the best default value, but I'm going with the
stable/classic one, at least for the initial release to avoid needing
migrations.

In the future we may reconsider this for new users.
2024-11-26 15:14:19 +09:00
Salman Alshamrani
7201bac60d Remove DailyChallengePlayer 2024-11-26 01:10:19 -05:00
Salman Alshamrani
c1416f9920 Bring back user-based endpoint for viewing result screen from playlists lounge 2024-11-26 01:10:12 -05:00
Salman Alshamrani
d150aeef2b Use score-based endpoint everywhere 2024-11-26 01:01:59 -05:00
Salman Alshamrani
dfa21574fd
Merge branch 'master' into xxx-add-localisation-support-for-menu-tip 2024-11-25 23:52:56 -05:00
Dean Herbert
17347563ee
Fix incorrect null handling 2024-11-26 13:25:57 +09:00
Dean Herbert
f708466a9b
Add test coverage 2024-11-26 13:25:55 +09:00
Dean Herbert
d6cf1db0f5
Add basic xmldoc to results class 2024-11-26 12:16:26 +09:00
Dean Herbert
d903d381d5
Rename NextProcessableIndex to EventCount in line with actual functionality 2024-11-26 12:10:34 +09:00
Bartłomiej Dach
285959943f
Merge pull request #30859 from frenzibyte/fix-grid-settings-missing-precision
Fix editor grid settings not displaying decimal portion in slider tooltips
2024-11-25 14:54:15 +01:00
Dean Herbert
9ca17f9b6b
Merge pull request #30748 from stanriders/scale-profile-beatmaps
Scale down beatmap cards
2024-11-25 22:53:51 +09:00
Bartłomiej Dach
b5f8773c0b
Merge pull request #30826 from peppy/beatmap-defaults-match-stable
Change some beatmap default settings to match stable
2024-11-25 13:40:19 +01:00
Bartłomiej Dach
0a3f3c3210
Add guard against fetching statistics for non-legacy rulesets 2024-11-25 13:14:22 +01:00
Dean Herbert
bbe8f2ec44
Only update unstable rate counter when an applicable hitobject is reached 2024-11-25 21:13:18 +09:00
Dean Herbert
ea68d4b33a
Use class instead of record for lower allocations 2024-11-25 21:13:18 +09:00
Dean Herbert
5668258182
Add incremental processing 2024-11-25 21:13:17 +09:00
Bartłomiej Dach
78c01c1b5a
Merge branch 'master' into beatmap-defaults-match-stable 2024-11-25 12:55:08 +01:00
Bartłomiej Dach
c8847e8da8
Fix incorrect unit test 2024-11-25 12:53:40 +01:00
Dean Herbert
33d725e889
Address unstable rate calculations as a list for marginal gains 2024-11-25 19:44:11 +09:00
Dean Herbert
605fe71f46
Make empty hitwindows readonly static and slightly improve comparison performance 2024-11-25 19:17:32 +09:00
Dean Herbert
a1916d12db
Ensure UR benchmark has hitwindows populated 2024-11-25 18:53:15 +09:00
Dean Herbert
82bdd8fbfc
Merge pull request #30861 from frenzibyte/fix-multiplayer-missing-hold-delay
Fix pause shortcut on multiplayer no longer requiring hold
2024-11-25 16:22:56 +09:00
Dean Herbert
876c2e468a
Merge pull request #30858 from frenzibyte/fix-match-settings-overlay-typo 2024-11-25 15:58:17 +09:00
tsrk.
cfaf972813
Merge branch 'master' into xxx-add-localisation-support-for-menu-tip 2024-11-24 18:24:43 +01:00
Dean Herbert
c34827a4ed
Merge pull request #30862 from frenzibyte/dont-scare-the-player
Don't play fail animation if restarting on fail
2024-11-24 23:11:19 +09:00
tsrk.
8611ed31c2
refactor(MenuTip): add localisation support
Signed-off-by: tsrk. <tsrk@tsrk.me>
2024-11-24 14:22:56 +01:00
Salman Alshamrani
53b390667a Fix failing test 2024-11-24 06:04:36 -05:00
Dean Herbert
888f02e3a6
Merge pull request #30855 from SupDos/tips-remove-fps
Remove FPS shortcut tip
2024-11-24 19:57:08 +09:00
Salman Alshamrani
ae9119eef0 Hide back button when quick-restarting unless load time takes long 2024-11-24 05:40:06 -05:00
Salman Alshamrani
2420793466 Allow controlling back button visibility state from screens 2024-11-24 05:39:43 -05:00
Salman Alshamrani
6d0d7f3e75 Don't play fail animation if restarting on failure 2024-11-24 04:45:48 -05:00
Salman Alshamrani
aa1358b2b4 Enable NRT and fix code 2024-11-24 04:33:03 -05:00
Salman Alshamrani
f3155bfc7d Fix pause shortcut on multiplayer not delayed 2024-11-24 04:24:31 -05:00
Salman Alshamrani
631bfadd68 Replace event subscription with callback in UserStatisticsWatcher
Also no longer cancels previous API requests as there's no actual need to do it.
2024-11-24 04:11:13 -05:00
Salman Alshamrani
354bc424a3
Merge pull request #30830 from smoogipoo/multiplayer-remove-expired-item-removal
No longer remove expired playlist items from `Room` model
2024-11-23 23:58:17 -05:00
Salman Alshamrani
cab26c70c1 Fix editor grid settings not displaying decimal portion in slider tooltips 2024-11-23 22:27:56 -05:00
Salman Alshamrani
956da0383f
Merge branch 'master' into multiplayer-remove-expired-item-removal 2024-11-23 22:19:21 -05:00
Salman Alshamrani
8f5d513d46 Fix room auto start duration setting applied to the wrong component 2024-11-23 22:16:29 -05:00
Salman Alshamrani
6b78553559
Merge branch 'master' into tips-remove-fps 2024-11-23 20:57:14 -05:00
Salman Alshamrani
7a973b0243
Merge pull request #30834 from peppy/fix-song-ticker-contrast
Fix song ticker having very bad contrast against bright backgrounds
2024-11-23 20:55:26 -05:00
SupDos
2f096f71d3 Remove FPS shortcut tip 2024-11-24 02:34:30 +01:00
Salman Alshamrani
608bda135a
Merge branch 'master' into fix-song-ticker-contrast 2024-11-23 20:14:33 -05:00
Salman Alshamrani
2f45ebeec8 Remove using directive 2024-11-23 20:13:57 -05:00
Salman Alshamrani
eed02c2ab1 Fix daily challenge results screen beginning score fetch from user highest 2024-11-23 15:45:29 -05:00
Sheppsu
3713bb48b7 expand and contract settings from hover 2024-11-23 01:09:58 -05:00
Joseph Madamba
62837c7e53
Fix discord "view beatmap" button being shown when editing and hide identifiable information is set 2024-11-22 17:35:33 -08:00
Dean Herbert
9930922769
Fix chat channel listing not being ordered to expectations
- Public channels (and announcements) are now alphabetically ordered.
- Private message channels are now ordered by most recent activity.

Closes https://github.com/ppy/osu/issues/30835.
2024-11-22 19:53:26 +09:00
Dean Herbert
c844d65a81
Use TryGetValue wherever possible
Rider says so.
2024-11-22 19:11:16 +09:00
Bartłomiej Dach
ead7e99c59
Fix incorrect comment 2024-11-22 11:06:36 +01:00
Dean Herbert
c590bef4c3
Remove legacy default setter for SamplesMatchPlaybackRate now that it's the default 2024-11-22 19:05:29 +09:00
Dean Herbert
086a34f5c0
Merge branch 'master' into beatmap-info-purge 2024-11-22 18:47:32 +09:00
Dean Herbert
e33e0e16e8
Merge branch 'master' into scale-profile-beatmaps 2024-11-22 18:33:37 +09:00
Dean Herbert
04ed954387
Fix song ticker having very bad contrast against bright backgrounds
Closes #30814.
2024-11-22 18:17:55 +09:00
Bartłomiej Dach
8b68859d9d
Fix Room.CopyFrom() skipping a field
Was making the close button not display when creating a room anew.
2024-11-22 09:57:57 +01:00
Bartłomiej Dach
cfc38df889
Add close button to playlists footer 2024-11-22 09:57:56 +01:00
Bartłomiej Dach
69c2c988a1
Add extra check to ensure closed rooms can't be closed harder 2024-11-22 09:54:56 +01:00
Dean Herbert
29757ffdf2
Allow setting osu!mania scroll speed to single decimal precision
Addresses https://github.com/ppy/osu/discussions/30663.
2024-11-22 17:36:28 +09:00
Dan Balasescu
39504c348d
Cleanup CopyFrom() method
Though the code appears slightly different, it should be semantically
equivalent. APIUser equality is implemented on `Id` and `Host` should
never transition from non-null to null.
2024-11-22 17:22:30 +09:00
Dan Balasescu
e59ac9e7c8
No longer remove expired playlist items from Room model 2024-11-22 17:19:26 +09:00
Bartłomiej Dach
3b2f43012e
Merge branch 'master' into close-playlists 2024-11-22 09:02:41 +01:00
Dean Herbert
a76b4418b9
Change some beatmap default settings to match stable
- Countdown should [be off by
default](9a07485638/osu!/GameplayElements/Beatmaps/Beatmap.cs#L372)
- Samples match playback rate
[also](9a07485638/osu!/GameplayElements/Beatmaps/Beatmap.cs#L210)
2024-11-22 16:55:37 +09:00
Bartłomiej Dach
a679f0736e
Add ability to close playlists within grace period after creation 2024-11-20 12:36:12 +01:00
Salman Alshamrani
0b52080a52 Handle logged out user 2024-11-18 06:47:22 -05:00
Salman Alshamrani
74daf85e48 Replace bindable with an event 2024-11-18 06:47:22 -05:00
StanR
4066186b24 Scale beatmap cards down by ~0.8 2024-11-18 14:48:51 +05:00
Sheppsu
7d4062d2ad remove redundant Scale attribute 2024-11-18 04:04:28 -05:00
StanR
dcf4674c6c Scale down beatmap cards in profile overlay 2024-11-18 14:01:17 +05:00
Sheppsu
29e7adcd3b add player settings to multi spectator screen 2024-11-18 03:57:50 -05:00
Salman Alshamrani
b106833663 Fix more test / component breakage 2024-11-17 20:36:12 -05:00
Salman Alshamrani
caf56afba6 Fix various test failures 2024-11-17 19:13:29 -05:00
Salman Alshamrani
1847b679db Only update user rank panel display when ruleset matches
Nothing behaviourally different, just reduce number of redundant calls.
2024-11-17 18:45:07 -05:00
Salman Alshamrani
07609b6267 Fix UserRankPanel not updating on ruleset change 2024-11-17 18:32:17 -05:00
Salman Alshamrani
28f87407f6 Make DifficultyRecommender rely on the statistics provider 2024-11-17 18:32:17 -05:00
Salman Alshamrani
4a628287e2 Decouple game-wide ruleset bindable and refactor LocalUserStatisticsProvider
This also throws away the logic of updating
`API.LocalUser.Value.Statistics`. Components should rely on
`LocalUserStatisticsProvider` instead for proper behaviour and ability
to update on statistics updates.
2024-11-17 18:13:37 -05:00
Salman Alshamrani
6c8a900dcc Merge branch 'master' into user-statistics-provider 2024-11-17 15:34:56 -05:00
Dean Herbert
0760451f3f
Merge branch 'master' into user-statistics-provider 2024-11-13 15:21:55 +09:00
Salman Alshamrani
979065c421 Reorder code slightly 2024-10-26 23:09:17 -04:00
Salman Alshamrani
663b769c71 Update DiscordRichPresence to use new statistics provider component 2024-10-25 03:30:43 -04:00
Salman Alshamrani
fdeb8b907e
Merge branch 'master' into user-statistics-provider 2024-10-25 03:17:37 -04:00
Salman Alshamrani
44dd81363a Make UserStatisticsWatcher fully rely on LocalUserStatisticsProvider 2024-10-25 03:15:41 -04:00
Salman Alshamrani
3a57b21c89 Move LocalUserStatisticsProvider to non-base game class and make dependency optional 2024-10-25 02:38:41 -04:00
Salman Alshamrani
2fd495228c Fix post-merge errors 2024-10-25 02:38:01 -04:00
Salman Alshamrani
701fb565b1 Merge branch 'master' into user-statistics-provider 2024-10-25 01:35:24 -04:00
Dean Herbert
58fe502af4
Merge branch 'master' into beatmap-info-purge 2024-09-15 04:53:35 +09:00
Bartłomiej Dach
1d4d806362
Fix WidescreenStoryboard breakage after moving out of BeatmapInfo 2024-07-23 12:19:45 +02:00
Dean Herbert
d707e29ff7
Merge branch 'master' into beatmap-info-purge 2024-07-23 12:09:32 +09:00
Bartłomiej Dach
04527f3c9d
Fix TestBeatmap not transferring newly migrated properties 2024-06-13 09:30:09 +02:00
Bartłomiej Dach
c67e2dc301
Bump schema version 2024-06-13 09:30:08 +02:00
Bartłomiej Dach
9fbf2872e1
Remove no longer applicable region marking 2024-06-12 14:27:40 +02:00
Bartłomiej Dach
dd50d6fa6e
Move CountdownOffset out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
d373f752d6
Move Countdown out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
7f2a6f6f5a
Move TimelineZoom out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
6685c5ab74
Move GridSize out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
3634307d7c
Move DistanceSpacing out of BeatmapInfo 2024-06-12 14:14:58 +02:00
Bartłomiej Dach
c216283bf4
Move SamplesMatchPlaybackRate out of BeatmapInfo 2024-06-12 13:32:23 +02:00
Bartłomiej Dach
f64a0624a5
Move EpilepsyWarning out of BeatmapInfo 2024-06-12 13:28:41 +02:00
Bartłomiej Dach
1ab86ebd24
Move WidescreenStoryboard out of BeatmapInfo 2024-06-12 13:23:53 +02:00
Bartłomiej Dach
a6b7600bf2
Move LetterboxInBreaks out of BeatmapInfo 2024-06-12 13:18:44 +02:00
Bartłomiej Dach
011c2e3651
Move SpecialStyle out of BeatmapInfo 2024-06-12 13:12:30 +02:00
Bartłomiej Dach
0a4560a03e
Move StackLeniency out of BeatmapInfo 2024-06-12 13:04:33 +02:00
Bartłomiej Dach
4339e2dc4a
Move AudioLeadIn out of BeatmapInfo 2024-06-12 13:04:28 +02:00
Salman Ahmed
11b3fa8691 Fix TestSceneUserPanel tests failing 2024-02-11 11:39:12 +03:00
Salman Ahmed
bc2b705063 Fix ImportTest.TestOsuGameBase having null ruleset 2024-02-11 11:16:54 +03:00
Salman Ahmed
633d85431b Update UserRankPanel implementation to use new component 2024-02-11 08:22:31 +03:00
Salman Ahmed
3ab60b76df Remove IAPIProvider.Statistics in favour of the new component 2024-02-11 08:22:31 +03:00
Salman Ahmed
91fb59ee15 Introduce LocalUserStatisticsProvider component 2024-02-11 08:22:31 +03:00
173 changed files with 2625 additions and 1114 deletions

View File

@ -114,7 +114,10 @@ jobs:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Install .NET workloads - name: Install .NET workloads
run: dotnet workload install android # since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
- name: Compile - name: Compile
run: dotnet build -c Debug osu.Android.slnf run: dotnet build -c Debug osu.Android.slnf

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1118.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.1128.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

@ -15,6 +15,7 @@ using osu.Framework.Threading;
using osu.Game; using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -47,6 +48,9 @@ namespace osu.Desktop
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!; private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -117,7 +121,9 @@ namespace osu.Desktop
status.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated; multiplayerClient.RoomUpdated += onRoomUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
} }
private void onReady(object _, ReadyMessage __) private void onReady(object _, ReadyMessage __)
@ -133,6 +139,8 @@ namespace osu.Desktop
private void onRoomUpdated() => schedulePresenceUpdate(); private void onRoomUpdated() => schedulePresenceUpdate();
private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate();
private ScheduledDelegate? presenceUpdateDelegate; private ScheduledDelegate? presenceUpdateDelegate;
private void schedulePresenceUpdate() private void schedulePresenceUpdate()
@ -167,7 +175,7 @@ namespace osu.Desktop
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{ {
presence.Buttons = new[] presence.Buttons = new[]
{ {
@ -229,10 +237,8 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
{ {
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value);
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics?.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
} }
// small image // small image
@ -327,25 +333,14 @@ namespace osu.Desktop
return true; return true;
} }
private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
case UserActivity.InGame game:
return game.BeatmapID;
case UserActivity.EditingBeatmap edit:
return edit.BeatmapID;
}
return null;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (multiplayerClient.IsNotNull()) if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated; multiplayerClient.RoomUpdated -= onRoomUpdated;
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
client.Dispose(); client.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -4,28 +4,54 @@
using System.Collections.Generic; using System.Collections.Generic;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Objects; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
{ {
public class BenchmarkUnstableRate : BenchmarkTest public class BenchmarkUnstableRate : BenchmarkTest
{ {
private List<HitEvent> events = null!; private readonly List<List<HitEvent>> incrementalEventLists = new List<List<HitEvent>>();
public override void SetUp() public override void SetUp()
{ {
base.SetUp(); base.SetUp();
events = new List<HitEvent>();
for (int i = 0; i < 1000; i++) var events = new List<HitEvent>();
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
for (int i = 0; i < 2048; i++)
{
// Ensure the object has hit windows populated.
var hitObject = new HitCircle();
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null));
incrementalEventLists.Add(new List<HitEvent>(events));
}
} }
[Benchmark] [Benchmark]
public void CalculateUnstableRate() public void CalculateUnstableRate()
{ {
_ = events.CalculateUnstableRate(); for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
_ = events.CalculateUnstableRate();
}
}
[Benchmark]
public void CalculateUnstableRateUsingIncrementalCalculation()
{
HitEventExtensions.UnstableRateCalculationResult? last = null;
for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
last = events.CalculateUnstableRate(last);
}
} }
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
base.InitialiseDefaults(); base.InitialiseDefaults();
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40); SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime) if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
{ {
SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null); SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
} }
#pragma warning restore CS0618 #pragma warning restore CS0618
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{ {
new TrackedSetting<int>(ManiaRulesetSetting.ScrollSpeed, new TrackedSetting<double>(ManiaRulesetSetting.ScrollSpeed,
speed => new SettingDescription( speed => new SettingDescription(
rawValue: speed, rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed, name: RulesetSettingsStrings.ScrollSpeed,

View File

@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override void Update() protected override void Update()
{ {
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<int>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<double>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
base.Update(); base.Update();
} }
} }

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{ {
Caption = "Use special (N+1) style", Caption = "Use special (N+1) style",
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } Current = { Value = Beatmap.SpecialStyle }
}, },
healthDrainSlider = new FormSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
// for now, update these on commit rather than making BeatmapMetadata bindables. // for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; Beatmap.SpecialStyle = specialStyle.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;

View File

@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection, LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<int, ManiaScrollSlider> new SettingsSlider<double, ManiaScrollSlider>
{ {
LabelText = RulesetSettingsStrings.ScrollSpeed, LabelText = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed), Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5 KeyboardStep = 1
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
}; };
} }
private partial class ManiaScrollSlider : RoundedSliderBar<int> private partial class ManiaScrollSlider : RoundedSliderBar<double>
{ {
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value); public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
} }

View File

@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)
{ {
if (!hit_result_mapping.ContainsKey(result)) if (!hit_result_mapping.TryGetValue(result, out var value))
return null; return null;
string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value string filename = this.GetManiaSkinConfig<string>(value)?.Value
?? default_hit_result_skin_filenames[result]; ?? default_hit_result_skin_filenames[result];
var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d);

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableInt configScrollSpeed = new BindableInt(); private readonly BindableDouble configScrollSpeed = new BindableDouble();
private double currentTimeRange; private double currentTimeRange;
protected double TargetTimeRange; protected double TargetTimeRange;
@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// </summary> /// </summary>
/// <param name="scrollSpeed">The scroll speed.</param> /// <param name="scrollSpeed">The scroll speed.</param>
/// <returns>The scroll time.</returns> /// <returns>The scroll time.</returns>
public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();

View File

@ -231,6 +231,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2)); AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
} }
[Test]
public void TestControlClickDoesNotDiscardExistingSelectionEvenIfNothingHit()
{
var firstSlider = new Slider
{
StartTime = 0,
Position = new Vector2(0, 0),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
AddStep("add object", () => EditorBeatmap.AddRange([firstSlider]));
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange([firstSlider]));
AddStep("move mouse to middle of playfield", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
AddStep("control-click left mouse", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
}
private ComposeBlueprintContainer blueprintContainer private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First(); => Editor.ChildrenOfType<ComposeBlueprintContainer>().First();

View File

@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void gridSizeIs(int size) private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size); && EditorBeatmap.GridSize == size);
[Test] [Test]
public void TestGridTypeToggling() public void TestGridTypeToggling()

View File

@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}) })
} }
}, },
BeatmapInfo = StackLeniency = 0,
{
StackLeniency = 0,
}
}, },
ReplayFrames = new List<ReplayFrame> ReplayFrames = new List<ReplayFrame>
{ {

View File

@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
StackLeniency = 0,
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
ApproachRate = 8.5f ApproachRate = 8.5f
} }
}, },
StackLeniency = 0,
ControlPointInfo = controlPointInfo ControlPointInfo = controlPointInfo
}; };

View File

@ -465,7 +465,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap) private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap)
{ {
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
beatmap.BeatmapInfo.StackLeniency = 0; beatmap.StackLeniency = 0;
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
{ {
SliderMultiplier = 4, SliderMultiplier = 4,

View File

@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
h.StackHeight = 0; h.StackHeight = 0;
if (beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap, hitObjects);
} }
} }
private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(IBeatmap beatmap, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
continue; continue;
double endTime = stackBaseObject.GetEndTime(); double endTime = stackBaseObject.GetEndTime();
double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency;
if (objectN.StartTime - endTime > stackThreshold) if (objectN.StartTime - endTime > stackThreshold)
// We are no longer within stacking range of the next object. // We are no longer within stacking range of the next object.
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
OsuHitObject objectI = hitObjects[i]; OsuHitObject objectI = hitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue; if (objectI.StackHeight != 0 || objectI is Spinner) continue;
double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency;
/* If this object is a hitcircle, then we enter this "special" case. /* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(IBeatmap beatmap, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
for (int j = i + 1; j < hitObjects.Count; j++) for (int j = i + 1; j < hitObjects.Count; j++)
{ {
double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency;
if (hitObjects[j].StartTime - stackThreshold > startTime) if (hitObjects[j].StartTime - stackThreshold > startTime)
break; break;

View File

@ -38,6 +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,
}; };
/// <summary> /// <summary>
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 0f, MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.Y, MaxValue = OsuPlayfield.BASE_SIZE.Y,
Precision = 0.01f,
}; };
/// <summary> /// <summary>
@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 4f, MinValue = 4f,
MaxValue = 128f, MaxValue = 128f,
Precision = 0.01f,
}; };
/// <summary> /// <summary>
@ -65,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = -180f, MinValue = -180f,
MaxValue = 180f, MaxValue = 180f,
Precision = 0.01f,
}; };
/// <summary> /// <summary>
@ -166,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}, },
}; };
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; Spacing.Value = editorBeatmap.GridSize;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -200,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}"; spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
SpacingVector.Value = new Vector2(spacing.NewValue); SpacingVector.Value = new Vector2(spacing.NewValue);
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; editorBeatmap.GridSize = (int)spacing.NewValue;
}, true); }, true);
GridLinesRotation.BindValueChanged(rotation => GridLinesRotation.BindValueChanged(rotation =>

View File

@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{ {
Caption = "Stack Leniency", Caption = "Stack Leniency",
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) Current = new BindableFloat(Beatmap.StackLeniency)
{ {
Default = 0.7f, Default = 0.7f,
MinValue = 0, MinValue = 0,
@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; Beatmap.StackLeniency = stackLeniency.Current.Value;
Beatmap.UpdateAllHitObjects(); Beatmap.UpdateAllHitObjects();
Beatmap.SaveState(); Beatmap.SaveState();

View File

@ -63,18 +63,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-top"), Texture = source.GetTexture("spinner-top"),
}, },
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
spinningMiddle = new Sprite spinningMiddle = new Sprite
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle2"), Texture = source.GetTexture("spinner-middle2"),
}, },
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
} }
}); });

View File

@ -21,14 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
} }
/// <summary>
/// Gives a bonus for target ratio using a bell-shaped function.
/// </summary>
private static double bellCurve(double ratio, double targetRatio, double width, double multiplier)
{
return multiplier * Math.Exp(Math.E * -(Math.Pow(ratio - targetRatio, 2) / Math.Pow(width, 2)));
}
/// <summary> /// <summary>
/// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
/// </summary> /// </summary>
@ -45,10 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
difficulty += terms; difficulty += terms;
// Give bonus to near-1 ratios // Give bonus to near-1 ratios
difficulty += bellCurve(ratio, 1, 0.5, 1); difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
// Penalise ratios that are VERY near 1 // Penalize ratios that are VERY near 1
difficulty -= bellCurve(ratio, 1, 0.3, 1); difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
return difficulty / Math.Sqrt(8); return difficulty / Math.Sqrt(8);
} }

View File

@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
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.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
@ -215,8 +216,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double staminaPeak = staminaPeaks[i] * 0.0317; double staminaPeak = staminaPeaks[i] * 0.0317;
double readingPeak = readingPeaks[i] * reading_skill_multiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak); double peak = DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak, readingPeak); peak = DifficultyCalculationUtils.Norm(2, peak, rhythmPeak, readingPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty. // These sections will not contribute to the difficulty.
@ -263,12 +264,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return 10.43 * Math.Log(sr / 8 + 1); return 10.43 * Math.Log(sr / 8 + 1);
} }
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
} }
} }

View File

@ -144,6 +144,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
foreach (var nested in hitObject.NestedHitObjects) foreach (var nested in hitObject.NestedHitObjects)
simulateHit(nested, ref attributes); simulateHit(nested, ref attributes);
return; return;
case StrongNestedHitObject:
// we never need to deal with these directly.
// the only thing strong hits do in terms of scoring is double their object's score increase,
// which is already handled at the parent object level via the `strongable.IsStrong` check lower down in this method.
// not handling these here can lead to them falsely being counted as combo-increasing when handling strong drum rolls!
return;
} }
if (hitObject is DrumRollTick tick) if (hitObject is DrumRollTick tick)

View File

@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double n = totalHits; double n = totalHits;
// Proportion of greats + goods hit. // Proportion of greats + goods hit.
double p = Math.Max(0, totalSuccessfulHits - 0.1 * h100) / n; double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n;
// We can be 99% confident that p is at least this value. // We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);

View File

@ -80,16 +80,16 @@ namespace osu.Game.Tests.Beatmaps.Formats
var metadata = beatmap.Metadata; var metadata = beatmap.Metadata;
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmap.AudioLeadIn);
Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(164471, metadata.PreviewTime);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmap.StackLeniency);
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmap.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmap.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmap.WidescreenStoryboard);
Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); Assert.IsFalse(beatmap.SamplesMatchPlaybackRate);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmap.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmap.CountdownOffset);
} }
} }
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
{ {
var beatmapInfo = decoder.Decode(stream).BeatmapInfo; var beatmap = decoder.Decode(stream);
int[] expectedBookmarks = int[] expectedBookmarks =
{ {
@ -109,13 +109,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
95901, 106450, 116999, 119637, 130186, 140735, 151285, 95901, 106450, 116999, 119637, 130186, 140735, 151285,
161834, 164471, 175020, 185570, 196119, 206669, 209306 161834, 164471, 175020, 185570, 196119, 206669, 209306
}; };
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); Assert.AreEqual(expectedBookmarks.Length, beatmap.BeatmapInfo.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++) for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(expectedBookmarks[i], beatmap.BeatmapInfo.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); Assert.AreEqual(1.8, beatmap.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(4, beatmap.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom); Assert.AreEqual(2, beatmap.TimelineZoom);
} }
} }
@ -993,15 +993,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0)); Assert.That(decoded.AudioLeadIn, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f));
Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); Assert.That(decoded.SpecialStyle, Is.False);
Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); Assert.That(decoded.LetterboxInBreaks, Is.False);
Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.WidescreenStoryboard, Is.False);
Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.EpilepsyWarning, Is.False);
Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.SamplesMatchPlaybackRate, Is.False);
Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); Assert.That(decoded.Countdown, Is.EqualTo(CountdownType.None));
Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.CountdownOffset, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1));
Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0));
}); });

View File

@ -51,14 +51,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmap.AudioLeadIn);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmap.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.AreEqual(false, beatmap.SpecialStyle);
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmap.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(false, beatmap.WidescreenStoryboard);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmap.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmap.CountdownOffset);
} }
[Test] [Test]
@ -76,10 +76,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++) for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); Assert.AreEqual(1.8, beatmap.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(4, beatmap.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom); Assert.AreEqual(2, beatmap.TimelineZoom);
} }
[Test] [Test]

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database
Assert.That(lastChanges?.ModifiedIndices, Is.Empty); Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty); Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5); realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().EditorTimestamp = 5);
realm.Run(r => r.Refresh()); realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1)); Assert.That(collectionChanges, Is.EqualTo(1));

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; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
@ -64,6 +65,10 @@ namespace osu.Game.Tests
// Beatmap must be imported before the collection manager is loaded. // Beatmap must be imported before the collection manager is loaded.
if (withBeatmap) if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
// the logic for setting the initial ruleset exists in OsuGame rather than OsuGameBase.
// the ruleset bindable is not meant to be nullable, so assign any ruleset in here.
Ruleset.Value = RulesetStore.AvailableRulesets.First();
} }
} }
} }

View File

@ -20,12 +20,53 @@ namespace osu.Game.Tests.NonVisual.Ranking
public void TestDistributedHits() public void TestDistributedHits()
{ {
var events = Enumerable.Range(-5, 11) var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
var unstableRate = new UnstableRate(events); var unstableRate = new UnstableRate(events);
Assert.IsNotNull(unstableRate.Value); Assert.IsNotNull(unstableRate.Value);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); Assert.AreEqual(unstableRate.Value.Value, 10 * Math.Sqrt(10), Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncrementalRewind()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
result = events.GetRange(0, 2).CalculateUnstableRate(result);
Assert.IsNotNull(result!.Result);
Assert.AreEqual(5, result.Result, Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncremental()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
Assert.IsNotNull(result!.Result);
Assert.AreEqual(10 * Math.Sqrt(10), result.Result, Precision.DOUBLE_EPSILON);
} }
[Test] [Test]

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false); AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.None); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.None);
AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent); AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent);
} }
@ -65,12 +65,12 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal);
AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent); AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent);
AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed); AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.DoubleSpeed); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.DoubleSpeed);
AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent); AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent);
} }
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal);
checkOffsetAfter("1", 1); checkOffsetAfter("1", 1);
checkOffsetAfter(string.Empty, 0); checkOffsetAfter(string.Empty, 0);
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("commit text", () => InputManager.Key(Key.Enter)); AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture)); AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture));
AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.BeatmapInfo.CountdownOffset == expectedFinalValue); AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.CountdownOffset == expectedFinalValue);
} }
private partial class TestDesignSection : DesignSection private partial class TestDesignSection : DesignSection

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16); AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16);
AddStep("Set timeline zoom", () => AddStep("Set timeline zoom", () =>
{ {
originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; originalTimelineZoom = EditorBeatmap.TimelineZoom;
var timeline = Editor.ChildrenOfType<Timeline>().Single(); var timeline = Editor.ChildrenOfType<Timeline>().Single();
InputManager.MoveMouseTo(timeline); InputManager.MoveMouseTo(timeline);
@ -81,19 +81,19 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Ensure timeline zoom changed", () => AddAssert("Ensure timeline zoom changed", () =>
{ {
changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; changedTimelineZoom = EditorBeatmap.TimelineZoom;
return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom); return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom);
}); });
SaveEditor(); SaveEditor();
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom);
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom);
} }
[Test] [Test]

View File

@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
double originalSpacing = 0; double originalSpacing = 0;
AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.BeatmapInfo.DistanceSpacing); AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.DistanceSpacing);
AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl)); AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt)); AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt)); AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl)); AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); AddAssert("distance spacing increased by 0.5", () => editorBeatmap.DistanceSpacing == originalSpacing + 0.5);
} }
public partial class EditorBeatmapContainer : PopoverContainer public partial class EditorBeatmapContainer : PopoverContainer

View File

@ -527,8 +527,11 @@ namespace osu.Game.Tests.Visual.Editing
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}",
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
} }
[Test] [Test]
@ -781,15 +784,39 @@ namespace osu.Game.Tests.Visual.Editing
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
dismissPopover(); dismissPopover();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); assertNoChanges();
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); AddStep("select first object", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]);
});
assertNoChanges();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); AddStep("select second object", () =>
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); {
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]);
});
assertNoChanges();
AddStep("select first object", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]);
});
assertNoChanges();
void assertNoChanges()
{
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
}
} }
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
@ -883,11 +910,12 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.All(o => o.Volume == volume); return h.Samples.All(o => o.Volume == volume);
}); });
private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume);
});
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{ {
@ -944,29 +972,33 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
}); });
private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples);
});
private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () => private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}",
{ () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank);
});
private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1)); private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{ {
BeatmapInfo = { AudioLeadIn = leadIn } AudioLeadIn = leadIn
}); });
checkFirstFrameTime(expectedStartTime); checkFirstFrameTime(expectedStartTime);
@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} "
+ $"FirstHitObjectTime: {FirstHitObjectTime} " + $"FirstHitObjectTime: {FirstHitObjectTime} "
+ $"LeadInTime: {Beatmap.Value.BeatmapInfo.AudioLeadIn} " + $"LeadInTime: {Beatmap.Value.Beatmap.AudioLeadIn} "
+ $"FirstFrameClockTime: {FirstFrameClockTime}" + $"FirstFrameClockTime: {FirstFrameClockTime}"
}); });
} }

View File

@ -136,10 +136,10 @@ namespace osu.Game.Tests.Visual.Gameplay
var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// Add intro time to test quick retry skipping (TestQuickRetry). // Add intro time to test quick retry skipping (TestQuickRetry).
workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; workingBeatmap.Beatmap.AudioLeadIn = 60000;
// Set up data for testing disclaimer display. // Set up data for testing disclaimer display.
workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false; workingBeatmap.Beatmap.EpilepsyWarning = epilepsyWarning ?? false;
workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked; workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked;
Beatmap.Value = workingBeatmap; Beatmap.Value = workingBeatmap;

View File

@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{ {
BeatmapInfo = { AudioLeadIn = 60000 } AudioLeadIn = 60000
}); });
AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible); AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible);

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load storyboard with only video", () => AddStep("load storyboard with only video", () =>
{ {
// LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually // LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually
loadStoryboard("storyboard_only_video.osu", s => s.BeatmapInfo.WidescreenStoryboard = false); loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false);
}); });
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));

View File

@ -10,11 +10,13 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Login; using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.Online;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK.Input; using osuTK.Input;
@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved] [Resolved]
private OsuConfigManager configManager { get; set; } = null!; private OsuConfigManager configManager { get; set; } = null!;
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088"); AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online); assertAPIState(APIState.Online);
AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
AddStep("click on flag", () => AddStep("click on flag", () =>
{ {
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First()); InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());

View File

@ -3,8 +3,10 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus
((StarFountain)Children[1]).Shoot(-1); ((StarFountain)Children[1]).Shoot(-1);
}); });
} }
[Test]
public void TestGameplayStarFountainsSetting()
{
Bindable<bool> starFountainsEnabled = null!;
AddStep("load configuration", () =>
{
var config = new OsuConfigManager(LocalStorage);
starFountainsEnabled = config.GetBindable<bool>(OsuSetting.StarFountains);
});
AddStep("make fountains", () =>
{
Children = new Drawable[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
};
});
AddStep("enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("disable KiaiStarEffects", () => starFountainsEnabled.Value = false);
AddRepeatStep("attempt to activate fountains (disabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("re-enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (re-enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
}
} }
} }

View File

@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Gain", () => AddStep("Gain", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Loss", () => AddStep("Loss", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Tiny increase in PP", () => AddStep("Tiny increase in PP", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("No change 1", () => AddStep("No change 1", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Was null", () => AddStep("Was null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Became null", () => AddStep("Became null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {

View File

@ -406,13 +406,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
/// <summary> /// <summary>
/// Tests spectating with a beatmap that has a high <see cref="BeatmapInfo.AudioLeadIn"/> value. /// Tests spectating with a beatmap that has a high <see cref="IBeatmap.AudioLeadIn"/> value.
/// ///
/// This test is not intended not to check the correct initial time value, but only to guard against /// This test is not intended not to check the correct initial time value, but only to guard against
/// gameplay potentially getting stuck in a stopped state due to lead in time being present. /// gameplay potentially getting stuck in a stopped state due to lead in time being present.
/// </summary> /// </summary>
[Test] [Test]
public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000); public void TestAudioLeadIn() => testLeadIn(b => b.Beatmap.AudioLeadIn = 2000);
/// <summary> /// <summary>
/// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element). /// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element).

View File

@ -354,6 +354,23 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("retry count is 1", () => player.RestartCount == 1); AddAssert("retry count is 1", () => player.RestartCount == 1);
} }
[Test]
public void TestLastScoreNullAfterExitingPlayer()
{
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
var getOriginalPlayer = playToCompletion();
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
}
[Test] [Test]
public void TestRetryImmediatelyAfterCompletion() public void TestRetryImmediatelyAfterCompletion()
{ {

View File

@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible(); waitForChannel1Visible();
} }
[Test]
public void TestPublicChannelsSortedByName()
{
// Intentionally join back to front.
AddStep("Show overlay with channel 2", () =>
{
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel2);
chatOverlay.Show();
});
AddUntilStep("second channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel2);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddUntilStep("first channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
AddStep("message in channel 2", () =>
{
testChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("first channel still at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
ChannelListItem getFirstVisiblePublicChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PublicChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.Public);
}
[Test]
public void TestPrivateChannelsSortedByRecent()
{
Channel pmChannel1 = createPrivateChannel();
Channel pmChannel2 = createPrivateChannel();
joinChannel(pmChannel1);
joinChannel(pmChannel2);
AddStep("Show overlay", () => chatOverlay.Show());
AddUntilStep("first channel is at top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
AddStep("message in channel 2", () =>
{
pmChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel2);
AddStep("message in channel 1", () =>
{
pmChannel1.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
ChannelListItem getFirstVisiblePMChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PrivateChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.PM);
}
[Test] [Test]
public void TestKeyboardNewChannel() public void TestKeyboardNewChannel()
{ {

View File

@ -0,0 +1,179 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneLocalUserStatisticsProvider : OsuTestScene
{
private LocalUserStatisticsProvider statisticsProvider = null!;
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear statistics", () => serverSideStatistics.Clear());
setUser(1000);
AddStep("setup provider", () =>
{
OsuTextFlowContainer text;
((DummyAPIAccess)API).HandleRequest = r =>
{
switch (r)
{
case GetUserRequest userRequest:
int userId = int.Parse(userRequest.Lookup);
string rulesetName = userRequest.Ruleset!.ShortName;
var response = new APIUser
{
Id = userId,
Statistics = tryGetStatistics(userId, rulesetName)
};
userRequest.TriggerSuccess(response);
return true;
default:
return false;
}
};
Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(text = new OsuTextFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
statisticsProvider.StatisticsUpdated += update =>
{
text.Clear();
foreach (var ruleset in Dependencies.Get<RulesetStore>().AvailableRulesets)
{
text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics
? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})"
: $"{ruleset.Name} statistics: (null)");
text.NewLine();
}
text.AddText($"latest update: {update.Ruleset}"
+ $" ({(update.OldStatistics?.TotalScore.ToString() ?? "null")} -> {update.NewStatistics.TotalScore})");
};
Ruleset.Value = new OsuRuleset().RulesetInfo;
});
}
[Test]
public void TestInitialStatistics()
{
AddAssert("osu statistics populated", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("taiko statistics populated", () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(3_000_000));
AddAssert("catch statistics populated", () => statisticsProvider.GetStatisticsFor(new CatchRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(2_000_000));
AddAssert("mania statistics populated", () => statisticsProvider.GetStatisticsFor(new ManiaRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(1_000_000));
}
[Test]
public void TestUserChanges()
{
setUser(1001);
AddStep("update statistics for user 1000", () =>
{
serverSideStatistics[(1000, "osu")] = new UserStatistics { TotalScore = 5_000_000 };
serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 };
});
AddAssert("statistics matches user 1001 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));
AddAssert("statistics matches user 1001 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(3_000_000));
setUser(1000, false);
AddAssert("statistics matches user 1000 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(5_000_000));
AddAssert("statistics matches user 1000 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(6_000_000));
}
[Test]
public void TestRefetchStatistics()
{
UserStatisticsUpdate? update = null;
setUser(1001);
AddStep("update statistics server side",
() => serverSideStatistics[(1001, "osu")] = new UserStatistics { TotalScore = 9_000_000 });
AddAssert("statistics match old score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));
AddStep("setup event", () =>
{
update = null;
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
});
AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo));
AddUntilStep("statistics update raised",
() => update?.NewStatistics.TotalScore,
() => Is.EqualTo(9_000_000));
AddAssert("statistics match new score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(9_000_000));
void onStatisticsUpdated(UserStatisticsUpdate u) => update = u;
}
private UserStatistics tryGetStatistics(int userId, string rulesetName)
=> serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
private void setUser(int userId, bool generateStatistics = true)
{
AddStep($"set local user to {userId}", () =>
{
if (generateStatistics)
{
serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
}
((DummyAPIAccess)API).LocalUser.Value = new APIUser { Id = userId };
});
}
}
}

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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -11,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -24,17 +23,20 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public partial class TestSceneUserPanel : OsuTestScene public partial class TestSceneUserPanel : OsuTestScene
{ {
private readonly Bindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly Bindable<UserActivity?> activity = new Bindable<UserActivity?>();
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>(); private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
private UserGridPanel boundPanel1; private UserGridPanel boundPanel1 = null!;
private TestUserListPanel boundPanel2; private TestUserListPanel boundPanel2 = null!;
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider();
[Resolved] [Resolved]
private IRulesetStore rulesetStore { get; set; } private IRulesetStore rulesetStore { get; set; } = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -42,7 +44,11 @@ namespace osu.Game.Tests.Visual.Online
activity.Value = null; activity.Value = null;
status.Value = null; status.Value = null;
Child = new FillFlowContainer Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);
Add(new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -108,7 +114,7 @@ namespace osu.Game.Tests.Visual.Online
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 } }) { Width = 300 }
} }
}; });
boundPanel1.Status.BindTo(status); boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity); boundPanel1.Activity.BindTo(activity);
@ -162,24 +168,21 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("update statistics", () => AddStep("update statistics", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(100000), GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000) CountryRank = RNG.Next(100000)
}); }, Ruleset.Value);
}); });
AddStep("set statistics to something big", () => AddStep("set statistics to something big", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(1_000_000, 100_000_000), GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000) CountryRank = RNG.Next(1_000_000, 100_000_000)
}); }, Ruleset.Value);
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
}); });
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
} }
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
@ -201,5 +204,11 @@ namespace osu.Game.Tests.Visual.Online
public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; public new TextFlowContainer LastVisitMessage => base.LastVisitMessage;
} }
public partial class TestUserStatisticsProvider : LocalUserStatisticsProvider
{
public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
=> base.UpdateStatistics(newStatistics, ruleset, callback);
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v));
AddStep("create", () => AddStep("create", () =>
{ {
Clear(); Clear();
@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestPlayCountRankingTier() public void TestPlayCountRankingTier()
{ {
AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(29) == RankingTier.Bronze);
AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Silver);
} }
} }
} }

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; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -58,6 +59,16 @@ namespace osu.Game.Tests.Visual.Online
return true; return true;
} }
if (req is GetUserBeatmapsRequest getUserBeatmapsRequest)
{
getUserBeatmapsRequest.TriggerSuccess(new List<APIBeatmapSet>
{
CreateAPIBeatmapSet(),
CreateAPIBeatmapSet()
});
return true;
}
return false; return false;
}; };
}); });

View File

@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
protected override bool UseOnlineAPI => false; protected override bool UseOnlineAPI => false;
private LocalUserStatisticsProvider statisticsProvider = null!;
private UserStatisticsWatcher watcher = null!; private UserStatisticsWatcher watcher = null!;
[Resolved] [Resolved]
@ -107,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online
AddStep("create watcher", () => AddStep("create watcher", () =>
{ {
Child = watcher = new UserStatisticsWatcher(); Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(watcher = new UserStatisticsWatcher(statisticsProvider));
}); });
} }
@ -123,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -146,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online
// note ordering - in this test processing completes *before* the registration is added. // note ordering - in this test processing completes *before* the registration is added.
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
@ -164,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -212,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -241,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online
feignScoreProcessing(userId, ruleset, 6_000_000); feignScoreProcessing(userId, ruleset, 6_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
@ -259,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null); AddUntilStep("update received", () => update != null);
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000));
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
} }
private int nextUserId = 2000; private int nextUserId = 2000;
@ -289,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<UserStatisticsUpdate> onUpdateReady) => private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<ScoreBasedUserStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => AddStep("register for updates", () =>
{ {
watcher.RegisterForStatisticsUpdateAfter( watcher.RegisterForStatisticsUpdateAfter(

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net; using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists
private const int scores_per_result = 10; private const int scores_per_result = 10;
private const int real_user_position = 200; private const int real_user_position = 200;
private TestResultsScreen resultsScreen = null!; private ResultsScreen resultsScreen = null!;
private int lowestScoreId; // Score ID of the lowest score in the list. private int lowestScoreId; // Score ID of the lowest score in the list.
private int highestScoreId; // Score ID of the highest score in the list. private int highestScoreId; // Score ID of the highest score in the list.
@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowWithUserScore() public void TestShowUserScore()
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScore() public void TestShowUserBest()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createUserBestResults();
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position);
}
[Test]
public void TestShowNonUserScores()
{ {
AddStep("bind user score info handler", () => bindHandler()); AddStep("bind user score info handler", () => bindHandler());
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind user score info handler", () => bindHandler(true, userScore)); AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScoreWithDelay() public void TestShowNonUserScoresWithDelay()
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
} }
} }
@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount); AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero); AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
} }
@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false)); AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); AddAssert("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
} }
} }
/// <summary>
/// Shows the <see cref="TestUserBestResultsScreen"/> with no scores provided by the API.
/// </summary>
[Test] [Test]
public void TestShowWithNoScores() public void TestShowUserBestWithNoScoresPresent()
{ {
AddStep("bind user score info handler", () => bindHandler(noScores: true)); AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults(); createUserBestResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1)); AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
} }
private void createResults(Func<ScoreInfo>? getScore = null) private void createResultsWithScore(Func<ScoreInfo> getScore)
{ {
AddStep("load results", () => AddStep("load results", () =>
{ {
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID RulesetID = new OsuRuleset().RulesetInfo.OnlineID
})); }));
@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
} }
private void createUserBestResults()
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}, 2));
});
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for scores loaded", () => AddUntilStep("wait for scores loaded", () =>
requestComplete requestComplete
// request handler may need to fire more than once to get scores. // request handler may need to fire more than once to get scores.
&& totalCount > 0 && totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible); && resultsScreen.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);
} }
@ -231,6 +274,7 @@ namespace osu.Game.Tests.Visual.Playlists
// pre-check for requests we should be handling (as they are scheduled below). // pre-check for requests we should be handling (as they are scheduled below).
switch (request) switch (request)
{ {
case ShowPlaylistScoreRequest:
case ShowPlaylistUserScoreRequest: case ShowPlaylistUserScoreRequest:
case IndexPlaylistScoresRequest: case IndexPlaylistScoresRequest:
break; break;
@ -253,7 +297,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request) switch (request)
{ {
case ShowPlaylistUserScoreRequest s: case ShowPlaylistScoreRequest s:
if (userScore == null) if (userScore == null)
triggerFail(s); triggerFail(s);
else else
@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists
break; break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
triggerFail(u);
else
triggerSuccess(u, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i: case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i, noScores)); triggerSuccess(i, createIndexResponse(i, noScores));
break; break;
@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = 1000, MaxCombo = 1000,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -410,18 +462,22 @@ namespace osu.Game.Tests.Visual.Playlists
}; };
} }
private partial class TestResultsScreen : PlaylistItemUserResultsScreen private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen
{ {
public new LoadingSpinner LeftSpinner => base.LeftSpinner; public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
public TestResultsScreen(ScoreInfo? score, int roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem) : base(score, roomId, playlistItem)
{ {
AllowRetry = true; AllowRetry = true;
} }
} }
private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen
{
public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId)
: base(roomId, playlistItem, userId)
{
AllowRetry = true;
}
}
} }
} }

View File

@ -2,20 +2,67 @@
// 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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists namespace osu.Game.Tests.Visual.Playlists
{ {
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{ {
private const double track_length = 10000;
[Resolved]
private IAPIProvider api { get; set; } = null!;
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private BeatmapManager beatmaps = null!;
private RulesetStore rulesets = null!;
private BeatmapSetInfo? importedSet;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API));
Dependencies.Cache(Realm);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var set in r.All<BeatmapSetInfo>())
{
foreach (var b in set.Beatmaps)
{
// These will all have a virtual track length of 1000, see WorkingBeatmap.GetVirtualTrack().
b.Length = track_length - 1000;
}
}
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
}
[Test] [Test]
public void TestStatusUpdateOnEnter() public void TestStatusUpdateOnEnter()
{ {
@ -37,5 +84,66 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>); AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>);
} }
[Test]
public void TestCloseButtonGoesAwayAfterGracePeriod()
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3),
EndDate = DateTimeOffset.Now.AddMinutes(30)
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
}
[TestCase(120_000, true)] // Definitely enough time.
[TestCase(45_000, true)] // Enough time.
[TestCase(35_000, false)] // Not enough time to complete beatmap after lenience.
[TestCase(20_000, false)] // Not enough time.
[TestCase(5_000, false)] // Not enough time to complete beatmap before lenience.
[TestCase(37_500, true, 2)] // Enough time to complete beatmap after mods are applied.
public void TestReadyButtonEnablementPeriod(int offsetMs, bool enabled, double rate = 1)
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now,
EndDate = DateTimeOffset.Now.AddMilliseconds(offsetMs),
Playlist =
[
new PlaylistItem(importedSet!.Beatmaps[0])
{
RequiredMods = rate == 1
? []
: [new APIMod(new OsuModDoubleTime { SpeedChange = { Value = rate } })]
}
]
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddUntilStep("ready button enabled", () => roomScreen.ChildrenOfType<PlaylistsReadyButton>().SingleOrDefault()?.Enabled.Value, () => Is.EqualTo(enabled));
}
} }
} }

View File

@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking
}); });
private void displayUpdate(UserStatistics before, UserStatistics after) => private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after));
} }
} }

View File

@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking
UserStatisticsWatcher userStatisticsWatcher = null!; UserStatisticsWatcher userStatisticsWatcher = null!;
ScoreInfo score = null!; ScoreInfo score = null!;
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider())));
AddStep("set user statistics update", () => AddStep("set user statistics update", () =>
{ {
score = TestResources.CreateTestScoreInfo(); score = TestResources.CreateTestScoreInfo();
score.OnlineID = 1234; score.OnlineID = 1234;
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, ((Bindable<ScoreBasedUserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score,
new UserStatistics new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking
Score = { Value = score }, Score = { Value = score },
DisplayedUserStatisticsUpdate = DisplayedUserStatisticsUpdate =
{ {
Value = new UserStatisticsUpdate(score, new UserStatistics Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
{ {

View File

@ -8,14 +8,14 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
@ -28,25 +28,31 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
public partial class TestSceneBeatmapRecommendations : OsuGameTestScene public partial class TestSceneBeatmapRecommendations : OsuGameTestScene
{ {
[Resolved]
private IRulesetStore rulesetStore { get; set; }
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
AddStep("populate ruleset statistics", () => AddStep("populate ruleset statistics", () =>
{ {
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>(); ((DummyAPIAccess)API).HandleRequest = r =>
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{ {
rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics switch (r)
{ {
PP = getNecessaryPP(rulesetInfo.OnlineID) case GetUserRequest userRequest:
}; userRequest.TriggerSuccess(new APIUser
}); {
Id = 99,
Statistics = new UserStatistics
{
PP = getNecessaryPP(userRequest.Ruleset?.OnlineID ?? 0)
}
});
API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; return true;
default:
return false;
}
};
}); });
decimal getNecessaryPP(int? rulesetID) decimal getNecessaryPP(int? rulesetID)

View File

@ -115,6 +115,30 @@ namespace osu.Game.Beatmaps
return mostCommon.beatLength; return mostCommon.beatLength;
} }
public double AudioLeadIn { get; set; }
public float StackLeniency { get; set; } = 0.7f;
public bool SpecialStyle { get; set; }
public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; } = true;
public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; }
public double DistanceSpacing { get; set; } = 1.0;
public int GridSize { get; set; }
public double TimelineZoom { get; set; } = 1.0;
public CountdownType Countdown { get; set; } = CountdownType.None;
public int CountdownOffset { get; set; }
IBeatmap IBeatmap.Clone() => Clone(); IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone(); public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();

View File

@ -73,6 +73,18 @@ namespace osu.Game.Beatmaps
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks; beatmap.Breaks = original.Breaks;
beatmap.UnhandledEventLines = original.UnhandledEventLines; beatmap.UnhandledEventLines = original.UnhandledEventLines;
beatmap.AudioLeadIn = original.AudioLeadIn;
beatmap.StackLeniency = original.StackLeniency;
beatmap.SpecialStyle = original.SpecialStyle;
beatmap.LetterboxInBreaks = original.LetterboxInBreaks;
beatmap.WidescreenStoryboard = original.WidescreenStoryboard;
beatmap.EpilepsyWarning = original.EpilepsyWarning;
beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate;
beatmap.DistanceSpacing = original.DistanceSpacing;
beatmap.GridSize = original.GridSize;
beatmap.TimelineZoom = original.TimelineZoom;
beatmap.Countdown = original.Countdown;
beatmap.CountdownOffset = original.CountdownOffset;
return beatmap; return beatmap;
} }

View File

@ -428,17 +428,7 @@ namespace osu.Game.Beatmaps
Hash = hash, Hash = hash,
DifficultyName = decodedInfo.DifficultyName, DifficultyName = decodedInfo.DifficultyName,
OnlineID = decodedInfo.OnlineID, OnlineID = decodedInfo.OnlineID,
AudioLeadIn = decodedInfo.AudioLeadIn,
StackLeniency = decodedInfo.StackLeniency,
SpecialStyle = decodedInfo.SpecialStyle,
LetterboxInBreaks = decodedInfo.LetterboxInBreaks,
WidescreenStoryboard = decodedInfo.WidescreenStoryboard,
EpilepsyWarning = decodedInfo.EpilepsyWarning,
SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate,
DistanceSpacing = decodedInfo.DistanceSpacing,
BeatDivisor = decodedInfo.BeatDivisor, BeatDivisor = decodedInfo.BeatDivisor,
GridSize = decodedInfo.GridSize,
TimelineZoom = decodedInfo.TimelineZoom,
MD5Hash = memoryStream.ComputeMD5Hash(), MD5Hash = memoryStream.ComputeMD5Hash(),
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration), EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = decoded.HitObjects.Count TotalObjectCount = decoded.HitObjects.Count

View File

@ -6,14 +6,12 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Scoring; using osu.Game.Scoring;
using Realms; using Realms;
@ -136,60 +134,18 @@ namespace osu.Game.Beatmaps
Status = BeatmapOnlineStatus.None; Status = BeatmapOnlineStatus.None;
} }
#region Properties we may not want persisted (but also maybe no harm?)
public double AudioLeadIn { get; set; }
public float StackLeniency { get; set; } = 0.7f;
public bool SpecialStyle { get; set; }
public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; } = true;
public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; } = true;
/// <summary> /// <summary>
/// The time at which this beatmap was last played by the local user. /// The time at which this beatmap was last played by the local user.
/// </summary> /// </summary>
public DateTimeOffset? LastPlayed { get; set; } public DateTimeOffset? LastPlayed { get; set; }
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
public double DistanceSpacing { get; set; } = 1.0;
public int BeatDivisor { get; set; } = 4; public int BeatDivisor { get; set; } = 4;
public int GridSize { get; set; }
public double TimelineZoom { get; set; } = 1.0;
/// <summary> /// <summary>
/// The time in milliseconds when last exiting the editor with this beatmap loaded. /// The time in milliseconds when last exiting the editor with this beatmap loaded.
/// </summary> /// </summary>
public double? EditorTimestamp { get; set; } public double? EditorTimestamp { get; set; }
[Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal;
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).
/// </summary>
public int CountdownOffset { get; set; }
#endregion
public bool Equals(BeatmapInfo? other) public bool Equals(BeatmapInfo? other)
{ {
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;

View File

@ -408,7 +408,7 @@ namespace osu.Game.Beatmaps
// user requested abort // user requested abort
return; return;
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); var video = b.Files.FirstOrDefault(f => SupportedExtensions.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null) if (video != null)
{ {
@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps
// If we seem to be missing files, now is a good time to re-fetch. // If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0; bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0;
if (refetch || beatmapInfo.IsManaged || missingFiles) if (beatmapInfo.IsManaged)
{
beatmapInfo = beatmapInfo.Detach();
}
else if (refetch || missingFiles)
{ {
Guid id = beatmapInfo.ID; Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo; beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;

View File

@ -9,9 +9,11 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API; using osu.Game.Online;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -21,18 +23,63 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public partial class DifficultyRecommender : Component public partial class DifficultyRecommender : Component
{ {
[Resolved] private readonly LocalUserStatisticsProvider statisticsProvider;
private IAPIProvider api { get; set; }
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } private Bindable<RulesetInfo> gameRuleset { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>(); private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>();
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || gameRuleset.Value == null)
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(gameRuleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(gameRuleset.Value.ShortName);
}
}
public DifficultyRecommender(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
api.LocalUser.BindValueChanged(_ => populateValues(), true); foreach (var ruleset in rulesets.AvailableRulesets)
{
if (statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics)
updateMapping(ruleset, statistics);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
}
private void onStatisticsUpdated(UserStatisticsUpdate update) => updateMapping(update.Ruleset, update.NewStatistics);
private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
} }
/// <summary> /// <summary>
@ -64,35 +111,12 @@ namespace osu.Game.Beatmaps
return null; return null;
} }
private void populateValues() protected override void Dispose(bool isDisposing)
{ {
if (api.LocalUser.Value.RulesetsStatistics == null) if (statisticsProvider.IsNotNull())
return; statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
foreach (var kvp in api.LocalUser.Value.RulesetsStatistics) base.Dispose(isDisposing);
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195;
}
}
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(ruleset.Value.ShortName);
}
} }
} }
} }

View File

@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables
}; };
Status = BeatmapOnlineStatus.None; Status = BeatmapOnlineStatus.None;
TextPadding = new MarginPadding { Horizontal = 5, Bottom = 1 }; TextPadding = new MarginPadding { Horizontal = 4, Bottom = 1 };
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -20,9 +20,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{ {
public const float TRANSITION_DURATION = 340; public const float TRANSITION_DURATION = 340;
public const float CORNER_RADIUS = 10; public const float CORNER_RADIUS = 8;
protected const float WIDTH = 430; protected const float WIDTH = 345;
public IBindable<bool> Expanded { get; } public IBindable<bool> Expanded { get; }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override Drawable IdleContent => idleBottomContent; protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar; protected override Drawable DownloadInProgressContent => downloadProgressBar;
private const float height = 140; private const float height = 112;
[Cached] [Cached]
private readonly BeatmapCardContent content; private readonly BeatmapCardContent content;
@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Padding = new MarginPadding { Right = CORNER_RADIUS }, Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer Child = leftIconArea = new FillFlowContainer
{ {
Margin = new MarginPadding(5), Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(1) Spacing = new Vector2(1)
@ -80,7 +80,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Width = WIDTH - height + CORNER_RADIUS, Width = WIDTH - height + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState }, FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30, ButtonsExpandedWidth = 24,
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
titleBadgeArea = new FillFlowContainer titleBadgeArea = new FillFlowContainer
@ -142,7 +142,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = createArtistText(), Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
Empty() Empty()
@ -154,7 +154,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = BeatmapSet.Source, Text = BeatmapSet.Source,
Shadow = false, Shadow = false,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold),
Colour = colourProvider.Content2 Colour = colourProvider.Content2
}, },
} }
@ -173,18 +173,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3), Spacing = new Vector2(0, 2),
AlwaysPresent = true, AlwaysPresent = true,
Children = new Drawable[] Children = new Drawable[]
{ {
new LinkFlowContainer(s => new LinkFlowContainer(s =>
{ {
s.Shadow = false; s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d => }).With(d =>
{ {
d.AutoSizeAxes = Axes.Both; d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 }; d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author); d.AddUserLink(BeatmapSet.Author);
}), }),
@ -215,7 +215,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
downloadProgressBar = new BeatmapCardDownloadProgressBar downloadProgressBar = new BeatmapCardDownloadProgressBar
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 6, Height = 5,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State }, State = { BindTarget = DownloadTracker.State },
@ -231,17 +231,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, Padding = new MarginPadding { Horizontal = 8, Vertical = 10 },
Child = new BeatmapCardDifficultyList(BeatmapSet) Child = new BeatmapCardDifficultyList(BeatmapSet)
}; };
c.Expanded.BindTarget = Expanded; c.Expanded.BindTarget = Expanded;
}); });
if (BeatmapSet.HasVideo) if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard) if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight) if (BeatmapSet.FeaturedInSpotlight)
{ {
@ -249,7 +249,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -259,7 +259,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}; };
} }
@ -288,7 +288,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
BeatmapCardStatistic withMargin(BeatmapCardStatistic original) BeatmapCardStatistic withMargin(BeatmapCardStatistic original)
{ {
original.Margin = new MarginPadding { Right = 10 }; original.Margin = new MarginPadding { Right = 8 };
return original; return original;
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0), Spacing = new Vector2(3, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
new BeatmapSetOnlineStatusPill new BeatmapSetOnlineStatusPill
@ -33,13 +33,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status, Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft Origin = Anchor.CentreLeft,
TextSize = 13f
}, },
new DifficultySpectrumDisplay(beatmapSet) new DifficultySpectrumDisplay(beatmapSet)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
DotSize = new Vector2(6, 12) DotSize = new Vector2(5, 10)
} }
} }
}; };

View File

@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override Drawable IdleContent => idleBottomContent; protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar; protected override Drawable DownloadInProgressContent => downloadProgressBar;
public const float HEIGHT = 100; public const float HEIGHT = 80;
[Cached] [Cached]
private readonly BeatmapCardContent content; private readonly BeatmapCardContent content;
@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Padding = new MarginPadding { Right = CORNER_RADIUS }, Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer Child = leftIconArea = new FillFlowContainer
{ {
Margin = new MarginPadding(5), Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(1) Spacing = new Vector2(1)
@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Width = WIDTH - HEIGHT + CORNER_RADIUS, Width = WIDTH - HEIGHT + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState }, FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30, ButtonsExpandedWidth = 24,
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
titleBadgeArea = new FillFlowContainer titleBadgeArea = new FillFlowContainer
@ -143,7 +143,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = createArtistText(), Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
Empty() Empty()
@ -153,11 +153,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new LinkFlowContainer(s => new LinkFlowContainer(s =>
{ {
s.Shadow = false; s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d => }).With(d =>
{ {
d.AutoSizeAxes = Axes.Both; d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 }; d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author); d.AddUserLink(BeatmapSet.Author);
}), }),
@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3), Spacing = new Vector2(0, 2),
AlwaysPresent = true, AlwaysPresent = true,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0), Spacing = new Vector2(8, 0),
Alpha = 0, Alpha = 0,
AlwaysPresent = true, AlwaysPresent = true,
ChildrenEnumerable = createStatistics() ChildrenEnumerable = createStatistics()
@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
downloadProgressBar = new BeatmapCardDownloadProgressBar downloadProgressBar = new BeatmapCardDownloadProgressBar
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 6, Height = 5,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State }, State = { BindTarget = DownloadTracker.State },
@ -213,17 +213,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, Padding = new MarginPadding { Horizontal = 8, Vertical = 10 },
Child = new BeatmapCardDifficultyList(BeatmapSet) Child = new BeatmapCardDifficultyList(BeatmapSet)
}; };
c.Expanded.BindTarget = Expanded; c.Expanded.BindTarget = Expanded;
}); });
if (BeatmapSet.HasVideo) if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard) if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight) if (BeatmapSet.FeaturedInSpotlight)
{ {
@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -241,7 +241,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}; };
} }
} }

View File

@ -46,21 +46,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0), Spacing = new Vector2(4, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
spriteIcon = new SpriteIcon spriteIcon = new SpriteIcon
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Size = new Vector2(10), Size = new Vector2(8),
Margin = new MarginPadding { Top = 1 } Margin = new MarginPadding { Top = 1 }
}, },
spriteText = new OsuSpriteText spriteText = new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.Default.With(size: 14) Font = OsuFont.Default.With(size: 11)
} }
} }
}; };

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Utils;
namespace osu.Game.Beatmaps.Formats namespace osu.Game.Beatmaps.Formats
{ {
@ -81,7 +82,7 @@ namespace osu.Game.Beatmaps.Formats
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion); parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
applyLegacyDefaults(this.beatmap.BeatmapInfo); ApplyLegacyDefaults(this.beatmap);
base.ParseStreamInto(stream, beatmap); base.ParseStreamInto(stream, beatmap);
@ -189,10 +190,9 @@ namespace osu.Game.Beatmaps.Formats
/// This method's intention is to restore those legacy defaults. /// This method's intention is to restore those legacy defaults.
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary> /// </summary>
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) internal static void ApplyLegacyDefaults(Beatmap beatmap)
{ {
beatmapInfo.WidescreenStoryboard = false; beatmap.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false;
} }
protected override void ParseLine(Beatmap beatmap, Section section, string line) protected override void ParseLine(Beatmap beatmap, Section section, string line)
@ -244,7 +244,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"AudioLeadIn": case @"AudioLeadIn":
beatmap.BeatmapInfo.AudioLeadIn = Parsing.ParseInt(pair.Value); beatmap.AudioLeadIn = Parsing.ParseInt(pair.Value);
break; break;
case @"PreviewTime": case @"PreviewTime":
@ -261,7 +261,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"StackLeniency": case @"StackLeniency":
beatmap.BeatmapInfo.StackLeniency = Parsing.ParseFloat(pair.Value); beatmap.StackLeniency = Parsing.ParseFloat(pair.Value);
break; break;
case @"Mode": case @"Mode":
@ -269,31 +269,31 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"LetterboxInBreaks": case @"LetterboxInBreaks":
beatmap.BeatmapInfo.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1; beatmap.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"SpecialStyle": case @"SpecialStyle":
beatmap.BeatmapInfo.SpecialStyle = Parsing.ParseInt(pair.Value) == 1; beatmap.SpecialStyle = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"WidescreenStoryboard": case @"WidescreenStoryboard":
beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; beatmap.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"EpilepsyWarning": case @"EpilepsyWarning":
beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; beatmap.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"SamplesMatchPlaybackRate": case @"SamplesMatchPlaybackRate":
beatmap.BeatmapInfo.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1; beatmap.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"Countdown": case @"Countdown":
beatmap.BeatmapInfo.Countdown = Enum.Parse<CountdownType>(pair.Value); beatmap.Countdown = Enum.Parse<CountdownType>(pair.Value);
break; break;
case @"CountdownOffset": case @"CountdownOffset":
beatmap.BeatmapInfo.CountdownOffset = Parsing.ParseInt(pair.Value); beatmap.CountdownOffset = Parsing.ParseInt(pair.Value);
break; break;
} }
} }
@ -313,7 +313,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"DistanceSpacing": case @"DistanceSpacing":
beatmap.BeatmapInfo.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value)); beatmap.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value));
break; break;
case @"BeatDivisor": case @"BeatDivisor":
@ -321,11 +321,11 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"GridSize": case @"GridSize":
beatmap.BeatmapInfo.GridSize = Parsing.ParseInt(pair.Value); beatmap.GridSize = Parsing.ParseInt(pair.Value);
break; break;
case @"TimelineZoom": case @"TimelineZoom":
beatmap.BeatmapInfo.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value)); beatmap.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value));
break; break;
} }
} }
@ -447,7 +447,7 @@ namespace osu.Game.Beatmaps.Formats
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match. // video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{ {
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true; lineSupportedByEncoder = true;

View File

@ -79,14 +79,14 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[General]"); writer.WriteLine("[General]");
if (!string.IsNullOrEmpty(beatmap.Metadata.AudioFile)) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); if (!string.IsNullOrEmpty(beatmap.Metadata.AudioFile)) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}"));
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.Countdown}"));
writer.WriteLine(FormattableString.Invariant( writer.WriteLine(FormattableString.Invariant(
$"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.StackLeniency}"));
writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}"));
writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.LetterboxInBreaks ? '1' : '0')}"));
// if (beatmap.BeatmapInfo.UseSkinSprites) // if (beatmap.BeatmapInfo.UseSkinSprites)
// writer.WriteLine(@"UseSkinSprites: 1"); // writer.WriteLine(@"UseSkinSprites: 1");
// if (b.AlwaysShowPlayfield) // if (b.AlwaysShowPlayfield)
@ -95,14 +95,14 @@ namespace osu.Game.Beatmaps.Formats
// writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition); // writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition);
// if (!string.IsNullOrEmpty(b.SkinPreference)) // if (!string.IsNullOrEmpty(b.SkinPreference))
// writer.WriteLine(@"SkinPreference:" + b.SkinPreference); // writer.WriteLine(@"SkinPreference:" + b.SkinPreference);
if (beatmap.BeatmapInfo.EpilepsyWarning) if (beatmap.EpilepsyWarning)
writer.WriteLine(@"EpilepsyWarning: 1"); writer.WriteLine(@"EpilepsyWarning: 1");
if (beatmap.BeatmapInfo.CountdownOffset > 0) if (beatmap.CountdownOffset > 0)
writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.CountdownOffset}"));
if (onlineRulesetID == 3) if (onlineRulesetID == 3)
writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}"));
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}"));
if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate) if (beatmap.SamplesMatchPlaybackRate)
writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); writer.WriteLine(@"SamplesMatchPlaybackRate: 1");
} }
@ -112,10 +112,10 @@ namespace osu.Game.Beatmaps.Formats
if (beatmap.BeatmapInfo.Bookmarks.Length > 0) if (beatmap.BeatmapInfo.Bookmarks.Length > 0)
writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}"));
writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.BeatmapInfo.DistanceSpacing}")); writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}"));
writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}"));
writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.BeatmapInfo.GridSize}")); writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.GridSize}"));
writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.TimelineZoom}"));
} }
private void handleMetadata(TextWriter writer) private void handleMetadata(TextWriter writer)

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands; using osu.Game.Storyboards.Commands;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -37,6 +38,17 @@ namespace osu.Game.Beatmaps.Formats
SetFallbackDecoder<Storyboard>(() => new LegacyStoryboardDecoder()); SetFallbackDecoder<Storyboard>(() => new LegacyStoryboardDecoder());
} }
protected override Storyboard CreateTemplateObject()
{
var sb = base.CreateTemplateObject();
var beatmap = new Beatmap();
LegacyBeatmapDecoder.ApplyLegacyDefaults(beatmap);
sb.Beatmap = beatmap;
return sb;
}
protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard) protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard)
{ {
this.storyboard = storyboard; this.storyboard = storyboard;
@ -72,6 +84,10 @@ namespace osu.Game.Beatmaps.Formats
case "UseSkinSprites": case "UseSkinSprites":
storyboard.UseSkinSprites = pair.Value == "1"; storyboard.UseSkinSprites = pair.Value == "1";
break; break;
case @"WidescreenStoryboard":
storyboard.Beatmap.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break;
} }
} }
@ -112,7 +128,7 @@ namespace osu.Game.Beatmaps.Formats
// //
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break; break;
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset)); storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset));

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -69,6 +70,43 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
double GetMostCommonBeatLength(); double GetMostCommonBeatLength();
double AudioLeadIn { get; internal set; }
float StackLeniency { get; internal set; }
bool SpecialStyle { get; internal set; }
bool LetterboxInBreaks { get; internal set; }
bool WidescreenStoryboard { get; internal set; }
bool EpilepsyWarning { get; internal set; }
bool SamplesMatchPlaybackRate { get; internal set; }
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
double DistanceSpacing { get; internal set; }
int GridSize { get; internal set; }
double TimelineZoom { get; internal set; }
CountdownType Countdown { get; internal set; }
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).
/// </summary>
int CountdownOffset { get; internal set; }
/// <summary> /// <summary>
/// Creates a shallow-clone of this beatmap and returns it. /// Creates a shallow-clone of this beatmap and returns it.
/// </summary> /// </summary>

View File

@ -62,7 +62,12 @@ namespace osu.Game.Beatmaps
#region Resource getters #region Resource getters
protected virtual Waveform GetWaveform() => new Waveform(null); protected virtual Waveform GetWaveform() => new Waveform(null);
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
protected virtual Storyboard GetStoryboard() => new Storyboard
{
BeatmapInfo = BeatmapInfo,
Beatmap = Beatmap,
};
protected abstract IBeatmap GetBeatmap(); protected abstract IBeatmap GetBeatmap();
public abstract Texture GetBackground(); public abstract Texture GetBackground();

View File

@ -138,6 +138,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.LightenDuringBreaks, true); SetDefault(OsuSetting.LightenDuringBreaks, true);
SetDefault(OsuSetting.HitLighting, true); SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.StarFountains, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
@ -214,6 +215,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorContractSidebars, false); SetDefault(OsuSetting.EditorContractSidebars, false);
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false);
} }
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
@ -413,6 +415,7 @@ namespace osu.Game.Configuration
NotifyOnPrivateMessage, NotifyOnPrivateMessage,
UIHoldActivationDelay, UIHoldActivationDelay,
HitLighting, HitLighting,
StarFountains,
MenuBackgroundSource, MenuBackgroundSource,
GameplayDisableWinKey, GameplayDisableWinKey,
SeasonalBackgroundMode, SeasonalBackgroundMode,
@ -444,5 +447,6 @@ namespace osu.Game.Configuration
EditorRotationOrigin, EditorRotationOrigin,
EditorTimelineShowBreaks, EditorTimelineShowBreaks,
EditorAdjustExistingObjectsOnTimingChanges, EditorAdjustExistingObjectsOnTimingChanges,
AlwaysRequireHoldingForPause
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -77,7 +78,8 @@ namespace osu.Game.Configuration
TouchInputActive, TouchInputActive,
/// <summary> /// <summary>
/// Stores the local user's last score (can be completed or aborted). /// Contains the local user's last score (can be completed or aborted) after exiting <see cref="Player"/>.
/// Will be cleared to <c>null</c> when leaving <see cref="PlayerLoader"/>.
/// </summary> /// </summary>
LastLocalUserScore, LastLocalUserScore,

View File

@ -94,8 +94,9 @@ namespace osu.Game.Database
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction /// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
/// </summary> /// </summary>
private const int schema_version = 43; private const int schema_version = 44;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -245,8 +245,8 @@ namespace osu.Game.Database
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = ComputeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = ComputeRank(score, scoreProcessor);
(score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap);
} }
@ -269,8 +269,8 @@ namespace osu.Game.Database
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = ComputeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = ComputeRank(score, scoreProcessor);
(score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes);
} }
@ -313,7 +313,8 @@ namespace osu.Game.Database
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param> /// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty,
LegacyScoreAttributes attributes)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return (score.TotalScoreWithoutMods, score.TotalScore); return (score.TotalScoreWithoutMods, score.TotalScore);
@ -620,24 +621,31 @@ namespace osu.Game.Database
} }
} }
private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) public static double ComputeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor)
=> ComputeAccuracy(scoreInfo.Statistics, scoreInfo.MaximumStatistics, scoreProcessor);
public static double ComputeAccuracy(IReadOnlyDictionary<HitResult, int> statistics, IReadOnlyDictionary<HitResult, int> maximumStatistics, ScoreProcessor scoreProcessor)
{ {
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) int baseScore = statistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) int maxBaseScore = maximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
} }
public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); public static ScoreRank ComputeRank(ScoreInfo scoreInfo) =>
ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor());
private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) public static ScoreRank ComputeRank(ScoreInfo scoreInfo, ScoreProcessor processor) =>
ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, processor);
public static ScoreRank ComputeRank(double accuracy, IReadOnlyDictionary<HitResult, int> statistics, IList<Mod> mods, ScoreProcessor scoreProcessor)
{ {
var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); var rank = scoreProcessor.RankFromScore(accuracy, statistics);
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>()) foreach (var mod in mods.OfType<IApplicableToScoreProcessor>())
rank = mod.AdjustRank(rank, scoreInfo.Accuracy); rank = mod.AdjustRank(rank, accuracy);
return rank; return rank;
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -96,24 +97,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
get get
{ {
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant())) string extension = File.Extension.ToLowerInvariant();
if (SupportedExtensions.VIDEO_EXTENSIONS.Contains(extension))
return FontAwesome.Regular.FileVideo; return FontAwesome.Regular.FileVideo;
switch (File.Extension) if (SupportedExtensions.AUDIO_EXTENSIONS.Contains(extension))
{ return FontAwesome.Regular.FileAudio;
case @".ogg":
case @".mp3":
case @".wav":
return FontAwesome.Regular.FileAudio;
case @".jpg": if (SupportedExtensions.IMAGE_EXTENSIONS.Contains(extension))
case @".jpeg": return FontAwesome.Regular.FileImage;
case @".png":
return FontAwesome.Regular.FileImage;
default: return FontAwesome.Regular.File;
return FontAwesome.Regular.File;
}
} }
} }

View File

@ -74,6 +74,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low"); public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low");
/// <summary>
/// "Star fountains"
/// </summary>
public static LocalisableString StarFountains => new TranslatableString(getKey(@"star_fountains"), @"Star fountains");
/// <summary> /// <summary>
/// "Always show key overlay" /// "Always show key overlay"
/// </summary> /// </summary>
@ -89,6 +94,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button"); public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button");
/// <summary>
/// "Require holding key to pause gameplay"
/// </summary>
public static LocalisableString AlwaysRequireHoldForMenu => new TranslatableString(getKey(@"require_holding_key_to_pause_gameplay"), @"Require holding key to pause gameplay");
/// <summary> /// <summary>
/// "Always play first combo break sound" /// "Always play first combo break sound"
/// </summary> /// </summary>

View File

@ -44,6 +44,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString CheckUpdate => new TranslatableString(getKey(@"check_update"), @"Check for updates"); public static LocalisableString CheckUpdate => new TranslatableString(getKey(@"check_update"), @"Check for updates");
/// <summary>
/// "Checking for updates"
/// </summary>
public static LocalisableString CheckingForUpdates => new TranslatableString(getKey(@"checking_for_updates"), @"Checking for updates");
/// <summary> /// <summary>
/// "Open osu! folder" /// "Open osu! folder"
/// </summary> /// </summary>

View File

@ -0,0 +1,154 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class MenuTipStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.MenuTip";
/// <summary>
/// "Press Ctrl-T anywhere in the game to toggle the toolbar!"
/// </summary>
public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!");
/// <summary>
/// "Press Ctrl-O anywhere in the game to access settings!"
/// </summary>
public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!");
/// <summary>
/// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!"
/// </summary>
public static LocalisableString DynamicSettings => new TranslatableString(getKey(@"dynamic_settings"), @"All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!");
/// <summary>
/// "New features are coming online every update. Make sure to stay up-to-date!"
/// </summary>
public static LocalisableString NewFeaturesAreComingOnline => new TranslatableString(getKey(@"new_features_are_coming_online"), @"New features are coming online every update. Make sure to stay up-to-date!");
/// <summary>
/// "If you find the UI too large or small, try adjusting UI scale in settings!"
/// </summary>
public static LocalisableString UIScalingSettings => new TranslatableString(getKey(@"ui_scaling_settings"), @"If you find the UI too large or small, try adjusting UI scale in settings!");
/// <summary>
/// "Try adjusting the &quot;Screen Scaling&quot; mode to change your gameplay or UI area, even in fullscreen!"
/// </summary>
public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!");
/// <summary>
/// "What used to be &quot;osu!direct&quot; is available to all users just like on the website. You can access it anywhere using Ctrl-B!"
/// </summary>
public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!");
/// <summary>
/// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!"
/// </summary>
public static LocalisableString ReplaySeeking => new TranslatableString(getKey(@"replay_seeking"), @"Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!");
/// <summary>
/// "Try scrolling right in mod select to find a bunch of new fun mods!"
/// </summary>
public static LocalisableString TryNewMods => new TranslatableString(getKey(@"try_new_mods"), @"Try scrolling right in mod select to find a bunch of new fun mods!");
/// <summary>
/// "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!"
/// </summary>
public static LocalisableString EmbeddedWebContent => new TranslatableString(getKey(@"embedded_web_content"), @"Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!");
/// <summary>
/// "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!"
/// </summary>
public static LocalisableString BeatmapRightClick => new TranslatableString(getKey(@"beatmap_right_click"), @"Get more details, hide or delete a beatmap by right-clicking on its panel at song select!");
/// <summary>
/// "Check out the &quot;playlists&quot; system, which lets users create their own custom and permanent leaderboards!"
/// </summary>
public static LocalisableString DiscoverPlaylists => new TranslatableString(getKey(@"discover_playlists"), @"Check out the ""playlists"" system, which lets users create their own custom and permanent leaderboards!");
/// <summary>
/// "Toggle advanced frame / thread statistics with Ctrl-F11!"
/// </summary>
public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!");
/// <summary>
/// "You can pause during a replay by pressing Space!"
/// </summary>
public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!");
/// <summary>
/// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"
/// </summary>
public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!");
/// <summary>
/// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"
/// </summary>
public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!");
/// <summary>
/// "You can create mod presets to make toggling your favourite mod combinations easier!"
/// </summary>
public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favourite mod combinations easier!");
/// <summary>
/// "Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"
/// </summary>
public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!");
/// <summary>
/// "Press Ctrl-Shift-R to switch to a random skin!"
/// </summary>
public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!");
/// <summary>
/// "While watching a replay, press Ctrl-H to toggle replay settings!"
/// </summary>
public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!");
/// <summary>
/// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!"
/// </summary>
public static LocalisableString CopyModsFromScore => new TranslatableString(getKey(@"copy_mods_from_score"), @"You can easily copy the mods from scores on a leaderboard by right-clicking on them!");
/// <summary>
/// "Ctrl-Enter at song select will start a beatmap in autoplay mode!"
/// </summary>
public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!");
/// <summary>
/// "Multithreading support means that even with low &quot;FPS&quot; your input and judgements will be accurate!"
/// </summary>
public static LocalisableString MultithreadingSupport => new TranslatableString(getKey(@"multithreading_support"), @"Multithreading support means that even with low ""FPS"" your input and judgements will be accurate!");
/// <summary>
/// "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!"
/// </summary>
public static LocalisableString TemporaryDeleteOperations => new TranslatableString(getKey(@"temporary_delete_operations"), @"All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!");
/// <summary>
/// "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"
/// </summary>
public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!");
/// <summary>
/// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"
/// </summary>
public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!");
/// <summary>
/// "Drag and drop any image into the skin editor to load it in quickly!"
/// </summary>
public static LocalisableString DragAndDropImageInSkinEditor => new TranslatableString(getKey(@"drag_and_drop_image_in_skin_editor"), @"Drag and drop any image into the skin editor to load it in quickly!");
/// <summary>
/// "a tip for you:"
/// </summary>
public static LocalisableString MenuTipTitle => new TranslatableString(getKey(@"menu_tip_title"), @"a tip for you:");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -80,9 +80,9 @@ namespace osu.Game.Localisation
public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring"); public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring");
/// <summary> /// <summary>
/// "{0}ms (speed {1})" /// "{0}ms (speed {1:N1})"
/// </summary> /// </summary>
public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed); public static LocalisableString ScrollSpeedTooltip(int scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed);
/// <summary> /// <summary>
/// "Touch control scheme" /// "Touch control scheme"

View File

@ -59,7 +59,6 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser; public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIRelation> Friends => friends; public IBindableList<APIRelation> Friends => friends;
public IBindable<UserActivity> Activity => activity; public IBindable<UserActivity> Activity => activity;
public IBindable<UserStatistics> Statistics => statistics;
public INotificationsClient NotificationsClient { get; } public INotificationsClient NotificationsClient { get; }
@ -74,8 +73,6 @@ namespace osu.Game.Online.API
private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>(); private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>(); private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatistics> statistics { get; } = new Bindable<UserStatistics>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -604,14 +601,6 @@ namespace osu.Game.Online.API
flushQueue(); flushQueue();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
statistics.Value = newStatistics;
if (IsLoggedIn)
localUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends() public void UpdateLocalFriends()
{ {
if (!IsLoggedIn) if (!IsLoggedIn)
@ -630,11 +619,7 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser(); private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() => private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
{
localUser.Value = user;
statistics.Value = user.Statistics;
}, false);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -30,8 +30,6 @@ namespace osu.Game.Online.API
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>(); public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
@ -178,11 +176,6 @@ namespace osu.Game.Online.API
private void onSuccessfulLogin() private void onSuccessfulLogin()
{ {
state.Value = APIState.Online; state.Value = APIState.Online;
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
} }
public void Logout() public void Logout()
@ -193,14 +186,6 @@ namespace osu.Game.Online.API
LocalUser.Value = new GuestUser(); LocalUser.Value = new GuestUser();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
Statistics.Value = newStatistics;
if (IsLoggedIn)
LocalUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends() public void UpdateLocalFriends()
{ {
} }
@ -220,7 +205,6 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser; IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIRelation> IAPIProvider.Friends => Friends; IBindableList<APIRelation> IAPIProvider.Friends => Friends;
IBindable<UserActivity> IAPIProvider.Activity => Activity; IBindable<UserActivity> IAPIProvider.Activity => Activity;
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
/// <summary> /// <summary>
/// Skip 2FA requirement for next login. /// Skip 2FA requirement for next login.

View File

@ -29,11 +29,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
IBindable<UserActivity> Activity { get; } IBindable<UserActivity> Activity { get; }
/// <summary>
/// The current user's online statistics.
/// </summary>
IBindable<UserStatistics?> Statistics { get; }
/// <summary> /// <summary>
/// The language supplied by this provider to API requests. /// The language supplied by this provider to API requests.
/// </summary> /// </summary>
@ -129,11 +124,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
void Logout(); void Logout();
/// <summary>
/// Sets Statistics bindable.
/// </summary>
void UpdateStatistics(UserStatistics newStatistics);
/// <summary> /// <summary>
/// Update the friends status of the current user. /// Update the friends status of the current user.
/// </summary> /// </summary>

View File

@ -0,0 +1,27 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class ClosePlaylistRequest : APIRequest
{
private readonly long roomId;
public ClosePlaylistRequest(long roomId)
{
this.roomId = roomId;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
request.Method = HttpMethod.Delete;
return request;
}
protected override string Target => $@"rooms/{roomId}";
}
}

View File

@ -223,8 +223,10 @@ namespace osu.Game.Online.API.Requests.Responses
/// <summary> /// <summary>
/// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response). /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response).
/// Otherwise empty.
/// </summary> /// </summary>
/// <remarks>
/// This returns null when accessed from <see cref="IAPIProvider.LocalUser"/>. Use <see cref="LocalUserStatisticsProvider"/> instead.
/// </remarks>
[JsonProperty(@"statistics")] [JsonProperty(@"statistics")]
public UserStatistics Statistics public UserStatistics Statistics
{ {

View File

@ -161,7 +161,7 @@ namespace osu.Game.Online.Chat
Messages.AddRange(messages); Messages.AddRange(messages);
long? maxMessageId = messages.Max(m => m.Id); long? maxMessageId = messages.Max(m => m.Id);
if (maxMessageId > LastMessageId) if (LastMessageId == null || maxMessageId > LastMessageId)
LastMessageId = maxMessageId; LastMessageId = maxMessageId;
purgeOldMessages(); purgeOldMessages();

View File

@ -0,0 +1,92 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Online
{
/// <summary>
/// A component that keeps track of the latest statistics for the local user.
/// </summary>
public partial class LocalUserStatisticsProvider : Component
{
/// <summary>
/// Invoked whenever a change occured to the statistics of any ruleset,
/// either due to change in local user (log out and log in) or as a result of score submission.
/// </summary>
/// <remarks>
/// This does not guarantee the presence of the old statistics,
/// specifically in the case of initial population or change in local user.
/// </remarks>
public event Action<UserStatisticsUpdate>? StatisticsUpdated;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>();
/// <summary>
/// Returns the <see cref="UserStatistics"/> currently available for the given ruleset.
/// This may return null if the requested statistics has not been fetched before yet.
/// </summary>
/// <param name="ruleset">The ruleset to return the corresponding <see cref="UserStatistics"/> for.</param>
public UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => statisticsCache.GetValueOrDefault(ruleset.ShortName);
protected override void LoadComplete()
{
base.LoadComplete();
api.LocalUser.BindValueChanged(_ =>
{
// queuing up requests directly on user change is unsafe, as the API status may have not been updated yet.
// schedule a frame to allow the API to be in its correct state sending requests.
Schedule(initialiseStatistics);
}, true);
}
private void initialiseStatistics()
{
statisticsCache.Clear();
if (api.LocalUser.Value == null || api.LocalUser.Value.Id <= 1)
return;
foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset()))
RefetchStatistics(ruleset);
}
public void RefetchStatistics(RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
{
if (!ruleset.IsLegacyRuleset())
throw new InvalidOperationException($@"Retrieving statistics is not supported for ruleset {ruleset.ShortName}");
var request = new GetUserRequest(api.LocalUser.Value.Id, ruleset);
request.Success += u => UpdateStatistics(u.Statistics, ruleset, callback);
api.Queue(request);
}
protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
{
var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName);
statisticsCache[ruleset.ShortName] = newStatistics;
var update = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics);
callback?.Invoke(update);
StatisticsUpdated?.Invoke(update);
}
}
public record UserStatisticsUpdate(RulesetInfo Ruleset, UserStatistics? OldStatistics, UserStatistics NewStatistics);
}

View File

@ -366,12 +366,8 @@ namespace osu.Game.Online.Rooms
{ {
RoomID = other.RoomID; RoomID = other.RoomID;
Name = other.Name; Name = other.Name;
Category = other.Category; Category = other.Category;
Host = other.Host;
if (other.Host != null && Host?.Id != other.Host.Id)
Host = other.Host;
ChannelId = other.ChannelId; ChannelId = other.ChannelId;
Status = other.Status; Status = other.Status;
Availability = other.Availability; Availability = other.Availability;
@ -379,6 +375,7 @@ namespace osu.Game.Online.Rooms
Type = other.Type; Type = other.Type;
MaxParticipants = other.MaxParticipants; MaxParticipants = other.MaxParticipants;
ParticipantCount = other.ParticipantCount; ParticipantCount = other.ParticipantCount;
StartDate = other.StartDate;
EndDate = other.EndDate; EndDate = other.EndDate;
UserScore = other.UserScore; UserScore = other.UserScore;
QueueMode = other.QueueMode; QueueMode = other.QueueMode;
@ -387,22 +384,10 @@ namespace osu.Game.Online.Rooms
PlaylistItemStats = other.PlaylistItemStats; PlaylistItemStats = other.PlaylistItemStats;
CurrentPlaylistItem = other.CurrentPlaylistItem; CurrentPlaylistItem = other.CurrentPlaylistItem;
AutoSkip = other.AutoSkip; AutoSkip = other.AutoSkip;
other.RemoveExpiredPlaylistItems();
Playlist = other.Playlist; Playlist = other.Playlist;
RecentParticipants = other.RecentParticipants; RecentParticipants = other.RecentParticipants;
} }
public void RemoveExpiredPlaylistItems()
{
// Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended,
// and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
// More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
if (Status is not RoomStatusEnded)
Playlist = Playlist.Where(i => !i.Expired).ToArray();
}
[JsonObject(MemberSerialization.OptIn)] [JsonObject(MemberSerialization.OptIn)]
public class RoomPlaylistItemStats public class RoomPlaylistItemStats
{ {

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online
/// <summary> /// <summary>
/// Contains data about the change in a user's profile statistics after completing a score. /// Contains data about the change in a user's profile statistics after completing a score.
/// </summary> /// </summary>
public class UserStatisticsUpdate public class ScoreBasedUserStatisticsUpdate
{ {
/// <summary> /// <summary>
/// The score set by the user that triggered the update. /// The score set by the user that triggered the update.
@ -27,12 +27,12 @@ namespace osu.Game.Online
public UserStatistics After { get; } public UserStatistics After { get; }
/// <summary> /// <summary>
/// Creates a new <see cref="UserStatisticsUpdate"/>. /// Creates a new <see cref="ScoreBasedUserStatisticsUpdate"/>.
/// </summary> /// </summary>
/// <param name="score">The score set by the user that triggered the update.</param> /// <param name="score">The score set by the user that triggered the update.</param>
/// <param name="before">The user's profile statistics prior to the score being set.</param> /// <param name="before">The user's profile statistics prior to the score being set.</param>
/// <param name="after">The user's profile statistics after the score was set.</param> /// <param name="after">The user's profile statistics after the score was set.</param>
public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) public ScoreBasedUserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
{ {
Score = score; Score = score;
Before = before; Before = before;

View File

@ -2,18 +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.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online namespace osu.Game.Online
{ {
@ -22,8 +18,10 @@ namespace osu.Game.Online
/// </summary> /// </summary>
public partial class UserStatisticsWatcher : Component public partial class UserStatisticsWatcher : Component
{ {
public IBindable<UserStatisticsUpdate?> LatestUpdate => latestUpdate; private readonly LocalUserStatisticsProvider statisticsProvider;
private readonly Bindable<UserStatisticsUpdate?> latestUpdate = new Bindable<UserStatisticsUpdate?>();
public IBindable<ScoreBasedUserStatisticsUpdate?> LatestUpdate => latestUpdate;
private readonly Bindable<ScoreBasedUserStatisticsUpdate?> latestUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
[Resolved] [Resolved]
private SpectatorClient spectatorClient { get; set; } = null!; private SpectatorClient spectatorClient { get; set; } = null!;
@ -33,13 +31,15 @@ namespace osu.Game.Online
private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>(); private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>();
private Dictionary<string, UserStatistics>? latestStatistics; public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
spectatorClient.OnUserScoreProcessed += userScoreProcessed; spectatorClient.OnUserScoreProcessed += userScoreProcessed;
} }
@ -61,35 +61,6 @@ namespace osu.Game.Online
}); });
} }
private void onUserChanged(APIUser? localUser) => Schedule(() =>
{
latestStatistics = null;
if (localUser == null || localUser.OnlineID <= 1)
return;
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
userRequest.Success += initialiseUserStatistics;
api.Queue(userRequest);
});
private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
{
var user = response.Users.SingleOrDefault();
// possible if the user is restricted or similar.
if (user == null)
return;
latestStatistics = new Dictionary<string, UserStatistics>();
if (user.RulesetsStatistics != null)
{
foreach (var rulesetStats in user.RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
}
});
private void userScoreProcessed(int userId, long scoreId) private void userScoreProcessed(int userId, long scoreId)
{ {
if (userId != api.LocalUser.Value?.OnlineID) if (userId != api.LocalUser.Value?.OnlineID)
@ -98,30 +69,11 @@ namespace osu.Game.Online
if (!watchedScores.Remove(scoreId, out var scoreInfo)) if (!watchedScores.Remove(scoreId, out var scoreInfo))
return; return;
requestStatisticsUpdate(userId, scoreInfo); statisticsProvider.RefetchStatistics(scoreInfo.Ruleset, u => Schedule(() =>
} {
if (u.OldStatistics != null)
private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scoreInfo, u.OldStatistics, u.NewStatistics);
{ }));
var request = new GetUserRequest(userId, scoreInfo.Ruleset);
request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics));
api.Queue(request);
}
private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics)
{
string rulesetName = scoreInfo.Ruleset.ShortName;
api.UpdateStatistics(updatedStatistics);
if (latestStatistics == null)
return;
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
latestRulesetStatistics ??= new UserStatistics();
latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics);
latestStatistics[rulesetName] = updatedStatistics;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -148,8 +148,7 @@ namespace osu.Game
[Resolved] [Resolved]
private FrameworkConfigManager frameworkConfig { get; set; } private FrameworkConfigManager frameworkConfig { get; set; }
[Cached] private DifficultyRecommender difficultyRecommender;
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
[Cached] [Cached]
private readonly LegacyImportManager legacyImportManager = new LegacyImportManager(); private readonly LegacyImportManager legacyImportManager = new LegacyImportManager();
@ -175,6 +174,11 @@ namespace osu.Game
/// </summary> /// </summary>
public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(); public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary>
/// Whether the back button is currently displayed.
/// </summary>
private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>();
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState; IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState;
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>(); private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
@ -196,7 +200,8 @@ namespace osu.Game
private MainMenu menuScreen; private MainMenu menuScreen;
private VersionManager versionManager; [CanBeNull]
private DevBuildBanner devBuildBanner;
[CanBeNull] [CanBeNull]
private IntroScreen introScreen; private IntroScreen introScreen;
@ -1019,7 +1024,7 @@ namespace osu.Game
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen))
return; return;
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton())) if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton()))
ScreenStack.Exit(); ScreenStack.Exit();
} }
}, },
@ -1056,10 +1061,7 @@ namespace osu.Game
}, topMostOverlayContent.Add); }, topMostOverlayContent.Add);
if (!IsDeployedBuild) if (!IsDeployedBuild)
{ loadComponentSingleFile(devBuildBanner = new DevBuildBanner(), ScreenContainer.Add);
dependencies.Cache(versionManager = new VersionManager());
loadComponentSingleFile(versionManager, ScreenContainer.Add);
}
loadComponentSingleFile(osuLogo, _ => loadComponentSingleFile(osuLogo, _ =>
{ {
@ -1069,7 +1071,11 @@ namespace osu.Game
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
}); });
loadComponentSingleFile(new UserStatisticsWatcher(), Add, true); LocalUserStatisticsProvider statisticsProvider;
loadComponentSingleFile(statisticsProvider = new LocalUserStatisticsProvider(), Add, true);
loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true);
loadComponentSingleFile(new UserStatisticsWatcher(statisticsProvider), Add, true);
loadComponentSingleFile(Toolbar = new Toolbar loadComponentSingleFile(Toolbar = new Toolbar
{ {
OnHome = delegate OnHome = delegate
@ -1139,7 +1145,6 @@ namespace osu.Game
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler()); Add(new MusicKeyBindingHandler());
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));
@ -1189,6 +1194,14 @@ namespace osu.Game
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
}; };
backButtonVisibility.ValueChanged += visible =>
{
if (visible.NewValue)
BackButton.Show();
else
BackButton.Hide();
};
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
handleStartupImport(); handleStartupImport();
} }
@ -1562,12 +1575,12 @@ namespace osu.Game
{ {
case IntroScreen intro: case IntroScreen intro:
introScreen = intro; introScreen = intro;
versionManager?.Show(); devBuildBanner?.Show();
break; break;
case MainMenu menu: case MainMenu menu:
menuScreen = menu; menuScreen = menu;
versionManager?.Show(); devBuildBanner?.Show();
break; break;
case Player player: case Player player:
@ -1575,18 +1588,20 @@ namespace osu.Game
break; break;
default: default:
versionManager?.Hide(); devBuildBanner?.Hide();
break; break;
} }
if (current is IOsuScreen currentOsuScreen) if (current is IOsuScreen currentOsuScreen)
{ {
backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility);
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode);
API.Activity.UnbindFrom(currentOsuScreen.Activity); API.Activity.UnbindFrom(currentOsuScreen.Activity);
} }
if (newScreen is IOsuScreen newOsuScreen) if (newScreen is IOsuScreen newOsuScreen)
{ {
backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility);
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
API.Activity.BindTo(newOsuScreen.Activity); API.Activity.BindTo(newOsuScreen.Activity);
@ -1597,11 +1612,6 @@ namespace osu.Game
else else
Toolbar.Show(); Toolbar.Show();
if (newOsuScreen.AllowBackButton)
BackButton.Show();
else
BackButton.Hide();
if (newOsuScreen.ShowFooter) if (newOsuScreen.ShowFooter)
{ {
BackButton.Hide(); BackButton.Hide();

View File

@ -73,8 +73,6 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))] [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{ {
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
#if DEBUG #if DEBUG
public const string GAME_NAME = "osu! (development)"; public const string GAME_NAME = "osu! (development)";
#else #else

View File

@ -198,7 +198,6 @@ namespace osu.Game.Overlays
{ {
c.Anchor = Anchor.TopCentre; c.Anchor = Anchor.TopCentre;
c.Origin = Anchor.TopCentre; c.Origin = Anchor.TopCentre;
c.Scale = new Vector2(0.8f);
})).ToArray(); })).ToArray();
private static ReverseChildIDFillFlowContainer<BeatmapCard> createCardContainerFor(IEnumerable<BeatmapCard> newCards) private static ReverseChildIDFillFlowContainer<BeatmapCard> createCardContainerFor(IEnumerable<BeatmapCard> newCards)

View File

@ -37,11 +37,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>(); private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
public ChannelGroup AnnounceChannelGroup { get; private set; } = null!;
public ChannelGroup PublicChannelGroup { get; private set; } = null!;
public ChannelGroup PrivateChannelGroup { get; private set; } = null!;
private OsuScrollContainer scroll = null!; private OsuScrollContainer scroll = null!;
private SearchContainer groupFlow = null!; private SearchContainer groupFlow = null!;
private ChannelGroup announceChannelGroup = null!;
private ChannelGroup publicChannelGroup = null!;
private ChannelGroup privateChannelGroup = null!;
private ChannelListItem selector = null!; private ChannelListItem selector = null!;
private TextBox searchTextBox = null!; private TextBox searchTextBox = null!;
@ -77,10 +79,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
} }
}, },
announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()), AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
selector = new ChannelListItem(ChannelListingChannel), selector = new ChannelListItem(ChannelListingChannel),
privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()), PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true),
}, },
}, },
}, },
@ -111,69 +113,70 @@ namespace osu.Game.Overlays.Chat.ChannelList
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel); ChannelGroup group = getGroupFromChannel(channel);
channelMap.Add(channel, item); channelMap.Add(channel, item);
flow.Add(item); group.AddChannel(item);
updateVisibility(); updateVisibility();
} }
public void RemoveChannel(Channel channel) public void RemoveChannel(Channel channel)
{ {
if (!channelMap.ContainsKey(channel)) if (!channelMap.TryGetValue(channel, out var item))
return; return;
ChannelListItem item = channelMap[channel]; ChannelGroup group = getGroupFromChannel(channel);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
channelMap.Remove(channel); channelMap.Remove(channel);
flow.Remove(item, true); group.RemoveChannel(item);
updateVisibility(); updateVisibility();
} }
public ChannelListItem GetItem(Channel channel) public ChannelListItem GetItem(Channel channel)
{ {
if (!channelMap.ContainsKey(channel)) if (!channelMap.TryGetValue(channel, out var item))
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
return channelMap[channel]; return item;
} }
public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
private FillFlowContainer<ChannelListItem> getFlowForChannel(Channel channel) private ChannelGroup getGroupFromChannel(Channel channel)
{ {
switch (channel.Type) switch (channel.Type)
{ {
case ChannelType.Public: case ChannelType.Public:
return publicChannelGroup.ItemFlow; return PublicChannelGroup;
case ChannelType.PM: case ChannelType.PM:
return privateChannelGroup.ItemFlow; return PrivateChannelGroup;
case ChannelType.Announce: case ChannelType.Announce:
return announceChannelGroup.ItemFlow; return AnnounceChannelGroup;
default: default:
return publicChannelGroup.ItemFlow; return PublicChannelGroup;
} }
} }
private void updateVisibility() private void updateVisibility()
{ {
if (announceChannelGroup.ItemFlow.Children.Count == 0) if (AnnounceChannelGroup.ItemFlow.Children.Count == 0)
announceChannelGroup.Hide(); AnnounceChannelGroup.Hide();
else else
announceChannelGroup.Show(); AnnounceChannelGroup.Show();
} }
private partial class ChannelGroup : FillFlowContainer public partial class ChannelGroup : FillFlowContainer
{ {
public readonly FillFlowContainer<ChannelListItem> ItemFlow; private readonly bool sortByRecent;
public readonly ChannelListItemFlow ItemFlow;
public ChannelGroup(LocalisableString label) public ChannelGroup(LocalisableString label, bool sortByRecent)
{ {
this.sortByRecent = sortByRecent;
Direction = FillDirection.Vertical; Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -187,7 +190,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Margin = new MarginPadding { Left = 18, Bottom = 5 }, Margin = new MarginPadding { Left = 18, Bottom = 5 },
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
}, },
ItemFlow = new FillFlowContainer<ChannelListItem> ItemFlow = new ChannelListItemFlow(sortByRecent)
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -195,6 +198,60 @@ namespace osu.Game.Overlays.Chat.ChannelList
}, },
}; };
} }
public partial class ChannelListItemFlow : FillFlowContainer<ChannelListItem>
{
private readonly bool sortByRecent;
public ChannelListItemFlow(bool sortByRecent)
{
this.sortByRecent = sortByRecent;
}
public void Reflow() => InvalidateLayout();
public override IEnumerable<Drawable> FlowingChildren => sortByRecent
? base.FlowingChildren.OfType<ChannelListItem>().OrderByDescending(i => i.Channel.LastMessageId ?? long.MinValue)
: base.FlowingChildren.OfType<ChannelListItem>().OrderBy(i => i.Channel.Name);
}
public void AddChannel(ChannelListItem item)
{
ItemFlow.Add(item);
if (sortByRecent)
{
item.Channel.NewMessagesArrived += newMessagesArrived;
item.Channel.PendingMessageResolved += pendingMessageResolved;
}
ItemFlow.Reflow();
}
public void RemoveChannel(ChannelListItem item)
{
if (sortByRecent)
{
item.Channel.NewMessagesArrived -= newMessagesArrived;
item.Channel.PendingMessageResolved -= pendingMessageResolved;
}
ItemFlow.Remove(item, true);
}
private void pendingMessageResolved(LocalEchoMessage _, Message __) => ItemFlow.Reflow();
private void newMessagesArrived(IEnumerable<Message> _) => ItemFlow.Reflow();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var item in ItemFlow)
{
item.Channel.NewMessagesArrived -= newMessagesArrived;
item.Channel.PendingMessageResolved -= pendingMessageResolved;
}
}
} }
private partial class ChannelSearchTextBox : BasicSearchTextBox private partial class ChannelSearchTextBox : BasicSearchTextBox

View File

@ -0,0 +1,58 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays
{
public partial class DevBuildBanner : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Alpha = 0;
AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12),
Colour = colours.YellowDark,
Text = @"DEVELOPER BUILD",
},
new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
Scale = new Vector2(0.4f, 1),
Y = 2,
},
});
}
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(500, Easing.OutQuint);
}
}
}

View File

@ -138,34 +138,31 @@ namespace osu.Game.Overlays.Profile.Header.Components
topFifty.ValueColour = colourProvider.Content2; topFifty.ValueColour = colourProvider.Content2;
} }
// reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47 // reference: https://github.com/ppy/osu-web/blob/a97f156014e00ea1aa315140da60542e798a9f06/resources/js/profile-page/daily-challenge.tsx#L13-L47
// Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Floor(playCount / 3.0d));
// This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would
// get truncated to 10 with an integer division and show a lower tier.
public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d));
public static RankingTier TierForDaily(int daily) public static RankingTier TierForDaily(int daily)
{ {
if (daily > 360) if (daily >= 360)
return RankingTier.Lustrous; return RankingTier.Lustrous;
if (daily > 240) if (daily >= 240)
return RankingTier.Radiant; return RankingTier.Radiant;
if (daily > 120) if (daily >= 120)
return RankingTier.Rhodium; return RankingTier.Rhodium;
if (daily > 60) if (daily >= 60)
return RankingTier.Platinum; return RankingTier.Platinum;
if (daily > 30) if (daily >= 30)
return RankingTier.Gold; return RankingTier.Gold;
if (daily > 10) if (daily >= 10)
return RankingTier.Silver; return RankingTier.Silver;
if (daily > 5) if (daily >= 5)
return RankingTier.Bronze; return RankingTier.Bronze;
return RankingTier.Iron; return RankingTier.Iron;

View File

@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
? new BeatmapCardNormal(model) ? new BeatmapCardNormal(model)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre
} }
: null; : null;
} }

View File

@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = GraphicsSettingsStrings.HitLighting, LabelText = GraphicsSettingsStrings.HitLighting,
Current = config.GetBindable<bool>(OsuSetting.HitLighting) Current = config.GetBindable<bool>(OsuSetting.HitLighting)
}, },
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.StarFountains,
Current = config.GetBindable<bool>(OsuSetting.StarFountains)
},
}; };
} }
} }

View File

@ -41,6 +41,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard), Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard),
}, },
new SettingsCheckbox new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysRequireHoldForMenu,
Current = config.GetBindable<bool>(OsuSetting.AlwaysRequireHoldingForPause),
},
new SettingsCheckbox
{ {
LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton, LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton,
Current = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton), Current = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton),

View File

@ -4,7 +4,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -13,6 +12,7 @@ using osu.Framework.Screens;
using osu.Framework.Statistics; using osu.Framework.Statistics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater; using osu.Game.Updater;
@ -36,8 +36,11 @@ namespace osu.Game.Overlays.Settings.Sections.General
[Resolved] [Resolved]
private Storage storage { get; set; } = null!; private Storage storage { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, OsuGame? game) private void load(OsuConfigManager config)
{ {
Add(new SettingsEnumDropdown<ReleaseStream> Add(new SettingsEnumDropdown<ReleaseStream>
{ {
@ -50,23 +53,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(checkForUpdatesButton = new SettingsButton Add(checkForUpdatesButton = new SettingsButton
{ {
Text = GeneralSettingsStrings.CheckUpdate, Text = GeneralSettingsStrings.CheckUpdate,
Action = () => Action = () => checkForUpdates().FireAndForget()
{
checkForUpdatesButton.Enabled.Value = false;
Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(task => Schedule(() =>
{
if (!task.GetResultSafely())
{
notifications?.Post(new SimpleNotification
{
Text = GeneralSettingsStrings.RunningLatestRelease(game!.Version),
Icon = FontAwesome.Solid.CheckCircle,
});
}
checkForUpdatesButton.Enabled.Value = true;
}));
}
}); });
} }
@ -94,6 +81,44 @@ namespace osu.Game.Overlays.Settings.Sections.General
} }
} }
private async Task checkForUpdates()
{
if (updateManager == null || game == null)
return;
checkForUpdatesButton.Enabled.Value = false;
var checkingNotification = new ProgressNotification
{
Text = GeneralSettingsStrings.CheckingForUpdates,
};
notifications?.Post(checkingNotification);
try
{
bool foundUpdate = await updateManager.CheckForUpdateAsync().ConfigureAwait(true);
if (!foundUpdate)
{
notifications?.Post(new SimpleNotification
{
Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
Icon = FontAwesome.Solid.CheckCircle,
});
}
}
catch
{
}
finally
{
// This sequence allows the notification to be immediately dismissed.
checkingNotification.State = ProgressNotificationState.Cancelled;
checkingNotification.Close(false);
checkForUpdatesButton.Enabled.Value = true;
}
}
private void exportLogs() private void exportLogs()
{ {
ProgressNotification notification = new ProgressNotification ProgressNotification notification = new ProgressNotification

View File

@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private OsuGame game { get; set; } private OsuGame game { get; set; }
public override bool AllowBackButton => false; public override bool AllowUserExit => false;
public override bool AllowExternalScreenChange => false; public override bool AllowExternalScreenChange => false;

View File

@ -184,7 +184,7 @@ namespace osu.Game.Overlays
content.ResizeHeightTo(0, animate ? transition_duration : 0, Easing.OutQuint); content.ResizeHeightTo(0, animate ? transition_duration : 0, Easing.OutQuint);
} }
headerContent.FadeColour(Expanded.Value ? Color4.White : OsuColour.Gray(0.5f), 200, Easing.OutQuint); headerContent.FadeColour(Expanded.Value ? Color4.White : OsuColour.Gray(0.7f), 200, Easing.OutQuint);
} }
private void updateFadeState() private void updateFadeState()

Some files were not shown because too many files have changed in this diff Show More