92 Commits

Author SHA1 Message Date
2e4b0ff197 make score cards for failed scores look better 2025-11-18 15:33:37 +03:00
499f410c94 slightly changed warnings for old windows versions 2025-11-18 14:44:34 +03:00
616c0d8ecd make key counters show proper readable key names 2025-11-18 14:24:05 +03:00
ed889138a0 break countdown now uses argon-style counter 2025-11-18 13:23:44 +03:00
18075bef29 display score retractions in recent activity 2025-11-17 21:28:03 +03:00
bc7780a870 updated the first players cutoff date for user profiles 2025-11-17 20:55:42 +03:00
8930b8fadb synchronize with github (tag 2025.1105.0) 2025-11-17 20:41:55 +03:00
2d457f4305 use online status of beatmap instead of mapset 2025-11-17 20:14:23 +03:00
a784734f94 fix up a couple strings 2025-11-17 20:10:54 +03:00
ecfd4764e7 fixed a blunder 2025-11-16 22:29:25 +03:00
f5ca5083d6 implement pp for legacy song select 2025-11-16 21:21:12 +03:00
1d7c77d8d6 add performance points to selectv2 beatmap info wedge 2025-11-16 20:49:27 +03:00
774e52fbd6 slight result screen score panel redesign 2025-11-16 18:52:04 +03:00
a9d7a9d5d5 score panel is now tinted, plus some other changes 2025-11-16 02:48:41 +03:00
f7069b1009 minor fixes for some mods 2025-11-16 02:47:38 +03:00
c3ce5dc787 add argon-style longest combo counter 2025-11-15 19:24:49 +03:00
98076e2092 add skinnable online status and star rating components 2025-11-15 19:03:14 +03:00
b7d1092f90 Merge branch 'master' of https://gitea.jvnko.boats/jvnkosu/client 2025-11-15 17:02:04 +03:00
08db90c278 some minor hud changes
- argon-style cps counter
- keybinds in key counters (for now, only default)
2025-11-15 16:45:14 +03:00
b7e36164c3 update issue templates 2025-11-14 22:22:43 +01:00
0f5f13858d exit game option for fail condition mods (SD, PF, AC) 2025-11-13 21:41:09 +03:00
89a0c75156 all user-playable mods are now always ranked 2025-11-12 20:53:07 +03:00
Bartłomiej Dach
243cd9c073 Merge pull request #35542 from smoogipoo/mp-vote-to-skip
Implement vote-to-skip in multiplayer
2025-11-05 10:06:59 +01:00
Dan Balasescu
e8db35a5c9 Merge branch 'master' into mp-vote-to-skip 2025-11-05 16:53:44 +09:00
Bartłomiej Dach
0f54608cee Merge pull request #35575 from smoogipoo/qp-player-download-progress
Add download progress bars to quick play users
2025-11-04 14:54:03 +01:00
Bartłomiej Dach
f8331e0b28 Apply one more missed rename 2025-11-04 12:56:03 +01:00
Bartłomiej Dach
6ff2a6225d Merge branch 'master' into qp-player-download-progress 2025-11-04 12:52:01 +01:00
Bartłomiej Dach
a8020dea7c Bring back size spec in a better way 2025-11-04 12:51:53 +01:00
Dan Balasescu
88dd458394 Apply suggestions from review
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-11-04 11:37:12 +09:00
Dan Balasescu
23cb7f3b23 Add download progress bars to quick play users 2025-11-04 11:37:12 +09:00
Dan Balasescu
7da051b144 Add test 2025-11-04 11:37:07 +09:00
Dan Balasescu
4d706b12ac Fix missing disposal 2025-11-04 11:09:02 +09:00
Dan Balasescu
c44f701abe Also update text when users leave 2025-11-04 11:09:02 +09:00
Dan Balasescu
4c81d661aa Bypass vote for auto-skip
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-11-04 11:08:57 +09:00
Dan Balasescu
f4049c7ec1 Suffix introp methods with "Intro" 2025-11-04 11:05:41 +09:00
Bartłomiej Dach
645d27bb32 Add tiered colours for global rank (#35597)
* Add new API property backing for tiered rank

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

* Extract separate component for global rank display

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

* Localize jump link string

* Mark ILinkHandler dependency as nullable

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

* Use existing web string instead of inventing a new one

* Bind value change callback more reliably

---------

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModRateAdjustConcrete : ModRateAdjustConcrete
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -62,5 +64,41 @@ namespace osu.Game.Tests.Visual.Matchmaking
panel.AllowSelection = value;
});
}
[Test]
public void TestFailedBeatmapLookup()
{
AddStep("setup request handle", () =>
{
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", () =>
{
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
}
}
}

View File

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

View File

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

View File

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

View File

@@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
MatchmakingRoomState state = new MatchmakingRoomState();
for (int i = 0; i < room.Users.Count; i++)
state.Users[room.Users[i].UserID].Placement = placements[i];
state.Users.GetOrAdd(room.Users[i].UserID).Placement = placements[i];
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Represents a mod which can override a fail and quit the game instead.
/// </summary>
public interface IApplicableFailExit : IApplicableMod
{
/// <summary>
/// Whether we should allow failing at the current point in time.
/// </summary>
/// <returns>Whether the fail should be allowed to proceed. Return false to block.</returns>
bool PerformFail();
/// <summary>
/// Whether we want to exit the game on fail. Only used if <see cref="PerformFail"/> returns true.
/// </summary>
bool ExitOnFail { get; }
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// An interface for mods that apply changes to the <see cref="OsuGameBase"/>.
/// This is really stupid and f%%king dangerous, possibly disasterous even.
/// </summary>
public interface IApplicableToOsuGameBase : IApplicableMod
{
/// <summary>
/// Provide a <see cref="OsuGameBase"/>. Called once on initialisation of a play instance.
/// </summary>
void ApplyToOsuGameBase(OsuGameBase game);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
@@ -9,17 +10,23 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride, IApplicableFailExit
{
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModCinema) };
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
public BindableBool Restart { get; } = new BindableBool();
[SettingSource("Exit game on fail", "Automatically exits the game when failed."), JsonIgnore]
public BindableBool Exit { get; } = new BindableBool();
public virtual bool PerformFail() => true;
public virtual bool RestartOnFail => Restart.Value;
[JsonIgnore]
public virtual bool ExitOnFail => Exit.Value;
private Action? triggerFailureDelegate;
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
@@ -39,7 +46,7 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
/// <param name="healthProcessor">The loaded <see cref="HealthProcessor"/>.</param>
/// <param name="result">The latest <see cref="JudgementResult"/>.</param>
/// <returns>Whether the fail condition has been met.</returns>
/// <returns>Whether the fail condition has been met.</returns>z
/// <remarks>
/// This method should only be used to trigger failures based on <paramref name="result"/>.
/// Using outside values to evaluate failure may introduce event ordering discrepancies, use

View File

@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModFlashlight;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Restricted view area.";
public override bool Ranked => UsesDefaultConfiguration;
public override bool Ranked => true;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public abstract BindableFloat SizeMultiplier { get; }

View File

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

View File

@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Everything just got a bit harder...";
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
public override bool Ranked => UsesDefaultConfiguration;
public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => true;
protected const float ADJUST_RATIO = 1.4f;

View File

@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "HD";
public override IconUsage? Icon => OsuIcon.ModHidden;
public override ModType Type => ModType.DifficultyIncrease;
public override bool Ranked => UsesDefaultConfiguration;
public override bool Ranked => true;
public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{

View File

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

View File

@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "You can't fail, no matter what.";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition), typeof(ModCinema) };
public override bool Ranked => UsesDefaultConfiguration;
public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => true;
private readonly Bindable<bool> showHealthBar = new Bindable<bool>();

View File

@@ -0,0 +1,47 @@
// 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 osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModRateAdjustConcrete : ModRateAdjust
{
public override string Name => "Rate Adjust";
public override LocalisableString Description => "[DEBUG BUILDS ONLY] Set any speed";
public override string Acronym => "_R";
private readonly RateAdjustModHelper rateAdjustHelper;
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
{
MinValue = 0.1, // BASS breaks at lower rates
MaxValue = 10,
Precision = 0.01
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
protected ModRateAdjustConcrete()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
}
public override double ScoreMultiplier => 1.0;
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
rateAdjustHelper.ApplyToTrack(track);
}
public override bool Ranked => false;
public override ModType Type => ModType.Special;
}
}

