150 Commits

Author SHA1 Message Date
3bd996ee43 synchronize with github (tag 2025.1119.0-tachyon) 2025-11-19 15:13:25 +03:00
37b9f91d42 make discord rich presence work 2025-11-19 14:03:57 +03:00
1a5a5606dc don't log that we're running an unofficial build 2025-11-19 12:02:27 +03:00
87ff1051e9 set up sentry (glitchtip) logging properly 2025-11-18 23:27:57 +03:00
Bartłomiej Dach
80474565fc Merge pull request #35726 from peppy/update-framework
Update framework
2025-11-18 14:19:47 +01:00
2e4b0ff197 make score cards for failed scores look better 2025-11-18 15:33:37 +03:00
499f410c94 slightly changed warnings for old windows versions 2025-11-18 14:44:34 +03:00
616c0d8ecd make key counters show proper readable key names 2025-11-18 14:24:05 +03:00
ed889138a0 break countdown now uses argon-style counter 2025-11-18 13:23:44 +03:00
Dean Herbert
89f2c7160d Update framework 2025-11-18 18:45:38 +09:00
Urantij
f0ca079fe6 Fix cursor incorrectly flashing red after a rewind in replays with Alternate mod active (#35725)
* Fix red cursor with alt mod when rewind

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

Matches break overlay:

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

Can test using something dumb like

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

             Texture? texture = null;
2025-11-17 11:58:28 +01:00
Bartłomiej Dach
4bf3d9397f Merge pull request #35714 from smoogipoo/fix-preview-track-owners
Fix various screens not registering themselves as `IPreviewTrackOwner`
2025-11-17 08:52:43 +01:00
maarvin
8b778e8106 Split quickplay beatmap & "random" panel into separate classes (V2) (#35701)
* Load all beatmaps in bulk for SubScreenBeatmapSelect

* Fix tests no longer working due to drawable changes

* Remove test that no longer makes sense

* Split matchmaking panel into subclasses for each panel type

* Adjust tests to match new structure

* Add `ConfigureAwait`

* Display loading spinner while beatmaps are being fetched

* Fix test failure

* Load playlist items directly in `LoadComplete`

* Convert `MatchmakingSelectPanel` card content classes into nested classes

* Wait for panels to be loaded before operating on them

* Add ConfigureAwait()

---------

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

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

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

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

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

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

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

* Fix mania beatmap conversion assigning wrong samples to spinners

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

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

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

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

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

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

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

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

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

Co-authored-by: WitherFlower <maxime.barniaudy@gmail.com>
2025-11-13 14:10:24 +09:00
89a0c75156 all user-playable mods are now always ranked 2025-11-12 20:53:07 +03:00
Bartłomiej Dach
cb9d9734d6 Move realm collection writes off of update thread (#35681)
Probably closes https://github.com/ppy/osu/issues/35650.

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

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

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

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

Few other things we might want to happen here:

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

but I want to see the reaction to this first.
2025-11-11 14:18:40 +09:00
复予
013de9f85d Add circular progress display to back-to-top button (#35625)
* Show circular progress on ScrollBackButton of OverlayScrollContainer

* Adjust standardization of position progress
2025-11-10 18:08:00 +09:00
Bartłomiej Dach
cd6c9405fe Fix legacy skin drum roll head circle being underneath ticks (#35647)
Closes https://github.com/ppy/osu/issues/35321.
2025-11-10 15:43:59 +09:00
Dean Herbert
822cb9e2fb Merge pull request #35643 from diquoks/localisation/wasapi
Localise `WASAPI` setting
2025-11-09 02:16:37 +09:00
Bartłomiej Dach
680614fbee Fix messages from blocked users being visible in public channels (#35645)
* Add failing test coverage for blocking users not removing their messages from public channels

* Fix messages from blocked users being visible in public channels

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

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

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

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

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

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

Most easily tested on a local full-stack environment with

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

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

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

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

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

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

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

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

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

* Extract separate component for global rank display

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

* Localize jump link string

* Mark ILinkHandler dependency as nullable

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

* Use existing web string instead of inventing a new one

* Bind value change callback more reliably

---------

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

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

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

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

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

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

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

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

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

Certified OOP moment.
2025-10-30 13:25:38 +01:00
Dan Balasescu
a435dfe93e Add interop models 2025-10-30 19:04:49 +09:00
Bartłomiej Dach
5c1171f358 Merge pull request #35537 from smoogipoo/qp-fix-view-beatmap
Fix quick play "view beatmap" showing incorrect difficulty
2025-10-30 11:04:46 +01:00
Bartłomiej Dach
3fcc626e29 Merge pull request #35511 from smoogipoo/qp-fix-empty-sequence
Fix potential sources of empty sequence errors
2025-10-30 10:49:12 +01:00
Dan Balasescu
7ff6edeb64 Fix quick play "view beatmap" showing incorrect difficulty 2025-10-30 15:27:28 +09:00
Dan Balasescu
657bc31539 Fix potential sources of empty sequence errors 2025-10-29 23:06:28 +09:00
Dan Balasescu
f9f7740acb Add failing test 2025-10-29 23:06:28 +09:00
Bartłomiej Dach
5e4dd77e64 Merge branch 'master' into show-hud-while-editing-skin-layout 2025-10-29 14:27:28 +01:00
Bartłomiej Dach
ce96c0b037 Merge extremely similar setting-enforcing flows in skin editor 2025-10-29 14:24:18 +01:00
Dean Herbert
5af9bb784b Merge pull request #35495 from Joehuu/fix-drawable-date-update
Fix `DrawableDate` not updating
2025-10-29 20:17:15 +09:00
Bartłomiej Dach
4c60df21db Fix DrawableDate not updating
Co-authored-by: Dean Herbert <pe@ppy.sh>
2025-10-29 11:51:31 +01:00
Bartłomiej Dach
3c6fb14a32 Merge pull request #35501 from peppy/more-quick-play-notification-improvements
More quick play notification improvements
2025-10-29 11:33:34 +01:00
Dan Balasescu
3afc7b045c Remove redundant default value 2025-10-29 17:27:33 +09:00
Bartłomiej Dach
2f2847f1dd Merge pull request #35498 from smoogipoo/qp-add-helpers
Add quick play helpers to add users/rounds
2025-10-29 09:13:03 +01:00
Dean Herbert
ee7c52465b Allow queue completion notification to show even during gameplay 2025-10-29 16:58:18 +09:00
Dean Herbert
beb977892e Use better iconography and colour for queue completion notification 2025-10-29 16:58:17 +09:00
Bartłomiej Dach
7203f419a2 Merge branch 'master' into qp-add-helpers 2025-10-29 08:13:51 +01:00
Dan Balasescu
722cfb72d8 Replace indexers with GetOrAdd()
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-10-29 16:07:46 +09:00
Bartłomiej Dach
5da132cc2f Merge pull request #35482 from smoogipoo/qp-fix-initial-placement-display
Ensure to never display "0th" placement
2025-10-29 07:47:35 +01:00
Dean Herbert
9fac96cf07 Merge pull request #35499 from smoogipoo/qp-fix-beatmap-nullref
Fix potential quick play crash if beatmap lookup fails
2025-10-29 15:43:31 +09:00
Jamie Taylor
fadcb9882c Merge branch 'master' into matchmaking-jumpy-jump 2025-10-29 15:34:50 +09:00
Bartłomiej Dach
0610781c6c Merge pull request #35483 from smoogipoo/qp-fix-results-no-scores-crash
Fix quick play results screen crash when no one plays
2025-10-29 07:30:59 +01:00
Dan Balasescu
e9260de56f Fix potential nullref if beatmap lookup fails 2025-10-29 15:15:36 +09:00
Dan Balasescu
2d177226fd Add failing test 2025-10-29 15:08:40 +09:00
Dan Balasescu
bd912710f1 Add quick play helpers to add users/rounds 2025-10-29 14:49:22 +09:00
Dean Herbert
4e76bd0f24 Play sound when match is available even when queueing in background (#35496) 2025-10-29 13:58:20 +09:00
Joseph Madamba
9a965a2546 Add failing drawable date seconds update test 2025-10-28 19:39:07 -07:00
Dan Balasescu
7b0121a430 Fix quick play results screen when no one plays 2025-10-29 11:18:25 +09:00
Dan Balasescu
627fec2e3a Add failing test case 2025-10-29 11:18:25 +09:00
Glacc
c779e142e6 Code quality fix. 2025-10-28 23:04:09 +08:00
Glacc
89fffa5a1a Code quality fix. 2025-10-28 22:54:07 +08:00
Glacc
6d597fc815 Null check for configVisibilityMode. 2025-10-28 21:51:21 +08:00
Glacc
a78b456e20 Revert value after closing editor. 2025-10-28 21:42:34 +08:00
Glacc
9237c76942 And make HUD visibility mode lease when Skin Layout Editor is visible. 2025-10-28 21:38:28 +08:00
Glacc
378c64b7f8 Only set HUD visibility mode to non-Never when skin layout editor is visible by saving and restoring HUD visibility mode setting. 2025-10-28 21:21:07 +08:00
Glacc
87b66685d6 Always show HUD while editing skin layout. 2025-10-28 19:42:47 +08:00
Dan Balasescu
c524bf5432 Make MachmakingUser.Placement nullable 2025-10-28 20:39:09 +09:00
Dan Balasescu
a40230da4b Ensure to never display "0th" placement 2025-10-28 19:35:15 +09:00
Jamie Taylor
0558f9f2d9 Add SFX for 'jumping' in quickplay 2025-10-24 22:42:28 +09:00
171 changed files with 3651 additions and 1278 deletions

View File

@@ -1,12 +1,6 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links: contact_links:
- name: Help - name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a url: https://t.me/jvnkosu_chat
about: osu! not working or performing as you'd expect? Not sure it's a bug? Check the Q&A section! about: Your jvnkosu! is not working right? Please contact us using our Telegram chat
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
about: For osu!(stable) - ie. the current "live" game version, check out the dedicated repository. Note that this is for serious bug reports only, not tech support.

View File

@@ -73,6 +73,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
After you're done with your changes and you wish to open the PR, please observe the following recommendations: After you're done with your changes and you wish to open the PR, please observe the following recommendations:
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary. - Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
- Please pick the following target branch for your pull request:
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
- `master`, otherwise.
- Please avoid pushing untested or incomplete code. - Please avoid pushing untested or incomplete code.
- Please do not force-push or rebase unless we ask you to. - Please do not force-push or rebase unless we ask you to.
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge. - Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.

View File

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

@@ -29,7 +29,7 @@ namespace osu.Desktop
{ {
internal partial class DiscordRichPresence : Component internal partial class DiscordRichPresence : Component
{ {
private const string client_id = "1216669957799018608"; private const string client_id = "1440647613358800918";
private DiscordRpcClient client = null!; private DiscordRpcClient client = null!;

View File

@@ -44,7 +44,7 @@ namespace osu.Desktop
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher. // While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/ // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) if (windowsVersion.Major < 6)
{ {
unsafe unsafe
{ {
@@ -53,13 +53,26 @@ namespace osu.Desktop
// We could also better detect compatibility mode if required: // We could also better detect compatibility mode if required:
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!"u8, "Your operating system is too old to run this game!"u8,
"This version of osu! requires at least Windows 8.1 to run.\n"u8 "This version of the game requires at least Windows 8.1 to run reliably.\n"u8
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8 + "You may try to run it on Windows 8 or older, but it's not guaranteed to actually work.\n\n"u8
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null); + "Please upgrade your operating system or consider using an older game version.\n\n"u8
+ "If you are running a newer version of Windows, please check you don't have \"Compatibility mode\" turned on for the game's executable."u8, null);
return; return;
} }
} }
if (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)
{
unsafe
{
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_WARNING,
"Your operating system is too old to run this game!"u8,
"While the version of Windows you're using may be able to launch it, it's likely to work unreliably and crash.\n"u8
+ "Please upgrade your operating system or consider using an older game version.\n\n"u8
+ "If you are running a newer version of Windows, please check you don't have \"Compatibility mode\" turned on for the game's executable."u8, null);
}
}
} }
// NVIDIA profiles are based on the executable name of a process. // NVIDIA profiles are based on the executable name of a process.
@@ -208,9 +221,10 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private static void configureWindows(VelopackApp app) private static void configureWindows(VelopackApp app)
{ {
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations()); // we do not want associations here, as that breaks official lazer's associations
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations()); // app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations()); // app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
// app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
} }
} }
} }

View File

