58 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
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
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
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
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
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
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
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
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
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
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
Jamie Taylor
fadcb9882c Merge branch 'master' into matchmaking-jumpy-jump 2025-10-29 15:34:50 +09:00
Jamie Taylor
0558f9f2d9 Add SFX for 'jumping' in quickplay 2025-10-24 22:42:28 +09:00
77 changed files with 1875 additions and 972 deletions

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

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

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

@@ -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);
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
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);
(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

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

@@ -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 =>
{ {
ID = i, var beatmap = beatmaps[i % beatmaps.Length];
BeatmapID = beatmaps[i % beatmaps.Length].OnlineID,
StarRating = i / 10.0, return new MatchmakingPlaylistItem(
}).ToArray(); new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = beatmap.OnlineID,
StarRating = i / 10.0,
},
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, {
BeatmapID = i, ID = i,
StarRating = i / 10.0, BeatmapID = i,
}).ToArray(); StarRating = i / 10.0,
},
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,16 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.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.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.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;
@@ -21,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,
@@ -58,47 +76,55 @@ 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);
{
if (panel != null)
panel.AllowSelection = value;
});
} }
[Test] [Test]
public void TestFailedBeatmapLookup() public void TestRandomPanel()
{ {
AddStep("setup request handle", () => MatchmakingSelectPanelRandom? panel = null;
{
var api = (DummyAPIAccess)API;
var handler = api.HandleRequest;
api.HandleRequest = req =>
{
switch (req)
{
case GetBeatmapRequest:
case GetBeatmapsRequest:
req.TriggerFailure(new InvalidOperationException());
return false;
default:
return handler?.Invoke(req) ?? false;
}
};
});
AddStep("add panel", () => AddStep("add panel", () =>
{ {
Child = new OsuContextMenuContainer Child = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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

@@ -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;
@@ -158,5 +160,64 @@ namespace osu.Game.Tests.Visual.Matchmaking
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

@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Online
Direction = FillDirection.Full, Direction = FillDirection.Full,
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Spacing = new Vector2(40), Spacing = new Vector2(40),
ChildrenEnumerable = new int?[] { 64, 423, 1453, 3468, 18_367, 48_342, 178_432, 375_231, 897_783, null }.Select(createDisplay) 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 private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay

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

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

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

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

@@ -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,18 +304,21 @@ namespace osu.Game.Overlays
var track = musicController.CurrentTrack; var track = musicController.CurrentTrack;
if (!track.IsDummyDevice) if (!progressBar.Seeking)
{ {
progressBar.EndTime = track.Length; if (!track.IsDummyDevice)
progressBar.CurrentTime = track.CurrentTime; {
progressBar.EndTime = track.Length;
progressBar.CurrentTime = track.CurrentTime;
playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
} }
else else
{ {
progressBar.CurrentTime = 0; progressBar.CurrentTime = 0;
progressBar.EndTime = 1; progressBar.EndTime = 1;
playButton.Icon = FontAwesome.Regular.PlayCircle; playButton.Icon = FontAwesome.Regular.PlayCircle;
}
} }
} }

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

@@ -75,19 +75,19 @@ namespace osu.Game.Overlays.Profile.Header.Components
if (percent < 0.0005) if (percent < 0.0005)
return RankingTier.Radiant; return RankingTier.Radiant;
if (percent < 0.0025) if (percent < 0.0015)
return RankingTier.Rhodium; return RankingTier.Rhodium;
if (percent < 0.005) if (percent < 0.005)
return RankingTier.Platinum; return RankingTier.Platinum;
if (percent < 0.025) if (percent < 0.015)
return RankingTier.Gold; return RankingTier.Gold;
if (percent < 0.05) if (percent < 0.05)
return RankingTier.Silver; return RankingTier.Silver;
if (percent < 0.25) if (percent < 0.15)
return RankingTier.Bronze; return RankingTier.Bronze;
if (percent < 0.5) if (percent < 0.5)

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

@@ -550,7 +550,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
} }
else else
{ {
// Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume)); soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
} }
@@ -680,14 +679,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
} }
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo> public class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
{ {
public readonly string Filename; public readonly string Filename;
public FileHitSampleInfo(string filename, int volume) public FileHitSampleInfo(string filename, int volume)
// Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin. // Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin.
// Note that this does not change the lookup names, as they are overridden locally. : base(HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, customSampleBank: 1, volume: volume)
: base(string.Empty, customSampleBank: 1, volume: volume)
{ {
Filename = filename; Filename = filename;
} }
@@ -696,7 +694,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
Filename, Filename,
Path.ChangeExtension(Filename, null) Path.ChangeExtension(Filename, null)
}; }.Concat(base.LookupNames);
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default) Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)

View File