View File

@@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Mods
Conversion,
Automation,
Fun,
System
System,
Special
}
}

View File

@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
public readonly string OriginalAcronym;
public override string Name => $"Unknown mod ({OriginalAcronym})";
public override string Acronym => $"{OriginalAcronym}??";
public override string Acronym => $"{OriginalAcronym}!";
public override LocalisableString Description => "This mod could not be resolved by the game.";
public override double ScoreMultiplier => 0;

View File

@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.UI
public readonly KeyBindingContainer<T> KeyBindingContainer;
private readonly RulesetKeyBindingContainer rulesetKeyBindingContainer;
[Resolved]
private ScoreProcessor? scoreProcessor { get; set; }
@@ -64,9 +68,9 @@ namespace osu.Game.Rulesets.UI
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{
InternalChild = KeyBindingContainer =
CreateKeyBindingContainer(ruleset, variant, unique)
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
rulesetKeyBindingContainer = createRulesetKeyBindingContainer(ruleset, variant, unique);
InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique).WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader(true)]
@@ -174,10 +178,9 @@ namespace osu.Game.Rulesets.UI
public void Attach(InputCountController inputCountController)
{
var triggers = KeyBindingContainer.DefaultKeyBindings
.Select(b => b.GetAction<T>())
.Distinct()
.Select(action => new KeyCounterActionTrigger<T>(action))
var bindings = rulesetKeyBindingContainer.DefaultKeyBindings;
var triggers = bindings.Select(b => new KeyCounterBindingTrigger<T>(b, b.GetAction<T>()))
.DistinctBy(b => b.Action)
.ToArray();
KeyBindingContainer.AddRange(triggers);
@@ -215,6 +218,9 @@ namespace osu.Game.Rulesets.UI
protected virtual KeyBindingContainer<T> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new RulesetKeyBindingContainer(ruleset, variant, unique);
private RulesetKeyBindingContainer createRulesetKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new RulesetKeyBindingContainer(ruleset, variant, unique);
public partial class RulesetKeyBindingContainer : DatabasedKeyBindingContainer<T>
{
protected override bool HandleRepeats => false;

View File

@@ -486,7 +486,7 @@ namespace osu.Game.Screens.Menu
private void loadSongSelect() => this.Push(forceSSV1.Value ? new PlaySongSelect() : new SoloSongSelect());
private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.IntroScreen());
private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.ScreenIntro());
private partial class MobileDisclaimerDialog : PopupDialog
{

View File

@@ -21,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro
/// <summary>
/// A brief intro animation that introduces matchmaking to the user.
/// </summary>
public partial class IntroScreen : OsuScreen
public partial class ScreenIntro : OsuScreen
{
public override bool DisallowExternalBeatmapRulesetChanges => false;
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro
protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider);
public IntroScreen()
public ScreenIntro()
{
ValidForResume = false;
}

View File

@@ -53,6 +53,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private BeatmapSetOverlay? beatmapSetOverlay { get; set; }
public BeatmapCardMatchmaking(APIBeatmap beatmap)
: base(beatmap.BeatmapSet!, false)
{
@@ -319,7 +322,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, DefaultAction)
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID))
};
foreach (var button in buttonContainer.Buttons)

View File

@@ -111,7 +111,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
Debug.Assert(card == null);
var beatmap = b.GetResultSafely()!;
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)