@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("mania-samples")] [TestCase("mania-samples")]
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407 [TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
[TestCase("slider-convert-samples")] [TestCase("slider-convert-samples")]
[TestCase("spinner-convert-samples")]
public void Test(string name) => base.Test(name); public void Test(string name) => base.Test(name);
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)

View File

@@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 0
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:

View File

@@ -0,0 +1,16 @@
{
"Mappings": [{
"StartTime": 1000.0,
"Objects": [{
"StartTime": 1000.0,
"EndTime": 8000.0,
"Column": 0,
"PlaySlidingSamples": false,
"NodeSamples": [
["Gameplay/soft-hitnormal"],
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
],
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
}]
}]
}

View File

@@ -0,0 +1,18 @@
osu file format v14
[General]
Mode: 0
[Difficulty]
HPDrainRate:5
CircleSize:5
OverallDifficulty:5
ApproachRate:5
SliderMultiplier:1.4
SliderTickRate:1
[TimingPoints]
0,500,4,2,0,100,1,0
[HitObjects]
256,192,1000,8,4,8000,0:2:0:0:

View File

@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
AssertBeatmapLookup(expected_sample); AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample); AssertNoLookup(unwanted_sample);
} }
[Test]
public void TestConvertHitObjectCustomSampleBank()
{
const string beatmap_sample = "normal-hitwhistle2";
const string user_skin_sample = "normal-hitnormal";
SetupSkins(beatmap_sample, user_skin_sample);
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(beatmap_sample);
AssertUserLookup(user_skin_sample);
}
} }
} }

View File

@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Duration = endTime - HitObject.StartTime, Duration = endTime - HitObject.StartTime,
Column = column, Column = column,
Samples = HitObject.Samples, Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples NodeSamples =
[
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
HitObject.Samples
]
}; };
} }
else else

View File

@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModFadeIn) typeof(ManiaModFadeIn)
}).ToArray(); }).ToArray();
public override bool Ranked => false; public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => false; public override bool ValidForFreestyleAsRequiredMod => false;

View File

@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
{ {
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => false; public override bool Ranked => true;
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4; public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;

View File

@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModMirror : ModMirror, IApplicableToBeatmap public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{ {
public override LocalisableString Description => "Notes are flipped horizontally."; public override LocalisableString Description => "Notes are flipped horizontally.";
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {

View File

@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly Lazy<bool> hasKeyTexture; private readonly Lazy<bool> hasKeyTexture;
private readonly ManiaBeatmap beatmap; private readonly ManiaBeatmap beatmap;
private readonly bool isBeatmapConverted;
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap) public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
: base(skin) : base(skin)
{ {
this.beatmap = (ManiaBeatmap)beatmap; this.beatmap = (ManiaBeatmap)beatmap;
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null); isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() => hasKeyTexture = new Lazy<bool>(() =>
@@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public override ISample GetSample(ISampleInfo sampleInfo) public override ISample GetSample(ISampleInfo sampleInfo)
{ {
// layered hit sounds never play in mania // layered hit sounds never play in mania-native beatmaps (but do play on converts)
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered) if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
return new SampleVirtual(); return new SampleVirtual();
return base.GetSample(sampleInfo); return base.GetSample(sampleInfo);

View File

@@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@@ -10,6 +11,7 @@ using osu.Framework.Input.States;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var smokeContainer in smokeContainers) foreach (var smokeContainer in smokeContainers)
{ {
if (smokeContainer.Children.Count != 0) if (smokeContainer.Children.OfType<SkinnableDrawable>().Any())
return false; return false;
} }

View File

@@ -67,6 +67,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
LastAcceptedAction = null; LastAcceptedAction = null;
if (LastAcceptedAction != null && gameplayClock.IsRewinding)
LastAcceptedAction = null;
} }
protected abstract bool CheckValidNewAction(OsuAction action); protected abstract bool CheckValidNewAction(OsuAction action);

View File

@@ -0,0 +1,16 @@
// 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.Audio;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModRateAdjustConcrete : ModRateAdjustConcrete
{
}
}

View File

@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => @"Spinners will be automatically completed."; public override LocalisableString Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9; public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
public void ApplyToDrawableHitObject(DrawableHitObject hitObject) public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{ {

View File

@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModTouchDevice : ModTouchDevice public class OsuModTouchDevice : ModTouchDevice
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModBloom) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModBloom) }).ToArray();
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
} }
} }

View File

@@ -230,6 +230,14 @@ namespace osu.Game.Rulesets.Osu
new ModScoreV2(), new ModScoreV2(),
}; };
case ModType.Special:
#if DEBUG
return new Mod[]
{
new OsuModRateAdjustConcrete(),
};
#endif
default: default:
return Array.Empty<Mod>(); return Array.Empty<Mod>();
} }

View File

@@ -77,9 +77,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
base.LoadComplete(); base.LoadComplete();
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
}
LifetimeStart = smokeStartTime = Time.Current; public void StartDrawing(double time)
{
LifetimeStart = smokeStartTime = time;
LifetimeEnd = smokeEndTime = double.MaxValue;
SmokePoints.Clear();
lastPosition = null;
totalDistance = pointInterval; totalDistance = pointInterval;
} }

View File

@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@@ -19,17 +19,24 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary> /// </summary>
public partial class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction> public partial class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
{ {
private DrawablePool<SmokeSkinnableDrawable> segmentPool = null!;
private SmokeSkinnableDrawable? currentSegmentSkinnable; private SmokeSkinnableDrawable? currentSegmentSkinnable;
private Vector2 lastMousePosition; private Vector2 lastMousePosition;
public override bool ReceivePositionalInputAt(Vector2 _) => true; public override bool ReceivePositionalInputAt(Vector2 _) => true;
[BackgroundDependencyLoader]
private void load()
{
AddInternal(segmentPool = new DrawablePool<SmokeSkinnableDrawable>(10));
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e) public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{ {
if (e.Action == OsuAction.Smoke) if (e.Action == OsuAction.Smoke)
{ {
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())); AddInternal(currentSegmentSkinnable = segmentPool.Get(segment => segment.Segment?.StartDrawing(Time.Current)));
// Add initial position immediately. // Add initial position immediately.
addPosition(); addPosition();
@@ -59,17 +66,19 @@ namespace osu.Game.Rulesets.Osu.UI
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current); private void addPosition() => currentSegmentSkinnable?.Segment?.AddPosition(lastMousePosition, Time.Current);
private partial class SmokeSkinnableDrawable : SkinnableDrawable private partial class SmokeSkinnableDrawable : SkinnableDrawable
{ {
public SmokeSegment? Segment => Drawable as SmokeSegment;
public override bool RemoveWhenNotAlive => true; public override bool RemoveWhenNotAlive => true;
public override double LifetimeStart => Drawable.LifetimeStart; public override double LifetimeStart => Drawable.LifetimeStart;
public override double LifetimeEnd => Drawable.LifetimeEnd; public override double LifetimeEnd => Drawable.LifetimeEnd;
public SmokeSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) public SmokeSkinnableDrawable()
: base(lookup, defaultImplementation, confineMode) : base(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())
{ {
} }
} }

View File

@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private int rollingHits; private int rollingHits;
private readonly Container tickContainer; private readonly Container tickContainer;
private SkinnableDrawable headPiece;
private Color4 colourIdle; private Color4 colourIdle;
private Color4 colourEngaged; private Color4 colourEngaged;
@@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.Add(tickContainer = new Container Content.Add(tickContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = float.MinValue Depth = -1,
}); });
} }
@@ -79,7 +80,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void RecreatePieces() protected override void RecreatePieces()
{ {
if (headPiece != null)
Content.Remove(headPiece, true);
base.RecreatePieces(); base.RecreatePieces();
Content.Add(headPiece = createHeadPiece());
updateColour(); updateColour();
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE; Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
} }
@@ -122,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollBody), protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollBody),
_ => new ElongatedCirclePiece()); _ => new ElongatedCirclePiece());
private SkinnableDrawable createHeadPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollHead), _ => Empty())
{
RelativeSizeAxes = Axes.Y,
Depth = -2,
};
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false; public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
private void onNewResult(DrawableHitObject obj, JudgementResult result) private void onNewResult(DrawableHitObject obj, JudgementResult result)
@@ -174,7 +187,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private void updateColour(double fadeDuration = 0) private void updateColour(double fadeDuration = 0)
{ {
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
if (fadeDuration == 0)
{
// fade duration is 0 when calling via `RecreatePieces()`.
// in this case we want to apply the colour *without* using transforms.
// using transforms may result in the application of colour being undone via `DrawableHitObject.UpdateState()` clearing transforms.
if (MainPiece.Drawable is IHasAccentColour mainPieceWithAccentColour)
mainPieceWithAccentColour.AccentColour = newColour;
if (headPiece.Drawable is IHasAccentColour headPieceWithAccentColour)
headPieceWithAccentColour.AccentColour = newColour;
}
else
{
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
(headPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
}
} }
public partial class StrongNestedHit : DrawableStrongNestedHit public partial class StrongNestedHit : DrawableStrongNestedHit

View File

@@ -1,7 +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.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@@ -21,14 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
get get
{ {
// the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii. var headCentre = (body.ScreenSpaceDrawQuad.TopLeft + body.ScreenSpaceDrawQuad.BottomLeft) / 2;
// therefore naively taking the SSDQs of them and making a quad out of them results in a trapezoid shape and not a box.
var headCentre = headCircle.ScreenSpaceDrawQuad.Centre;
var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2; var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2;
float headRadius = headCircle.ScreenSpaceDrawQuad.Height / 2; float radius = body.ScreenSpaceDrawQuad.Height / 2;
float tailRadius = tailCircle.ScreenSpaceDrawQuad.Height / 2;
float radius = Math.Max(headRadius, tailRadius);
var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius); var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius);
return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight); return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight);
@@ -37,8 +32,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos);
private LegacyCirclePiece headCircle = null!;
private Sprite body = null!; private Sprite body = null!;
private Sprite tailCircle = null!; private Sprite tailCircle = null!;
@@ -66,10 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge), Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
}, },
headCircle = new LegacyCirclePiece
{
RelativeSizeAxes = Axes.Y,
},
}; };
AccentColour = colours.YellowDark; AccentColour = colours.YellowDark;
@@ -101,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
headCircle.AccentColour = colour;
body.Colour = colour; body.Colour = colour;
tailCircle.Colour = colour; tailCircle.Colour = colour;
} }

View File

@@ -103,6 +103,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
switch (taikoComponent.Component) switch (taikoComponent.Component)
{ {
case TaikoSkinComponents.DrumRollHead:
if (GetTexture("taiko-roll-middle") != null)
return new LegacyCirclePiece();
return null;
case TaikoSkinComponents.DrumRollBody: case TaikoSkinComponents.DrumRollBody:
if (GetTexture("taiko-roll-middle") != null) if (GetTexture("taiko-roll-middle") != null)
return new LegacyDrumRoll(); return new LegacyDrumRoll();

View File

@@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Taiko
InputDrum, InputDrum,
CentreHit, CentreHit,
RimHit, RimHit,
DrumRollHead,
DrumRollBody, DrumRollBody,
DrumRollTick, DrumRollTick,
Swell, Swell,

View File

@@ -42,6 +42,7 @@ namespace osu.Game.Tests.Chat
sentMessages = new List<Message>(); sentMessages = new List<Message>();
silencedUserIds = new List<int>(); silencedUserIds = new List<int>();
((DummyAPIAccess)API).LocalUserState.Blocks.Clear();
((DummyAPIAccess)API).HandleRequest = req => ((DummyAPIAccess)API).HandleRequest = req =>
{ {
switch (req) switch (req)
@@ -63,6 +64,10 @@ namespace osu.Game.Tests.Chat
silencedUserIds.Clear(); silencedUserIds.Clear();
return true; return true;
case GetMessagesRequest getMessages:
getMessages.TriggerSuccess(sentMessages);
return true;
case GetUpdatesRequest updatesRequest: case GetUpdatesRequest updatesRequest:
updatesRequest.TriggerSuccess(new GetUpdatesResponse updatesRequest.TriggerSuccess(new GetUpdatesResponse
{ {
@@ -161,6 +166,60 @@ namespace osu.Game.Tests.Chat
AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands")); AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands"));
} }
[Test]
public void TestBlockedUserMessagesAreDeletedFromInitialMessageBatch()
{
Channel channel = null;
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
{
ChannelId = channel.Id,
Content = "i am blocked",
SenderId = 1234
}));
AddStep("mark user as blocked", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
{
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
TargetID = 1234,
}));
AddStep("join channel and select it", () =>
{
channelManager.JoinChannel(channel);
channelManager.CurrentChannel.Value = channel;
});
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
}
[Test]
public void TestBlockedUserMessagesAreDeletedImmediatelyOnBlock()
{
Channel channel = null;
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
AddStep("join channel and select it", () =>
{
channelManager.JoinChannel(channel);
channelManager.CurrentChannel.Value = channel;
});
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
{
ChannelId = channel.Id,
Content = "i am blocked",
SenderId = 1234
}));
AddUntilStep("channel has message", () => channel.Messages, () => Is.Not.Empty);
AddStep("block user", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
{
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
TargetID = 1234,
}));
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
}
private void handlePostMessageRequest(PostMessageRequest request) private void handlePostMessageRequest(PostMessageRequest request)
{ {
var message = new Message(++currentMessageId) var message = new Message(++currentMessageId)

View File

@@ -126,6 +126,22 @@ namespace osu.Game.Tests.Gameplay
AssertBeatmapLookup(expected_sample); AssertBeatmapLookup(expected_sample);
} }
/// <summary>
/// Tests that a hitobject which specifies a specific sample file which doesn't exist (or isn't allowed to be looked up)
/// falls back to a normal sample.
/// </summary>
[Test]
public void TestFileSampleFallsBackToNormal()
{
const string expected_sample = "normal-hitnormal";
SetupSkins(null, expected_sample);
CreateTestWithBeatmap("file-beatmap-sample.osu");
AssertUserLookup(expected_sample);
}
/// <summary> /// <summary>
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>. /// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
/// </summary> /// </summary>