@@ -44,7 +44,6 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{ {
[Cached(typeof(IPreviewTrackOwner))]
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap
{ {
private readonly Room room; private readonly Room room;

View File

@@ -1,466 +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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class BeatmapCardMatchmaking : BeatmapCard
{
private readonly APIBeatmap beatmap;
protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar;
public const float HEIGHT = 80;
[Cached]
private readonly BeatmapCardContent content;
private BeatmapCardThumbnail thumbnail = null!;
private CollapsibleButtonContainer buttonContainer = null!;
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
public AvatarOverlay SelectionOverlay = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private BeatmapSetOverlay? beatmapSetOverlay { get; set; }
public BeatmapCardMatchmaking(APIBeatmap beatmap)
: base(beatmap.BeatmapSet!, false)
{
this.beatmap = beatmap;
content = new BeatmapCardContent(HEIGHT);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Width = WIDTH;
Height = HEIGHT;
FillFlowContainer leftIconArea = null!;
FillFlowContainer titleBadgeArea = null!;
GridContainer artistContainer = null!;
Child = content.With(c =>
{
c.MainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true)
{
Name = @"Left (icon) area",
Size = new Vector2(HEIGHT),
Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
}
},
buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
{
X = HEIGHT - CORNER_RADIUS,
Width = WIDTH - HEIGHT + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = 0,
ButtonsExpandedWidth = 24,
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
titleBadgeArea = new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}
}
}
},
artistContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
Empty()
},
}
},
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author);
}),
}
},
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2),
AlwaysPresent = true,
Children = new Drawable[]
{
new Container
{
Masking = true,
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Padding = new MarginPadding(4),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(6, 0),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.9f),
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
},
}
},
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 5,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State },
Progress = { BindTarget = DownloadTracker.Progress }
}
}
},
SelectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
}
}
};
c.Expanded.BindTarget = Expanded;
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight)
{
titleBadgeArea.Add(new SpotlightBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (BeatmapSet.HasExplicitContent)
{
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (BeatmapSet.TrackId != null)
{
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
};
}
}
private LocalisableString createArtistText()
{
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
}
protected override void UpdateState()
{
base.UpdateState();
bool showDetails = IsHovered;
buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails;
}
public override MenuItem[] ContextMenuItems
{
get
{
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID))
};
foreach (var button in buttonContainer.Buttons)
{
if (button.Enabled.Value)
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
}
return items.ToArray();
}
}
public partial class AvatarOverlay : CompositeDrawable
{
private readonly Container<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
AutoSizeAxes = Axes.X,
Height = SelectionAvatar.AVATAR_SIZE,
};
Padding = new MarginPadding { Vertical = 5 };
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
public bool AddUser(APIUser user)
{
if (avatars.Any(a => a.User.Id == user.Id))
return false;
var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value));
avatars.Add(avatar);
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
userAddedSample?.Play();
lastSamplePlayback = Time.Current;
}
updateAvatarLayout();
avatar.FinishTransforms();
return true;
}
public bool RemoveUser(int id)
{
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
return false;
avatar.PopOutAndExpire();
avatars.ChangeChildDepth(avatar, float.MaxValue);
updateAvatarLayout();
return true;
}
private void updateAvatarLayout()
{
const double stagger = 30;
const float spacing = 4;
double delay = 0;
float x = 0;
for (int i = avatars.Count - 1; i >= 0; i--)
{
var avatar = avatars[i];
if (avatar.Expired)
continue;
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
x -= avatar.LayoutSize.X + spacing;
delay += stagger;
}
}
public partial class SelectionAvatar : CompositeDrawable
{
public const float AVATAR_SIZE = 30;
public APIUser User { get; }
public bool Expired { get; private set; }
private readonly MatchmakingAvatar avatar;
public SelectionAvatar(APIUser user, bool isOwnUser)
{
User = user;
Size = new Vector2(AVATAR_SIZE);
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
public void PopOutAndExpire()
{
avatar.ScaleTo(0, 400, Easing.OutExpo);
this.FadeOut(100).Expire();
Expired = true;
}
}
}
}
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.Toolkit.HighPerformance; using Microsoft.Toolkit.HighPerformance;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@@ -33,16 +34,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
public event Action<MultiplayerPlaylistItem>? ItemSelected; public event Action<MultiplayerPlaylistItem>? ItemSelected;
private readonly Dictionary<long, BeatmapSelectPanel> panelLookup = new Dictionary<long, BeatmapSelectPanel>(); private readonly Dictionary<long, MatchmakingSelectPanel> panelLookup = new Dictionary<long, MatchmakingSelectPanel>();
private readonly Dictionary<long, MatchmakingPlaylistItem> playlistItems = new Dictionary<long, MatchmakingPlaylistItem>();
private MatchmakingSelectPanelRandom randomPanel = null!;
private readonly PanelGridContainer panelGridContainer; private readonly PanelGridContainer panelGridContainer;
private readonly Container<BeatmapSelectPanel> rollContainer; private readonly Container<MatchmakingSelectPanel> rollContainer;
private readonly OsuScrollContainer scroll; private readonly OsuScrollContainer scroll;
private bool allowSelection = true; private bool allowSelection = true;
private readonly Sample?[] spinSamples = new Sample?[5]; private readonly Sample?[] spinSamples = new Sample?[5];
private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4]; private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4];
private Sample? randomRevealSample;
private Sample? resultSample; private Sample? resultSample;
private Sample? swooshSample; private Sample? swooshSample;
private double? lastSamplePlayback; private double? lastSamplePlayback;
@@ -63,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
Spacing = new Vector2(panel_spacing) Spacing = new Vector2(panel_spacing)
}, },
}, },
rollContainer = new Container<BeatmapSelectPanel> rollContainer = new Container<MatchmakingSelectPanel>
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
@@ -77,13 +81,38 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
for (int i = 0; i < spinSamples.Length; i++) for (int i = 0; i < spinSamples.Length; i++)
spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}"); spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}");
randomRevealSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/random-reveal");
resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result");
swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out");
} }
protected override void LoadComplete() public void AddItems(IEnumerable<MatchmakingPlaylistItem> items)
{ {
base.LoadComplete(); foreach (var item in items)
{
playlistItems[item.ID] = item;
var panel = panelLookup[item.ID] = new MatchmakingSelectPanelBeatmap(item)
{
AllowSelection = allowSelection,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = i => ItemSelected?.Invoke(i),
};
panelGridContainer.Add(panel);
panelGridContainer.SetLayoutPosition(panel, (float)panel.Item.StarRating);
}
panelLookup[-1] = randomPanel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
{
AllowSelection = allowSelection,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = i => ItemSelected?.Invoke(i),
};
panelGridContainer.Add(randomPanel);
panelGridContainer.SetLayoutPosition(randomPanel, float.MinValue);
const double enter_duration = 500; const double enter_duration = 500;
@@ -99,32 +128,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay); panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay);
} }
panelsLoaded.SetResult();
}); });
} }
public void AddItem(MultiplayerPlaylistItem item) public void SetUserSelection(APIUser user, long itemId, bool selected) => whenPanelsLoaded(() =>
{
var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item)
{
AllowSelection = allowSelection,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = ItemSelected,
};
panelGridContainer.Add(panel);
panelGridContainer.SetLayoutPosition(panel, (float)item.StarRating);
}
public void RemoveItem(long id)
{
if (!panelLookup.Remove(id, out var panel))
return;
panel.Expire();
}
public void SetUserSelection(APIUser user, long itemId, bool selected)
{ {
if (!panelLookup.TryGetValue(itemId, out var panel)) if (!panelLookup.TryGetValue(itemId, out var panel))
return; return;
@@ -133,9 +142,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
panel.AddUser(user); panel.AddUser(user);
else else
panel.RemoveUser(user); panel.RemoveUser(user);
} });
public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) public void RevealRandomItem(MultiplayerPlaylistItem item) => whenPanelsLoaded(() =>
{
playlistItems.TryGetValue(item.ID, out var playlistItem);
Debug.Assert(playlistItem != null);
randomRevealSample?.Play();
randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods);
});
public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) => whenPanelsLoaded(() =>
{ {
Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Length >= 1);
Debug.Assert(candidateItemIds.Contains(finalItemId)); Debug.Assert(candidateItemIds.Contains(finalItemId));
@@ -162,7 +181,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
.Delay(roll_duration + present_beatmap_delay) .Delay(roll_duration + present_beatmap_delay)
.Schedule(() => PresentRolledBeatmap(finalItemId)); .Schedule(() => PresentRolledBeatmap(finalItemId));
} }
} });
internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration) internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration)
{ {
@@ -171,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
var rng = new Random(); var rng = new Random();
var remainingPanels = new List<BeatmapSelectPanel>(); var remainingPanels = new List<MatchmakingSelectPanel>();
foreach (var panel in panelGridContainer.Children.ToArray()) foreach (var panel in panelGridContainer.Children.ToArray())
{ {
@@ -211,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{ {
var panel = rollContainer.Children[i]; var panel = rollContainer.Children[i];
var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing)); var position = positions[i] * (MatchmakingSelectPanel.SIZE + new Vector2(panel_spacing));
panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f));
@@ -280,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex)
numSteps++; numSteps++;
BeatmapSelectPanel? lastPanel = null; MatchmakingSelectPanel? lastPanel = null;
for (int i = 0; i < numSteps; i++) for (int i = 0; i < numSteps; i++)
{ {
@@ -341,7 +360,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
PresentRolledBeatmap(finalItem); PresentRolledBeatmap(finalItem);
} }
private partial class PanelGridContainer : FillFlowContainer<BeatmapSelectPanel> private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource();
private void whenPanelsLoaded(Action action) => Task.Run(async () =>
{
await panelsLoaded.Task.ConfigureAwait(false);
Schedule(action);
});
private partial class PanelGridContainer : FillFlowContainer<MatchmakingSelectPanel>
{ {
public bool LayoutDisabled; public bool LayoutDisabled;

View File

@@ -1,241 +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 System;
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class BeatmapSelectPanel : Container
{
public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT);
public bool AllowSelection { get; set; }
public readonly MultiplayerPlaylistItem Item;
public Action<MultiplayerPlaylistItem>? Action { private get; init; }
private const float border_width = 3;
private Container scaleContainer = null!;
private Drawable lighting = null!;
private Container border = null!;
private Container mainContent = null!;
private readonly List<APIUser> users = new List<APIUser>();
private BeatmapCardMatchmaking? card;
public BeatmapSelectPanel(MultiplayerPlaylistItem item)
{
Item = item;
Size = SIZE;
}
[BackgroundDependencyLoader]
private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider)
{
InternalChild = scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
mainContent = new Container
{
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
lighting = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
}
},
border = new Container
{
Alpha = 0,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
BorderThickness = border_width,
BorderColour = colourProvider.Light1,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 40,
Roundness = 300,
Colour = colourProvider.Light3.Opacity(0.1f),
},
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0,
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
}
},
}
};
lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
{
Debug.Assert(card == null);
APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap
{
BeatmapSet = new APIBeatmapSet
{
Title = "unknown beatmap",
TitleUnicode = "unknown beatmap",
Artist = "unknown artist",
ArtistUnicode = "unknown artist",
}
};
beatmap.StarRating = Item.StarRating;
mainContent.Add(card = new BeatmapCardMatchmaking(beatmap)
{
Depth = float.MaxValue,
Action = () =>
{
if (AllowSelection)
Action?.Invoke(Item);
},
});
foreach (var user in users)
card.SelectionOverlay.AddUser(user);
}));
}
public void AddUser(APIUser user)
{
users.Add(user);
card?.SelectionOverlay.AddUser(user);
}
public void RemoveUser(APIUser user)
{
users.Remove(user);
card?.SelectionOverlay.RemoveUser(user.Id);
}
protected override bool OnHover(HoverEvent e)
{
if (AllowSelection)
{
lighting.FadeTo(0.2f, 50)
.Then()
.FadeTo(0.1f, 300);
return true;
}
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
lighting.FadeOut(200);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (AllowSelection && e.Button == MouseButton.Left)
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
if (e.Button == MouseButton.Left)
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
}
protected override bool OnClick(ClickEvent e)
{
if (AllowSelection)
{
lighting.FadeTo(0.5f, 50)
.Then()
.FadeTo(0.1f, 400);
}
// pass through to let the beatmap card handle actual click.
return false;
}
public void ShowChosenBorder()
{
border.FadeTo(1, 1000, Easing.OutQuint);
}
public void ShowBorder()
{
border.FadeTo(1, 80, Easing.OutQuint)
.Then()
.FadeTo(0.7f, 800, Easing.OutQuint);
}
public void HideBorder()
{
border.FadeOut(500, Easing.OutQuint);
}
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
{
scaleContainer
.FadeOut()
.MoveToY(distance)
.Delay(delay)
.FadeIn(duration / 2)
.MoveToY(0, duration, Easing.OutExpo);
}
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
{
AllowSelection = false;
scaleContainer.Delay(delay)
.ScaleTo(0, duration, easing)
.FadeOut(duration);
this.Delay(delay + duration).FadeOut().Expire();
}
}
}