View File

@@ -20,6 +20,7 @@ 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;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
@@ -27,6 +28,7 @@ using osu.Game.Online.Matchmaking.Events;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
@@ -107,6 +109,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private BufferedContainer backgroundQuitTarget = null!;
private BufferedContainer avatarQuitTarget = null!;
private Box downloadProgressBar = null!;
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
private bool hasQuit;
@@ -158,7 +162,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = mainContent = new Container
Children = new Drawable[]
{
mainContent = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -233,6 +239,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
Text = "0 pts"
}
}
},
downloadProgressBar = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Size = new Vector2(0, 4),
Colour = colourProvider?.Content2 ?? colours.Gray3
}
}
}
}
@@ -250,6 +265,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
client.MatchRoomStateChanged += onRoomStateChanged;
client.MatchEvent += onMatchEvent;
client.BeatmapAvailabilityChanged += onBeatmapAvailabilityChanged;
onRoomStateChanged(client.Room!.MatchState);
@@ -414,8 +430,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore))
return;
rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture);
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement));
if (userScore.Placement == null)
return;
rankText.Text = userScore.Placement.Value.Ordinalize(CultureInfo.CurrentCulture);
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement.Value));
scoreText.Text = $"{userScore.Points} pts";
});
@@ -469,6 +488,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
}
}
private void onBeatmapAvailabilityChanged(MultiplayerRoomUser user, BeatmapAvailability availability) => Scheduler.Add(() =>
{
if (availability.State == DownloadState.Downloading)
downloadProgressBar.FadeIn(200, Easing.OutPow10);
else
downloadProgressBar.FadeOut(200, Easing.OutPow10);
downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10);
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -477,6 +506,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
client.MatchRoomStateChanged -= onRoomStateChanged;
client.MatchEvent -= onMatchEvent;
client.BeatmapAvailabilityChanged -= onBeatmapAvailabilityChanged;
}
}

View File

@@ -239,8 +239,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState)
continue;
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user))
SetLayoutPosition(Children[i], user.Placement);
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user) && user.Placement != null)
SetLayoutPosition(Children[i], user.Placement.Value);
else
SetLayoutPosition(Children[i], float.MaxValue);
}

View File

@@ -194,20 +194,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
{
userStatistics.Clear();
if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0)
var localUserState = state.Users.GetOrAdd(client.LocalUser!.UserID);
if (localUserState.Rounds.Count == 0)
{
placementText.Text = "-";
placementText.Colour = OsuColour.Gray(1f);
return;
}
int overallPlacement = state.Users[client.LocalUser!.UserID].Placement;
int? overallPlacement = localUserState.Placement;
placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture);
placementText.Colour = ColourForPlacement(overallPlacement);
if (overallPlacement != null)
{
placementText.Text = overallPlacement.Value.Ordinalize(CultureInfo.CurrentCulture);
placementText.Colour = ColourForPlacement(overallPlacement.Value);
int overallPoints = state.Users[client.LocalUser!.UserID].Points;
addStatistic(overallPlacement, $"Overall position ({overallPoints} points)");
int overallPoints = localUserState.Points;
addStatistic(overallPlacement.Value, $"Overall position ({overallPoints} points)");
}
var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average()))
.OrderByDescending(t => t.avgAcc)
@@ -216,15 +221,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
int accuracyPlacement = accuracyOrderedUsers.index + 1;
addStatistic(accuracyPlacement, $"Overall accuracy ({accuracyOrderedUsers.info.avgAcc.FormatAccuracy()})");
var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Max(r => r.MaxCombo)))
var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Select(r => r.MaxCombo).DefaultIfEmpty(0).Max()))
.OrderByDescending(t => t.maxCombo)
.Select((t, i) => (info: t, index: i))
.Single(t => t.info.user.UserId == client.LocalUser!.UserID);
int maxComboPlacement = maxComboOrderedUsers.index + 1;
addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)");
var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement);
addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})");
var bestPlacement = localUserState.Rounds.MinBy(r => r.Placement);
if (bestPlacement != null)
addStatistic(bestPlacement.Placement, $"Best round placement (round {bestPlacement.Round})");
void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text));
}
@@ -255,27 +261,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
roomAwards.Clear();
long maxScore = long.MinValue;
int maxScoreUserId = 0;
int maxScoreUserId = -1;
double maxAccuracy = double.MinValue;
int maxAccuracyUserId = 0;
int maxAccuracyUserId = -1;
int maxCombo = int.MinValue;
int maxComboUserId = 0;
int maxComboUserId = -1;
long maxBonusScore = 0;
int maxBonusScoreUserId = 0;
int maxBonusScoreUserId = -1;
long largestScoreDifference = long.MinValue;
int largestScoreDifferenceUserId = 0;
int largestScoreDifferenceUserId = -1;
long smallestScoreDifference = long.MaxValue;
int smallestScoreDifferenceUserId = 0;
int smallestScoreDifferenceUserId = -1;
for (int round = 1; round <= state.CurrentRound; round++)
{
long roundHighestScore = long.MinValue;
int roundHighestScoreUserId = 0;
int roundHighestScoreUserId = -1;
long roundLowestScore = long.MaxValue;
@@ -344,10 +350,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
}
}
if (maxScoreUserId > 0)
addAward(maxScoreUserId, "Score champ", "Highest score in a single round");
if (maxAccuracyUserId > 0)
addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round");
if (maxComboUserId > 0)
addAward(maxComboUserId, "Top combo", "Highest combo in a single round");
if (maxBonusScoreUserId > 0)

View File