View File

@@ -29,17 +29,17 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 3, TotalScore = 750 }, new SoloScoreInfo { UserID = 3, TotalScore = 750 },
], placement_points); ], placement_points);
Assert.AreEqual(8, state.Users[1].Points); Assert.AreEqual(8, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users[1].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(1, state.Users[1].Rounds[1].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(6, state.Users[2].Points); Assert.AreEqual(6, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(3, state.Users[2].Placement); Assert.AreEqual(3, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(3, state.Users[2].Rounds[1].Placement); Assert.AreEqual(3, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(7, state.Users[3].Points); Assert.AreEqual(7, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(2, state.Users[3].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(2, state.Users[3].Rounds[1].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement);
// 2 -> 1 -> 3 // 2 -> 1 -> 3
@@ -51,17 +51,17 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 3, TotalScore = 500 }, new SoloScoreInfo { UserID = 3, TotalScore = 500 },
], placement_points); ], placement_points);
Assert.AreEqual(15, state.Users[1].Points); Assert.AreEqual(15, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users[1].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users[1].Rounds[2].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(2).Placement);
Assert.AreEqual(14, state.Users[2].Points); Assert.AreEqual(14, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(2, state.Users[2].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(1, state.Users[2].Rounds[2].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(2).Rounds.GetOrAdd(2).Placement);
Assert.AreEqual(13, state.Users[3].Points); Assert.AreEqual(13, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(3, state.Users[3].Placement); Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(3, state.Users[3].Rounds[2].Placement); Assert.AreEqual(3, state.Users.GetOrAdd(3).Rounds.GetOrAdd(2).Placement);
} }
[Test] [Test]
@@ -80,21 +80,21 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 4, TotalScore = 500 }, new SoloScoreInfo { UserID = 4, TotalScore = 500 },
], placement_points); ], placement_points);
Assert.AreEqual(7, state.Users[1].Points); Assert.AreEqual(7, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users[1].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users[1].Rounds[1].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(7, state.Users[2].Points); Assert.AreEqual(7, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(2, state.Users[2].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(2, state.Users[2].Rounds[1].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(5, state.Users[3].Points); Assert.AreEqual(5, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(3, state.Users[3].Placement); Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(4, state.Users[3].Rounds[1].Placement); Assert.AreEqual(4, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(5, state.Users[4].Points); Assert.AreEqual(5, state.Users.GetOrAdd(4).Points);
Assert.AreEqual(4, state.Users[4].Placement); Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement);
Assert.AreEqual(4, state.Users[4].Rounds[1].Placement); Assert.AreEqual(4, state.Users.GetOrAdd(4).Rounds.GetOrAdd(1).Placement);
} }
[Test] [Test]
@@ -120,8 +120,8 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, new SoloScoreInfo { UserID = 2, TotalScore = 1000 },
], placement_points); ], placement_points);
Assert.AreEqual(1, state.Users[1].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users[2].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
} }
[Test] [Test]
@@ -142,12 +142,12 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 5, TotalScore = 1000 }, new SoloScoreInfo { UserID = 5, TotalScore = 1000 },
], placement_points); ], placement_points);
Assert.AreEqual(1, state.Users[1].Placement); Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users[2].Placement); Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(3, state.Users[3].Placement); Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(4, state.Users[4].Placement); Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement);
Assert.AreEqual(5, state.Users[5].Placement); Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement);
Assert.AreEqual(6, state.Users[6].Placement); Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement);
} }
} }
} }

View File

@@ -220,10 +220,13 @@ namespace osu.Game.Tests.Visual.Components
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
if (registerAsOwner) if (registerAsOwner)
dependencies.CacheAs<IPreviewTrackOwner>(this); {
return dependencies; // Automatically handled by interface caching.
return base.CreateChildDependencies(parent);
}
return new DependencyContainer();
} }
} }

View File

@@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Drawable OverlayContent => InternalChild; public Drawable OverlayContent => InternalChild;
public Drawable FadingContent => (OverlayContent as Container)?.Child; public new Drawable FadingContent => (OverlayContent as Container)?.Child;
} }
} }
} }

View File

@@ -2,17 +2,20 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
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.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay;
using osuTK; using osuTK;
@@ -21,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{ {
public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene
{ {
private MultiplayerPlaylistItem[] items = null!; private MatchmakingPlaylistItem[] items = null!;
private BeatmapSelectGrid grid = null!; private BeatmapSelectGrid grid = null!;
@@ -36,24 +39,44 @@ namespace osu.Game.Tests.Visual.Matchmaking
.Take(50) .Take(50)
.ToArray(); .ToArray();
IEnumerable<MatchmakingPlaylistItem> playlistItems;
if (beatmaps.Length > 0) if (beatmaps.Length > 0)
{ {
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem playlistItems = Enumerable.Range(1, 50).Select(i =>
{
var beatmap = beatmaps[i % beatmaps.Length];
return new MatchmakingPlaylistItem(
new MultiplayerPlaylistItem
{ {
ID = i, ID = i,
BeatmapID = beatmaps[i % beatmaps.Length].OnlineID, BeatmapID = beatmap.OnlineID,
StarRating = i / 10.0, StarRating = i / 10.0,
}).ToArray(); },
CreateAPIBeatmap(beatmap),
Array.Empty<Mod>()
);
});
} }
else else
{ {
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem playlistItems = Enumerable.Range(1, 50).Select(i => new MatchmakingPlaylistItem(
new MultiplayerPlaylistItem
{ {
ID = i, ID = i,
BeatmapID = i, BeatmapID = i,
StarRating = i / 10.0, StarRating = i / 10.0,
}).ToArray(); },
CreateAPIBeatmap(),
Array.Empty<Mod>()
));
} }
foreach (var item in playlistItems)
item.Beatmap.StarRating = item.PlaylistItem.StarRating;
items = playlistItems.ToArray();
} }
public override void SetUpSteps() public override void SetUpSteps()
@@ -70,8 +93,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("add items", () => AddStep("add items", () =>
{ {
foreach (var item in items) grid.AddItems(items);
grid.AddItem(item);
}); });
AddWaitStep("wait for panels", 3); AddWaitStep("wait for panels", 3);
@@ -85,17 +107,17 @@ namespace osu.Game.Tests.Visual.Matchmaking
// test scene is weird. // test scene is weird.
}); });
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser AddStep("add selection 1", () => grid.ChildrenOfType<MatchmakingSelectPanel>().First().AddUser(new APIUser
{ {
Id = DummyAPIAccess.DUMMY_USER_ID, Id = DummyAPIAccess.DUMMY_USER_ID,
Username = "Maarvin", Username = "Maarvin",
})); }));
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser AddStep("add selection 2", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(5).First().AddUser(new APIUser
{ {
Id = 2, Id = 2,
Username = "peppy", Username = "peppy",
})); }));
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser AddStep("add selection 3", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(10).First().AddUser(new APIUser
{ {
Id = 1040328, Id = 1040328,
Username = "smoogipoo", Username = "smoogipoo",
@@ -180,7 +202,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("display roll order", () => AddStep("display roll order", () =>
{ {
var panels = grid.ChildrenOfType<BeatmapSelectPanel>().ToArray(); var panels = grid.ChildrenOfType<MatchmakingSelectPanel>().ToArray();
for (int i = 0; i < panels.Length; i++) for (int i = 0; i < panels.Length; i++)
{ {
@@ -197,6 +219,23 @@ namespace osu.Game.Tests.Visual.Matchmaking
}); });
} }
[Test]
public void TestPresentRandomItem()
{
AddStep("present random item panel", () =>
{
grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0);
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
grid.PlayRollAnimation(-1, duration: 0);
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(-1), 500);
});
AddWaitStep("wait for animation", 5);
AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem));
}
private (long[] candidateItems, long finalItem) pickRandomItems(int count) private (long[] candidateItems, long finalItem) pickRandomItems(int count)
{ {
long[] candidateItems = items.Select(it => it.ID).ToArray(); long[] candidateItems = items.Select(it => it.ID).ToArray();

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@@ -9,6 +10,7 @@ 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.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Multiplayer;
@@ -19,17 +21,35 @@ namespace osu.Game.Tests.Visual.Matchmaking
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () =>
{
var room = CreateDefaultRoom(MatchType.Matchmaking);
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = 0,
StarRating = i / 10.0,
})).ToArray();
JoinRoom(room);
});
}
[Test] [Test]
public void TestBeatmapPanel() public void TestBeatmapPanel()
{ {
BeatmapSelectPanel? panel = null; MatchmakingSelectPanel? panel = null;
AddStep("add panel", () => AddStep("add panel", () =>
{ {
Child = new OsuContextMenuContainer Child = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), []))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@@ -56,10 +76,54 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 })); AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 }));
AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 })); AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 }));
AddToggleStep("allow selection", value => AddToggleStep("allow selection", value => panel!.AllowSelection = value);
}
[Test]
public void TestRandomPanel()
{ {
if (panel != null) MatchmakingSelectPanelRandom? panel = null;
panel.AllowSelection = value;
AddStep("add panel", () =>
{
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), []));
}
[Test]
public void TestBeatmapWithMods()
{
AddStep("add panel", () =>
{
MatchmakingSelectPanel? panel;
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [new OsuModHardRock(), new OsuModDoubleTime()]))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
panel.AddUser(new APIUser
{
Id = 2,
Username = "peppy",
});
}); });
} }
} }

View File

@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("load screen", () => LoadScreen(new IntroScreen())); AddStep("load screen", () => LoadScreen(new ScreenIntro()));
} }
[Test] [Test]

View File

@@ -124,11 +124,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next())) foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next()))
{ {
state.Users[user.UserID].Placement = i++; state.Users.GetOrAdd(user.UserID).Placement = i++;
state.Users[user.UserID].Points = (8 - i) * 7; state.Users.GetOrAdd(user.UserID).Points = (8 - i) * 7;
state.Users[user.UserID].Rounds[1].Placement = 1; state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Placement = 1;
state.Users[user.UserID].Rounds[1].TotalScore = 1; state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).TotalScore = 1;
state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1; state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Statistics[HitResult.LargeBonus] = 1;
} }
}); });
} }

View File