View File

@@ -0,0 +1,14 @@
// 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.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public record MatchmakingPlaylistItem(MultiplayerPlaylistItem PlaylistItem, APIBeatmap Beatmap, Mod[] Mods)
{
public long ID => PlaylistItem.ID;
}
}

View File

@@ -0,0 +1,156 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class MatchmakingSelectPanel
{
public abstract partial class CardContent : CompositeDrawable
{
public abstract AvatarOverlay SelectionOverlay { get; }
protected CardContent()
{
RelativeSizeAxes = Axes.Both;
}
public partial class AvatarOverlay : CompositeDrawable
{
private readonly Container<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
AutoSizeAxes = Axes.X,
Height = SelectionAvatar.AVATAR_SIZE,
};
Padding = new MarginPadding { Vertical = 5 };
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
public bool AddUser(APIUser user)
{
if (avatars.Any(a => a.User.Id == user.Id))
return false;
var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value));
avatars.Add(avatar);
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
userAddedSample?.Play();
lastSamplePlayback = Time.Current;
}
updateAvatarLayout();
avatar.FinishTransforms();
return true;
}
public bool RemoveUser(int id)
{
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
return false;
avatar.PopOutAndExpire();
avatars.ChangeChildDepth(avatar, float.MaxValue);
updateAvatarLayout();
return true;
}
private void updateAvatarLayout()
{
const double stagger = 30;
const float spacing = 4;
double delay = 0;
float x = 0;
for (int i = avatars.Count - 1; i >= 0; i--)
{
var avatar = avatars[i];
if (avatar.Expired)
continue;
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
x -= avatar.LayoutSize.X + spacing;
delay += stagger;
}
}
public partial class SelectionAvatar : CompositeDrawable
{
public const float AVATAR_SIZE = 30;
public APIUser User { get; }
public bool Expired { get; private set; }
private readonly MatchmakingAvatar avatar;
public SelectionAvatar(APIUser user, bool isOwnUser)
{
User = user;
Size = new Vector2(AVATAR_SIZE);
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
public void PopOutAndExpire()
{
avatar.ScaleTo(0, 400, Easing.OutExpo);
this.FadeOut(100).Expire();
Expired = true;
}
}
}
}
}
}

View File