@@ -2,10 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -32,11 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
[Resolved]
private INotificationOverlay? notifications { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
private ProgressNotification? backgroundNotification;
private Notification? readyNotification;
private BackgroundQueueNotification? backgroundNotification;
private bool isBackgrounded;
protected override void LoadComplete()
@@ -118,27 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
if (backgroundNotification != null)
return;
notifications?.Post(backgroundNotification = new ProgressNotification
{
Text = "Searching for opponents...",
CompletionTarget = n => notifications.Post(readyNotification = n),
CompletionText = "Your match is ready! Click to join.",
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
performer?.PerformFromScreen(s => s.Push(new IntroScreen()));
closeNotifications();
return true;
},
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
closeNotifications();
return true;
}
});
notifications?.Post(backgroundNotification = new BackgroundQueueNotification(this));
}
private void closeNotifications()
@@ -146,13 +127,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
if (backgroundNotification != null)
{
backgroundNotification.State = ProgressNotificationState.Cancelled;
backgroundNotification.Close(false);
}
readyNotification?.Close(false);
backgroundNotification.CloseAll();
backgroundNotification = null;
readyNotification = null;
}
}
protected override void Dispose(bool isDisposing)
@@ -168,5 +145,87 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
client.MatchmakingRoomReady -= onMatchmakingRoomReady;
}
}
private partial class BackgroundQueueNotification : ProgressNotification
{
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private readonly QueueController controller;
private Notification? foundNotification;
private Sample? matchFoundSample;
public BackgroundQueueNotification(QueueController controller)
{
this.controller = controller;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
Text = "Searching for opponents...";
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
controller.CurrentState.Value = ScreenQueue.MatchmakingScreenState.AcceptedWaitingForRoom;
performer?.PerformFromScreen(s => s.Push(new ScreenIntro()));
Close(false);
return true;
};
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
return true;
};
matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found");
}
protected override Notification CreateCompletionNotification()
{
// Playing here means it will play even if notification overlay is hidden.
//
// If we add support for the completion notification to be processed during gameplay,
// this can be moved inside the `MatchFoundNotification` implementation.
matchFoundSample?.Play();
return foundNotification = new MatchFoundNotification
{
Activated = CompletionClickAction,
Text = "Your match is ready! Click to join.",
};
}
public void CloseAll()
{
foundNotification?.Close(false);
Close(false);
}
public partial class MatchFoundNotification : ProgressCompletionNotification
{
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
public MatchFoundNotification()
{
IsCritical = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.Bolt;
IconContent.Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.YellowLight);
}
}
}
}
}

View File

@@ -56,7 +56,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
AllowPause = false,
AllowRestart = false,
AllowSkipping = room.AutoSkip,
AutomaticallySkipIntro = room.AutoSkip,
ShowLeaderboard = true,
})
@@ -121,6 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
client.GameplayStarted += onGameplayStarted;
client.ResultsReady += onResultsReady;
client.VoteToSkipIntroPassed += onVoteToSkipIntroPassed;
ScoreProcessor.HasCompleted.BindValueChanged(_ =>
{
@@ -148,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.Room != null);
}
protected override SkipOverlay CreateSkipOverlay(double startTime) => new MultiplayerSkipOverlay(startTime);
protected override void StartGameplay()
{
// We can enter this screen one of two ways:
@@ -219,6 +221,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false);
}
protected override void RequestIntroSkip()
{
// If the room is set up such that the intro is automatically skipped, there's no need to vote on it.
if (Configuration.AutomaticallySkipIntro)
{
base.RequestIntroSkip();
return;
}
// No base call because we aren't skipping yet.
client.VoteToSkipIntro().FireAndForget();
}
private void onVoteToSkipIntroPassed()
{
Schedule(() => PerformIntroSkip(true));
}
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(Room.RoomID != null);
@@ -242,6 +262,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
client.GameplayStarted -= onGameplayStarted;
client.ResultsReady -= onResultsReady;
client.VoteToSkipIntroPassed -= onVoteToSkipIntroPassed;
}
}
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerSkipOverlay : SkipOverlay
{
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private Drawable votedIcon = null!;
private OsuSpriteText countText = null!;
public MultiplayerSkipOverlay(double startTime)
: base(startTime)
{
}
[BackgroundDependencyLoader]
private void load()
{
FadingContent.AddRange(
[
votedIcon = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(50, 0),
Size = new Vector2(20),
Alpha = 0,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Green
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.5f),
Icon = FontAwesome.Solid.Check
}
}
},
countText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
Position = new Vector2(0.75f, 0),
Font = OsuFont.Default.With(size: 36, weight: FontWeight.Bold)
}
]);
}
protected override void LoadComplete()
{
base.LoadComplete();
client.UserLeft += onUserLeft;
client.UserStateChanged += onUserStateChanged;
client.UserVotedToSkipIntro += onUserVotedToSkipIntro;
updateText();
}
private void onUserLeft(MultiplayerRoomUser user)
{
Schedule(updateText);
}
private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state)
{
Schedule(updateText);
}
private void onUserVotedToSkipIntro(int userId) => Schedule(() =>
{
updateText();
countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine);
if (userId == client.LocalUser?.UserID)
{
votedIcon.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine);
votedIcon.FadeInFromZero(100);
}
});
private void updateText()
{
if (client.Room == null || client.Room.Settings.AutoSkip)
return;
int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing);
int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkipIntro);
int countRequired = countTotal / 2 + 1;
countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}";
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
{
client.UserLeft -= onUserLeft;
client.UserStateChanged -= onUserStateChanged;
client.UserVotedToSkipIntro -= onUserVotedToSkipIntro;
}
}
}
}

View File

@@ -2,28 +2,33 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Screens.Play.Break
{
public partial class RemainingTimeCounter : Counter
{
private readonly OsuSpriteText counter;
private readonly ArgonCounterTextComponent counter;
public RemainingTimeCounter()
{
AutoSizeAxes = Axes.Both;
InternalChild = counter = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 33),
};
InternalChild = counter = new ArgonCounterTextComponent(Anchor.Centre);
counter.Scale *= 1.25f; // this seems to be the only way to make the counter bigger, I hope I'm wrong
counter.WireframeOpacity.BindTo(new BindableFloat(0.125f));
}
private string lookup(char c)
{
return c.ToString();
}
protected override void OnCountChanged(double count)
{
string displayText = ((int)Math.Ceiling(count / 1000)).ToString();
counter.Text = displayText;
counter.WireframeTemplate = new string('#', displayText.Length);
}
protected override void OnCountChanged(double count) => counter.Text = ((int)Math.Ceiling(count / 1000)).ToString();
}
}