@@ -27,7 +27,13 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined(); WaitForJoined();
AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) AddStep("join other player to room", () => MultiplayerClient.AddUser(new APIUser
{
Id = 2,
Username = "peppy",
}));
AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(2)
{ {
User = new APIUser User = new APIUser
{ {
@@ -85,9 +91,9 @@ namespace osu.Game.Tests.Visual.Matchmaking
UserDictionary = UserDictionary =
{ {
{ {
1, new MatchmakingUser 2, new MatchmakingUser
{ {
UserId = 1, UserId = 2,
Placement = 1, Placement = 1,
Points = ++points Points = ++points
} }
@@ -100,7 +106,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
[Test] [Test]
public void TestJump() public void TestJump()
{ {
AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(2, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely());
} }
[Test] [Test]
@@ -108,5 +114,14 @@ namespace osu.Game.Tests.Visual.Matchmaking
{ {
AddToggleStep("toggle quit", quit => panel.HasQuit = quit); AddToggleStep("toggle quit", quit => panel.HasQuit = quit);
} }
[Test]
public void TestDownloadProgress()
{
AddStep("set download progress 20%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.2f)));
AddStep("set download progress 50%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.5f)));
AddStep("set download progress 90%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.9f)));
AddStep("set locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.LocallyAvailable()));
}
} }
} }

View File

@@ -8,7 +8,9 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Matchmaking.Events;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@@ -153,10 +155,69 @@ namespace osu.Game.Tests.Visual.Matchmaking
MatchmakingRoomState state = new MatchmakingRoomState(); MatchmakingRoomState state = new MatchmakingRoomState();
for (int i = 0; i < room.Users.Count; i++) for (int i = 0; i < room.Users.Count; i++)
state.Users[room.Users[i].UserID].Placement = placements[i]; state.Users.GetOrAdd(room.Users[i].UserID).Placement = placements[i];
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
}); });
} }
[Test]
public void InteractionSpam()
{
AddStep("join users", () =>
{
for (int i = 0; i < 7; i++)
{
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
{
User = new APIUser
{
Username = $"User {i}"
}
});
}
});
AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid);
AddStep("player jump", () => { MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); });
AddStep("local jumping", () => jumpSpam(false));
AddWaitStep("wait", 25);
AddStep("group jumping spam", () => jumpSpam(true));
AddWaitStep("wait", 25);
AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split);
AddStep("local jumping", () => jumpSpam(false));
AddWaitStep("wait", 25);
AddStep("group jumping spam", () => jumpSpam(true));
AddWaitStep("wait", 25);
AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden);
AddStep("local jumping", () => jumpSpam(false));
AddWaitStep("wait", 25);
AddStep("group jumping spam", () => jumpSpam(true));
AddWaitStep("wait", 25);
}
private void jumpSpam(bool everyone)
{
for (int i = 0; i < 30; i++)
{
Scheduler.AddDelayed(() =>
{
MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
}, i * 150 + RNG.NextDouble(0, 140));
if (!everyone)
continue;
for (int ii = 0; ii < 7; ii++)
{
int iii = ii;
Scheduler.AddDelayed(() =>
{
MultiplayerClient.SendUserMatchRequest(iii, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
}, i * 150 + RNG.NextDouble(0, 140));
}
}
}
} }
} }

View File

@@ -36,28 +36,28 @@ namespace osu.Game.Tests.Visual.Matchmaking
int localUserId = API.LocalUser.Value.OnlineID; int localUserId = API.LocalUser.Value.OnlineID;
// Overall state. // Overall state.
state.Users[localUserId].Placement = 1; state.Users.GetOrAdd(localUserId).Placement = 1;
state.Users[localUserId].Points = 8; state.Users.GetOrAdd(localUserId).Points = 8;
for (int round = 1; round <= state.CurrentRound; round++) for (int round = 1; round <= state.CurrentRound; round++)
state.Users[localUserId].Rounds[round].Placement = round; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round;
// Highest score. // Highest score.
state.Users[localUserId].Rounds[1].TotalScore = 1000; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
// Highest accuracy. // Highest accuracy.
state.Users[localUserId].Rounds[2].Accuracy = 0.9995; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
// Highest combo. // Highest combo.
state.Users[localUserId].Rounds[3].MaxCombo = 100; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
// Most bonus score. // Most bonus score.
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
// Smallest score difference. // Smallest score difference.
state.Users[localUserId].Rounds[5].TotalScore = 1000; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
// Largest score difference. // Largest score difference.
state.Users[localUserId].Rounds[6].TotalScore = 1000; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
}); });
@@ -103,36 +103,78 @@ namespace osu.Game.Tests.Visual.Matchmaking
int localUserId = API.LocalUser.Value.OnlineID; int localUserId = API.LocalUser.Value.OnlineID;
// Overall state. // Overall state.
state.Users[localUserId].Placement = 1; state.Users.GetOrAdd(localUserId).Placement = 1;
state.Users[localUserId].Points = 8; state.Users.GetOrAdd(localUserId).Points = 8;
state.Users[invalid_user_id].Placement = 2; state.Users.GetOrAdd(invalid_user_id).Placement = 2;
state.Users[invalid_user_id].Points = 7; state.Users.GetOrAdd(invalid_user_id).Points = 7;
for (int round = 1; round <= state.CurrentRound; round++) for (int round = 1; round <= state.CurrentRound; round++)
state.Users[localUserId].Rounds[round].Placement = round; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round;
// Highest score. // Highest score.
state.Users[localUserId].Rounds[1].TotalScore = 1000; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
state.Users[invalid_user_id].Rounds[1].TotalScore = 990; state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(1).TotalScore = 990;
// Highest accuracy. // Highest accuracy.
state.Users[localUserId].Rounds[2].Accuracy = 0.9995; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5; state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(2).Accuracy = 0.5;
// Highest combo. // Highest combo.
state.Users[localUserId].Rounds[3].MaxCombo = 100; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
state.Users[invalid_user_id].Rounds[3].MaxCombo = 10; state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(3).MaxCombo = 10;
// Most bonus score. // Most bonus score.
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
state.Users[invalid_user_id].Rounds[4].Statistics[HitResult.LargeBonus] = 25; state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 25;
// Smallest score difference. // Smallest score difference.
state.Users[localUserId].Rounds[5].TotalScore = 1000; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
state.Users[invalid_user_id].Rounds[5].TotalScore = 999; state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(5).TotalScore = 999;
// Largest score difference. // Largest score difference.
state.Users[localUserId].Rounds[6].TotalScore = 1000; state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
state.Users[invalid_user_id].Rounds[6].TotalScore = 0; state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(6).TotalScore = 0;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
[Test]
public void TestNoUsers()
{
AddStep("show results with no users", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
[Test]
public void TestUserWithNoScore()
{
AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(2)
{
User = new APIUser
{
Id = 2,
Username = "Other user"
}
}));
AddStep("show results with no score", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
state.Users.GetOrAdd(API.LocalUser.Value.OnlineID).Rounds.GetOrAdd(1).Placement = 1;
state.Users.GetOrAdd(2);
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
}); });

View File

@@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerSkipOverlay : MultiplayerTestScene
{
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
AddStep("add skip overlay", () =>
{
GameplayClockContainer gameplayClockContainer;
var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new MultiplayerSkipOverlay(120000)
},
};
gameplayClockContainer.Start();
});
AddStep("set playing state", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Playing));
}
[Test]
public void TestSkip()
{
for (int i = 0; i < 4; i++)
{
int i2 = i;
AddStep($"join user {i2}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = i2,
Username = $"User {i2}"
});
MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing);
});
}
AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely());
for (int i = 0; i < 4; i++)
{
int i2 = i;
AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely());
}
}
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Tests.Visual.UserInterface;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneGlobalRankDisplay : ThemeComparisonTestScene
{
public TestSceneGlobalRankDisplay()
: base(false)
{
}
protected override Drawable CreateContent() => new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Full,
Padding = new MarginPadding(20),
Spacing = new Vector2(40),
ChildrenEnumerable = new int?[] { 64, 423, 1_453, 3_468, 8_367, 48_342, 78_432, 375_231, 897_783, null }.Select(createDisplay)
};
private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay
{
UserStatistics =
{
Value = new UserStatistics
{
GlobalRank = rank,
GlobalRankPercent = rank / 1_000_000f,
Variants =
[
new UserStatistics.Variant
{
VariantType = UserStatistics.RulesetVariant.FourKey,
GlobalRank = rank / 3,
},
new UserStatistics.Variant
{
VariantType = UserStatistics.RulesetVariant.SevenKey,
GlobalRank = 2 * rank / 3,
}
]
},
},
HighestRank =
{
Value = rank == null
? null
: new APIUser.UserRankHighest
{
Rank = rank.Value / 2,
UpdatedAt = DateTimeOffset.Now.AddMonths(-3),
}
}
};
}
}

View File

@@ -4,9 +4,12 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2; using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2 namespace osu.Game.Tests.Visual.SongSelectV2
@@ -322,5 +325,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SelectNextSet(); SelectNextSet();
AddUntilStep("no beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.Zero); AddUntilStep("no beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.Zero);
} }
[Test]
public void TestGroupChangedAfterEngagingArtistGrouping()
{
RemoveAllBeatmaps();
AddStep("add test beatmaps", () =>
{
for (int i = 0; i < 5; ++i)
{
var baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3);
var metadata = new BeatmapMetadata
{
Artist = $"{(char)('A' + i)} artist",
Title = $"{(char)('A' + 4 - i)} title",
};
foreach (var b in baseTestBeatmap.Beatmaps)
b.Metadata = metadata;
Realm.Write(r => r.Add(baseTestBeatmap, update: true));
BeatmapSets.Add(baseTestBeatmap.Detach());
}
SortAndGroupBy(SortMode.Title, GroupMode.Title);
SelectNextSet();
SelectNextSet();
WaitForExpandedGroup(1);
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
WaitForExpandedGroup(3);
});
}
} }
} }

View File

@@ -2,9 +2,12 @@
// 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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@@ -13,7 +16,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public partial class TestSceneDrawableDate : OsuTestScene public partial class TestSceneDrawableDate : OsuTestScene
{ {
public TestSceneDrawableDate() [SetUpSteps]
public void SetUpSteps()
{
AddStep("Create 7 dates", () =>
{ {
Child = new FillFlowContainer Child = new FillFlowContainer
{ {
@@ -32,6 +38,13 @@ namespace osu.Game.Tests.Visual.UserInterface
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))), new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
} }
}; };
});
}
[Test]
public void TestSecondsUpdate()
{
AddUntilStep("4th date says \"2 seconds ago\"", () => this.ChildrenOfType<DrawableDate>().ElementAt(3).Current.Value == "2 seconds ago");
} }
private partial class PokeyDrawableDate : CompositeDrawable private partial class PokeyDrawableDate : CompositeDrawable

View File

@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
namespace osu.Game.Audio namespace osu.Game.Audio
{ {
/// <summary> /// <summary>
@@ -10,6 +12,7 @@ namespace osu.Game.Audio
/// <see cref="IPreviewTrackOwner"/>s can cancel the currently playing <see cref="PreviewTrack"/> through the /// <see cref="IPreviewTrackOwner"/>s can cancel the currently playing <see cref="PreviewTrack"/> through the
/// global <see cref="PreviewTrackManager"/> if they're the owner of the playing <see cref="PreviewTrack"/>. /// global <see cref="PreviewTrackManager"/> if they're the owner of the playing <see cref="PreviewTrack"/>.
/// </remarks> /// </remarks>
[Cached]
public interface IPreviewTrackOwner public interface IPreviewTrackOwner
{ {
} }

View File

@@ -157,6 +157,12 @@ namespace osu.Game.Beatmaps
public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b); public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b);
public override int GetHashCode()
{
// ReSharper disable once NonReadonlyMemberInGetHashCode
return ID.GetHashCode();
}
public bool AudioEquals(BeatmapInfo? other) => other != null public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null && BeatmapSet != null
&& other.BeatmapSet != null && other.BeatmapSet != null

View File

@@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps
LocallyModified = -4, LocallyModified = -4,
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.StatusUnknown))] [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.StatusUnknown))]
[Description("Unknown")] [Description("Offline")]
None = -3, None = -3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]

View File

@@ -33,7 +33,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Status = beatmapSet.Status, Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
TextSize = 13f TextSize = 13f,
ShowUnknownStatus = true
}, },
new DifficultySpectrumDisplay new DifficultySpectrumDisplay
{ {

View File

@@ -544,7 +544,7 @@ namespace osu.Game.Beatmaps.Formats
if (!banksOnly) if (!banksOnly)
{ {
int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))); int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; string sampleFilename = samples.FirstOrDefault(s => s is ConvertHitObjectParser.FileHitSampleInfo)?.LookupNames.First() ?? string.Empty;
int volume = samples.FirstOrDefault()?.Volume ?? 100; int volume = samples.FirstOrDefault()?.Volume ?? 100;
// We want to ignore custom sample banks and volume when not encoding to the mania game mode, // We want to ignore custom sample banks and volume when not encoding to the mania game mode,

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
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;
@@ -263,11 +264,11 @@ namespace osu.Game.Collections
{ {
Debug.Assert(collection != null); Debug.Assert(collection != null);
collection.PerformWrite(c => Task.Run(() => collection.PerformWrite(c =>
{ {
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
}); }));
} }
protected override Drawable CreateContent() => (Content)base.CreateContent(); protected override Drawable CreateContent() => (Content)base.CreateContent();

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@@ -10,7 +11,7 @@ namespace osu.Game.Collections
public class CollectionToggleMenuItem : ToggleMenuItem public class CollectionToggleMenuItem : ToggleMenuItem
{ {
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap) public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => : base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => Task.Run(() =>
{ {
collection.PerformWrite(c => collection.PerformWrite(c =>
{ {
@@ -19,7 +20,7 @@ namespace osu.Game.Collections
else else
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash); c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
}); });
}) }))
{ {
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash)); State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
} }