@@ -0,0 +1,381 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class MatchmakingSelectPanel
{
public partial class CardContentBeatmap : CardContent, IHasContextMenu
{
public override AvatarOverlay SelectionOverlay => selectionOverlay;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private BeatmapSetOverlay? beatmapSetOverlay { get; set; }
private readonly IBindable<DownloadState> downloadState = new Bindable<DownloadState>();
private readonly IBindableNumber<double> downloadProgress = new BindableDouble();
private readonly Bindable<BeatmapSetFavouriteState> favouriteState = new Bindable<BeatmapSetFavouriteState>();
private readonly APIBeatmapSet beatmapSet;
private readonly APIBeatmap beatmap;
private readonly Mod[] mods;
private BeatmapCardThumbnail thumbnail = null!;
private CollapsibleButtonContainer buttonContainer = null!;
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
private AvatarOverlay selectionOverlay = null!;
public CardContentBeatmap(APIBeatmap beatmap, Mod[] mods)
{
this.beatmap = beatmap;
this.mods = mods;
beatmapSet = beatmap.BeatmapSet!;
favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
FillFlowContainer leftIconArea;
FillFlowContainer titleBadgeArea;
GridContainer artistContainer;
InternalChildren = new Drawable[]
{
new BeatmapDownloadTracker(beatmap.BeatmapSet!)
{
State = { BindTarget = downloadState },
Progress = { BindTarget = downloadProgress },
},
thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true)
{
Name = @"Left (icon) area",
Size = new Vector2(MatchmakingSelectPanel.HEIGHT),
Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
}
},
buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
{
X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS,
Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS,
FavouriteState = { BindTarget = favouriteState },
ButtonsCollapsedWidth = 0,
ButtonsExpandedWidth = 24,
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
titleBadgeArea = new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}
}
}
},
artistContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
new TruncatingSpriteText
{
Text = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)),
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
Empty()
},
}
},
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(beatmapSet.Author);
}),
}
},
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2),
AlwaysPresent = true,
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new Container
{
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Padding = new MarginPadding(4),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(6, 0),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.9f),
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
}
},
new ModFlowDisplay
{
AutoSizeAxes = Axes.Both,
Scale = new Vector2(0.5f),
Margin = new MarginPadding { Left = 5 },
Current = { Value = mods }
},
},
}
},
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 5,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = downloadState },
Progress = { BindTarget = downloadProgress }
}
}
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
}
};
if (beatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (beatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (beatmapSet.FeaturedInSpotlight)
{
titleBadgeArea.Add(new SpotlightBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (beatmapSet.HasExplicitContent)
{
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (beatmapSet.TrackId != null)
{
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
};
}
}
protected override void LoadComplete()
{
base.LoadComplete();
downloadState.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
bool showDetails = IsHovered;
buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails;
bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing;
idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint);
downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint);
}
public MenuItem[] ContextMenuItems
{
get
{
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID))
};
foreach (var button in buttonContainer.Buttons)
{
if (button.Enabled.Value)
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
}
return items.ToArray();
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class MatchmakingSelectPanel
{
public partial class CardContentRandom : CardContent
{
public override AvatarOverlay SelectionOverlay => selectionOverlay;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private AvatarOverlay selectionOverlay = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background2,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 10,
Vertical = 4
},
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children =
[
new SpriteIcon
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(32),
Icon = FontAwesome.Solid.Random,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Random",
}
]
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
}
};
}
}
}
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public abstract partial class MatchmakingSelectPanel : Container
{
public const float WIDTH = 345;
public const float HEIGHT = 80;
public static readonly Vector2 SIZE = new Vector2(WIDTH, HEIGHT);
public bool AllowSelection { get; set; }
public readonly MultiplayerPlaylistItem Item;
public Action<MultiplayerPlaylistItem>? Action { private get; init; }
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private const float border_width = 3;
private Container scaleContainer = null!;
private Drawable lighting = null!;
private Container border = null!;
protected MatchmakingSelectPanel(MultiplayerPlaylistItem item)
{
Item = item;
Size = SIZE;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
InternalChildren = new Drawable[]
{
scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
new Container
{
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
Content,
lighting = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
}
},
border = new Container
{
Alpha = 0,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
BorderThickness = border_width,
BorderColour = colourProvider.Light1,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 40,
Roundness = 300,
Colour = colourProvider.Light3.Opacity(0.1f),
},
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0,
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
}
},
}
},
new HoverClickSounds(),
};
}
// TODO: making these abstract for now but avatar overlay should really be owned by the top level class
public abstract void AddUser(APIUser user);
public abstract void RemoveUser(APIUser user);
protected override bool OnHover(HoverEvent e)
{
if (AllowSelection)
{
lighting.FadeTo(0.2f, 50)
.Then()
.FadeTo(0.1f, 300);
return true;
}
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
lighting.FadeOut(200);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (AllowSelection && e.Button == MouseButton.Left)
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
if (e.Button == MouseButton.Left)
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
}
protected override bool OnClick(ClickEvent e)
{
if (AllowSelection)
{
lighting.FadeTo(0.5f, 50)
.Then()
.FadeTo(0.1f, 400);
Action?.Invoke(Item);
}
return true;
}
public void ShowChosenBorder()
{
border.FadeTo(1, 1000, Easing.OutQuint);
}
public void ShowBorder()
{
border.FadeTo(1, 80, Easing.OutQuint)
.Then()
.FadeTo(0.7f, 800, Easing.OutQuint);
}
public void HideBorder()
{
border.FadeOut(500, Easing.OutQuint);
}
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
{
scaleContainer
.FadeOut()
.MoveToY(distance)
.Delay(delay)
.FadeIn(duration / 2)
.MoveToY(0, duration, Easing.OutExpo);
}
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
{
AllowSelection = false;
scaleContainer.Delay(delay)
.ScaleTo(0, duration, easing)
.FadeOut(duration);
this.Delay(delay + duration).FadeOut().Expire();
}
}
}

View File

@@ -0,0 +1,40 @@
// 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.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class MatchmakingSelectPanelBeatmap : MatchmakingSelectPanel
{
private readonly APIBeatmap beatmap;
private readonly Mod[] mods;
public MatchmakingSelectPanelBeatmap(MatchmakingPlaylistItem item)
: base(item.PlaylistItem)
{
beatmap = item.Beatmap;
mods = item.Mods;
}
private CardContent content = null!;
[BackgroundDependencyLoader]
private void load()
{
Add(content = new CardContentBeatmap(beatmap, mods));
}
public override void AddUser(APIUser user)
{
content.SelectionOverlay.AddUser(user);
}
public override void RemoveUser(APIUser user)
{
content.SelectionOverlay.RemoveUser(user.Id);
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class MatchmakingSelectPanelRandom : MatchmakingSelectPanel
{
public MatchmakingSelectPanelRandom(MultiplayerPlaylistItem item)
: base(item)
{
}
private CardContent content = null!;
private readonly List<APIUser> users = new List<APIUser>();
[BackgroundDependencyLoader]
private void load()
{
Add(content = new CardContentRandom());
}
public void RevealBeatmap(APIBeatmap beatmap, Mod[] mods)
{
content.Expire();
var flashLayer = new Box { RelativeSizeAxes = Axes.Both };
AddRange(new Drawable[]
{
content = new CardContentBeatmap(beatmap, mods),
flashLayer,
});
foreach (var user in users)
content.SelectionOverlay.AddUser(user);
flashLayer.FadeOutFromOne(1000, Easing.In);
}
public override void AddUser(APIUser user)
{
users.Add(user);
content.SelectionOverlay.AddUser(user);
}
public override void RemoveUser(APIUser user)
{
users.Remove(user);
content.SelectionOverlay.RemoveUser(user.Id);
}
}
}

View File

@@ -1,13 +1,23 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{ {
@@ -17,10 +27,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
public override Drawable PlayersDisplayArea { get; } public override Drawable PlayersDisplayArea { get; }
private readonly BeatmapSelectGrid beatmapSelectGrid; private readonly BeatmapSelectGrid beatmapSelectGrid;
private readonly LoadingSpinner loadingSpinner;
[Resolved] [Resolved]
private MultiplayerClient client { get; set; } = null!; private MultiplayerClient client { get; set; } = null!;
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
public SubScreenBeatmapSelect() public SubScreenBeatmapSelect()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@@ -29,9 +46,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 200 }, Padding = new MarginPadding { Horizontal = 200 },
Child = beatmapSelectGrid = new BeatmapSelectGrid Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, beatmapSelectGrid = new BeatmapSelectGrid
{
RelativeSizeAxes = Axes.Both,
},
loadingSpinner = new LoadingSpinner
{
Size = new Vector2(64),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { Value = Visibility.Visible }
}
}, },
}, },
new Container new Container
@@ -49,24 +76,53 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{ {
base.LoadComplete(); base.LoadComplete();
client.ItemAdded += onItemAdded;
foreach (var item in client.Room!.Playlist)
onItemAdded(item);
beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID);
client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemSelected += onItemSelected;
client.MatchmakingItemDeselected += onItemDeselected; client.MatchmakingItemDeselected += onItemDeselected;
client.SettingsChanged += onSettingsChanged;
Debug.Assert(client.Room != null);
loadItems(client.Room.Playlist.ToArray()).FireAndForget();
} }
private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => private async Task loadItems(MultiplayerPlaylistItem[] items)
{ {
if (item.Expired) var beatmaps = await beatmapLookupCache.GetBeatmapsAsync(items.Select(it => it.BeatmapID).ToArray()).ConfigureAwait(false);
return; var matchmakingItems = new List<MatchmakingPlaylistItem>();
beatmapSelectGrid.AddItem(item); foreach (var entry in items.Zip(beatmaps))
}); {
var (item, beatmap) = entry;
beatmap ??= new APIBeatmap
{
BeatmapSet = new APIBeatmapSet
{
Title = "unknown beatmap",
TitleUnicode = "unknown beatmap",
Artist = "unknown artist",
ArtistUnicode = "unknown artist",
}
};
beatmap.StarRating = item.StarRating;
Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance();
Debug.Assert(ruleset != null);
Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray();
matchmakingItems.Add(new MatchmakingPlaylistItem(item, beatmap, mods));
}
Scheduler.Add(() =>
{
loadingSpinner.Hide();
beatmapSelectGrid.AddItems(matchmakingItems);
});
}
private void onItemSelected(int userId, long itemId) private void onItemSelected(int userId, long itemId)
{ {
@@ -80,6 +136,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
beatmapSelectGrid.SetUserSelection(user, itemId, false); beatmapSelectGrid.SetUserSelection(user, itemId, false);
} }
private void onSettingsChanged(MultiplayerRoomSettings settings)
{
if (client.Room!.MatchState is not MatchmakingRoomState matchmakingState)
return;
if (matchmakingState.Stage != MatchmakingStage.ServerBeatmapFinalised)
return;
if (matchmakingState.CandidateItem != -1)
return;
beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem);
}
public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@@ -88,9 +158,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
if (client.IsNotNull()) if (client.IsNotNull())
{ {
client.ItemAdded -= onItemAdded;
client.MatchmakingItemSelected -= onItemSelected; client.MatchmakingItemSelected -= onItemSelected;
client.MatchmakingItemDeselected -= onItemDeselected; client.MatchmakingItemDeselected -= onItemDeselected;
client.SettingsChanged -= onSettingsChanged;
} }
} }
} }