View File

@@ -0,0 +1,97 @@
// 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.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonLongestComboCounter : ComboCounter
{
protected ArgonCounterTextComponent Text = null!;
protected override double RollingDuration => 250;
protected virtual bool DisplayXSymbol => true;
[SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel))]
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
[SettingSource("Show animation on increase", "Shows a bouncing animation when the combo increases")]
public Bindable<bool> ShowRolling { get; } = new BindableBool(true);
[BackgroundDependencyLoader]
private void load(ScoreProcessor scoreProcessor)
{
Current.BindTo(scoreProcessor.HighestCombo);
Current.BindValueChanged(combo =>
{
bool wasIncrease = combo.NewValue > combo.OldValue;
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;
float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f);
float duration = ShowRolling.Value ? 500 : 0;
Text.NumberContainer
.ScaleTo(new Vector2(newScale))
.ScaleTo(Vector2.One, duration, Easing.OutQuint);
});
}
public override int DisplayedCount
{
get => base.DisplayedCount;
set
{
base.DisplayedCount = value;
updateWireframe();
}
}
private void updateWireframe()
{
int digitsRequiredForDisplayCount = getDigitsRequiredForDisplayCount();
if (digitsRequiredForDisplayCount != Text.WireframeTemplate.Length)
Text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount);
}
private int getDigitsRequiredForDisplayCount()
{
// one for the single presumed starting digit, one for the "x" at the end (unless disabled).
int digitsRequired = DisplayXSymbol ? 2 : 1;
long c = DisplayedCount;
while ((c /= 10) > 0)
digitsRequired++;
return digitsRequired;
}
protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? $@"{count}x" : count.ToString();
protected override IHasText CreateText() => Text = new ArgonCounterTextComponent(Anchor.TopLeft, "MAX COMBO")
{
WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel },
};
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
{
public partial class ArgonClicksPerSecondCounter : RollingCounter<int>, ISerialisableDrawable
{
[Resolved]
private ClicksPerSecondController controller { get; set; } = null!;
protected override double RollingDuration => 175;
[SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel))]
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
public bool UsesFixedAnchor { get; set; }
public ArgonClicksPerSecondCounter()
{
Current.Value = 0;
}
protected override void Update()
{
base.Update();
Current.Value = controller.Value;
}
protected override IHasText CreateText() => new TextComponent()
{
WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel },
};
private partial class TextComponent : CompositeDrawable, IHasText
{
private readonly ArgonCounterTextComponent cpsValue;
public IBindable<float> WireframeOpacity { get; } = new BindableFloat();
public Bindable<bool> ShowLabel { get; } = new BindableBool();
public LocalisableString Text
{
get => cpsValue.Text;
set => cpsValue.Text = value;
}
public TextComponent()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Child = cpsValue = new ArgonCounterTextComponent(Anchor.TopLeft, "KEYS/SEC") // welp, not good
{
WireframeOpacity = { BindTarget = WireframeOpacity },
WireframeTemplate = @"##",
ShowLabel = { BindTarget = ShowLabel },
}
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
}
}
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
namespace osu.Game.Screens.Play.HUD
{
public partial class KeyCounterBindingTrigger<T> : InputTrigger, IKeyBindingHandler<T>
where T : struct
{
public IKeyBinding Key { get; }
public T Action { get; }
[Resolved]
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(ReadableKeyCombinationProvider keys)
{
keyCombinationProvider = keys;
}
private string getName(IKeyBinding key, T action)
{
return keyCombinationProvider?.GetReadableString(key.KeyCombination) ?? $"B{(int)(object)action + 1}";
}
public KeyCounterBindingTrigger(IKeyBinding key, T action)
: base("")
{
Key = key;
Action = action;
}
protected override void LoadComplete()
{
base.LoadComplete();
Name = getName(Key, Action);
}
public bool OnPressed(KeyBindingPressEvent<T> e)
{
if (!EqualityComparer<T>.Default.Equals(e.Action, Action))
return false;
Activate(Clock.Rate >= 0);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<T> e)
{
if (!EqualityComparer<T>.Default.Equals(e.Action, Action))
return;
Deactivate(Clock.Rate >= 0);
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public partial class SkinnableBeatmapSetOnlineStatusPill : CompositeDrawable, ISerialisableDrawable
{
private BeatmapSetOnlineStatusPill beatmapSetOnlineStatusPill { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
public SkinnableBeatmapSetOnlineStatusPill() // WARNING: this is awful
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
beatmapSetOnlineStatusPill = new BeatmapSetOnlineStatusPill()
{
ShowUnknownStatus = true
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindValueChanged(b =>
{
beatmapSetOnlineStatusPill.Status = beatmap.Value.BeatmapInfo.Status;
}, true);
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@@ -0,0 +1,64 @@
// 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.Threading;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public partial class SkinnableStarRatingDisplay : CompositeDrawable, ISerialisableDrawable
{
private StarRatingDisplay starRatingDisplay { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
private CancellationTokenSource? difficultyCancellationSource;
private IBindable<StarDifficulty>? difficultyBindable;
public SkinnableStarRatingDisplay() // WARNING: this is awful
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(new StarDifficulty(0.00, 0), animated: true)
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindValueChanged(b =>
{
difficultyCancellationSource?.Cancel();
difficultyCancellationSource = new CancellationTokenSource();
difficultyBindable?.UnbindAll();
difficultyBindable = difficultyCache.GetBindableDifficulty(b.NewValue.BeatmapInfo, difficultyCancellationSource.Token);
}, true);
starRatingDisplay.Current.BindTo((Bindable<StarDifficulty>)difficultyBindable!);
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@@ -115,14 +115,15 @@ namespace osu.Game.Screens.Play
/// <summary>
/// Skip forward to the next valid skip point.
/// </summary>
public void Skip()
/// <param name="fullLength"><c>true</c> to skip as close to gameplay as possible, or <c>false</c> to skip only to the next valid skip point.</param>
public void Skip(bool fullLength = false)
{
if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME)
return;
double skipTarget = GameplayStartTime - MINIMUM_SKIP_TIME;
if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000)
if (!fullLength && StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000)
// double skip exception for storyboards with very long intros
skipTarget = 0;

View File

@@ -154,7 +154,7 @@ namespace osu.Game.Screens.Play
private BreakTracker breakTracker;
private SkipOverlay skipIntroOverlay;
protected SkipOverlay SkipIntroOverlay { get; private set; }
private SkipOverlay skipOutroOverlay;
protected ScoreProcessor ScoreProcessor { get; private set; }
@@ -200,6 +200,8 @@ namespace osu.Game.Screens.Play
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
private OsuConfigManager config;
protected override void LoadComplete()
{
base.LoadComplete();
@@ -226,12 +228,13 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken)
{
this.config = config;
var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray();
if (gameplayMods.Any(m => m is UnknownMod))
{
Logger.Log("Gameplay was started with an unknown mod applied.", level: LogLevel.Important);
return;
// return;
}
if (Beatmap.Value is DummyWorkingBeatmap)
@@ -500,10 +503,10 @@ namespace osu.Game.Screens.Play
},
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
SkipIntroOverlay = CreateSkipOverlay(DrawableRuleset.GameplayStartTime).With(o =>
{
RequestSkip = performUserRequestedSkip
},
o.RequestSkip = RequestIntroSkip;
}),
skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0)
{
RequestSkip = () => progressToResults(false),
@@ -523,13 +526,15 @@ namespace osu.Game.Screens.Play
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
{
skipIntroOverlay.Expire();
SkipIntroOverlay.Expire();
skipOutroOverlay.Expire();
}
return container;
}
protected virtual SkipOverlay CreateSkipOverlay(double startTime) => new SkipOverlay(startTime);
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
{
updateGameplayState();
@@ -716,13 +721,22 @@ namespace osu.Game.Screens.Play
PerformExit();
}
private void performUserRequestedSkip()
protected virtual void RequestIntroSkip()
{
PerformIntroSkip();
}
/// <summary>
/// Skip forward to the next valid skip point.
/// </summary>
/// <param name="fullLength"><c>true</c> to skip as close to gameplay as possible, or <c>false</c> to skip only to the next valid skip point.</param>
protected void PerformIntroSkip(bool fullLength = false)
{
// user requested skip
// disable sample playback to stop currently playing samples and perform skip
samplePlaybackDisabled.Value = true;
(GameplayClockContainer as MasterGameplayClockContainer)?.Skip();
(GameplayClockContainer as MasterGameplayClockContainer)?.Skip(fullLength);
// return samplePlaybackDisabled.Value to what is defined by the beatmap's current state
updateSampleDisabledState();
@@ -983,6 +997,15 @@ namespace osu.Game.Screens.Play
if (PauseOverlay.State.Value == Visibility.Visible)
PauseOverlay.Hide();
bool exitOnFail = GameplayState.Mods.OfType<IApplicableFailExit>().Any(m => m.ExitOnFail)
&& Score.ScoreInfo.User.Username == config.Get<string>(OsuSetting.Username); // TODO: do more concrete checks
if (exitOnFail)
{
// game.AttemptExit();
game.Exit();
}
bool restartOnFail = GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail);
if (!restartOnFail)
failAnimationContainer.Start();
@@ -1168,7 +1191,7 @@ namespace osu.Game.Screens.Play
GameplayClockContainer.Reset(startClock: true);
if (Configuration.AutomaticallySkipIntro)
skipIntroOverlay.SkipWhenReady();
SkipIntroOverlay.SkipWhenReady();
}
public override void OnSuspending(ScreenTransitionEvent e)

View File

@@ -38,20 +38,21 @@ namespace osu.Game.Screens.Play
private readonly double startTime;
public Action RequestSkip;
protected FadeContainer FadingContent { get; private set; }
private Button button;
private ButtonContainer buttonContainer;
private Circle remainingTimeBox;
private FadeContainer fadeContainer;
private double displayTime;
private bool isClickable;
private bool skipQueued;
[Resolved]
private IGameplayClock gameplayClock { get; set; }
internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible;
internal bool IsButtonVisible => FadingContent.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
/// <summary>
@@ -77,7 +78,7 @@ namespace osu.Game.Screens.Play
InternalChild = buttonContainer = new ButtonContainer
{
RelativeSizeAxes = Axes.Both,
Child = fadeContainer = new FadeContainer
Child = FadingContent = new FadeContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@@ -107,13 +108,13 @@ namespace osu.Game.Screens.Play
public override void Hide()
{
base.Hide();
fadeContainer.Hide();
FadingContent.Hide();
}
public override void Show()
{
base.Show();
fadeContainer.TriggerShow();
FadingContent.TriggerShow();
}
protected override void LoadComplete()
@@ -136,7 +137,7 @@ namespace osu.Game.Screens.Play
RequestSkip?.Invoke();
};
fadeContainer.TriggerShow();
FadingContent.TriggerShow();
}
/// <summary>
@@ -183,7 +184,7 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (isClickable && !e.HasAnyButtonPressed)
fadeContainer.TriggerShow();
FadingContent.TriggerShow();
return base.OnMouseMove(e);
}

View File

@@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -215,6 +216,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
this.ScaleTo(0).Then().ScaleTo(1, APPEAR_DURATION, Easing.OutQuint);
accuracyCircle.Colour = DrawableRank.GetRankLetterColour(ScoreRank.F); // default for failed scores
if (!withFlair && score.Rank != ScoreRank.F)
accuracyCircle.Colour = OsuColour.ForRank(score.Rank).Lighten(0.125f);
if (withFlair)
{
const double swoosh_pre_delay = 443f;
@@ -307,6 +313,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
{
var dink = badgeNum < badges.Count - 1 ? badgeTickSound : badgeMaxSound;
accuracyCircle.FadeColour(OsuColour.ForRank(badge.Rank), 100, Easing.InOutSine); // TODO: nicer animations
dink!.FrequencyTo(1 + badgeNum++ * 0.05);
dink!.Play();
});

View File

@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -43,46 +44,64 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new[]
Children = new Drawable[]
{
new CircularContainer
new Container
{
RelativeSizeAxes = Axes.X,
Height = 12,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 8f,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#222")
Colour = Color4Extensions.FromHex("#2222229f"),
},
HeaderText = new OsuSpriteText
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
Text = header.ToUpper(),
}
}
},
new Container
Anchor = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
HeaderText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Children = new[]
{
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
Text = header.ToUpper(),
},
content = CreateContent().With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
d.Anchor = Anchor.TopCentre;
d.Alpha = 0;
d.AlwaysPresent = true;
}),
}
}
}
},
// new Container
// {
// Anchor = Anchor.TopCentre,
// Origin = Anchor.TopCentre,
// AutoSizeAxes = Axes.Both,
// Children = new[]
// {
// content = CreateContent().With(d =>
// {
// d.Anchor = Anchor.TopCentre;
// d.Origin = Anchor.TopCentre;
// d.Alpha = 0;
// d.AlwaysPresent = true;
// }),
// }
// }
}
};
}