View File

@@ -785,32 +785,20 @@ namespace osu.Game.Graphics.Carousel
// We are performing two important operations here: // We are performing two important operations here:
// - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions. // - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions.
// - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use. // - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use.
FindCarouselItemsForSelection(ref currentKeyboardSelection, ref currentSelection, carouselItems);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
var item = carouselItems[i]; var item = carouselItems[i];
bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!);
bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!);
// while we don't know the Y position of the item yet, as it's about to be updated,
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
// at the correct item to avoid redundant local equality checks.
// the Y positions will be filled in after they're computed.
if (isKeyboardSelection)
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i);
if (isSelection)
currentSelection = new Selection(currentSelection.Model, item, null, i);
updateItemYPosition(item, ref lastVisible, ref yPos); updateItemYPosition(item, ref lastVisible, ref yPos);
if (isKeyboardSelection)
currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
if (isSelection)
currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
} }
if (currentKeyboardSelection.CarouselItem is CarouselItem currentKeyboardSelectionItem)
currentKeyboardSelection = currentKeyboardSelection with { YPosition = currentKeyboardSelectionItem.CarouselYPosition + currentKeyboardSelectionItem.DrawHeight / 2 };
if (currentSelection.CarouselItem is CarouselItem currentSelectionItem)
currentSelection = currentSelection with { YPosition = currentSelectionItem.CarouselYPosition + currentSelectionItem.DrawHeight / 2 };
// Update the total height of all items (to make the scroll container scrollable through the full height even though // Update the total height of all items (to make the scroll container scrollable through the full height even though
// most items are not displayed / loaded). // most items are not displayed / loaded).
Scroll.SetLayoutHeight(yPos + visibleHalfHeight); Scroll.SetLayoutHeight(yPos + visibleHalfHeight);
@@ -821,6 +809,27 @@ namespace osu.Game.Graphics.Carousel
Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
} }
protected virtual void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
{
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
bool isKeyboardSelection = CheckModelEquality(item.Model, keyboardSelection.Model!);
bool isSelection = CheckModelEquality(item.Model, selection.Model!);
// while we don't know the Y position of the item yet, as it's about to be updated,
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
// at the correct item to avoid redundant local equality checks.
// the Y positions will be filled in after they're computed.
if (isKeyboardSelection)
keyboardSelection = new Selection(keyboardSelection.Model, item, null, i);
if (isSelection)
selection = new Selection(selection.Model, item, null, i);
}
}
#endregion #endregion
#region Display handling #region Display handling
@@ -1081,7 +1090,7 @@ namespace osu.Game.Graphics.Carousel
/// <param name="CarouselItem">A related carousel item representation for the model. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param> /// <param name="CarouselItem">A related carousel item representation for the model. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
/// <param name="YPosition">The Y position of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param> /// <param name="YPosition">The Y position of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param> /// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null); protected record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
private record DisplayRange(int First, int Last) private record DisplayRange(int First, int Last)
{ {

View File

@@ -15,7 +15,6 @@ using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
[Cached(typeof(IPreviewTrackOwner))]
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction> public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
{ {
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);

View File

@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Utils; using osu.Game.Utils;
@@ -80,7 +81,7 @@ namespace osu.Game.Graphics
public DateTimeOffset TooltipContent => Date; public DateTimeOffset TooltipContent => Date;
private class HumanisedDate : IEquatable<HumanisedDate>, ILocalisableStringData private class HumanisedDate : ILocalisableStringData
{ {
public readonly DateTimeOffset Date; public readonly DateTimeOffset Date;
@@ -89,11 +90,18 @@ namespace osu.Game.Graphics
Date = date; Date = date;
} }
public bool Equals(HumanisedDate? other) /// <remarks>
=> other?.Date != null && Date.Equals(other.Date); /// Humanizer formats the <see cref="Date"/> relative to the local computer time.
/// Therefore, replacing a <see cref="HumanisedDate"/> instance with another instance of the class with the same <see cref="Date"/>
public bool Equals(ILocalisableStringData? other) /// should have the effect of replacing and re-formatting the text.
=> other is HumanisedDate otherDate && Equals(otherDate); /// Including <see cref="Date"/> in equality members would stop this from happening, as <see cref="SpriteText.Text"/>
/// has equality-based early guards to prevent redundant text replaces.
/// Thus, instances of these class just compare <see langword="false"/> to any <see cref="ILocalisableStringData"/> to ensure re-formatting happens correctly.
/// There are "technically" more "correct" ways to do this (like also including the current time into equality checks),
/// but they are simultaneously functionally equivalent to this and overly convoluted.
/// This is a private hack-job of a wrapper around humanizer anyway.
/// </remarks>
public bool Equals(ILocalisableStringData? other) => false;
public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date); public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date);

View File

@@ -129,7 +129,7 @@ namespace osu.Game.Graphics
switch (status) switch (status)
{ {
case BeatmapOnlineStatus.None: case BeatmapOnlineStatus.None:
return Color4.RosyBrown; return Color4.AliceBlue;
case BeatmapOnlineStatus.LocallyModified: case BeatmapOnlineStatus.LocallyModified:
return Color4.OrangeRed; return Color4.OrangeRed;
@@ -183,6 +183,9 @@ namespace osu.Game.Graphics
case ModType.System: case ModType.System:
return Yellow; return Yellow;
case ModType.Special:
return Orange2;
default: default:
throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type"); throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type");
} }

View File

@@ -13,6 +13,8 @@ namespace osu.Game.Graphics.UserInterface
{ {
public partial class ProgressBar : SliderBar<double> public partial class ProgressBar : SliderBar<double>
{ {
public bool Seeking { get; private set; }
public Action<double> OnSeek; public Action<double> OnSeek;
private readonly Box fill; private readonly Box fill;
@@ -75,6 +77,16 @@ namespace osu.Game.Graphics.UserInterface
fill.Width = value * UsableWidth; fill.Width = value * UsableWidth;
} }
protected override void OnUserChange(double value) => OnSeek?.Invoke(value); protected override void OnUserChange(double value)
{
Seeking = true;
}
protected override bool Commit()
{
OnSeek?.Invoke(CurrentNumber.Value);
Seeking = false;
return base.Commit();
}
} }
} }

View File

@@ -54,7 +54,13 @@ namespace osu.Game.Graphics.UserInterface
{ {
case Key.KeypadEnter: case Key.KeypadEnter:
case Key.Enter: case Key.Enter:
return false; // even if committing per se is not allowed for this textbox,
// the commit flow is also responsible for terminating any active IME.
// ensure that the Enter press terminates IME correctly
// and is also handled if it needs to be, so that it doesn't leak to some other non-focused drawable and cause breakage.
bool wasImeComposing = ImeCompositionActive;
FinalizeImeComposition(true);
return wasImeComposing;
} }
} }

View File

@@ -99,6 +99,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied."); public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied.");
/// <summary>
/// "Use experimental audio mode"
/// </summary>
public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode");
/// <summary>
/// "This will attempt to initialise the audio engine in a lower latency mode."
/// </summary>
public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode.");
/// <summary>
/// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."
/// </summary>
public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@@ -84,9 +84,12 @@ Please bear with us as we continue to improve the game for you!");
public static LocalisableString GreetingNotification => new TranslatableString(getKey(@"greeting_notification"), @"Welcome to jvnkosu!"); public static LocalisableString GreetingNotification => new TranslatableString(getKey(@"greeting_notification"), @"Welcome to jvnkosu!");
/// <summary> /// <summary>
/// "Failed to load backgrounds!\nCheck your internet connection" /// "Failed to load backgrounds!
/// Please check your internet connection"
/// </summary> /// </summary>
public static LocalisableString SeasonalBackgroundsFail => new TranslatableString(getKey(@"seasonal_backgrounds_fail"), @"Failed to load backgrounds!\nCheck your internet connection"); // TODO: implement l10n in osu-resources public static LocalisableString SeasonalBackgroundsFail => new TranslatableString(getKey(@"seasonal_backgrounds_fail"),
@"Failed to load backgrounds!
Please check your internet connection"); // TODO: implement l10n in osu-resources
/// <summary> /// <summary>
/// "Successfully refreshed background categories!" /// "Successfully refreshed background categories!"

View File

@@ -15,9 +15,9 @@ namespace osu.Game.Localisation
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"settings"); public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"settings");
/// <summary> /// <summary>
/// "change the way game behaves" /// "change the way your game behaves"
/// </summary> /// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"change the way game behaves"); public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"change the way your game behaves");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }

View File

@@ -40,9 +40,9 @@ namespace osu.Game.Localisation
public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified"); public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified");
/// <summary> /// <summary>
/// "Unknown" /// "Offline"
/// </summary> /// </summary>
public static LocalisableString StatusUnknown => new TranslatableString(getKey(@"status_unknown"), @"Unknown"); public static LocalisableString StatusUnknown => new TranslatableString(getKey(@"status_unknown"), @"Offline");
/// <summary> /// <summary>
/// "Total Plays" /// "Total Plays"

View File

@@ -33,6 +33,7 @@ namespace osu.Game.Online.API.Requests
Medal, Medal,
Rank, Rank,
RankLost, RankLost,
RankRetracted,
UserSupportAgain, UserSupportAgain,
UserSupportFirst, UserSupportFirst,
UserSupportGift, UserSupportGift,

View File

@@ -44,7 +44,7 @@ namespace osu.Game.Online.API.Requests
private class VerificationFailureResponse private class VerificationFailureResponse
{ {
[JsonProperty("method")] [JsonProperty("method")]
public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; } public SessionVerificationMethod? RequiredSessionVerificationMethod { get; set; }
} }
} }
} }

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@@ -70,6 +71,7 @@ namespace osu.Game.Online.Chat
private UserLookupCache users { get; set; } private UserLookupCache users { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>(); private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
private ScheduledDelegate scheduledAck; private ScheduledDelegate scheduledAck;
private IChatClient chatClient = null!; private IChatClient chatClient = null!;
@@ -95,6 +97,9 @@ namespace osu.Game.Online.Chat
apiState.BindTo(api.State); apiState.BindTo(api.State);
apiState.BindValueChanged(_ => SendAck(), true); apiState.BindValueChanged(_ => SendAck(), true);
localUserBlocks.BindTo(api.LocalUserState.Blocks);
localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args)));
} }
/// <summary> /// <summary>
@@ -311,8 +316,9 @@ namespace osu.Game.Online.Chat
private void addMessages(List<Message> messages) private void addMessages(List<Message> messages)
{ {
var channels = JoinedChannels.ToList(); var channels = JoinedChannels.ToList();
var blockedUserIds = localUserBlocks.Select(b => b.TargetID).ToList();
foreach (var group in messages.GroupBy(m => m.ChannelId)) foreach (var group in messages.Where(m => !blockedUserIds.Contains(m.SenderId)).GroupBy(m => m.ChannelId))
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
lastSilenceMessageId ??= messages.LastOrDefault()?.Id; lastSilenceMessageId ??= messages.LastOrDefault()?.Id;
@@ -641,6 +647,18 @@ namespace osu.Game.Online.Chat
api.Queue(req); api.Queue(req);
} }
private void onBlocksChanged(NotifyCollectionChangedEventArgs args)
{
if (args.Action != NotifyCollectionChangedAction.Add)
return;
foreach (APIRelation newBlock in args.NewItems!)
{
foreach (var channel in joinedChannels)
channel.RemoveMessagesFromUser(newBlock.TargetID);
}
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@@ -43,11 +43,15 @@ namespace osu.Game.Online.Matchmaking
/// <summary> /// <summary>
/// The user has raised a candidate playlist item to be played. /// The user has raised a candidate playlist item to be played.
/// </summary> /// </summary>
/// <param name="userId">The notifying user.</param>
/// <param name="playlistItemId">The playlist item candidate raised, or -1 as a special value that indicates a random selection.</param>
Task MatchmakingItemSelected(int userId, long playlistItemId); Task MatchmakingItemSelected(int userId, long playlistItemId);
/// <summary> /// <summary>
/// The user has removed a candidate playlist item. /// The user has removed a candidate playlist item.
/// </summary> /// </summary>
/// <param name="userId">The notifying user.</param>
/// <param name="playlistItemId">The playlist item candidate removed, or -1 as a special value that indicates a random selection.</param>
Task MatchmakingItemDeselected(int userId, long playlistItemId); Task MatchmakingItemDeselected(int userId, long playlistItemId);
} }
} }

