Compare commits
156 Commits
2025.1108.
...
2025.1119.
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bd996ee43 | |||
| 37b9f91d42 | |||
| 1a5a5606dc | |||
| 87ff1051e9 | |||
|
|
80474565fc | ||
| 2e4b0ff197 | |||
| 499f410c94 | |||
| 616c0d8ecd | |||
| ed889138a0 | |||
|
|
89f2c7160d | ||
|
|
f0ca079fe6 | ||
|
|
19b6761697 | ||
|
|
edf08b176a | ||
| 18075bef29 | |||
| bc7780a870 | |||
| 8930b8fadb | |||
| 2d457f4305 | |||
| a784734f94 | |||
|
|
7b952b83bf | ||
|
|
214122f633 | ||
|
|
76c0bd4750 | ||
|
|
4bf3d9397f | ||
|
|
8b778e8106 | ||
|
|
ce5e54c9d2 | ||
| ecfd4764e7 | |||
| f5ca5083d6 | |||
| 1d7c77d8d6 | |||
| 774e52fbd6 | |||
|
|
45e8df7af2 | ||
|
|
1c30cb8371 | ||
| a9d7a9d5d5 | |||
| f7069b1009 | |||
| c3ce5dc787 | |||
| 98076e2092 | |||
| b7d1092f90 | |||
| 08db90c278 | |||
|
|
bd4ed49c06 | ||
| b7e36164c3 | |||
|
|
a593a40429 | ||
|
|
02b88de76e | ||
| 0f5f13858d | |||
|
|
b64abbf1f5 | ||
|
|
4265e72180 | ||
| 89a0c75156 | |||
| ab7e5c94f1 | |||
| 8dc9ea4553 | |||
| dcf553c252 | |||
|
|
cb9d9734d6 | ||
|
|
5763b7dbe9 | ||
|
|
e1baa03622 | ||
|
|
4f783f8c41 | ||
|
|
4c72a60ee2 | ||
| 43f3a506ea | |||
| ab51579c27 | |||
| d8e977c05f | |||
|
|
013de9f85d | ||
|
|
cd6c9405fe | ||
|
|
822cb9e2fb | ||
|
|
680614fbee | ||
|
|
cb8ddc706f | ||
|
|
04d2ce150a | ||
|
|
eaffb89b4c | ||
|
|
650a61539b | ||
|
|
75bc934aa5 | ||
|
|
8d80e2bd2c | ||
|
|
34a3b1ba78 | ||
|
|
b354fa4472 | ||
|
|
1fbe1bd6c9 | ||
|
|
3c215f6574 | ||
|
|
8c28d26130 | ||
|
|
933fbd274d | ||
|
|
55ae7e8bb8 | ||
|
|
4a22ef88ce | ||
|
|
43ca046f9b | ||
|
|
dbefba57ce | ||
|
|
20904de276 | ||
|
|
fb2fe65a77 | ||
|
|
4662c5d678 | ||
|
|
243cd9c073 | ||
|
|
e8db35a5c9 | ||
|
|
d98cb9ca45 | ||
|
|
a7e4aa8b12 | ||
|
|
0f54608cee | ||
|
|
f8331e0b28 | ||
|
|
6ff2a6225d | ||
|
|
a8020dea7c | ||
|
|
88dd458394 | ||
|
|
23cb7f3b23 | ||
|
|
7da051b144 | ||
|
|
78f639d760 | ||
|
|
4ea03d0e07 | ||
|
|
4d706b12ac | ||
|
|
c44f701abe | ||
|
|
4c81d661aa | ||
|
|
f4049c7ec1 | ||
|
|
645d27bb32 | ||
|
|
73f1849365 | ||
|
|
89b443bccc | ||
|
|
be9170832c | ||
|
|
9db200ed41 | ||
|
|
1ab017d4e2 | ||
|
|
2413e98108 | ||
|
|
65fb5311ea | ||
|
|
14cdc40f0f | ||
|
|
9a393f912b | ||
|
|
a9ca4634fc | ||
|
|
bdcc0ee937 | ||
|
|
6f94b1ab6d | ||
|
|
b20a41c1e8 | ||
|
|
d0ce74063d | ||
|
|
373162df02 | ||
|
|
8e0c9281d3 | ||
|
|
25a1a1ba37 | ||
|
|
73e05e3fae | ||
|
|
2a01e3d148 | ||
|
|
cf0e5edf34 | ||
|
|
a825104688 | ||
|
|
ea1798d731 | ||
|
|
a435dfe93e | ||
|
|
5c1171f358 | ||
|
|
3fcc626e29 | ||
|
|
7ff6edeb64 | ||
|
|
657bc31539 | ||
|
|
f9f7740acb | ||
|
|
5e4dd77e64 | ||
|
|
ce96c0b037 | ||
|
|
5af9bb784b | ||
|
|
4c60df21db | ||
|
|
3c6fb14a32 | ||
|
|
3afc7b045c | ||
|
|
2f2847f1dd | ||
|
|
ee7c52465b | ||
|
|
beb977892e | ||
|
|
7203f419a2 | ||
|
|
722cfb72d8 | ||
|
|
5da132cc2f | ||
|
|
9fac96cf07 | ||
|
|
fadcb9882c | ||
|
|
0610781c6c | ||
|
|
e9260de56f | ||
|
|
2d177226fd | ||
|
|
bd912710f1 | ||
|
|
4e76bd0f24 | ||
|
|
9a965a2546 | ||
|
|
7b0121a430 | ||
|
|
627fec2e3a | ||
|
|
c779e142e6 | ||
|
|
89fffa5a1a | ||
|
|
6d597fc815 | ||
|
|
a78b456e20 | ||
|
|
9237c76942 | ||
|
|
378c64b7f8 | ||
|
|
87b66685d6 | ||
|
|
c524bf5432 | ||
|
|
a40230da4b | ||
|
|
0558f9f2d9 |
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||
|
||||
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
||||
- Please pick the following target branch for your pull request:
|
||||
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
|
||||
- `master`, otherwise.
|
||||
- Please avoid pushing untested or incomplete code.
|
||||
- Please do not force-push or rebase unless we ask you to.
|
||||
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1028.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1118.1" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Desktop
|
||||
{
|
||||
internal partial class DiscordRichPresence : Component
|
||||
{
|
||||
private const string client_id = "1216669957799018608";
|
||||
private const string client_id = "1440647613358800918";
|
||||
|
||||
private DiscordRpcClient client = null!;
|
||||
|
||||
|
||||
@@ -115,12 +115,10 @@ namespace osu.Desktop
|
||||
if (IsFirstRun)
|
||||
LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
||||
|
||||
// if (IsPackageManaged)
|
||||
// return new NoActionUpdateManager();
|
||||
if (IsPackageManaged)
|
||||
return new NoActionUpdateManager();
|
||||
|
||||
// return new VelopackUpdateManager();
|
||||
|
||||
return new NoActionUpdateManager(); // for now, APIs are useless for actually downloading the releases. TODO: adapt UpdateManager for gitea
|
||||
return new VelopackUpdateManager(); // yay
|
||||
}
|
||||
|
||||
public override bool RestartAppWhenExited()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Desktop.Updater
|
||||
|
||||
try
|
||||
{
|
||||
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||
IUpdateSource updateSource = new GiteaSource(@"https://gitea.jvnko.boats/jvnkosu/client", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
||||
{
|
||||
AllowVersionDowngrade = true
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 401 KiB |
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[TestCase("mania-samples")]
|
||||
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
||||
[TestCase("slider-convert-samples")]
|
||||
[TestCase("spinner-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 1000.0,
|
||||
"Objects": [{
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 8000.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:5
|
||||
OverallDifficulty:5
|
||||
ApproachRate:5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,2,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,1000,8,4,8000,0:2:0:0:
|
||||
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConvertHitObjectCustomSampleBank()
|
||||
{
|
||||
const string beatmap_sample = "normal-hitwhistle2";
|
||||
const string user_skin_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(beatmap_sample, user_skin_sample);
|
||||
|
||||
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(beatmap_sample);
|
||||
AssertUserLookup(user_skin_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
NodeSamples =
|
||||
[
|
||||
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
|
||||
HitObject.Samples
|
||||
]
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
@@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => Name;
|
||||
public abstract int KeyCount { get; }
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
|
||||
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "1K";
|
||||
public override IconUsage? Icon => OsuIcon.ModOneKey;
|
||||
public override LocalisableString Description => @"Play with one key.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "10K";
|
||||
public override IconUsage? Icon => OsuIcon.ModTenKeys;
|
||||
public override LocalisableString Description => @"Play with ten keys.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "2K";
|
||||
public override IconUsage? Icon => OsuIcon.ModTwoKeys;
|
||||
public override LocalisableString Description => @"Play with two keys.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "3K";
|
||||
public override IconUsage? Icon => OsuIcon.ModThreeKeys;
|
||||
public override LocalisableString Description => @"Play with three keys.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private readonly Lazy<bool> hasKeyTexture;
|
||||
|
||||
private readonly ManiaBeatmap beatmap;
|
||||
private readonly bool isBeatmapConverted;
|
||||
|
||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
@@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
// layered hit sounds never play in mania
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
|
||||
return new SampleVirtual();
|
||||
|
||||
return base.GetSample(sampleInfo);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -10,6 +11,7 @@ using osu.Framework.Input.States;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
foreach (var smokeContainer in smokeContainers)
|
||||
{
|
||||
if (smokeContainer.Children.Count != 0)
|
||||
if (smokeContainer.Children.OfType<SkinnableDrawable>().Any())
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
LastAcceptedAction = null;
|
||||
|
||||
if (LastAcceptedAction != null && gameplayClock.IsRewinding)
|
||||
LastAcceptedAction = null;
|
||||
}
|
||||
|
||||
protected abstract bool CheckValidNewAction(OsuAction action);
|
||||
|
||||
16
osu.Game.Rulesets.Osu/Mods/OsuModRateAdjustConcrete.cs
Normal file
16
osu.Game.Rulesets.Osu/Mods/OsuModRateAdjustConcrete.cs
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -77,9 +77,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
base.LoadComplete();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
LifetimeStart = smokeStartTime = Time.Current;
|
||||
|
||||
public void StartDrawing(double time)
|
||||
{
|
||||
LifetimeStart = smokeStartTime = time;
|
||||
LifetimeEnd = smokeEndTime = double.MaxValue;
|
||||
SmokePoints.Clear();
|
||||
lastPosition = null;
|
||||
totalDistance = pointInterval;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -19,17 +19,24 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
public partial class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
|
||||
{
|
||||
private DrawablePool<SmokeSkinnableDrawable> segmentPool = null!;
|
||||
private SmokeSkinnableDrawable? currentSegmentSkinnable;
|
||||
|
||||
private Vector2 lastMousePosition;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 _) => true;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(segmentPool = new DrawablePool<SmokeSkinnableDrawable>(10));
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
if (e.Action == OsuAction.Smoke)
|
||||
{
|
||||
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
|
||||
AddInternal(currentSegmentSkinnable = segmentPool.Get(segment => segment.Segment?.StartDrawing(Time.Current)));
|
||||
|
||||
// Add initial position immediately.
|
||||
addPosition();
|
||||
@@ -59,17 +66,19 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
|
||||
private void addPosition() => currentSegmentSkinnable?.Segment?.AddPosition(lastMousePosition, Time.Current);
|
||||
|
||||
private partial class SmokeSkinnableDrawable : SkinnableDrawable
|
||||
{
|
||||
public SmokeSegment? Segment => Drawable as SmokeSegment;
|
||||
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
public override double LifetimeStart => Drawable.LifetimeStart;
|
||||
public override double LifetimeEnd => Drawable.LifetimeEnd;
|
||||
|
||||
public SmokeSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(lookup, defaultImplementation, confineMode)
|
||||
public SmokeSkinnableDrawable()
|
||||
: base(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private int rollingHits;
|
||||
|
||||
private readonly Container tickContainer;
|
||||
private SkinnableDrawable headPiece;
|
||||
|
||||
private Color4 colourIdle;
|
||||
private Color4 colourEngaged;
|
||||
@@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Content.Add(tickContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MinValue
|
||||
Depth = -1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +80,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
protected override void RecreatePieces()
|
||||
{
|
||||
if (headPiece != null)
|
||||
Content.Remove(headPiece, true);
|
||||
|
||||
base.RecreatePieces();
|
||||
|
||||
Content.Add(headPiece = createHeadPiece());
|
||||
|
||||
updateColour();
|
||||
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
|
||||
}
|
||||
@@ -122,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollBody),
|
||||
_ => new ElongatedCirclePiece());
|
||||
|
||||
private SkinnableDrawable createHeadPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollHead), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Depth = -2,
|
||||
};
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
|
||||
private void onNewResult(DrawableHitObject obj, JudgementResult result)
|
||||
@@ -174,7 +187,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private void updateColour(double fadeDuration = 0)
|
||||
{
|
||||
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
|
||||
|
||||
if (fadeDuration == 0)
|
||||
{
|
||||
// fade duration is 0 when calling via `RecreatePieces()`.
|
||||
// in this case we want to apply the colour *without* using transforms.
|
||||
// using transforms may result in the application of colour being undone via `DrawableHitObject.UpdateState()` clearing transforms.
|
||||
if (MainPiece.Drawable is IHasAccentColour mainPieceWithAccentColour)
|
||||
mainPieceWithAccentColour.AccentColour = newColour;
|
||||
|
||||
if (headPiece.Drawable is IHasAccentColour headPieceWithAccentColour)
|
||||
headPieceWithAccentColour.AccentColour = newColour;
|
||||
}
|
||||
else
|
||||
{
|
||||
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
|
||||
(headPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class StrongNestedHit : DrawableStrongNestedHit
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -21,14 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
get
|
||||
{
|
||||
// the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii.
|
||||
// therefore naively taking the SSDQs of them and making a quad out of them results in a trapezoid shape and not a box.
|
||||
var headCentre = headCircle.ScreenSpaceDrawQuad.Centre;
|
||||
var headCentre = (body.ScreenSpaceDrawQuad.TopLeft + body.ScreenSpaceDrawQuad.BottomLeft) / 2;
|
||||
var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2;
|
||||
|
||||
float headRadius = headCircle.ScreenSpaceDrawQuad.Height / 2;
|
||||
float tailRadius = tailCircle.ScreenSpaceDrawQuad.Height / 2;
|
||||
float radius = Math.Max(headRadius, tailRadius);
|
||||
float radius = body.ScreenSpaceDrawQuad.Height / 2;
|
||||
|
||||
var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius);
|
||||
return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight);
|
||||
@@ -37,8 +32,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos);
|
||||
|
||||
private LegacyCirclePiece headCircle = null!;
|
||||
|
||||
private Sprite body = null!;
|
||||
|
||||
private Sprite tailCircle = null!;
|
||||
@@ -66,10 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
|
||||
},
|
||||
headCircle = new LegacyCirclePiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
};
|
||||
|
||||
AccentColour = colours.YellowDark;
|
||||
@@ -101,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
|
||||
|
||||
headCircle.AccentColour = colour;
|
||||
body.Colour = colour;
|
||||
tailCircle.Colour = colour;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
switch (taikoComponent.Component)
|
||||
{
|
||||
case TaikoSkinComponents.DrumRollHead:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyCirclePiece();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.DrumRollBody:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyDrumRoll();
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
InputDrum,
|
||||
CentreHit,
|
||||
RimHit,
|
||||
DrumRollHead,
|
||||
DrumRollBody,
|
||||
DrumRollTick,
|
||||
Swell,
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace osu.Game.Tests.Chat
|
||||
sentMessages = new List<Message>();
|
||||
silencedUserIds = new List<int>();
|
||||
|
||||
((DummyAPIAccess)API).LocalUserState.Blocks.Clear();
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
@@ -63,6 +64,10 @@ namespace osu.Game.Tests.Chat
|
||||
silencedUserIds.Clear();
|
||||
return true;
|
||||
|
||||
case GetMessagesRequest getMessages:
|
||||
getMessages.TriggerSuccess(sentMessages);
|
||||
return true;
|
||||
|
||||
case GetUpdatesRequest updatesRequest:
|
||||
updatesRequest.TriggerSuccess(new GetUpdatesResponse
|
||||
{
|
||||
@@ -161,6 +166,60 @@ namespace osu.Game.Tests.Chat
|
||||
AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockedUserMessagesAreDeletedFromInitialMessageBatch()
|
||||
{
|
||||
Channel channel = null;
|
||||
|
||||
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
|
||||
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
Content = "i am blocked",
|
||||
SenderId = 1234
|
||||
}));
|
||||
AddStep("mark user as blocked", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
|
||||
{
|
||||
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
|
||||
TargetID = 1234,
|
||||
}));
|
||||
|
||||
AddStep("join channel and select it", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel);
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
});
|
||||
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockedUserMessagesAreDeletedImmediatelyOnBlock()
|
||||
{
|
||||
Channel channel = null;
|
||||
|
||||
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
|
||||
|
||||
AddStep("join channel and select it", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel);
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
});
|
||||
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
Content = "i am blocked",
|
||||
SenderId = 1234
|
||||
}));
|
||||
AddUntilStep("channel has message", () => channel.Messages, () => Is.Not.Empty);
|
||||
|
||||
AddStep("block user", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
|
||||
{
|
||||
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
|
||||
TargetID = 1234,
|
||||
}));
|
||||
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
|
||||
}
|
||||
|
||||
private void handlePostMessageRequest(PostMessageRequest request)
|
||||
{
|
||||
var message = new Message(++currentMessageId)
|
||||
|
||||
@@ -126,6 +126,22 @@ namespace osu.Game.Tests.Gameplay
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which specifies a specific sample file which doesn't exist (or isn't allowed to be looked up)
|
||||
/// falls back to a normal sample.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestFileSampleFallsBackToNormal()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(null, expected_sample);
|
||||
|
||||
CreateTestWithBeatmap("file-beatmap-sample.osu");
|
||||
|
||||
AssertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,10 +220,13 @@ namespace osu.Game.Tests.Visual.Components
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
if (registerAsOwner)
|
||||
dependencies.CacheAs<IPreviewTrackOwner>(this);
|
||||
return dependencies;
|
||||
{
|
||||
// Automatically handled by interface caching.
|
||||
return base.CreateChildDependencies(parent);
|
||||
}
|
||||
|
||||
return new DependencyContainer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK;
|
||||
@@ -21,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene
|
||||
{
|
||||
private MultiplayerPlaylistItem[] items = null!;
|
||||
private MatchmakingPlaylistItem[] items = null!;
|
||||
|
||||
private BeatmapSelectGrid grid = null!;
|
||||
|
||||
@@ -36,24 +39,44 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
.Take(50)
|
||||
.ToArray();
|
||||
|
||||
IEnumerable<MatchmakingPlaylistItem> playlistItems;
|
||||
|
||||
if (beatmaps.Length > 0)
|
||||
{
|
||||
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
playlistItems = Enumerable.Range(1, 50).Select(i =>
|
||||
{
|
||||
var beatmap = beatmaps[i % beatmaps.Length];
|
||||
|
||||
return new MatchmakingPlaylistItem(
|
||||
new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = beatmaps[i % beatmaps.Length].OnlineID,
|
||||
BeatmapID = beatmap.OnlineID,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
},
|
||||
CreateAPIBeatmap(beatmap),
|
||||
Array.Empty<Mod>()
|
||||
);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
playlistItems = Enumerable.Range(1, 50).Select(i => new MatchmakingPlaylistItem(
|
||||
new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
},
|
||||
CreateAPIBeatmap(),
|
||||
Array.Empty<Mod>()
|
||||
));
|
||||
}
|
||||
|
||||
foreach (var item in playlistItems)
|
||||
item.Beatmap.StarRating = item.PlaylistItem.StarRating;
|
||||
|
||||
items = playlistItems.ToArray();
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
@@ -70,8 +93,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
|
||||
AddStep("add items", () =>
|
||||
{
|
||||
foreach (var item in items)
|
||||
grid.AddItem(item);
|
||||
grid.AddItems(items);
|
||||
});
|
||||
|
||||
AddWaitStep("wait for panels", 3);
|
||||
@@ -85,17 +107,17 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
// test scene is weird.
|
||||
});
|
||||
|
||||
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser
|
||||
AddStep("add selection 1", () => grid.ChildrenOfType<MatchmakingSelectPanel>().First().AddUser(new APIUser
|
||||
{
|
||||
Id = DummyAPIAccess.DUMMY_USER_ID,
|
||||
Username = "Maarvin",
|
||||
}));
|
||||
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
|
||||
AddStep("add selection 2", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(5).First().AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
}));
|
||||
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser
|
||||
AddStep("add selection 3", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(10).First().AddUser(new APIUser
|
||||
{
|
||||
Id = 1040328,
|
||||
Username = "smoogipoo",
|
||||
@@ -180,7 +202,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
|
||||
AddStep("display roll order", () =>
|
||||
{
|
||||
var panels = grid.ChildrenOfType<BeatmapSelectPanel>().ToArray();
|
||||
var panels = grid.ChildrenOfType<MatchmakingSelectPanel>().ToArray();
|
||||
|
||||
for (int i = 0; i < panels.Length; i++)
|
||||
{
|
||||
@@ -197,6 +219,23 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPresentRandomItem()
|
||||
{
|
||||
AddStep("present random item panel", () =>
|
||||
{
|
||||
grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0);
|
||||
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
|
||||
grid.PlayRollAnimation(-1, duration: 0);
|
||||
|
||||
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(-1), 500);
|
||||
});
|
||||
|
||||
AddWaitStep("wait for animation", 5);
|
||||
|
||||
AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem));
|
||||
}
|
||||
|
||||
private (long[] candidateItems, long finalItem) pickRandomItems(int count)
|
||||
{
|
||||
long[] candidateItems = items.Select(it => it.ID).ToArray();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -9,6 +10,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
@@ -19,17 +21,35 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom(MatchType.Matchmaking);
|
||||
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = 0,
|
||||
StarRating = i / 10.0,
|
||||
})).ToArray();
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapPanel()
|
||||
{
|
||||
BeatmapSelectPanel? panel = null;
|
||||
MatchmakingSelectPanel? panel = null;
|
||||
|
||||
AddStep("add panel", () =>
|
||||
{
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
|
||||
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), []))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -56,10 +76,54 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 }));
|
||||
AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 }));
|
||||
|
||||
AddToggleStep("allow selection", value =>
|
||||
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomPanel()
|
||||
{
|
||||
if (panel != null)
|
||||
panel.AllowSelection = value;
|
||||
MatchmakingSelectPanelRandom? panel = null;
|
||||
|
||||
AddStep("add panel", () =>
|
||||
{
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
|
||||
|
||||
AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), []));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapWithMods()
|
||||
{
|
||||
AddStep("add panel", () =>
|
||||
{
|
||||
MatchmakingSelectPanel? panel;
|
||||
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [new OsuModHardRock(), new OsuModDoubleTime()]))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
|
||||
panel.AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
@@ -153,10 +155,69 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InteractionSpam()
|
||||
{
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {i}"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid);
|
||||
AddStep("player jump", () => { MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); });
|
||||
AddStep("local jumping", () => jumpSpam(false));
|
||||
AddWaitStep("wait", 25);
|
||||
AddStep("group jumping spam", () => jumpSpam(true));
|
||||
AddWaitStep("wait", 25);
|
||||
|
||||
AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split);
|
||||
AddStep("local jumping", () => jumpSpam(false));
|
||||
AddWaitStep("wait", 25);
|
||||
AddStep("group jumping spam", () => jumpSpam(true));
|
||||
AddWaitStep("wait", 25);
|
||||
|
||||
AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden);
|
||||
AddStep("local jumping", () => jumpSpam(false));
|
||||
AddWaitStep("wait", 25);
|
||||
AddStep("group jumping spam", () => jumpSpam(true));
|
||||
AddWaitStep("wait", 25);
|
||||
}
|
||||
|
||||
private void jumpSpam(bool everyone)
|
||||
{
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
|
||||
}, i * 150 + RNG.NextDouble(0, 140));
|
||||
|
||||
if (!everyone)
|
||||
continue;
|
||||
|
||||
for (int ii = 0; ii < 7; ii++)
|
||||
{
|
||||
int iii = ii;
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
MultiplayerClient.SendUserMatchRequest(iii, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
|
||||
}, i * 150 + RNG.NextDouble(0, 140));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
29
osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs
Normal file
29
osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
public partial class TestSceneDisclaimer : ScreenTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddStep("load disclaimer", () => LoadScreen(new Disclaimer()));
|
||||
|
||||
AddStep("toggle support", () =>
|
||||
{
|
||||
((DummyAPIAccess)API).LocalUser.Value = new APIUser
|
||||
{
|
||||
Username = API.LocalUser.Value.Username,
|
||||
Id = API.LocalUser.Value.Id + 1,
|
||||
IsSupporter = !API.LocalUser.Value.IsSupporter,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs
Normal file
67
osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneGlobalRankDisplay : ThemeComparisonTestScene
|
||||
{
|
||||
public TestSceneGlobalRankDisplay()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(40),
|
||||
ChildrenEnumerable = new int?[] { 64, 423, 1_453, 3_468, 8_367, 48_342, 78_432, 375_231, 897_783, null }.Select(createDisplay)
|
||||
};
|
||||
|
||||
private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay
|
||||
{
|
||||
UserStatistics =
|
||||
{
|
||||
Value = new UserStatistics
|
||||
{
|
||||
GlobalRank = rank,
|
||||
GlobalRankPercent = rank / 1_000_000f,
|
||||
Variants =
|
||||
[
|
||||
new UserStatistics.Variant
|
||||
{
|
||||
VariantType = UserStatistics.RulesetVariant.FourKey,
|
||||
GlobalRank = rank / 3,
|
||||
},
|
||||
new UserStatistics.Variant
|
||||
{
|
||||
VariantType = UserStatistics.RulesetVariant.SevenKey,
|
||||
GlobalRank = 2 * rank / 3,
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
HighestRank =
|
||||
{
|
||||
Value = rank == null
|
||||
? null
|
||||
: new APIUser.UserRankHighest
|
||||
{
|
||||
Rank = rank.Value / 2,
|
||||
UpdatedAt = DateTimeOffset.Now.AddMonths(-3),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Audio
|
||||
{
|
||||
/// <summary>
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Audio
|
||||
/// <see cref="IPreviewTrackOwner"/>s can cancel the currently playing <see cref="PreviewTrack"/> through the
|
||||
/// global <see cref="PreviewTrackManager"/> if they're the owner of the playing <see cref="PreviewTrack"/>.
|
||||
/// </remarks>
|
||||
[Cached]
|
||||
public interface IPreviewTrackOwner
|
||||
{
|
||||
}
|
||||
|
||||
@@ -157,6 +157,12 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
return ID.GetHashCode();
|
||||
}
|
||||
|
||||
public bool AudioEquals(BeatmapInfo? other) => other != null
|
||||
&& BeatmapSet != null
|
||||
&& other.BeatmapSet != null
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -544,7 +544,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (!banksOnly)
|
||||
{
|
||||
int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
|
||||
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
|
||||
string sampleFilename = samples.FirstOrDefault(s => s is ConvertHitObjectParser.FileHitSampleInfo)?.LookupNames.First() ?? string.Empty;
|
||||
int volume = samples.FirstOrDefault()?.Volume ?? 100;
|
||||
|
||||
// We want to ignore custom sample banks and volume when not encoding to the mania game mode,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -263,11 +264,11 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
Task.Run(() => collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -10,7 +11,7 @@ namespace osu.Game.Collections
|
||||
public class CollectionToggleMenuItem : ToggleMenuItem
|
||||
{
|
||||
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => Task.Run(() =>
|
||||
{
|
||||
collection.PerformWrite(c =>
|
||||
{
|
||||
@@ -19,7 +20,7 @@ namespace osu.Game.Collections
|
||||
else
|
||||
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
|
||||
});
|
||||
})
|
||||
}))
|
||||
{
|
||||
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.Ruleset, string.Empty);
|
||||
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
|
||||
|
||||
SetDefault(OsuSetting.MenuCookieColor, Colour4.FromHex(@"ff66ba"));
|
||||
SetDefault(OsuSetting.MenuCookieColor, Colour4.FromHex(@"8400FF"));
|
||||
|
||||
SetDefault(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Local);
|
||||
SetDefault(OsuSetting.BeatmapLeaderboardSortMode, LeaderboardSortMode.Score);
|
||||
|
||||
@@ -785,32 +785,20 @@ namespace osu.Game.Graphics.Carousel
|
||||
// We are performing two important operations here:
|
||||
// - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions.
|
||||
// - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use.
|
||||
FindCarouselItemsForSelection(ref currentKeyboardSelection, ref currentSelection, carouselItems);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var item = carouselItems[i];
|
||||
|
||||
bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!);
|
||||
bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!);
|
||||
|
||||
// while we don't know the Y position of the item yet, as it's about to be updated,
|
||||
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
|
||||
// at the correct item to avoid redundant local equality checks.
|
||||
// the Y positions will be filled in after they're computed.
|
||||
if (isKeyboardSelection)
|
||||
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i);
|
||||
|
||||
if (isSelection)
|
||||
currentSelection = new Selection(currentSelection.Model, item, null, i);
|
||||
|
||||
updateItemYPosition(item, ref lastVisible, ref yPos);
|
||||
|
||||
if (isKeyboardSelection)
|
||||
currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
|
||||
|
||||
if (isSelection)
|
||||
currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
|
||||
}
|
||||
|
||||
if (currentKeyboardSelection.CarouselItem is CarouselItem currentKeyboardSelectionItem)
|
||||
currentKeyboardSelection = currentKeyboardSelection with { YPosition = currentKeyboardSelectionItem.CarouselYPosition + currentKeyboardSelectionItem.DrawHeight / 2 };
|
||||
|
||||
if (currentSelection.CarouselItem is CarouselItem currentSelectionItem)
|
||||
currentSelection = currentSelection with { YPosition = currentSelectionItem.CarouselYPosition + currentSelectionItem.DrawHeight / 2 };
|
||||
|
||||
// Update the total height of all items (to make the scroll container scrollable through the full height even though
|
||||
// most items are not displayed / loaded).
|
||||
Scroll.SetLayoutHeight(yPos + visibleHalfHeight);
|
||||
@@ -821,6 +809,27 @@ namespace osu.Game.Graphics.Carousel
|
||||
Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
|
||||
}
|
||||
|
||||
protected virtual void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
bool isKeyboardSelection = CheckModelEquality(item.Model, keyboardSelection.Model!);
|
||||
bool isSelection = CheckModelEquality(item.Model, selection.Model!);
|
||||
|
||||
// while we don't know the Y position of the item yet, as it's about to be updated,
|
||||
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
|
||||
// at the correct item to avoid redundant local equality checks.
|
||||
// the Y positions will be filled in after they're computed.
|
||||
if (isKeyboardSelection)
|
||||
keyboardSelection = new Selection(keyboardSelection.Model, item, null, i);
|
||||
|
||||
if (isSelection)
|
||||
selection = new Selection(selection.Model, item, null, i);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Display handling
|
||||
@@ -1081,7 +1090,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="CarouselItem">A related carousel item representation for the model. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
/// <param name="YPosition">The Y position of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
protected record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
|
||||
private record DisplayRange(int First, int Last)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ProgressBar : SliderBar<double>
|
||||
{
|
||||
public bool Seeking { get; private set; }
|
||||
|
||||
public Action<double> OnSeek;
|
||||
|
||||
private readonly Box fill;
|
||||
@@ -75,6 +77,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
fill.Width = value * UsableWidth;
|
||||
}
|
||||
|
||||
protected override void OnUserChange(double value) => OnSeek?.Invoke(value);
|
||||
protected override void OnUserChange(double value)
|
||||
{
|
||||
Seeking = true;
|
||||
}
|
||||
|
||||
protected override bool Commit()
|
||||
{
|
||||
OnSeek?.Invoke(CurrentNumber.Value);
|
||||
Seeking = false;
|
||||
return base.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
case Key.KeypadEnter:
|
||||
case Key.Enter:
|
||||
return false;
|
||||
// even if committing per se is not allowed for this textbox,
|
||||
// the commit flow is also responsible for terminating any active IME.
|
||||
// ensure that the Enter press terminates IME correctly
|
||||
// and is also handled if it needs to be, so that it doesn't leak to some other non-focused drawable and cause breakage.
|
||||
bool wasImeComposing = ImeCompositionActive;
|
||||
FinalizeImeComposition(true);
|
||||
return wasImeComposing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied.");
|
||||
|
||||
/// <summary>
|
||||
/// "Use experimental audio mode"
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode");
|
||||
|
||||
/// <summary>
|
||||
/// "This will attempt to initialise the audio engine in a lower latency mode."
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode.");
|
||||
|
||||
/// <summary>
|
||||
/// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace osu.Game.Online.API.Requests
|
||||
Medal,
|
||||
Rank,
|
||||
RankLost,
|
||||
RankRetracted,
|
||||
UserSupportAgain,
|
||||
UserSupportFirst,
|
||||
UserSupportGift,
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace osu.Game.Online.API.Requests
|
||||
private class VerificationFailureResponse
|
||||
{
|
||||
[JsonProperty("method")]
|
||||
public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; }
|
||||
public SessionVerificationMethod? RequiredSessionVerificationMethod { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -70,6 +71,7 @@ namespace osu.Game.Online.Chat
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
|
||||
private ScheduledDelegate scheduledAck;
|
||||
|
||||
private IChatClient chatClient = null!;
|
||||
@@ -95,6 +97,9 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(_ => SendAck(), true);
|
||||
|
||||
localUserBlocks.BindTo(api.LocalUserState.Blocks);
|
||||
localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -311,8 +316,9 @@ namespace osu.Game.Online.Chat
|
||||
private void addMessages(List<Message> messages)
|
||||
{
|
||||
var channels = JoinedChannels.ToList();
|
||||
var blockedUserIds = localUserBlocks.Select(b => b.TargetID).ToList();
|
||||
|
||||
foreach (var group in messages.GroupBy(m => m.ChannelId))
|
||||
foreach (var group in messages.Where(m => !blockedUserIds.Contains(m.SenderId)).GroupBy(m => m.ChannelId))
|
||||
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
||||
|
||||
lastSilenceMessageId ??= messages.LastOrDefault()?.Id;
|
||||
@@ -641,6 +647,18 @@ namespace osu.Game.Online.Chat
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
private void onBlocksChanged(NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
if (args.Action != NotifyCollectionChangedAction.Add)
|
||||
return;
|
||||
|
||||
foreach (APIRelation newBlock in args.NewItems!)
|
||||
{
|
||||
foreach (var channel in joinedChannels)
|
||||
channel.RemoveMessagesFromUser(newBlock.TargetID);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
Spacing = new Vector2(-3, 0),
|
||||
Padding = new MarginPadding { Top = -5 },
|
||||
Colour = GetRankLetterColour(rank),
|
||||
Font = OsuFont.TorusAlternate.With(size: 42, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.Bold),
|
||||
Text = GetRankLetter(rank),
|
||||
ShadowColour = Color4.Black.Opacity(0.3f),
|
||||
ShadowOffset = new Vector2(0, 0.08f),
|
||||
@@ -72,12 +72,14 @@ namespace osu.Game.Online.Leaderboards
|
||||
switch (rank)
|
||||
{
|
||||
case ScoreRank.SH:
|
||||
return @"S";
|
||||
return @"S+";
|
||||
|
||||
case ScoreRank.X:
|
||||
case ScoreRank.XH:
|
||||
return @"SS";
|
||||
|
||||
case ScoreRank.XH:
|
||||
return @"SS+";
|
||||
|
||||
default:
|
||||
return rank.ToString();
|
||||
}
|
||||
|
||||
@@ -43,11 +43,15 @@ namespace osu.Game.Online.Matchmaking
|
||||
/// <summary>
|
||||
/// The user has raised a candidate playlist item to be played.
|
||||
/// </summary>
|
||||
/// <param name="userId">The notifying user.</param>
|
||||
/// <param name="playlistItemId">The playlist item candidate raised, or -1 as a special value that indicates a random selection.</param>
|
||||
Task MatchmakingItemSelected(int userId, long playlistItemId);
|
||||
|
||||
/// <summary>
|
||||
/// The user has removed a candidate playlist item.
|
||||
/// </summary>
|
||||
/// <param name="userId">The notifying user.</param>
|
||||
/// <param name="playlistItemId">The playlist item candidate removed, or -1 as a special value that indicates a random selection.</param>
|
||||
Task MatchmakingItemDeselected(int userId, long playlistItemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Online.Matchmaking
|
||||
/// <summary>
|
||||
/// Raise a candidate playlist item to be played in the current round.
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The playlist item.</param>
|
||||
/// <param name="playlistItemId">The playlist item, or -1 to indicate a random selection.</param>
|
||||
Task MatchmakingToggleSelection(long playlistItemId);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -932,7 +932,7 @@ namespace osu.Game
|
||||
|
||||
protected virtual Loader CreateLoader() => new Loader();
|
||||
|
||||
protected virtual UpdateManager CreateUpdateManager() => new NoActionUpdateManager();
|
||||
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
|
||||
|
||||
/// <summary>
|
||||
/// Adjust the globally applied <see cref="DrawSizePreservingFillContainer.TargetDrawSize"/> in every <see cref="ScalingContainer"/>.
|
||||
@@ -1729,12 +1729,13 @@ namespace osu.Game
|
||||
{
|
||||
case IntroScreen intro:
|
||||
introScreen = intro;
|
||||
SimpleNotification notification = new SimpleNotification
|
||||
{
|
||||
Text = ButtonSystemStrings.GreetingNotification,
|
||||
Transient = true,
|
||||
};
|
||||
Notifications?.Post(notification);
|
||||
// SimpleNotification notification = new SimpleNotification
|
||||
// {
|
||||
// Text = ButtonSystemStrings.GreetingNotification,
|
||||
// Transient = true,
|
||||
// PopInSampleName = "",
|
||||
// };
|
||||
// Notifications?.Post(notification);
|
||||
devBuildBanner?.Show();
|
||||
break;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -9,6 +10,7 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@@ -25,31 +27,43 @@ namespace osu.Game.Overlays
|
||||
Alpha = 0;
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.Torus.With(size: 12),
|
||||
Colour = colours.GrayF,
|
||||
Text = $@"jvnkosu! " + game.Version,
|
||||
Y = -12,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold),
|
||||
Text = game.Name
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 15),
|
||||
Colour = colours.Yellow,
|
||||
Text = "Experimental version",
|
||||
Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
|
||||
Text = game.Version
|
||||
},
|
||||
}
|
||||
},
|
||||
new Sprite
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
// Anchor = Anchor.BottomCentre,
|
||||
// Origin = Anchor.BottomCentre,
|
||||
Texture = textures.Get(@"Menu/dev-build-footer"),
|
||||
Scale = new Vector2(0.4f, 1),
|
||||
Y = 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
IconContent.Width = IconContent.DrawHeight;
|
||||
IconContent.Width = Math.Min(78, IconContent.DrawHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
var track = musicController.CurrentTrack;
|
||||
|
||||
if (!progressBar.Seeking)
|
||||
{
|
||||
if (!track.IsDummyDevice)
|
||||
{
|
||||
progressBar.EndTime = track.Length;
|
||||
@@ -318,6 +320,7 @@ namespace osu.Game.Overlays
|
||||
playButton.Icon = FontAwesome.Regular.PlayCircle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Action? pendingBeatmapSwitch;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -36,6 +38,7 @@ namespace osu.Game.Overlays
|
||||
public ScrollBackButton Button { get; private set; }
|
||||
|
||||
private readonly Bindable<double?> lastScrollTarget = new Bindable<double?>();
|
||||
private readonly Bindable<double> progress = new Bindable<double>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@@ -46,7 +49,8 @@ namespace osu.Game.Overlays
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding(20),
|
||||
Action = scrollBack,
|
||||
LastScrollTarget = { BindTarget = lastScrollTarget }
|
||||
LastScrollTarget = { BindTarget = lastScrollTarget },
|
||||
Progress = { BindTarget = progress },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +58,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// Map current position to standardized progress
|
||||
float height = AvailableContent - DrawHeight;
|
||||
progress.Value = height == 0 ? 1 : Math.Round(Math.Clamp(Current / height, 0, 1), 3);
|
||||
|
||||
if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
|
||||
{
|
||||
Button.State = Visibility.Hidden;
|
||||
@@ -110,9 +118,11 @@ namespace osu.Game.Overlays
|
||||
|
||||
private readonly Container content;
|
||||
private readonly Box background;
|
||||
private readonly CircularProgress currentCircularProgress;
|
||||
private readonly SpriteIcon spriteIcon;
|
||||
|
||||
public Bindable<double?> LastScrollTarget = new Bindable<double?>();
|
||||
public Bindable<double> Progress = new Bindable<double>();
|
||||
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
||||
|
||||
@@ -145,6 +155,11 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
currentCircularProgress = new CircularProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
InnerRadius = 0.1f,
|
||||
},
|
||||
spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -164,6 +179,7 @@ namespace osu.Game.Overlays
|
||||
IdleColour = colourProvider.Background6;
|
||||
HoverColour = colourProvider.Background5;
|
||||
flashColour = colourProvider.Light1;
|
||||
currentCircularProgress.Colour = colourProvider.Highlight1;
|
||||
|
||||
scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top");
|
||||
scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous");
|
||||
@@ -173,6 +189,8 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Progress.BindValueChanged(p => currentCircularProgress.Progress = p.NewValue, true);
|
||||
|
||||
LastScrollTarget.BindValueChanged(target =>
|
||||
{
|
||||
spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
137
osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs
Normal file
137
osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public partial class GlobalRankDisplay : CompositeDrawable
|
||||
{
|
||||
public Bindable<UserStatistics?> UserStatistics = new Bindable<UserStatistics?>();
|
||||
public Bindable<APIUser.UserRankHighest?> HighestRank = new Bindable<APIUser.UserRankHighest?>();
|
||||
|
||||
private ProfileValueDisplay info = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public GlobalRankDisplay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = info = new ProfileValueDisplay(big: true)
|
||||
{
|
||||
Title = UsersStrings.ShowRankGlobalSimple
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
UserStatistics.BindValueChanged(_ => updateState());
|
||||
HighestRank.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
info.Content.Text = UserStatistics.Value?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
|
||||
info.Content.TooltipText = getGlobalRankTooltipText();
|
||||
|
||||
var tier = getRankingTier();
|
||||
info.Content.Colour = tier == null ? colourProvider.Content2 : OsuColour.ForRankingTier(tier.Value);
|
||||
info.Content.Font = info.Content.Font.With(weight: tier == null || tier == RankingTier.Iron ? FontWeight.Regular : FontWeight.Bold);
|
||||
}
|
||||
|
||||
/// <seealso href="https://github.com/ppy/osu-web/blob/6fcd85eb006ce7699d6f747597435c01344b2d2d/resources/js/profile-page/rank.tsx#L19-L46"/>
|
||||
private RankingTier? getRankingTier()
|
||||
{
|
||||
var stats = UserStatistics.Value;
|
||||
|
||||
int? rank = stats?.GlobalRank;
|
||||
float? percent = stats?.GlobalRankPercent;
|
||||
|
||||
if (rank == null || percent == null)
|
||||
return null;
|
||||
|
||||
if (rank <= 100)
|
||||
return RankingTier.Lustrous;
|
||||
|
||||
if (percent < 0.0005)
|
||||
return RankingTier.Radiant;
|
||||
|
||||
if (percent < 0.0015)
|
||||
return RankingTier.Rhodium;
|
||||
|
||||
if (percent < 0.005)
|
||||
return RankingTier.Platinum;
|
||||
|
||||
if (percent < 0.015)
|
||||
return RankingTier.Gold;
|
||||
|
||||
if (percent < 0.05)
|
||||
return RankingTier.Silver;
|
||||
|
||||
if (percent < 0.15)
|
||||
return RankingTier.Bronze;
|
||||
|
||||
if (percent < 0.5)
|
||||
return RankingTier.Iron;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private LocalisableString getGlobalRankTooltipText()
|
||||
{
|
||||
var rankHighest = HighestRank.Value;
|
||||
var variants = UserStatistics.Value?.Variants;
|
||||
|
||||
LocalisableString? result = null;
|
||||
|
||||
if (variants?.Count > 0)
|
||||
{
|
||||
foreach (var variant in variants)
|
||||
{
|
||||
if (variant.GlobalRank != null)
|
||||
{
|
||||
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
|
||||
|
||||
if (result == null)
|
||||
result = variantText;
|
||||
else
|
||||
result = LocalisableString.Interpolate($"{result}\n{variantText}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rankHighest != null)
|
||||
{
|
||||
var rankHighestText = UsersStrings.ShowRankHighest(
|
||||
rankHighest.Rank.ToLocalisableString("\\##,##0"),
|
||||
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
|
||||
|
||||
if (result == null)
|
||||
result = rankHighestText;
|
||||
else
|
||||
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
|
||||
}
|
||||
|
||||
return result ?? default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user