View File

@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ImageExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Colour;
@@ -14,12 +15,15 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
using ZstdSharp.Unsafe;
namespace osu.Game.Screens.Ranking
{
@@ -96,6 +100,7 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private OsuGameBase game { get; set; } = null!;
private OsuColour colour { get; set; } = null!;
private AudioContainer audioContent = null!;
@@ -124,12 +129,14 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, OsuColour colour)
{
// ScorePanel doesn't include the top extruding area in its own size.
// Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale.
const float vertical_fudge = 20;
this.colour = colour;
InternalChild = audioContent = new AudioContainer
{
Anchor = Anchor.Centre,
@@ -239,6 +246,20 @@ namespace osu.Game.Screens.Ranking
private void updateState()
{
ColourInfo getColour(ColourInfo info)
{
var ci = info.AverageColour;
(_, _, float v) = Color4Extensions.ToHSV(ci);
var rank = (ColourInfo)OsuColour.ForRank(Score.Rank);
(float rh, float rs, _) = Color4Extensions.ToHSV(rank);
if (Score.Rank != ScoreRank.F)
return Color4Extensions.FromHSV(rh, rs * 0.3f, v * 1.1f);
else
return Color4Extensions.FromHSV(rh, rs, v * 0.45f);
}
topLayerContent?.FadeOut(content_fade_duration).Expire();
middleLayerContent?.FadeOut(content_fade_duration).Expire();
@@ -247,8 +268,8 @@ namespace osu.Game.Screens.Ranking
case PanelState.Expanded:
Size = new Vector2(EXPANDED_WIDTH, expanded_height);
topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerBackground.FadeColour(getColour(expanded_top_layer_colour), RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(getColour(expanded_middle_layer_colour), RESIZE_DURATION, Easing.OutQuint);
bool firstLoad = topLayerContent == null;
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User, firstLoad) { Alpha = 0 });
@@ -261,8 +282,8 @@ namespace osu.Game.Screens.Ranking
case PanelState.Contracted:
Size = new Vector2(CONTRACTED_WIDTH, contracted_height);
topLayerBackground.FadeColour(contracted_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerBackground.FadeColour(getColour(contracted_top_layer_colour), RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(getColour(contracted_middle_layer_colour), RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent
{

View File

@@ -163,6 +163,7 @@ namespace osu.Game.Screens.Select
private FillFlowContainer infoLabelContainer;
private Container bpmLabelContainer;
private Container lengthLabelContainer;
private Container performanceLabelContainer;
private readonly WorkingBeatmap working;
private readonly RulesetInfo ruleset;
@@ -269,6 +270,7 @@ namespace osu.Game.Screens.Select
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Status = beatmapInfo.Status,
ShowUnknownStatus = true,
Alpha = string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? 0 : 1
}
}
@@ -343,9 +345,11 @@ namespace osu.Game.Screens.Select
settingChangeTracker?.Dispose();
refreshBPMAndLengthLabel();
refreshPerformanceLabel();
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
settingChangeTracker.SettingChanged += _ => refreshBPMAndLengthLabel();
settingChangeTracker.SettingChanged += _ => refreshPerformanceLabel();
}, true);
}
@@ -384,7 +388,11 @@ namespace osu.Game.Screens.Select
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(20, 0),
Children = playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray()
}
},
performanceLabelContainer = new Container
{
AutoSizeAxes = Axes.Both
},
};
}
catch (Exception e)
@@ -428,6 +436,27 @@ namespace osu.Game.Screens.Select
});
}
private void refreshPerformanceLabel()
{
var beatmap = working.Beatmap;
if (beatmap == null || performanceLabelContainer == null)
return;
var diff = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo);
diff.BindValueChanged(d =>
{
float perf = (float?)d.NewValue.PerformanceAttributes?.Total ?? 0.0f;
string disp = $"{Math.Round(perf, 1)} pp";
performanceLabelContainer.Child = new InfoLabel(new BeatmapStatistic
{
Name = "Max PP",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Accuracy),
Content = disp
});
}, true);
}
private Drawable getMapper(BeatmapMetadata metadata)
{
if (string.IsNullOrEmpty(metadata.Author.Username))

View File

@@ -81,6 +81,7 @@ namespace osu.Game.Screens.Select.Carousel
Anchor = Anchor.CentreLeft,
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
ShowUnknownStatus = true,
Status = beatmapSet.Status
},
iconFlow = new FillFlowContainer<DifficultyIcon>