View File

@@ -45,7 +45,7 @@ namespace osu.Game.Online.Matchmaking
/// <summary> /// <summary>
/// Raise a candidate playlist item to be played in the current round. /// Raise a candidate playlist item to be played in the current round.
/// </summary> /// </summary>
/// <param name="playlistItemId">The playlist item.</param> /// <param name="playlistItemId">The playlist item, or -1 to indicate a random selection.</param>
Task MatchmakingToggleSelection(long playlistItemId); Task MatchmakingToggleSelection(long playlistItemId);
/// <summary> /// <summary>

View File

@@ -149,5 +149,15 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
/// <param name="item">The changed item.</param> /// <param name="item">The changed item.</param>
Task PlaylistItemChanged(MultiplayerPlaylistItem item); Task PlaylistItemChanged(MultiplayerPlaylistItem item);
/// <summary>
/// Signals that a user has requested to skip the beatmap intro.
/// </summary>
Task UserVotedToSkipIntro(int userId);
/// <summary>
/// Signals that the vote to skip the beatmap intro has passed.
/// </summary>
Task VoteToSkipIntroPassed();
} }
} }

View File

@@ -112,6 +112,11 @@ namespace osu.Game.Online.Multiplayer
/// <param name="playlistItemId">The item to remove.</param> /// <param name="playlistItemId">The item to remove.</param>
Task RemovePlaylistItem(long playlistItemId); Task RemovePlaylistItem(long playlistItemId);
/// <summary>
/// Votes to skip the beatmap intro.
/// </summary>
Task VoteToSkipIntro();
/// <summary> /// <summary>
/// Invites a player to the current room. /// Invites a player to the current room.
/// </summary> /// </summary>

View File

@@ -81,10 +81,10 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
foreach (var score in scoreGroup) foreach (var score in scoreGroup)
{ {
MatchmakingUser mmUser = Users[score.UserID]; MatchmakingUser mmUser = Users.GetOrAdd(score.UserID);
mmUser.Points += placementPoints[placement - 1]; mmUser.Points += placementPoints[placement - 1];
MatchmakingRound mmRound = mmUser.Rounds[CurrentRound]; MatchmakingRound mmRound = mmUser.Rounds.GetOrAdd(CurrentRound);
mmRound.Placement = placement; mmRound.Placement = placement;
mmRound.TotalScore = score.TotalScore; mmRound.TotalScore = score.TotalScore;
mmRound.Accuracy = score.Accuracy; mmRound.Accuracy = score.Accuracy;

View File

@@ -22,25 +22,22 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
public IDictionary<int, MatchmakingRound> RoundsDictionary { get; set; } = new Dictionary<int, MatchmakingRound>(); public IDictionary<int, MatchmakingRound> RoundsDictionary { get; set; } = new Dictionary<int, MatchmakingRound>();
/// <summary> /// <summary>
/// Creates or retrieves the score for the given round. /// The total number of rounds.
/// </summary>
[IgnoreMember]
public int Count => RoundsDictionary.Count;
/// <summary>
/// Retrieves or adds a <see cref="MatchmakingRound"/> entry to this list.
/// </summary> /// </summary>
/// <param name="round">The round.</param> /// <param name="round">The round.</param>
public MatchmakingRound this[int round] public MatchmakingRound GetOrAdd(int round)
{
get
{ {
if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score)) if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score))
return score; return score;
return RoundsDictionary[round] = new MatchmakingRound { Round = round }; return RoundsDictionary[round] = new MatchmakingRound { Round = round };
} }
}
/// <summary>
/// The total number of rounds.
/// </summary>
[IgnoreMember]
public int Count => RoundsDictionary.Count;
public IEnumerator<MatchmakingRound> GetEnumerator() => RoundsDictionary.Values.GetEnumerator(); public IEnumerator<MatchmakingRound> GetEnumerator() => RoundsDictionary.Values.GetEnumerator();

View File

@@ -23,7 +23,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
/// The aggregate room placement (1-based). /// The aggregate room placement (1-based).
/// </summary> /// </summary>
[Key(1)] [Key(1)]
public int Placement { get; set; } public int? Placement { get; set; }
/// <summary> /// <summary>
/// The aggregate points. /// The aggregate points.

View File

@@ -22,25 +22,22 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
public IDictionary<int, MatchmakingUser> UserDictionary { get; set; } = new Dictionary<int, MatchmakingUser>(); public IDictionary<int, MatchmakingUser> UserDictionary { get; set; } = new Dictionary<int, MatchmakingUser>();
/// <summary> /// <summary>
/// Creates or retrieves the user for the given id. /// The total number of users.
/// </summary> /// </summary>
/// <param name="userId">The user id.</param> [IgnoreMember]
public MatchmakingUser this[int userId] public int Count => UserDictionary.Count;
{
get /// <summary>
/// Retrieves or adds a <see cref="MatchmakingUser"/> entry to this list.
/// </summary>
/// <param name="userId">The user ID.</param>
public MatchmakingUser GetOrAdd(int userId)
{ {
if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user)) if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user))
return user; return user;
return UserDictionary[userId] = new MatchmakingUser { UserId = userId }; return UserDictionary[userId] = new MatchmakingUser { UserId = userId };
} }
}
/// <summary>
/// The total number of users.
/// </summary>
[IgnoreMember]
public int Count => UserDictionary.Count;
public IEnumerator<MatchmakingUser> GetEnumerator() => UserDictionary.Values.GetEnumerator(); public IEnumerator<MatchmakingUser> GetEnumerator() => UserDictionary.Values.GetEnumerator();

View File

@@ -131,6 +131,11 @@ namespace osu.Game.Online.Multiplayer
public event Action<int, long>? MatchmakingItemDeselected; public event Action<int, long>? MatchmakingItemDeselected;
public event Action<MatchRoomState>? MatchRoomStateChanged; public event Action<MatchRoomState>? MatchRoomStateChanged;
public event Action<int>? UserVotedToSkipIntro;
public event Action? VoteToSkipIntroPassed;
public event Action<MultiplayerRoomUser, BeatmapAvailability>? BeatmapAvailabilityChanged;
/// <summary> /// <summary>
/// Whether the <see cref="MultiplayerClient"/> is currently connected. /// Whether the <see cref="MultiplayerClient"/> is currently connected.
/// This is NOT thread safe and usage should be scheduled. /// This is NOT thread safe and usage should be scheduled.
@@ -493,6 +498,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task RemovePlaylistItem(long playlistItemId); public abstract Task RemovePlaylistItem(long playlistItemId);
public abstract Task VoteToSkipIntro();
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{ {
handleRoomRequest(() => handleRoomRequest(() =>
@@ -770,6 +777,7 @@ namespace osu.Game.Online.Multiplayer
user.BeatmapAvailability = beatmapAvailability; user.BeatmapAvailability = beatmapAvailability;
BeatmapAvailabilityChanged?.Invoke(user, beatmapAvailability);
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
}); });
@@ -846,6 +854,10 @@ namespace osu.Game.Online.Multiplayer
handleRoomRequest(() => handleRoomRequest(() =>
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
foreach (var user in Room.Users)
user.VotedToSkipIntro = false;
GameplayStarted?.Invoke(); GameplayStarted?.Invoke();
}); });
@@ -916,6 +928,37 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask; return Task.CompletedTask;
} }
Task IMultiplayerClient.UserVotedToSkipIntro(int userId)
{
handleRoomRequest(() =>
{
Debug.Assert(Room != null);
var user = Room.Users.SingleOrDefault(u => u.UserID == userId);
// TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713.
if (user == null)
return;
user.VotedToSkipIntro = true;
UserVotedToSkipIntro?.Invoke(userId);
});
return Task.CompletedTask;
}
Task IMultiplayerClient.VoteToSkipIntroPassed()
{
handleRoomRequest(() =>
{
Debug.Assert(Room != null);
VoteToSkipIntroPassed?.Invoke();
});
return Task.CompletedTask;
}
/// <summary> /// <summary>
/// Populates the <see cref="APIUser"/> for a given collection of <see cref="MultiplayerRoomUser"/>s. /// Populates the <see cref="APIUser"/> for a given collection of <see cref="MultiplayerRoomUser"/>s.
/// </summary> /// </summary>

View File

@@ -49,6 +49,12 @@ namespace osu.Game.Online.Multiplayer
[Key(6)] [Key(6)]
public int? BeatmapId; public int? BeatmapId;
/// <summary>
/// Whether this user voted to skip the beatmap intro.
/// </summary>
[Key(7)]
public bool VotedToSkipIntro;
[IgnoreMember] [IgnoreMember]
public APIUser? User { get; set; } public APIUser? User { get; set; }

View File

@@ -70,7 +70,8 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded);
connection.On<long>(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On<long>(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved);
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); connection.On<int>(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro);
connection.On(nameof(IMultiplayerClient.VoteToSkipIntroPassed), ((IMultiplayerClient)this).VoteToSkipIntroPassed);
connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined);
connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft);
@@ -80,6 +81,8 @@ namespace osu.Game.Online.Multiplayer
connection.On<MatchmakingQueueStatus>(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged); connection.On<MatchmakingQueueStatus>(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged);
connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected); connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected);
connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected); connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested);
}; };
IsConnected.BindTo(connector.IsConnected); IsConnected.BindTo(connector.IsConnected);
@@ -312,6 +315,16 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
} }
public override Task VoteToSkipIntro()
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkipIntro));
}
public override Task DisconnectInternal() public override Task DisconnectInternal()
{ {
if (connector == null) if (connector == null)

View File

@@ -150,7 +150,7 @@ namespace osu.Game.Online
// compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Online/BanchoClient.cs#L539 // compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Online/BanchoClient.cs#L539
retryDelay = Math.Min(120000, (int)(retryDelay * 1.5)); retryDelay = Math.Min(120000, (int)(retryDelay * 1.5));
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}. Next attempt in {thisDelay / 1000:N0} seconds.", LoggingTarget.Network); Logger.Log($"{ClientName} connect attempt failed. Next attempt in {thisDelay / 1000:N0} seconds.\n{exception}", LoggingTarget.Network);
await Task.Delay(thisDelay, cancellationToken).ConfigureAwait(false); await Task.Delay(thisDelay, cancellationToken).ConfigureAwait(false);
} }

View File

@@ -191,7 +191,8 @@ namespace osu.Game.Overlays.BeatmapSet
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
TextSize = 14, TextSize = 14,
TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 },
ShowUnknownStatus = true
}, },
storyboardIconPill = new StoryboardIconPill storyboardIconPill = new StoryboardIconPill
{ {

View File

@@ -329,6 +329,7 @@ namespace osu.Game.Overlays.Mods
yield return createModColumnContent(ModType.Automation); yield return createModColumnContent(ModType.Automation);
yield return createModColumnContent(ModType.Conversion); yield return createModColumnContent(ModType.Conversion);
yield return createModColumnContent(ModType.Fun); yield return createModColumnContent(ModType.Fun);
yield return createModColumnContent(ModType.Special);
} }
private ColumnDimContainer createModColumnContent(ModType modType) private ColumnDimContainer createModColumnContent(ModType modType)

View File