View File

@@ -7,6 +7,8 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@@ -15,6 +17,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@@ -114,6 +117,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
private bool hasQuit; private bool hasQuit;
private enum InteractionSampleType
{
PlayerJump,
PlayerReJump,
OtherPlayerJump,
}
private Dictionary<InteractionSampleType, Sample?> interactionSamples = new Dictionary<InteractionSampleType, Sample?>();
private readonly Dictionary<InteractionSampleType, SampleChannel?> interactionSampleChannels = new Dictionary<InteractionSampleType, SampleChannel?>();
private double samplePitch;
private double? lastSamplePlayback;
public PlayerPanel(MultiplayerRoomUser user) public PlayerPanel(MultiplayerRoomUser user)
: base(HoverSampleSet.Button) : base(HoverSampleSet.Button)
{ {
@@ -130,7 +145,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
Content.Masking = true; Content.Masking = true;
Content.CornerRadius = 10; Content.CornerRadius = 10;
@@ -255,6 +270,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
// Allow avatar to exist outside of masking for when it jumps around and stuff. // Allow avatar to exist outside of masking for when it jumps around and stuff.
AddInternal(avatar.CreateProxy()); AddInternal(avatar.CreateProxy());
interactionSamples = new Dictionary<InteractionSampleType, Sample?>
{
{ InteractionSampleType.PlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump") },
{ InteractionSampleType.PlayerReJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-rejump") },
{ InteractionSampleType.OtherPlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump-other") }
};
} }
protected override void LoadComplete() protected override void LoadComplete()
@@ -272,6 +294,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
avatar.ScaleTo(0) avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf) .ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200); .FadeIn(200);
// pick a random pitch to be used by the player for duration of this session
samplePitch = 0.75f + RNG.NextDouble(0f, 0.75f);
} }
public PlayerPanelDisplayMode DisplayMode public PlayerPanelDisplayMode DisplayMode
@@ -481,6 +506,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
scale.Then().ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out) scale.Then().ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out)
.Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In) .Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In)
.Then().ScaleTo(Vector2.One, 800, Easing.OutElastic); .Then().ScaleTo(Vector2.One, 800, Easing.OutElastic);
// only play jump sample if panel is visible
if (Alpha > 0)
playJumpSample(isConsecutive);
break; break;
} }
@@ -498,6 +528,44 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10); downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10);
}); });
private void playJumpSample(bool rejumping)
{
bool isLocalUser = User.OnlineID == client.LocalUser?.UserID;
if (isLocalUser)
playInteractionSample(rejumping ? InteractionSampleType.PlayerReJump : InteractionSampleType.PlayerJump);
else
playInteractionSample(InteractionSampleType.OtherPlayerJump);
}
private void playInteractionSample(InteractionSampleType sampleType)
{
bool enoughTimePassedSinceLastPlayback = lastSamplePlayback == null || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
if (!enoughTimePassedSinceLastPlayback)
return;
Sample? targetSample = interactionSamples[sampleType];
SampleChannel? targetChannel = interactionSampleChannels.GetValueOrDefault(sampleType);
targetChannel?.Stop();
targetChannel = targetSample?.GetChannel();
if (targetChannel == null)
return;
float horizontalPos = BoundingBox.Centre.X / Parent!.ToLocalSpace(Parent!.ScreenSpaceDrawQuad).Width;
// rescale balance from 0..1 to -1..1
float balance = -1f + horizontalPos * 2f;
targetChannel.Frequency.Value = samplePitch;
targetChannel.Balance.Value = balance * OsuGameBase.SFX_STEREO_STRENGTH;
targetChannel.Play();
interactionSampleChannels[sampleType] = targetChannel;
lastSamplePlayback = Time.Current;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Footer;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
public partial class ScreenMatchmaking
{
private partial class HistoryFooterButton : ScreenFooterButton
{
[Resolved]
private OsuGame? game { get; set; }
private readonly MultiplayerRoom room;
public HistoryFooterButton(MultiplayerRoom room)
{
this.room = room;
Action = openRoomHistory;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Text = "History";
Icon = FontAwesome.Solid.Globe;
AccentColour = colours.Lime1;
}
private void openRoomHistory()
=> game?.OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}/events");
}
}
}

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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@@ -29,6 +30,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Users; using osu.Game.Users;
@@ -47,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
/// </summary> /// </summary>
private const float row_padding = 10; private const float row_padding = 10;
private static readonly Vector2 chat_size = new Vector2(550, 130);
public override bool? ApplyModTrackAdjustments => true; public override bool? ApplyModTrackAdjustments => true;
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
@@ -104,8 +108,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Size = new Vector2(700, 130), Size = chat_size,
Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING }, Margin = new MarginPadding
{
Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING,
Bottom = row_padding
},
Alpha = 0 Alpha = 0
}; };
} }
@@ -162,9 +170,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
[ [
new Container new Container
{ {
Name = "Chat Area Space",
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Size = new Vector2(700, 130), Size = new Vector2(550, 130),
Margin = new MarginPadding { Bottom = row_padding } Margin = new MarginPadding { Bottom = row_padding }
} }
] ]
@@ -326,6 +335,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
return false; return false;
} }
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() =>
[
new HistoryFooterButton(room)
];
public override void OnEntering(ScreenTransitionEvent e) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(e); base.OnEntering(e);
@@ -463,7 +477,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
// This component is added to the screen footer which is only about 50px high. // This component is added to the screen footer which is only about 50px high.
// Therefore, it's given a large absolute size to give the context menu enough space to display correctly. // Therefore, it's given a large absolute size to give the context menu enough space to display correctly.
Size = new Vector2(700); Size = new Vector2(chat_size.X);
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {

View File

@@ -165,7 +165,6 @@ namespace osu.Game.Screens.Play
private void updateDisplay(ValueChangedEvent<Period?> period) private void updateDisplay(ValueChangedEvent<Period?> period)
{ {
FinishTransforms(true);
Scheduler.CancelDelayedTasks(); Scheduler.CancelDelayedTasks();
if (period.NewValue == null) if (period.NewValue == null)
@@ -180,12 +179,12 @@ namespace osu.Game.Screens.Play
remainingTimeAdjustmentBox remainingTimeAdjustmentBox
.ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint)
.Delay(b.Duration - BREAK_FADE_DURATION) .Delay(b.Duration)
.ResizeWidthTo(0); .ResizeWidthTo(0);
remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod); remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod);
remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); remainingTimeCounter.CountTo(b.Duration + BREAK_FADE_DURATION).CountTo(0, b.Duration + BREAK_FADE_DURATION);
remainingTimeCounter.MoveToX(-50) remainingTimeCounter.MoveToX(-50)
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);
@@ -193,7 +192,7 @@ namespace osu.Game.Screens.Play
info.MoveToX(50) info.MoveToX(50)
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);
using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) using (BeginDelayedSequence(b.Duration))
{ {
fadeContainer.FadeOut(BREAK_FADE_DURATION); fadeContainer.FadeOut(BREAK_FADE_DURATION);
breakArrows.Hide(BREAK_FADE_DURATION); breakArrows.Hide(BREAK_FADE_DURATION);

View File

@@ -23,6 +23,7 @@ using osu.Game.Input.Bindings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Utils; using osu.Game.Utils;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -256,7 +257,7 @@ namespace osu.Game.Screens.Play
if (gameplayState != null) if (gameplayState != null)
{ {
playInfoText.NewLine(); playInfoText.NewLine();
playInfoText.AddText(SongSelectStrings.Accuracy); playInfoText.AddText(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy);
playInfoText.AddText(": "); playInfoText.AddText(": ");
playInfoText.AddText(gameplayState!.ScoreProcessor.Accuracy.Value.FormatAccuracy(), cp => cp.Font = cp.Font.With(weight: FontWeight.Bold)); playInfoText.AddText(gameplayState!.ScoreProcessor.Accuracy.Value.FormatAccuracy(), cp => cp.Font = cp.Font.With(weight: FontWeight.Bold));
} }

View File

@@ -61,8 +61,6 @@ namespace osu.Game.Screens.Play
private void updateDisplay(ValueChangedEvent<Period?> period) private void updateDisplay(ValueChangedEvent<Period?> period)
{ {
FinishTransforms(true);
if (period.NewValue == null) if (period.NewValue == null)
return; return;
@@ -71,7 +69,7 @@ namespace osu.Game.Screens.Play
using (BeginAbsoluteSequence(b.Start)) using (BeginAbsoluteSequence(b.Start))
{ {
fadeContainer.FadeInFromZero(BreakOverlay.BREAK_FADE_DURATION); fadeContainer.FadeInFromZero(BreakOverlay.BREAK_FADE_DURATION);
using (BeginDelayedSequence(b.Duration - BreakOverlay.BREAK_FADE_DURATION)) using (BeginDelayedSequence(b.Duration))
fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION); fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION);
} }
} }