View File

@@ -497,25 +497,35 @@ 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.
// Check whether that is the case.
bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0;
bool groupStillExists = currentGroupedBeatmap?.Group != null && grouping.GroupItems.ContainsKey(currentGroupedBeatmap.Group);
if (groupingRemainsOff || groupStillExists)
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)
{
// Only update the visual state of the selected item.
HandleItemSelected(currentGroupedBeatmap);
}
else if (currentGroupedBeatmap != null)
{
// If the group no longer exists, grab an arbitrary other instance of the beatmap under the first group encountered.
// If the group no longer exists (or the item no longer exists in the previous group), grab an arbitrary other instance of the beatmap under the first group encountered.
var newSelection = GetCarouselItems()?.Select(i => i.Model).OfType<GroupedBeatmap>().FirstOrDefault(gb => gb.Beatmap.Equals(currentGroupedBeatmap.Beatmap));
// Only change the selection if we actually got a positive hit.
// This is necessary so that selection isn't lost if the panel reappears later due to e.g. unapplying some filter criteria that made it disappear in the first place.
if (newSelection != null)
{
CurrentSelection = newSelection;
groupForReselection = newSelection.Group;
}
}
// If a group was selected that is not the one containing the selection, attempt to reselect it.
// If the original group was not found, ExpandedGroup will already have been updated to a valid value in `HandleItemSelected` above.
if (groupForReselection != null && grouping.GroupItems.TryGetValue(groupForReselection, out _))
setExpandedGroup(groupForReselection);
}
@@ -689,7 +699,7 @@ namespace osu.Game.Screens.SelectV2
var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group));
if (groupItem != null)
HandleItemActivated(groupItem);
Activate(groupItem);
}
protected override double? GetScrollTarget()