@@ -162,16 +162,17 @@ namespace osu.Game.Overlays
private int runningDepth; private int runningDepth;
private readonly Scheduler postScheduler = new Scheduler(); private readonly Scheduler postScheduler = new Scheduler();
private readonly Scheduler criticalPostScheduler = new Scheduler();
public override bool IsPresent => public override bool IsPresent =>
// Delegate presence as we need to consider the toast tray in addition to the main overlay. // Delegate presence as we need to consider the toast tray in addition to the main overlay.
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks; State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks || criticalPostScheduler.HasPendingTasks;
private bool processingPosts = true; private bool processingPosts = true;
private double? lastSamplePlayback; private double? lastSamplePlayback;
public void Post(Notification notification) => postScheduler.Add(() => public void Post(Notification notification) => (notification.IsCritical ? criticalPostScheduler : postScheduler).Add(() =>
{ {
++runningDepth; ++runningDepth;
@@ -220,6 +221,8 @@ namespace osu.Game.Overlays
{ {
base.Update(); base.Update();
criticalPostScheduler.Update();
if (processingPosts) if (processingPosts)
postScheduler.Update(); postScheduler.Update();
} }

View File

@@ -91,8 +91,13 @@ namespace osu.Game.Overlays
public void FlushAllToasts() public void FlushAllToasts()
{ {
foreach (var notification in toastFlow.ToArray()) foreach (var notification in toastFlow.ToArray())
{
if (notification.IsCritical)
continue;
forwardNotification(notification); forwardNotification(notification);
} }
}
public void Post(Notification notification) public void Post(Notification notification)
{ {

View File

@@ -39,6 +39,11 @@ namespace osu.Game.Overlays.Notifications
/// </summary> /// </summary>
public bool IsImportant { get; init; } = true; public bool IsImportant { get; init; } = true;
/// <summary>
/// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed.
/// </summary>
public bool IsCritical { get; init; }
/// <summary> /// <summary>
/// Transient notifications only show as a toast, and do not linger in notification history. /// Transient notifications only show as a toast, and do not linger in notification history.
/// </summary> /// </summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Notifications
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
IconContent.Width = IconContent.DrawHeight; IconContent.Width = Math.Min(78, IconContent.DrawHeight);
} }
} }
} }

View File

@@ -304,6 +304,8 @@ namespace osu.Game.Overlays
var track = musicController.CurrentTrack; var track = musicController.CurrentTrack;
if (!progressBar.Seeking)
{
if (!track.IsDummyDevice) if (!track.IsDummyDevice)
{ {
progressBar.EndTime = track.Length; progressBar.EndTime = track.Length;
@@ -318,6 +320,7 @@ namespace osu.Game.Overlays
playButton.Icon = FontAwesome.Regular.PlayCircle; playButton.Icon = FontAwesome.Regular.PlayCircle;
} }
} }
}
private Action? pendingBeatmapSwitch; private Action? pendingBeatmapSwitch;

View File