View File

@@ -30,7 +30,6 @@ using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
[Cached(typeof(IPreviewTrackOwner))]
public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner
{ {
[Resolved] [Resolved]

View File

@@ -328,7 +328,7 @@ namespace osu.Game.Screens.Select.Carousel
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s => return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
{ {
liveCollection.PerformWrite(c => Task.Run(() => liveCollection.PerformWrite(c =>
{ {
foreach (var b in beatmapSet.Beatmaps) foreach (var b in beatmapSet.Beatmaps)
{ {
@@ -346,7 +346,7 @@ namespace osu.Game.Screens.Select.Carousel
break; break;
} }
} }
}); }));
}) })
{ {
State = { Value = state } State = { Value = state }

View File

@@ -459,6 +459,8 @@ namespace osu.Game.Screens.SelectV2
// - Background user tag population runs and causes a realm update. // - Background user tag population runs and causes a realm update.
// We don't display user tags so want to ignore this. // We don't display user tags so want to ignore this.
bool equalForDisplayPurposes = bool equalForDisplayPurposes =
// covers import-as-update flows, such as updating the beatmap with the latest online versions, or external editing inside editor
oldBeatmap.ID == newBeatmap.ID &&
// covers metadata changes // covers metadata changes
oldBeatmap.Hash == newBeatmap.Hash && oldBeatmap.Hash == newBeatmap.Hash &&
// sanity check // sanity check
@@ -483,6 +485,15 @@ namespace osu.Game.Screens.SelectV2
} }
} }
protected override void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
{
if (keyboardSelection.Model != null && grouping.ItemMap.TryGetValue(keyboardSelection.Model, out var keyboardSelectionItem))
keyboardSelection = keyboardSelection with { CarouselItem = keyboardSelectionItem.item, Index = keyboardSelectionItem.index };
if (selection.Model != null && grouping.ItemMap.TryGetValue(selection.Model, out var selectionItem))
selection = selection with { CarouselItem = selectionItem.item, Index = selectionItem.index };
}
protected override void HandleFilterCompleted() protected override void HandleFilterCompleted()
{ {
base.HandleFilterCompleted(); base.HandleFilterCompleted();
@@ -497,14 +508,7 @@ namespace osu.Game.Screens.SelectV2
// The filter might have changed the set of available groups, which means that the current selection may point to a stale group. // The filter might have changed the set of available groups, which means that the current selection may point to a stale group.
// Check whether that is the case. // Check whether that is the case.
bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0; bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0;
bool groupStillValid = currentGroupedBeatmap?.Group != null && grouping.ItemMap.ContainsKey(currentGroupedBeatmap);
bool groupStillValid = false;
if (currentGroupedBeatmap?.Group != null)
{
groupStillValid = grouping.GroupItems.TryGetValue(currentGroupedBeatmap.Group, out var items)
&& items.Any(i => CheckModelEquality(i.Model, currentGroupedBeatmap));
}
if (groupingRemainsOff || groupStillValid) if (groupingRemainsOff || groupStillValid)
{ {
@@ -697,9 +701,8 @@ namespace osu.Game.Screens.SelectV2
if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group)) if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group))
return; return;
var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group)); if (grouping.ItemMap.TryGetValue(CurrentGroupedBeatmap.Group, out var groupItem))
if (groupItem != null) Activate(groupItem.item);
Activate(groupItem);
} }
protected override double? GetScrollTarget() protected override double? GetScrollTarget()
@@ -710,9 +713,13 @@ namespace osu.Game.Screens.SelectV2
// attempt a fallback to other possibly expanded panels (set first, then group) // attempt a fallback to other possibly expanded panels (set first, then group)
if (target == null) if (target == null)
{ {
var items = GetCarouselItems(); CarouselItem? targetItem = null;
var targetItem = items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedBeatmapSet))
?? items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedGroup)); if (ExpandedBeatmapSet != null && grouping.ItemMap.TryGetValue(ExpandedBeatmapSet, out var setItem))
targetItem = setItem.item;
if (targetItem == null && ExpandedGroup != null && grouping.ItemMap.TryGetValue(ExpandedGroup, out var groupItem))
targetItem = groupItem.item;
target = targetItem?.CarouselYPosition; target = targetItem?.CarouselYPosition;
} }
@@ -922,9 +929,6 @@ namespace osu.Game.Screens.SelectV2
if (x is BeatmapInfo beatmapInfoX && y is BeatmapInfo beatmapInfoY) if (x is BeatmapInfo beatmapInfoX && y is BeatmapInfo beatmapInfoY)
return beatmapInfoX.Equals(beatmapInfoY); return beatmapInfoX.Equals(beatmapInfoY);
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
return groupX.Equals(groupY);
if (x is StarDifficultyGroupDefinition starX && y is StarDifficultyGroupDefinition starY) if (x is StarDifficultyGroupDefinition starX && y is StarDifficultyGroupDefinition starY)
return starX.Equals(starY); return starX.Equals(starY);
@@ -934,6 +938,14 @@ namespace osu.Game.Screens.SelectV2
if (x is RankedStatusGroupDefinition statusX && y is RankedStatusGroupDefinition statusY) if (x is RankedStatusGroupDefinition statusX && y is RankedStatusGroupDefinition statusY)
return statusX.Equals(statusY); return statusX.Equals(statusY);
// NOTE: this branch must be AFTER all branches that compare `GroupDefinition` subtypes!
// this is an optimisation measure. any subclass of `GroupDefinition` will pass the `is GroupDefinition` check,
// and testing a subclass of `GroupDefinition` against any other `GroupDefinition` (or subclass thereof)
// will result in a casting cascade of `Equals(GroupDefinition) -> Equals(object) -> Equals(GroupDefinitionSubClass)`
// (that last one only if the type check passes)
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
return groupX.Equals(groupY);
return base.CheckModelEquality(x, y); return base.CheckModelEquality(x, y);
} }