View File

@@ -12,6 +12,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
@@ -233,6 +234,7 @@ namespace osu.Game.Screens.SelectV2
private void updateDisplay()
{
countStatisticsDisplay.ForceTiny = true;
cancellationSource?.Cancel();
cancellationSource = new CancellationTokenSource();
@@ -293,8 +295,22 @@ namespace osu.Game.Screens.SelectV2
Ruleset rulesetInstance = ruleset.Value.CreateInstance();
var workingBeatmap = beatmap.Value;
var diff = difficultyCache.GetBindableDifficulty(workingBeatmap.BeatmapInfo);
float perf = 0.0f;
var displayAttributes = rulesetInstance.GetBeatmapAttributesForDisplay(beatmap.Value.BeatmapInfo, mods.Value).ToList();
difficultyStatisticsDisplay.Statistics = displayAttributes.Select(a => new StatisticDifficulty.Data(a)).ToList();
difficultyStatisticsDisplay.Statistics = displayAttributes.Select(a => new StatisticDifficulty.Data(a))
.Prepend(new StatisticDifficulty.Data("Max PP", perf, perf, perf));
// at first, performance points won't be available, so we'd have to update them later
diff.BindValueChanged(d =>
{
perf = (float)Math.Round((float?)d.NewValue.PerformanceAttributes?.Total ?? 0f, 1); // yikes
var arr = difficultyStatisticsDisplay.Statistics.ToArray();
arr[0] = new StatisticDifficulty.Data("Max PP", perf, perf, perf);
difficultyStatisticsDisplay.Statistics = arr.AsEnumerable();
});
});
protected override void Update()

View File

@@ -27,9 +27,11 @@ namespace osu.Game.Screens.SelectV2
private readonly FillFlowContainer<StatisticDifficulty> statisticsFlow;
private readonly GridContainer tinyStatisticsGrid;
private IReadOnlyList<StatisticDifficulty.Data> statistics = Array.Empty<StatisticDifficulty.Data>();
public bool ForceTiny { get; set; } = false;
public IReadOnlyList<StatisticDifficulty.Data> Statistics
private IEnumerable<StatisticDifficulty.Data> statistics = Array.Empty<StatisticDifficulty.Data>();
public IEnumerable<StatisticDifficulty.Data> Statistics
{
get => statistics;
set
@@ -137,7 +139,7 @@ namespace osu.Game.Screens.SelectV2
return;
float flowWidth = statisticsFlow[0].Width * statisticsFlow.Count + statisticsFlow.Spacing.X * (statisticsFlow.Count - 1);
bool tiny = !autoSize && DrawWidth < flowWidth - 20;
bool tiny = !autoSize && DrawWidth < flowWidth - 20 || ForceTiny;
if (displayedTinyStatistics != tiny)
{
@@ -180,8 +182,8 @@ namespace osu.Game.Screens.SelectV2
if (statisticsFlow.Select(s => s.Value.Label)
.SequenceEqual(statistics.Select(s => s.Label)))
{
for (int i = 0; i < statistics.Count; i++)
statisticsFlow[i].Value = statistics[i];
for (int i = 0; i < statistics.Count(); i++)
statisticsFlow[i].Value = statistics.ToArray()[i];
}
else
{

View File

@@ -143,6 +143,7 @@ namespace osu.Game.Screens.SelectV2
TextSize = OsuFont.Style.Caption2.Size,
Margin = new MarginPadding { Right = 5f },
Animated = false,
ShowUnknownStatus = true
},
updateButton = new PanelUpdateBeatmapButton
{

View File

@@ -149,6 +149,7 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.BottomLeft,
TextSize = OsuFont.Style.Caption2.Size,
Margin = new MarginPadding { Right = 4f },
ShowUnknownStatus = true
},
updateButton = new PanelUpdateBeatmapButton
{

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