@@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@@ -36,6 +38,7 @@ namespace osu.Game.Overlays
public ScrollBackButton Button { get; private set; } public ScrollBackButton Button { get; private set; }
private readonly Bindable<double?> lastScrollTarget = new Bindable<double?>(); private readonly Bindable<double?> lastScrollTarget = new Bindable<double?>();
private readonly Bindable<double> progress = new Bindable<double>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@@ -46,7 +49,8 @@ namespace osu.Game.Overlays
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding(20), Margin = new MarginPadding(20),
Action = scrollBack, Action = scrollBack,
LastScrollTarget = { BindTarget = lastScrollTarget } LastScrollTarget = { BindTarget = lastScrollTarget },
Progress = { BindTarget = progress },
}); });
} }
@@ -54,6 +58,10 @@ namespace osu.Game.Overlays
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
// Map current position to standardized progress
float height = AvailableContent - DrawHeight;
progress.Value = height == 0 ? 1 : Math.Round(Math.Clamp(Current / height, 0, 1), 3);
if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
{ {
Button.State = Visibility.Hidden; Button.State = Visibility.Hidden;
@@ -110,9 +118,11 @@ namespace osu.Game.Overlays
private readonly Container content; private readonly Container content;
private readonly Box background; private readonly Box background;
private readonly CircularProgress currentCircularProgress;
private readonly SpriteIcon spriteIcon; private readonly SpriteIcon spriteIcon;
public Bindable<double?> LastScrollTarget = new Bindable<double?>(); public Bindable<double?> LastScrollTarget = new Bindable<double?>();
public Bindable<double> Progress = new Bindable<double>();
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
@@ -145,6 +155,11 @@ namespace osu.Game.Overlays
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
currentCircularProgress = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = 0.1f,
},
spriteIcon = new SpriteIcon spriteIcon = new SpriteIcon
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@@ -164,6 +179,7 @@ namespace osu.Game.Overlays
IdleColour = colourProvider.Background6; IdleColour = colourProvider.Background6;
HoverColour = colourProvider.Background5; HoverColour = colourProvider.Background5;
flashColour = colourProvider.Light1; flashColour = colourProvider.Light1;
currentCircularProgress.Colour = colourProvider.Highlight1;
scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top"); scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top");
scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous"); scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous");
@@ -173,6 +189,8 @@ namespace osu.Game.Overlays
{ {
base.LoadComplete(); base.LoadComplete();
Progress.BindValueChanged(p => currentCircularProgress.Progress = p.NewValue, true);
LastScrollTarget.BindValueChanged(target => LastScrollTarget.BindValueChanged(target =>
{ {
spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint); spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint);

View File

@@ -78,12 +78,13 @@ namespace osu.Game.Overlays.Profile.Header
private void updateDisplay(APIUser? user) private void updateDisplay(APIUser? user)
{ {
var cutoffDate = new DateTime(2025, 8, 25);
topLinkContainer.Clear(); topLinkContainer.Clear();
bottomLinkContainer.Clear(); bottomLinkContainer.Clear();
if (user == null) return; if (user == null) return;
if (user.JoinDate.ToUniversalTime().Year < 2008) if (user.JoinDate.ToUniversalTime().Date < cutoffDate)
topLinkContainer.AddText(UsersStrings.ShowFirstMembers); topLinkContainer.AddText(UsersStrings.ShowFirstMembers);
else else
{ {

View File

@@ -0,0 +1,137 @@
// 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.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class GlobalRankDisplay : CompositeDrawable
{
public Bindable<UserStatistics?> UserStatistics = new Bindable<UserStatistics?>();
public Bindable<APIUser.UserRankHighest?> HighestRank = new Bindable<APIUser.UserRankHighest?>();
private ProfileValueDisplay info = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public GlobalRankDisplay()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = info = new ProfileValueDisplay(big: true)
{
Title = UsersStrings.ShowRankGlobalSimple
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UserStatistics.BindValueChanged(_ => updateState());
HighestRank.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
info.Content.Text = UserStatistics.Value?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
info.Content.TooltipText = getGlobalRankTooltipText();
var tier = getRankingTier();
info.Content.Colour = tier == null ? colourProvider.Content2 : OsuColour.ForRankingTier(tier.Value);
info.Content.Font = info.Content.Font.With(weight: tier == null || tier == RankingTier.Iron ? FontWeight.Regular : FontWeight.Bold);
}
/// <seealso href="https://github.com/ppy/osu-web/blob/6fcd85eb006ce7699d6f747597435c01344b2d2d/resources/js/profile-page/rank.tsx#L19-L46"/>
private RankingTier? getRankingTier()
{
var stats = UserStatistics.Value;
int? rank = stats?.GlobalRank;
float? percent = stats?.GlobalRankPercent;
if (rank == null || percent == null)
return null;
if (rank <= 100)
return RankingTier.Lustrous;
if (percent < 0.0005)
return RankingTier.Radiant;
if (percent < 0.0015)
return RankingTier.Rhodium;
if (percent < 0.005)
return RankingTier.Platinum;
if (percent < 0.015)
return RankingTier.Gold;
if (percent < 0.05)
return RankingTier.Silver;
if (percent < 0.15)
return RankingTier.Bronze;
if (percent < 0.5)
return RankingTier.Iron;
return null;
}
private LocalisableString getGlobalRankTooltipText()
{
var rankHighest = HighestRank.Value;
var variants = UserStatistics.Value?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.GlobalRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
if (rankHighest != null)
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
if (result == null)
result = rankHighestText;
else
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
}
return result ?? default;
}
}
}

View File

@@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly Dictionary<ScoreRank, ScoreRankInfo> scoreRankInfos = new Dictionary<ScoreRank, ScoreRankInfo>(); private readonly Dictionary<ScoreRank, ScoreRankInfo> scoreRankInfos = new Dictionary<ScoreRank, ScoreRankInfo>();
private ProfileValueDisplay medalInfo = null!; private ProfileValueDisplay medalInfo = null!;
private ProfileValueDisplay ppInfo = null!; private ProfileValueDisplay ppInfo = null!;
private ProfileValueDisplay detailGlobalRank = null!; private GlobalRankDisplay detailGlobalRank = null!;
private ProfileValueDisplay detailCountryRank = null!; private ProfileValueDisplay detailCountryRank = null!;
private RankGraph rankGraph = null!; private RankGraph rankGraph = null!;
@@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
new[] new[]
{ {
detailGlobalRank = new ProfileValueDisplay(true) detailGlobalRank = new GlobalRankDisplay(),
{
Title = UsersStrings.ShowRankGlobalSimple,
},
Empty(), Empty(),
detailCountryRank = new ProfileValueDisplay(true) detailCountryRank = new ProfileValueDisplay(true)
{ {
@@ -156,59 +153,22 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
var user = data?.User; var user = data?.User;
medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; medalInfo.Content.Text = user?.Achievements?.Length.ToString() ?? "0";
ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; ppInfo.Content.Text = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0";
ppInfo.Content.TooltipText = getPPInfoTooltipText(user);
foreach (var scoreRankInfo in scoreRankInfos) foreach (var scoreRankInfo in scoreRankInfos)
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; detailGlobalRank.HighestRank.Value = user?.RankHighest;
detailGlobalRank.ContentTooltipText = getGlobalRankTooltipText(user); detailGlobalRank.UserStatistics.Value = user?.Statistics;
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; detailCountryRank.Content.Text = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
detailCountryRank.ContentTooltipText = getCountryRankTooltipText(user); detailCountryRank.Content.TooltipText = getCountryRankTooltipText(user);
rankGraph.Statistics.Value = user?.Statistics; rankGraph.Statistics.Value = user?.Statistics;
} }
private static LocalisableString getGlobalRankTooltipText(APIUser? user)
{
var rankHighest = user?.RankHighest;
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.GlobalRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
if (rankHighest != null)
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
if (result == null)
result = rankHighestText;
else
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
}
return result ?? default;
}
private static LocalisableString getCountryRankTooltipText(APIUser? user) private static LocalisableString getCountryRankTooltipText(APIUser? user)
{ {
var variants = user?.Statistics?.Variants; var variants = user?.Statistics?.Variants;
@@ -234,6 +194,28 @@ namespace osu.Game.Overlays.Profile.Header.Components
return result ?? default; return result ?? default;
} }
private static LocalisableString getPPInfoTooltipText(APIUser? user)
{
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.PP.ToLocalisableString("#,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
return result ?? default;
}
private partial class ScoreRankInfo : CompositeDrawable private partial class ScoreRankInfo : CompositeDrawable
{ {
private readonly OsuSpriteText rankCount; private readonly OsuSpriteText rankCount;

View File

@@ -14,22 +14,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
public partial class ProfileValueDisplay : CompositeDrawable public partial class ProfileValueDisplay : CompositeDrawable
{ {
private readonly OsuSpriteText title; private readonly OsuSpriteText title;
private readonly ContentText content;
public LocalisableString Title public LocalisableString Title
{ {
set => title.Text = value; set => title.Text = value;
} }
public LocalisableString Content public ContentText Content { get; }
{
set => content.Text = value;
}
public LocalisableString ContentTooltipText
{
set => content.TooltipText = value;
}
public ProfileValueDisplay(bool big = false, int minimumWidth = 60) public ProfileValueDisplay(bool big = false, int minimumWidth = 60)
{ {
@@ -44,9 +35,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
Font = OsuFont.GetFont(size: 12) Font = OsuFont.GetFont(size: 12)
}, },
content = new ContentText Content = new ContentText
{ {
Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light), Font = OsuFont.GetFont(size: big ? 30 : 20, weight: big ? FontWeight.Regular : FontWeight.Light),
}, },
new Container // Add a minimum size to the FillFlowContainer new Container // Add a minimum size to the FillFlowContainer
{ {
@@ -60,10 +51,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
title.Colour = colourProvider.Content1; title.Colour = colourProvider.Content1;
content.Colour = colourProvider.Content2; Content.Colour = colourProvider.Content2;
} }
private partial class ContentText : OsuSpriteText, IHasTooltip public partial class ContentText : OsuSpriteText, IHasTooltip
{ {
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
} }

View File

@@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
InternalChild = info = new ProfileValueDisplay(minimumWidth: 140) InternalChild = info = new ProfileValueDisplay(minimumWidth: 140)
{ {
Title = UsersStrings.ShowStatsPlayTime, Title = UsersStrings.ShowStatsPlayTime,
ContentTooltipText = "0 hours", Content = { TooltipText = "0 hours", }
}; };
User.BindValueChanged(updateTime, true); User.BindValueChanged(updateTime, true);
@@ -35,8 +35,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateTime(ValueChangedEvent<UserProfileData?> user) private void updateTime(ValueChangedEvent<UserProfileData?> user)
{ {
int? playTime = user.NewValue?.User.Statistics?.PlayTime; int? playTime = user.NewValue?.User.Statistics?.PlayTime;
info.ContentTooltipText = (playTime ?? 0) / 3600 + " hours"; info.Content.TooltipText = (playTime ?? 0) / 3600 + " hours";
info.Content = formatTime(playTime); info.Content.Text = formatTime(playTime);
} }
private string formatTime(int? secondsNull) private string formatTime(int? secondsNull)

View File

@@ -189,6 +189,13 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
addText($" ({getRulesetName()})"); addText($" ({getRulesetName()})");
break; break;
case RecentActivityType.RankRetracted:
addUserLink();
addText("'s score on ");
addBeatmapLink();
addText($" has been retracted ({getRulesetName()})");
break;
case RecentActivityType.UserSupportAgain: case RecentActivityType.UserSupportAgain:
addUserLink(); addUserLink();
addText(" has once again chosen to support osu! - thanks for your generosity!"); addText(" has once again chosen to support osu! - thanks for your generosity!");

View File

@@ -75,6 +75,11 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
icon.Colour = Color4.White; icon.Colour = Color4.White;
break; break;
case RecentActivityType.RankRetracted:
icon.Icon = FontAwesome.Solid.Ban;
icon.Colour = colours.Red1;
break;
case RecentActivityType.UserSupportAgain: case RecentActivityType.UserSupportAgain:
icon.Icon = FontAwesome.Solid.Heart; icon.Icon = FontAwesome.Solid.Heart;
icon.Colour = colours.Pink; icon.Colour = colours.Pink;

View File

@@ -41,8 +41,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
Add(wasapiExperimental = new SettingsCheckbox Add(wasapiExperimental = new SettingsCheckbox
{ {
LabelText = "Use experimental audio mode", LabelText = AudioSettingsStrings.WasapiLabel,
TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.", TooltipText = AudioSettingsStrings.WasapiTooltip,
Current = audio.UseExperimentalWasapi, Current = audio.UseExperimentalWasapi,
Keywords = new[] { "wasapi", "latency", "exclusive" } Keywords = new[] { "wasapi", "latency", "exclusive" }
}); });
@@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
if (wasapiExperimental != null) if (wasapiExperimental != null)
{ {
if (wasapiExperimental.Current.Value) if (wasapiExperimental.Current.Value)
{ wasapiExperimental.SetNoticeText(AudioSettingsStrings.WasapiNotice, true);
wasapiExperimental.SetNoticeText(
"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true);
}
else else
wasapiExperimental.ClearNoticeText(); wasapiExperimental.ClearNoticeText();
} }

View File

@@ -91,6 +91,7 @@ namespace osu.Game.Overlays.SkinEditor
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
config.BindWith(OsuSetting.HUDVisibilityMode, configVisibilityMode);
} }
protected override void LoadComplete() protected override void LoadComplete()
@@ -117,7 +118,7 @@ namespace osu.Game.Overlays.SkinEditor
protected override void PopIn() protected override void PopIn()
{ {
globallyDisableBeatmapSkinSetting(); overrideSkinEditorRelevantSettings();
if (skinEditor != null) if (skinEditor != null)
{ {
@@ -159,7 +160,7 @@ namespace osu.Game.Overlays.SkinEditor
nestedInputManagerDisable?.Dispose(); nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null; nestedInputManagerDisable = null;
globallyReenableBeatmapSkinSetting(); restoreSkinEditorRelevantSettings();
} }
public void PresentGameplay() => presentGameplay(false); public void PresentGameplay() => presentGameplay(false);
@@ -330,11 +331,13 @@ namespace osu.Game.Overlays.SkinEditor
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>(); private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private LeasedBindable<bool>? leasedBeatmapSkins; private LeasedBindable<bool>? leasedBeatmapSkins;
private void globallyDisableBeatmapSkinSetting() private readonly Bindable<HUDVisibilityMode> configVisibilityMode = new Bindable<HUDVisibilityMode>();
{ private LeasedBindable<HUDVisibilityMode>? leasedVisibilityMode;
if (beatmapSkins.Disabled)
return;
private void overrideSkinEditorRelevantSettings()
{
if (!beatmapSkins.Disabled)
{
// The skin editor doesn't work well if beatmap skins are being applied to the player screen. // The skin editor doesn't work well if beatmap skins are being applied to the player screen.
// To keep things simple, disable the setting game-wide while using the skin editor. // To keep things simple, disable the setting game-wide while using the skin editor.
// //
@@ -344,10 +347,17 @@ namespace osu.Game.Overlays.SkinEditor
leasedBeatmapSkins.Value = false; leasedBeatmapSkins.Value = false;
} }
private void globallyReenableBeatmapSkinSetting() leasedVisibilityMode = configVisibilityMode.BeginLease(true);
leasedVisibilityMode.Value = HUDVisibilityMode.Always;
}
private void restoreSkinEditorRelevantSettings()
{ {
leasedBeatmapSkins?.Return(); leasedBeatmapSkins?.Return();
leasedBeatmapSkins = null; leasedBeatmapSkins = null;
leasedVisibilityMode?.Return();
leasedVisibilityMode = null;
} }
public new void ToggleVisibility() public new void ToggleVisibility()

View File

@@ -5,13 +5,20 @@
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Overlays.Wiki namespace osu.Game.Overlays.Wiki
{ {
@@ -19,11 +26,15 @@ namespace osu.Game.Overlays.Wiki
{ {
public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex; public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex;
private const string github_wiki_base = @"https://github.com/ppy/osu-wiki/blob/master/wiki";
public readonly Bindable<APIWikiPage> WikiPageData = new Bindable<APIWikiPage>(); public readonly Bindable<APIWikiPage> WikiPageData = new Bindable<APIWikiPage>();
public Action ShowIndexPage; public Action ShowIndexPage;
public Action ShowParentPage; public Action ShowParentPage;
private readonly Bindable<string> githubPath = new Bindable<string>();
public WikiHeader() public WikiHeader()
{ {
TabControl.AddItem(IndexPageString); TabControl.AddItem(IndexPageString);
@@ -35,6 +46,9 @@ namespace osu.Game.Overlays.Wiki
private void onWikiPageChange(ValueChangedEvent<APIWikiPage> e) private void onWikiPageChange(ValueChangedEvent<APIWikiPage> e)
{ {
// Clear the path beforehand in case we got an error page.
githubPath.Value = null;
if (e.NewValue == null) if (e.NewValue == null)
return; return;
@@ -42,6 +56,7 @@ namespace osu.Game.Overlays.Wiki
Current.Value = null; Current.Value = null;
TabControl.AddItem(IndexPageString); TabControl.AddItem(IndexPageString);
githubPath.Value = $"{github_wiki_base}/{e.NewValue.Path}/{e.NewValue.Locale}.md";
if (e.NewValue.Path == WikiOverlay.INDEX_PATH) if (e.NewValue.Path == WikiOverlay.INDEX_PATH)
{ {
@@ -56,6 +71,27 @@ namespace osu.Game.Overlays.Wiki
Current.Value = e.NewValue.Title; Current.Value = e.NewValue.Title;
} }
protected override Drawable CreateTabControlContent()
{
return new FillFlowContainer
{
Height = 40,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new ShowOnGitHubButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(32),
TargetPath = { BindTarget = githubPath },
},
},
};
}
private void onCurrentChange(ValueChangedEvent<LocalisableString?> e) private void onCurrentChange(ValueChangedEvent<LocalisableString?> e)
{ {
if (e.NewValue == TabControl.Items.LastOrDefault()) if (e.NewValue == TabControl.Items.LastOrDefault())
@@ -83,5 +119,39 @@ namespace osu.Game.Overlays.Wiki
Icon = OsuIcon.Wiki; Icon = OsuIcon.Wiki;
} }
} }
private partial class ShowOnGitHubButton : RoundedButton
{
public override LocalisableString TooltipText => WikiStrings.ShowEditLink;
public readonly Bindable<string> TargetPath = new Bindable<string>();
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] ILinkHandler linkHandler)
{
Width = 42;
Add(new SpriteIcon
{
Size = new Vector2(12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Brands.Github,
});
Action = () => linkHandler?.HandleLink(TargetPath.Value);
}
protected override void LoadComplete()
{
base.LoadComplete();
TargetPath.BindValueChanged(e =>
{
this.FadeTo(e.NewValue != null ? 1 : 0);
Enabled.Value = e.NewValue != null;
}, true);
}
}
} }
} }

View File

@@ -0,0 +1,22 @@
// 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.
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Represents a mod which can override a fail and quit the game instead.
/// </summary>
public interface IApplicableFailExit : IApplicableMod
{
/// <summary>
/// Whether we should allow failing at the current point in time.
/// </summary>
/// <returns>Whether the fail should be allowed to proceed. Return false to block.</returns>
bool PerformFail();
/// <summary>
/// Whether we want to exit the game on fail. Only used if <see cref="PerformFail"/> returns true.
/// </summary>
bool ExitOnFail { get; }
}
}

View File

@@ -1,19 +0,0 @@
// 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.Game;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// An interface for mods that apply changes to the <see cref="OsuGameBase"/>.
/// This is really stupid and f%%king dangerous, possibly disasterous even.
/// </summary>
public interface IApplicableToOsuGameBase : IApplicableMod
{
/// <summary>
/// Provide a <see cref="OsuGameBase"/>. Called once on initialisation of a play instance.
/// </summary>
void ApplyToOsuGameBase(OsuGameBase game);
}
}

View File

@@ -127,7 +127,9 @@ namespace osu.Game.Rulesets.Mods
/// The settings are returned in ascending key order as per <see cref="SettingsMap"/>. /// The settings are returned in ascending key order as per <see cref="SettingsMap"/>.
/// The ordering is intentionally enforced manually, as ordering of <see cref="Dictionary{TKey,TValue}.Values"/> is unspecified. /// The ordering is intentionally enforced manually, as ordering of <see cref="Dictionary{TKey,TValue}.Values"/> is unspecified.
/// </remarks> /// </remarks>
internal IEnumerable<IBindable> SettingsBindables => SettingsMap.OrderBy(pair => pair.Key).Select(pair => pair.Value); internal IEnumerable<IBindable> SettingsBindables => SettingsMap.OrderBy(pair => pair.Key)
.Select(pair => pair.Value)
.Where(x => !x.GetType().GetCustomAttributes(typeof(JsonIgnoreAttribute)).Any());
/// <summary> /// <summary>
/// Provides mapping of names to <see cref="IBindable"/>s of all settings within this mod. /// Provides mapping of names to <see cref="IBindable"/>s of all settings within this mod.

View File

@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
/// - Hit windows differ (https://github.com/ppy/osu/issues/11311). /// - Hit windows differ (https://github.com/ppy/osu/issues/11311).
/// - Sliders always gives combo for slider end, even on miss (https://github.com/ppy/osu/issues/11769). /// - Sliders always gives combo for slider end, even on miss (https://github.com/ppy/osu/issues/11769).
/// </summary> /// </summary>
public sealed override bool Ranked => false; public sealed override bool Ranked => true;
public sealed override bool ValidForFreestyleAsRequiredMod => false; public sealed override bool ValidForFreestyleAsRequiredMod => false;
} }

View File

@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModDaycore; public override IconUsage? Icon => OsuIcon.ModDaycore;
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Whoaaaaa..."; public override LocalisableString Description => "Whoaaaaa...";
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))] [SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75) public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)

View File

@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModDoubleTime; public override IconUsage? Icon => OsuIcon.ModDoubleTime;
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Zoooooooooom..."; public override LocalisableString Description => "Zoooooooooom...";
public override bool Ranked => SpeedChange.IsDefault; public override bool Ranked => true;
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))] [SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5) public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)

View File

@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.DifficultyReduction;
public override double ScoreMultiplier => 0.5; public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) };
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => true; public override bool ValidForFreestyleAsRequiredMod => true;
protected const float ADJUST_RATIO = 0.5f; protected const float ADJUST_RATIO = 0.5f;

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