View File

@@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2
/// </summary> /// </summary>
public int BeatmapItemsCount { get; private set; } public int BeatmapItemsCount { get; private set; }
public IDictionary<object, (CarouselItem item, int index)> ItemMap => itemMap;
/// <summary> /// <summary>
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
/// </summary> /// </summary>
@@ -36,6 +38,7 @@ namespace osu.Game.Screens.SelectV2
/// </summary> /// </summary>
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap; public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap;
private Dictionary<object, (CarouselItem, int)> itemMap = new Dictionary<object, (CarouselItem, int)>();
private Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>> setMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(); private Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>> setMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>();
private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(); private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
@@ -49,6 +52,7 @@ namespace osu.Game.Screens.SelectV2
return await Task.Run(() => return await Task.Run(() =>
{ {
// preallocate space for the new mappings using last known estimates // preallocate space for the new mappings using last known estimates
var newItemMap = new Dictionary<object, (CarouselItem, int)>(itemMap.Count);
var newSetMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(setMap.Count); var newSetMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(setMap.Count);
var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count); var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count);
@@ -127,6 +131,7 @@ namespace osu.Game.Screens.SelectV2
{ {
newItems.Add(i); newItems.Add(i);
newItemMap[i.Model] = (i, newItems.Count - 1);
currentGroupItems?.Add(i); currentGroupItems?.Add(i);
currentSetItems?.Add(i); currentSetItems?.Add(i);
@@ -136,6 +141,7 @@ namespace osu.Game.Screens.SelectV2
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Interlocked.Exchange(ref itemMap, newItemMap);
Interlocked.Exchange(ref setMap, newSetMap); Interlocked.Exchange(ref setMap, newSetMap);
Interlocked.Exchange(ref groupMap, newGroupMap); Interlocked.Exchange(ref groupMap, newGroupMap);
BeatmapItemsCount = displayedBeatmapsCount; BeatmapItemsCount = displayedBeatmapsCount;
@@ -209,7 +215,7 @@ namespace osu.Game.Screens.SelectV2
case GroupMode.Collections: case GroupMode.Collections:
{ {
var collections = GetCollections(); var collections = GetCollections();
return getGroupsBy(b => defineGroupByCollection(b, collections), items); return defineGroupsByCollection(items, collections);
} }
case GroupMode.MyMaps: case GroupMode.MyMaps:
@@ -396,29 +402,56 @@ namespace osu.Game.Screens.SelectV2
return new GroupDefinition(0, source).Yield(); return new GroupDefinition(0, source).Yield();
} }
private IEnumerable<GroupDefinition> defineGroupByCollection(BeatmapInfo beatmap, List<BeatmapCollection> collections) private List<GroupMapping> defineGroupsByCollection(List<CarouselItem> carouselItems, List<BeatmapCollection> allCollections)
{ {
bool anyCollections = false; Dictionary<GroupDefinition, GroupMapping> groupMappings = new Dictionary<GroupDefinition, GroupMapping>();
// this is a pre-built mapping of MD5s to a list of collections in which this MD5 is found in.
// the reason to pre-build this is that `BeatmapCollection.BeatmapMD5Hashes` is a list and therefore a naive implementation would be slow,
// particularly in edge cases where most beatmaps are in more than one collection.
Dictionary<string, List<GroupDefinition>> md5ToCollectionsMap = new Dictionary<string, List<GroupDefinition>>();
for (int i = 0; i < collections.Count; i++) for (int i = 0; i < allCollections.Count; i++)
{ {
var collection = collections[i]; var collection = allCollections[i];
// NOTE: the ordering of the incoming collection list is significant and needs to be preserved.
// the fallback to ordering by name cannot be relied on.
// see xmldoc of `BeatmapCarousel.GetAllCollections()`.
var groupDefinition = new GroupDefinition(i, collection.Name);
groupMappings[groupDefinition] = new GroupMapping(groupDefinition, []);
if (collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash)) foreach (string md5 in collection.BeatmapMD5Hashes)
{ {
// NOTE: the ordering of the incoming collection list is significant and needs to be preserved. if (!md5ToCollectionsMap.TryGetValue(md5, out var collections))
// the fallback to ordering by name cannot be relied on. md5ToCollectionsMap[md5] = collections = new List<GroupDefinition>();
// see xmldoc of `BeatmapCarousel.GetAllCollections()`.
yield return new GroupDefinition(i, collection.Name);
anyCollections = true; collections.Add(groupDefinition);
} }
} }
if (anyCollections) var notInCollection = new GroupDefinition(int.MaxValue, "Not in collection");
yield break; groupMappings[notInCollection] = new GroupMapping(notInCollection, []);
yield return new GroupDefinition(int.MaxValue, "Not in collection"); foreach (var item in carouselItems)
{
var beatmap = (BeatmapInfo)item.Model;
// as a side note, even reading the `MD5Hash` off a realm model is slow if done enough times,
// so it definitely helps that thanks to the mapping it needs to only be retrieved once
if (md5ToCollectionsMap.TryGetValue(beatmap.MD5Hash, out var collections))
{
foreach (var collection in collections)
groupMappings[collection].ItemsInGroup.Add(item);
}
else
groupMappings[notInCollection].ItemsInGroup.Add(item);
}
return groupMappings.Values
// safety against potentially empty eagerly-initialised groups
// (could happen if user has a collection with MD5s of maps that aren't locally available)
.Where(mapping => mapping.ItemsInGroup.Count > 0)
.OrderBy(mapping => mapping.Group!.Order)
.ToList();
} }
private IEnumerable<GroupDefinition> defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername) private IEnumerable<GroupDefinition> defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername)

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;
@@ -237,11 +238,11 @@ namespace osu.Game.Screens.SelectV2
{ {
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

@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
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.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
@@ -297,7 +298,7 @@ namespace osu.Game.Screens.SelectV2
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s => return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
{ {
liveCollection.PerformWrite(c => Task.Run(() => liveCollection.PerformWrite(c =>
{ {
foreach (var b in beatmapSet.Beatmaps) foreach (var b in beatmapSet.Beatmaps)
{ {
@@ -315,7 +316,7 @@ namespace osu.Game.Screens.SelectV2
break; break;
} }
} }
}); }));
}) })
{ {
State = { Value = state } State = { Value = state }

View File

@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osuTK.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@@ -40,6 +41,23 @@ namespace osu.Game.Skinning
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Retro") new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Retro")
) )
{ {
Configuration.ConfigDictionary[@"SliderBallFlip"] = "0";
Configuration.ConfigDictionary[@"SliderBallFrames"] = "10";
Configuration.ConfigDictionary[@"AllowSliderBallTint"] = "0";
Configuration.ConfigDictionary[@"CursorTrailRotate"] = "0";
Configuration.ConfigDictionary[@"Version"] = "1";
Configuration.CustomComboColours =
[
new Color4(255, 150, 0, 255),
new Color4(5, 240, 5, 255),
new Color4(5, 5, 240, 255),
new Color4(240, 5, 5, 255)
];
Configuration.ConfigDictionary[@"HitCircleOverlap"] = "3";
Configuration.ConfigDictionary[@"ScoreOverlap"] = "3";
Configuration.ConfigDictionary[@"ComboOverlap"] = "3";
} }
public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)

View File

@@ -228,8 +228,9 @@ namespace osu.Game.Skinning
// First attempt to deserialise using the new SkinLayoutInfo format // First attempt to deserialise using the new SkinLayoutInfo format
layout = JsonConvert.DeserializeObject<SkinLayoutInfo>(jsonContent); layout = JsonConvert.DeserializeObject<SkinLayoutInfo>(jsonContent);
} }
catch catch (Exception ex)
{ {
Logger.Log($"Deserialising skin layout to {nameof(SkinLayoutInfo)} failed. Falling back to {nameof(SerialisedDrawableInfo)}[].\nDetails: {ex}");
} }
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list. // If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.

View File

@@ -178,9 +178,10 @@ namespace osu.Game.Skinning
if (existingFile == null) if (existingFile == null)
{ {
// skins without a skin.ini are supposed to import using the "latest version" spec. // skins without a skin.ini are supposed to import using the "latest version" spec, unless we're making a copy of the retro skin which specifies 1.0.
// see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298 // see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298
newLines.Add(FormattableString.Invariant($"Version: {SkinConfiguration.LATEST_VERSION}")); decimal version = item.InstantiationInfo == typeof(RetroSkin).GetInvariantInstantiationInfo() ? 1.0M : SkinConfiguration.LATEST_VERSION;
newLines.Add(FormattableString.Invariant($"Version: {version}"));
// In the case a skin doesn't have a skin.ini yet, let's create one. // In the case a skin doesn't have a skin.ini yet, let's create one.
writeNewSkinIni(); writeNewSkinIni();

View File

@@ -86,9 +86,7 @@ namespace osu.Game.Tests.Beatmaps
currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader); currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
// populate ruleset for beatmap converters that require it to be present. // populate ruleset for beatmap converters that require it to be present.
var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID); var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID) ?? new RulesetInfo { OnlineID = currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID };
Debug.Assert(ruleset != null);
currentTestBeatmap.BeatmapInfo.Ruleset = ruleset; currentTestBeatmap.BeatmapInfo.Ruleset = ruleset;
}); });

View File

@@ -68,17 +68,6 @@ namespace osu.Game.Updater
// make sure the release stream setting matches the build which was just run. // make sure the release stream setting matches the build which was just run.
if (FixedReleaseStream != null) if (FixedReleaseStream != null)
config.SetValue(OsuSetting.ReleaseStream, FixedReleaseStream.Value); config.SetValue(OsuSetting.ReleaseStream, FixedReleaseStream.Value);
// nope, doesn't matter, we're already not official
// // notify the user if they're using a build that is not officially sanctioned.
// if (RuntimeInfo.EntryAssembly.GetCustomAttribute<OfficialBuildAttribute>() == null)
// Notifications.Post(new SimpleNotification { Text = NotificationsStrings.NotOfficialBuild });
}
else
{
// log that this is not an official build, for if users build their own game without an assembly version.
// this is only logged because a notification would be too spammy in local test builds.
Logger.Log(NotificationsStrings.NotOfficialBuild.ToString());
} }
// debug / local compilations will reset to a non-release string. // debug / local compilations will reset to a non-release string.

View File

@@ -41,18 +41,17 @@ namespace osu.Game.Utils
{ {
this.game = game; this.game = game;
if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".jvnko.boats", StringComparison.Ordinal))
return; return;
sentrySession = SentrySdk.Init(options => sentrySession = SentrySdk.Init(options =>
{ {
options.Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2"; options.Dsn = "https://8b67571746af4a07a09558574c7e2227@satellite.jvnko.boats/1";
options.AutoSessionTracking = true; options.AutoSessionTracking = true;
options.IsEnvironmentUser = false; options.IsEnvironmentUser = false;
options.IsGlobalModeEnabled = true; options.IsGlobalModeEnabled = true;
options.CacheDirectoryPath = storage?.GetFullPath(string.Empty); options.CacheDirectoryPath = storage?.GetFullPath(string.Empty);
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml options.Release = $"jvnkosu@{game.Version.Split('-').First()}";
options.Release = $"osu@{game.Version.Split('-').First()}";
}); });
Logger.NewEntry += processLogEntry; Logger.NewEntry += processLogEntry;

View File

@@ -35,9 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="20.1.0" /> <PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.1028.0" /> <PackageReference Include="ppy.osu.Framework" Version="2025.1118.1" />
<PackageReference Include="jvnkosu.Resources" Version="2025.1103.0" /> <PackageReference Include="jvnkosu.Resources" Version="2025.1119.0" />
<PackageReference Include="Sentry" Version="5.1.1" /> <PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" /> <PackageReference Include="SharpCompress" Version="0.39.0" />

View File

@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter> <MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1028.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1118.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>