diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 0b80ce44dd..6050036cbf 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -13,4 +13,6 @@ about: Issues regarding encountered bugs.
*please attach logs here, which are located at:*
- `%AppData%/osu/logs` *(on Windows),*
- `~/.local/share/osu/logs` *(on Linux & macOS).*
+- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
+- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
-->
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
index ada8de73c0..04170312d1 100644
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md
@@ -13,6 +13,8 @@ about: Issues regarding crashes or permanent freezes.
*please attach logs here, which are located at:*
- `%AppData%/osu/logs` *(on Windows),*
- `~/.local/share/osu/logs` *(on Linux & macOS).*
+- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
+- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
-->
**Computer Specifications:**
diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset
index d497365f87..6a99e230d1 100644
--- a/CodeAnalysis/osu.ruleset
+++ b/CodeAnalysis/osu.ruleset
@@ -30,7 +30,7 @@
-
+
diff --git a/osu.Android.props b/osu.Android.props
index 5d83bb9583..5b700224db 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index d087c6218d..cffcea22c2 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -100,15 +100,15 @@ namespace osu.Android
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = ContentResolver.OpenInputStream(uri))
- await stream.CopyToAsync(copy);
+ await stream.CopyToAsync(copy).ConfigureAwait(false);
lock (tasks)
{
tasks.Add(new ImportTask(copy, filename));
}
- }));
+ })).ConfigureAwait(false);
- await game.Import(tasks.ToArray());
+ await game.Import(tasks.ToArray()).ConfigureAwait(false);
}, TaskCreationOptions.LongRunning);
}
}
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 71f9fafe57..47cd39dc5a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -42,7 +42,7 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
- protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
+ protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
@@ -51,9 +51,9 @@ namespace osu.Desktop.Updater
try
{
- updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
+ updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
- var info = await updateManager.CheckForUpdate(!useDeltaPatching);
+ var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
if (info.ReleasesToApply.Count == 0)
{
@@ -79,12 +79,12 @@ namespace osu.Desktop.Updater
try
{
- await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f);
+ await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.Progress = 0;
notification.Text = @"Installing update...";
- await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
+ await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
@@ -97,7 +97,7 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
- await checkForUpdateAsync(false, notification);
+ await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
@@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
+ Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index bf3aba5859..728af5124e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index a317ef252d..10aae70722 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap)
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[]
{
- new Movement(halfCatcherWidth),
+ new Movement(mods, halfCatcherWidth),
};
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index e679231638..9ad719be1a 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -5,6 +5,7 @@ using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
@@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved;
private double lastStrainTime;
- public Movement(float halfCatcherWidth)
+ public Movement(Mod[] mods, float halfCatcherWidth)
+ : base(mods)
{
HalfCatcherWidth = halfCatcherWidth;
}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index fcc0cafefc..af16f39563 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index ade830764d..8c0b9ed8b7 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable SortObjects(IEnumerable input) => input;
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
- new Strain(((ManiaBeatmap)beatmap).TotalColumns)
+ new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
};
protected override Mod[] DifficultyAdjustmentMods
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index 7ebc1ff752..d6ea58ee78 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -6,6 +6,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
@@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
private double individualStrain;
private double overallStrain;
- public Strain(int totalColumns)
+ public Strain(Mod[] mods, int totalColumns)
+ : base(mods)
{
holdEndTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index b4c686ccea..3d2d1f3fec 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 6a7d76151c..75d6786d95 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
- new Aim(),
- new Speed()
+ new Aim(mods),
+ new Speed(mods)
};
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index e74f4933b2..90cba13c7c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -4,6 +4,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -17,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
+ public Aim(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 01f2fb8dc8..200bc7997d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -4,6 +4,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private const double max_speed_bonus = 45; // ~330BPM
private const double speed_balancing_factor = 40;
+ public Speed(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
index e58aacd86e..9f77175398 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
@@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.Judgements
///
public float RateAdjustedRotation;
+ ///
+ /// Time instant at which the spin was started (the first user input which caused an increase in spin).
+ ///
+ public double? TimeStarted;
+
///
/// Time instant at which the spinner has been completed (the user has executed all required spins).
/// Will be null if all required spins haven't been completed.
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 4f5afc85ab..8534cd89d7 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -159,6 +159,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ protected override void UpdateStartTimeStateTransforms()
+ {
+ base.UpdateStartTimeStateTransforms();
+
+ if (Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ fadeInCounter();
+ }
+ }
+
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
@@ -263,13 +274,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.UpdateAfterChildren();
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
- SpmCounter.FadeIn(HitObject.TimeFadeIn);
+ {
+ Result.TimeStarted ??= Time.Current;
+ fadeInCounter();
+ }
- SpmCounter.SetRotation(Result.RateAdjustedRotation);
+ // don't update after end time to avoid the rate display dropping during fade out.
+ // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
+ if (Time.Current <= HitObject.EndTime)
+ SpmCounter.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
}
+ private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
+
private int wholeSpins;
private void updateBonusScore()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index efeca53969..22fb3aab86 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
AddInternal(scaleContainer = new Container
{
Scale = new Vector2(SPRITE_SCALE),
- Anchor = Anchor.Centre,
+ Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
+ Y = SPINNER_Y_CENTRE,
Children = new Drawable[]
{
glow = new Sprite
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index 7e9f73a89b..19cb55c16e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -37,35 +37,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
new Sprite
{
- Anchor = Anchor.Centre,
+ Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-background"),
- Scale = new Vector2(SPRITE_SCALE)
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_Y_CENTRE,
},
disc = new Sprite
{
- Anchor = Anchor.Centre,
+ Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-circle"),
- Scale = new Vector2(SPRITE_SCALE)
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_Y_CENTRE,
},
- new LegacyCoordinatesContainer
+ metre = new Container
{
- Child = metre = new Container
+ AutoSizeAxes = Axes.Both,
+ // this anchor makes no sense, but that's what stable uses.
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET },
+ Masking = true,
+ Child = metreSprite = new Sprite
{
- AutoSizeAxes = Axes.Both,
- // this anchor makes no sense, but that's what stable uses.
+ Texture = source.GetTexture("spinner-metre"),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
- Margin = new MarginPadding { Top = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET },
- Masking = true,
- Child = metreSprite = new Sprite
- {
- Texture = source.GetTexture("spinner-metre"),
- Anchor = Anchor.TopLeft,
- Origin = Anchor.TopLeft,
- Scale = new Vector2(SPRITE_SCALE)
- }
+ Scale = new Vector2(SPRITE_SCALE)
}
}
});
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 06443ca8b8..513888db53 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public abstract class LegacySpinner : CompositeDrawable
{
- public const float SPRITE_SCALE = 0.625f;
+ ///
+ /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
+ /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
+ /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
+ ///
+ protected const float SPINNER_TOP_OFFSET = 45f - 16f;
+
+ protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
+
+ protected const float SPRITE_SCALE = 0.625f;
protected DrawableSpinner DrawableSpinner { get; private set; }
@@ -26,33 +35,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
{
- RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ // osu!stable positions spinner components in window-space (as opposed to gamefield-space). This is a 640x480 area taking up the entire screen.
+ // In lazer, the gamefield-space positional transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to make this area take up the entire window space.
+ Size = new Vector2(640, 480);
+ Position = new Vector2(0, -8f);
DrawableSpinner = (DrawableSpinner)drawableHitObject;
- AddInternal(new LegacyCoordinatesContainer
+ AddRangeInternal(new[]
{
- Depth = float.MinValue,
- Children = new Drawable[]
+ spin = new Sprite
{
- spin = new Sprite
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-spin"),
- Scale = new Vector2(SPRITE_SCALE),
- Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 335,
- },
- clear = new Sprite
- {
- Alpha = 0,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-clear"),
- Scale = new Vector2(SPRITE_SCALE),
- Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 115,
- },
- }
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Depth = float.MinValue,
+ Texture = source.GetTexture("spinner-spin"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_TOP_OFFSET + 335,
+ },
+ clear = new Sprite
+ {
+ Alpha = 0,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Depth = float.MinValue,
+ Texture = source.GetTexture("spinner-clear"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_TOP_OFFSET + 115,
+ },
});
}
@@ -129,33 +142,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (DrawableSpinner != null)
DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms;
}
-
- ///
- /// A simulating osu!stable's absolute screen-space,
- /// for perfect placements of legacy spinner components with legacy coordinates.
- ///
- public class LegacyCoordinatesContainer : Container
- {
- ///
- /// An offset that simulates stable's spinner top offset,
- /// for positioning some legacy spinner components perfectly as in stable.
- /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
- ///
- public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE);
-
- public LegacyCoordinatesContainer()
- {
- // legacy spinners relied heavily on absolute screen-space coordinate values.
- // wrap everything in a container simulating absolute coords to preserve alignment
- // as there are skins that depend on it.
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- Size = new Vector2(640, 480);
-
- // since legacy coordinates were on screen-space, they were accounting for the playfield shift offset.
- // therefore cancel it from here.
- Position = new Vector2(0, -8f);
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 2b084f3bee..fa00922706 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index 32421ee00a..cc0738e252 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -5,6 +5,7 @@ using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
@@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
private int currentMonoLength;
+ public Colour(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
// changing from/to a drum roll or a swell does not constitute a colour change.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index 5569b27ad5..f2b8309ac5 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -5,6 +5,7 @@ using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
@@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
private int notesSinceRhythmChange;
+ public Rhythm(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
// drum rolls and swells are exempt.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 0b61eb9930..c34cce0cd6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -5,6 +5,7 @@ using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
@@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Creates a skill.
///
+ /// Mods for use in skill calculations.
/// Whether this instance is performing calculations for the right hand.
- public Stamina(bool rightHand)
+ public Stamina(Mod[] mods, bool rightHand)
+ : base(mods)
{
hand = rightHand ? 1 : 0;
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index e5485db4df..fc198d2493 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
- new Colour(),
- new Rhythm(),
- new Stamina(true),
- new Stamina(false),
+ new Colour(mods),
+ new Rhythm(mods),
+ new Stamina(mods, true),
+ new Stamina(mods, false),
};
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 9f29675230..d97da40ef2 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
// if a taiko skin is providing explosion sprites, hide the judgements completely
if (hasExplosion.Value)
- return Drawable.Empty();
+ return Drawable.Empty().With(d => d.Expire());
}
if (!(component is TaikoSkinComponent taikoComponent))
@@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
// suppress the default kiai explosion if the skin brings its own sprites.
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
if (hasExplosion.Value)
- return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue);
+ return Drawable.Empty().With(d => d.Expire());
return null;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index b5e35f88b5..1ad1e4495c 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -3,7 +3,6 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -12,16 +11,6 @@ namespace osu.Game.Rulesets.Taiko.UI
///
public class DrawableTaikoJudgement : DrawableJudgement
{
- ///
- /// Creates a new judgement text.
- ///
- /// The object which is being judged.
- /// The judgement to visualise.
- public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject)
- : base(result, judgedObject)
- {
- }
-
protected override void ApplyHitAnimations()
{
this.MoveToY(-100, 500);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 148ec7755e..d2e7b604bb 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@@ -17,6 +19,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Skinning;
using osuTK;
@@ -38,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.UI
internal Drawable HitTarget;
private SkinnableDrawable mascot;
+ private readonly IDictionary> judgementPools = new Dictionary>();
+
private ProxyContainer topLevelHitContainer;
private Container rightArea;
private Container leftArea;
@@ -159,6 +164,12 @@ namespace osu.Game.Rulesets.Taiko.UI
RegisterPool(5);
RegisterPool(100);
+
+ var hitWindows = new TaikoHitWindows();
+ foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => hitWindows.IsHitResultAllowed(r)))
+ judgementPools.Add(result, new DrawablePool(15));
+
+ AddRangeInternal(judgementPools.Values);
}
protected override void LoadComplete()
@@ -283,13 +294,15 @@ namespace osu.Game.Rulesets.Taiko.UI
break;
default:
- judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject)
+ judgementContainer.Add(judgementPools[result.Type].Get(j =>
{
- Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft,
- Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre,
- RelativePositionAxes = Axes.X,
- X = result.IsHit ? judgedObject.Position.X : 0,
- });
+ j.Apply(result, judgedObject);
+
+ j.Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft;
+ j.Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre;
+ j.RelativePositionAxes = Axes.X;
+ j.X = result.IsHit ? judgedObject.Position.X : 0;
+ }));
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
addExplosion(judgedObject, result.Type, type);
diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index 19e36a63f1..c3d9cb5875 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -20,6 +20,9 @@
+
+ $(NoWarn);CA2007
+
%(RecursiveDir)%(Filename)%(Extension)
@@ -71,7 +74,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index 67b2298f4c..97df9b2cd5 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -21,6 +21,9 @@
%(RecursiveDir)%(Filename)%(Extension)
+
+ $(NoWarn);CA2007
+
{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
@@ -45,7 +48,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 5c7adb3f49..1c0bfd56dd 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -212,7 +212,7 @@ namespace osu.Game.Tests.NonVisual
throw new NotImplementedException();
}
- protected override Skill[] CreateSkills(IBeatmap beatmap)
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
throw new NotImplementedException();
}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
index 24a0a662ba..8ff2743b6a 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -4,8 +4,10 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
+using osu.Game.Screens.Select.Filter;
namespace osu.Game.Tests.NonVisual.Filtering
{
@@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
+
+ [Test]
+ public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria)
+ {
+ var beatmap = getExampleBeatmap();
+
+ var customCriteria = matchCustomCriteria is bool match ? new CustomCriteria(match) : null;
+ var criteria = new FilterCriteria { RulesetCriteria = customCriteria };
+ var carouselItem = new CarouselBeatmap(beatmap);
+ carouselItem.Filter(criteria);
+
+ Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value);
+ }
+
+ private class CustomCriteria : IRulesetFilterCriteria
+ {
+ private readonly bool match;
+
+ public CustomCriteria(bool shouldMatch)
+ {
+ match = shouldMatch;
+ }
+
+ public bool Matches(BeatmapInfo beatmap) => match;
+ public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
+ }
}
}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index d15682b1eb..49389e67aa 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -4,7 +4,9 @@
using System;
using NUnit.Framework;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Filter;
namespace osu.Game.Tests.NonVisual.Filtering
{
@@ -194,5 +196,63 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
}
+
+ [Test]
+ public void TestOperatorParsing()
+ {
+ const string query = "artist=>=bad")]
+ [TestCase("divisor true;
+
+ public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
+ {
+ if (key == "custom" && op == Operator.Equal)
+ {
+ CustomValue = value;
+ return true;
+ }
+
+ return false;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
new file mode 100644
index 0000000000..82ce588c6f
--- /dev/null
+++ b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Tests.Visual.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.OnlinePlay
+{
+ [HeadlessTest]
+ public class StatefulMultiplayerClientTest : MultiplayerTestScene
+ {
+ [Test]
+ public void TestUserAddedOnJoin()
+ {
+ var user = new User { Id = 33 };
+
+ AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
+ AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+ }
+
+ [Test]
+ public void TestUserRemovedOnLeave()
+ {
+ var user = new User { Id = 44 };
+
+ AddStep("add user", () => Client.AddUser(user));
+ AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+
+ AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
+ AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index 563d6be0da..dccde366c2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
@@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(0, 0)]
[TestCase(-1000, -1000)]
[TestCase(-10000, -10000)]
- public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime)
+ public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
+
sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
storyboard.GetLayer("Background").Add(sprite);
@@ -64,6 +65,43 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
+ [TestCase(1000, 0, false)]
+ [TestCase(0, 0, false)]
+ [TestCase(-1000, -1000, false)]
+ [TestCase(-10000, -10000, false)]
+ [TestCase(1000, 0, true)]
+ [TestCase(0, 0, true)]
+ [TestCase(-1000, -1000, true)]
+ [TestCase(-10000, -10000, true)]
+ public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop)
+ {
+ var storyboard = new Storyboard();
+
+ var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
+
+ // these should be ignored as we have an alpha visibility blocker proceeding this command.
+ sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
+ var loopGroup = sprite.AddLoop(-20000, 50);
+ loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
+
+ var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
+ target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
+
+ // these should be ignored due to being in the future.
+ sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
+ loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
+
+ storyboard.GetLayer("Background").Add(sprite);
+
+ loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
+
+ AddAssert($"first frame is {expectedStartTime}", () =>
+ {
+ Debug.Assert(player.FirstFrameClockTime != null);
+ return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
+ });
+ }
+
private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
AddStep("create player", () =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 2e39471dc0..78bc51e47b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -1,46 +1,41 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Components;
-using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMultiplayer : MultiplayerTestScene
+ public class TestSceneMultiplayer : ScreenTestScene
{
+ private TestMultiplayer multiplayerScreen;
+
public TestSceneMultiplayer()
{
- var multi = new TestMultiplayer();
+ AddStep("show", () =>
+ {
+ multiplayerScreen = new TestMultiplayer();
- AddStep("show", () => LoadScreen(multi));
- AddUntilStep("wait for loaded", () => multi.IsLoaded);
- }
+ // Needs to be added at a higher level since the multiplayer screen becomes non-current.
+ Child = multiplayerScreen.Client;
- [Test]
- public void TestOneUserJoinedMultipleTimes()
- {
- var user = new User { Id = 33 };
+ LoadScreen(multiplayerScreen);
+ });
- AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
-
- AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
- }
-
- [Test]
- public void TestOneUserLeftMultipleTimes()
- {
- var user = new User { Id = 44 };
-
- AddStep("add user", () => Client.AddUser(user));
- AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
-
- AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
- AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
+ AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded);
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
+ [Cached(typeof(StatefulMultiplayerClient))]
+ public readonly TestMultiplayerClient Client;
+
+ public TestMultiplayer()
+ {
+ Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
+ }
+
protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 2344ebea0e..8869718fd1 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -3,12 +3,10 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Beatmaps;
@@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerMatchSubScreen screen;
- [Cached]
- private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
-
public TestSceneMultiplayerMatchSubScreen()
: base(false)
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
index 6de5704410..91c15de69f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
@@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
+using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -21,15 +24,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- roomManager.CreateRoom(new Room { Name = { Value = "1" } });
+ roomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
roomManager.PartRoom();
- roomManager.CreateRoom(new Room { Name = { Value = "2" } });
+ roomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
roomManager.PartRoom();
roomManager.ClearRooms();
});
});
- AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
+ AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
}
@@ -40,16 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
});
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
- AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0);
+ AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
}
@@ -60,9 +63,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
});
});
@@ -70,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddStep("connect", () => roomContainer.Client.Connect());
- AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
+ AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
}
@@ -81,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
roomManager.ClearRooms();
});
});
- AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0);
+ AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
}
@@ -97,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
});
});
@@ -111,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- roomManager.CreateRoom(new Room());
+ roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
});
});
@@ -126,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
- var r = new Room();
+ var r = createRoom();
roomManager.CreateRoom(r);
roomManager.PartRoom();
roomManager.JoinRoom(r);
@@ -136,6 +139,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
}
+ private Room createRoom(Action initFunc = null)
+ {
+ var room = new Room();
+
+ room.Name.Value = "test room";
+ room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
+ Ruleset = { Value = Ruleset.Value }
+ });
+
+ initFunc?.Invoke(room);
+ return room;
+ }
+
private TestMultiplayerRoomManager createRoomManager()
{
Child = roomContainer = new TestMultiplayerRoomContainer
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
index 9bece39ca0..e8d9ff72af 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Online
Username = "flyte",
Id = 3103765,
IsOnline = true,
- CurrentModeRank = 1111,
+ Statistics = new UserStatistics { GlobalRank = 1111 },
Country = new Country { FlagName = "JP" },
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
},
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online
Username = "peppy",
Id = 2,
IsOnline = false,
- CurrentModeRank = 2222,
+ Statistics = new UserStatistics { GlobalRank = 2222 },
Country = new Country { FlagName = "AU" },
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
IsSupporter = true,
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
index 8dd81e02e2..255f147ec9 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Playlists
Room.RecentParticipants.Add(new User
{
Username = "peppy",
- CurrentModeRank = 1234,
+ Statistics = new UserStatistics { GlobalRank = 1234 },
Id = 2
});
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
new file mode 100644
index 0000000000..8f1c17ed29
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+ [TestFixture]
+ public class TestSceneSettingsItem : OsuTestScene
+ {
+ [Test]
+ public void TestRestoreDefaultValueButtonVisibility()
+ {
+ TestSettingsTextBox textBox = null;
+
+ AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox
+ {
+ Current = new Bindable
+ {
+ Default = "test",
+ Value = "test"
+ }
+ });
+ AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
+
+ AddStep("change value from default", () => textBox.Current.Value = "non-default");
+ AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0);
+
+ AddStep("restore default", () => textBox.Current.SetDefault());
+ AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
+ }
+
+ private class TestSettingsTextBox : SettingsTextBox
+ {
+ public new Drawable RestoreDefaultValueButton => this.ChildrenOfType().Single();
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 7e3868bd3b..e36b3cdc74 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,16 +3,19 @@
-
+
-
+
WinExe
net5.0
+
+ tests.ruleset
+
diff --git a/osu.Game.Tests/tests.ruleset b/osu.Game.Tests/tests.ruleset
new file mode 100644
index 0000000000..a0abb781d3
--- /dev/null
+++ b/osu.Game.Tests/tests.ruleset
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 77ae06d89c..b20583dd7e 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index d506724017..2ee52c35aa 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -150,7 +150,9 @@ namespace osu.Game.Tournament
{
foreach (var p in t.Players)
{
- if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null)
+ if (string.IsNullOrEmpty(p.Username)
+ || p.Statistics?.GlobalRank == null
+ || p.Statistics?.CountryRank == null)
{
PopulateUser(p, immediate: true);
addedInfo = true;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 3c6a6ba302..29b3f5d3a3 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -20,6 +20,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
+using osu.Framework.Statistics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
@@ -155,7 +156,7 @@ namespace osu.Game.Beatmaps
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
if (onlineLookupQueue != null)
- await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
+ await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
@@ -311,6 +312,9 @@ namespace osu.Game.Beatmaps
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
+ // best effort; may be higher than expected.
+ GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
+
return working;
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index aab8ff6bd6..f7f276230f 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -12,7 +12,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
-using osu.Framework.Statistics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -34,8 +33,6 @@ namespace osu.Game.Beatmaps
protected AudioManager AudioManager { get; }
- private static readonly GlobalStatistic total_count = GlobalStatistics.Get(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s");
-
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
{
AudioManager = audioManager;
@@ -47,8 +44,6 @@ namespace osu.Game.Beatmaps
waveform = new RecyclableLazy(GetWaveform);
storyboard = new RecyclableLazy(GetStoryboard);
skin = new RecyclableLazy(GetSkin);
-
- total_count.Value++;
}
protected virtual Track GetVirtualTrack(double emptyLength = 0)
@@ -331,11 +326,6 @@ namespace osu.Game.Beatmaps
protected virtual ISkin GetSkin() => new DefaultSkin();
private readonly RecyclableLazy skin;
- ~WorkingBeatmap()
- {
- total_count.Value--;
- }
-
public class RecyclableLazy
{
private Lazy lazy;
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index fb9c230c7a..9723409c79 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -124,7 +124,7 @@ namespace osu.Game.Collections
return Task.Run(async () =>
{
using (var stream = stable.GetStream(database_name))
- await Import(stream);
+ await Import(stream).ConfigureAwait(false);
});
}
@@ -139,7 +139,7 @@ namespace osu.Game.Collections
PostNotification?.Invoke(notification);
var collections = readCollections(stream, notification);
- await importCollections(collections);
+ await importCollections(collections).ConfigureAwait(false);
notification.CompletionText = $"Imported {collections.Count} collections";
notification.State = ProgressNotificationState.Completed;
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index daaba9098e..d809dbcb01 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -22,7 +22,6 @@ using osu.Game.IO.Archives;
using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using SharpCompress.Archives.Zip;
-using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Database
{
@@ -163,7 +162,7 @@ namespace osu.Game.Database
try
{
- var model = await Import(task, isLowPriorityImport, notification.CancellationToken);
+ var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false);
lock (imported)
{
@@ -183,7 +182,7 @@ namespace osu.Game.Database
{
Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database);
}
- }));
+ })).ConfigureAwait(false);
if (imported.Count == 0)
{
@@ -226,7 +225,7 @@ namespace osu.Game.Database
TModel import;
using (ArchiveReader reader = task.GetReader())
- import = await Import(reader, lowPriority, cancellationToken);
+ import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false);
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage.
@@ -358,7 +357,7 @@ namespace osu.Game.Database
item.Files = archive != null ? createFileInfos(archive, Files) : new List();
item.Hash = ComputeHash(item, archive);
- await Populate(item, archive, cancellationToken);
+ await Populate(item, archive, cancellationToken).ConfigureAwait(false);
using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{
@@ -410,7 +409,7 @@ namespace osu.Game.Database
flushEvents(true);
return item;
- }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap();
+ }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false);
///
/// Exports an item to a legacy (.zip based) package.
@@ -621,7 +620,7 @@ namespace osu.Game.Database
}
///
- /// Create all required s for the provided archive, adding them to the global file store.
+ /// Create all required s for the provided archive, adding them to the global file store.
///
private List createFileInfos(ArchiveReader reader, FileStore files)
{
@@ -699,7 +698,7 @@ namespace osu.Game.Database
return Task.CompletedTask;
}
- return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()));
+ return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false));
}
///
diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs
index ddafd77066..84c39e3532 100644
--- a/osu.Game/Database/DatabaseWriteUsage.cs
+++ b/osu.Game/Database/DatabaseWriteUsage.cs
@@ -54,10 +54,5 @@ namespace osu.Game.Database
Dispose(true);
GC.SuppressFinalize(this);
}
-
- ~DatabaseWriteUsage()
- {
- Dispose(false);
- }
}
}
diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs
index 50b022f9ff..da3144e8d0 100644
--- a/osu.Game/Database/DownloadableArchiveModelManager.cs
+++ b/osu.Game/Database/DownloadableArchiveModelManager.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Database
Task.Factory.StartNew(async () =>
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
- var imported = await Import(notification, new ImportTask(filename));
+ var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false);
// for now a failed import will be marked as a failed download for simplicity.
if (!imported.Any())
diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs
index d913e66428..a1a1279d71 100644
--- a/osu.Game/Database/MemoryCachingComponent.cs
+++ b/osu.Game/Database/MemoryCachingComponent.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Database
if (CheckExists(lookup, out TValue performance))
return performance;
- var computed = await ComputeValueAsync(lookup, token);
+ var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
if (computed != null || CacheNullValues)
cache[lookup] = computed;
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index 568726199c..19cc211709 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Database
public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
- => await queryUser(lookup);
+ => await queryUser(lookup).ConfigureAwait(false);
private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>();
private Task pendingRequestTask;
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index f7914cbbca..fb7fe4947b 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Graphics
}
}
- using (var image = await host.TakeScreenshotAsync())
+ using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
{
if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
cursorVisibility.Value = true;
@@ -116,13 +116,13 @@ namespace osu.Game.Graphics
switch (screenshotFormat.Value)
{
case ScreenshotFormat.Png:
- await image.SaveAsPngAsync(stream);
+ await image.SaveAsPngAsync(stream).ConfigureAwait(false);
break;
case ScreenshotFormat.Jpg:
const int jpeg_quality = 92;
- await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality });
+ await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
break;
default:
diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs
index f74574e60c..679ab40402 100644
--- a/osu.Game/IO/Archives/ArchiveReader.cs
+++ b/osu.Game/IO/Archives/ArchiveReader.cs
@@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives
return null;
byte[] buffer = new byte[input.Length];
- await input.ReadAsync(buffer);
+ await input.ReadAsync(buffer).ConfigureAwait(false);
return buffer;
}
}
diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs
index 029908ec9d..d9d0e4c0ea 100644
--- a/osu.Game/IPC/ArchiveImportIPCChannel.cs
+++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs
@@ -33,12 +33,12 @@ namespace osu.Game.IPC
if (importer == null)
{
// we want to contact a remote osu! to handle the import.
- await SendMessageAsync(new ArchiveImportMessage { Path = path });
+ await SendMessageAsync(new ArchiveImportMessage { Path = path }).ConfigureAwait(false);
return;
}
if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant()))
- await importer.Import(path);
+ await importer.Import(path).ConfigureAwait(false);
}
}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 569481d491..ede64c0340 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -10,7 +10,6 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
-using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Extensions.ObjectExtensions;
@@ -247,14 +246,8 @@ namespace osu.Game.Online.API
this.password = password;
}
- public IHubClientConnector GetHubConnector(string clientName, string endpoint)
- {
- // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
- if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
- return null;
-
- return new HubClientConnector(clientName, endpoint, this, versionHash);
- }
+ public IHubClientConnector GetHubConnector(string clientName, string endpoint) =>
+ new HubClientConnector(clientName, endpoint, this, versionHash);
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index fdb21c5000..3839762e46 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Online
{
cancelExistingConnect();
- if (!await connectionLock.WaitAsync(10000))
+ if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
try
@@ -88,7 +88,7 @@ namespace osu.Game.Online
{
// ensure any previous connection was disposed.
// this will also create a new cancellation token source.
- await disconnect(false);
+ await disconnect(false).ConfigureAwait(false);
// this token will be valid for the scope of this connection.
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
@@ -103,7 +103,7 @@ namespace osu.Game.Online
// importantly, rebuild the connection each attempt to get an updated access token.
CurrentConnection = buildConnection(cancellationToken);
- await CurrentConnection.StartAsync(cancellationToken);
+ await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false);
Logger.Log($"{clientName} connected!", LoggingTarget.Network);
isConnected.Value = true;
@@ -119,7 +119,7 @@ namespace osu.Game.Online
Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network);
// retry on any failure.
- await Task.Delay(5000, cancellationToken);
+ await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -174,14 +174,14 @@ namespace osu.Game.Online
if (takeLock)
{
- if (!await connectionLock.WaitAsync(10000))
+ if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (CurrentConnection != null)
- await CurrentConnection.DisposeAsync();
+ await CurrentConnection.DisposeAsync().ConfigureAwait(false);
}
finally
{
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 95d76f384f..4529dfd0a7 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -9,7 +9,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
namespace osu.Game.Online.Multiplayer
@@ -121,6 +123,29 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
+ protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
+ {
+ var tcs = new TaskCompletionSource();
+ var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
+
+ req.Success += res =>
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ tcs.SetCanceled();
+ return;
+ }
+
+ tcs.SetResult(res.ToBeatmapSet(Rulesets));
+ };
+
+ req.Failure += e => tcs.SetException(e);
+
+ API.Queue(req);
+
+ return tcs.Task;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
index bfd505fb19..0f7050596f 100644
--- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
@@ -17,8 +17,6 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
@@ -71,7 +69,7 @@ namespace osu.Game.Online.Multiplayer
///
/// The corresponding to the local player, if available.
///
- public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id);
+ public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
///
/// Whether the is the host in .
@@ -85,15 +83,15 @@ namespace osu.Game.Online.Multiplayer
}
}
+ [Resolved]
+ protected IAPIProvider API { get; private set; } = null!;
+
+ [Resolved]
+ protected RulesetStore Rulesets { get; private set; } = null!;
+
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
- [Resolved]
- private RulesetStore rulesets { get; set; } = null!;
-
// Only exists for compatibility with old osu-server-spectator build.
// Todo: Can be removed on 2021/02/26.
private long defaultPlaylistItemId;
@@ -133,12 +131,12 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(room.RoomID.Value != null);
// Join the server-side room.
- var joinedRoom = await JoinRoom(room.RoomID.Value.Value);
+ var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
Debug.Assert(joinedRoom != null);
// Populate users.
Debug.Assert(joinedRoom.Users != null);
- await Task.WhenAll(joinedRoom.Users.Select(PopulateUser));
+ await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
// Update the stored room (must be done on update thread for thread-safety).
await scheduleAsync(() =>
@@ -146,11 +144,11 @@ namespace osu.Game.Online.Multiplayer
Room = joinedRoom;
apiRoom = room;
defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
- }, cancellationSource.Token);
+ }, cancellationSource.Token).ConfigureAwait(false);
// Update room settings.
- await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token);
- }, cancellationSource.Token);
+ await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false);
+ }, cancellationSource.Token).ConfigureAwait(false);
}
///
@@ -180,8 +178,8 @@ namespace osu.Game.Online.Multiplayer
return joinOrLeaveTaskChain.Add(async () =>
{
- await scheduledReset;
- await LeaveRoomInternal();
+ await scheduledReset.ConfigureAwait(false);
+ await LeaveRoomInternal().ConfigureAwait(false);
});
}
@@ -239,11 +237,11 @@ namespace osu.Game.Online.Multiplayer
switch (localUser.State)
{
case MultiplayerUserState.Idle:
- await ChangeState(MultiplayerUserState.Ready);
+ await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false);
return;
case MultiplayerUserState.Ready:
- await ChangeState(MultiplayerUserState.Idle);
+ await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
return;
default:
@@ -309,7 +307,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
- await PopulateUser(user);
+ await PopulateUser(user).ConfigureAwait(false);
Scheduler.Add(() =>
{
@@ -488,7 +486,7 @@ namespace osu.Game.Online.Multiplayer
/// Populates the for a given .
///
/// The to populate.
- protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
+ protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false);
///
/// Updates the local room settings with the given .
@@ -515,30 +513,26 @@ namespace osu.Game.Online.Multiplayer
RoomUpdated?.Invoke();
- var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
- req.Success += res =>
+ GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() =>
{
if (cancellationToken.IsCancellationRequested)
return;
- updatePlaylist(settings, res);
- };
-
- api.Queue(req);
+ updatePlaylist(settings, set.Result);
+ }), TaskContinuationOptions.OnlyOnRanToCompletion);
}, cancellationToken);
- private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
+ private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet)
{
if (Room == null || !Room.Settings.Equals(settings))
return;
Debug.Assert(apiRoom != null);
- var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
beatmap.MD5Hash = settings.BeatmapChecksum;
- var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
+ var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance();
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
@@ -568,6 +562,14 @@ namespace osu.Game.Online.Multiplayer
}
}
+ ///
+ /// Retrieves a from an online source.
+ ///
+ /// The beatmap set ID.
+ /// A token to cancel the request.
+ /// The retrieval task.
+ protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default);
+
///
/// For the provided user ID, update whether the user is included in .
///
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 771bcd2310..b7398efdc2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -361,14 +361,6 @@ namespace osu.Game
PerformFromScreen(screen =>
{
- // we might already be at song select, so a check is required before performing the load to solo.
- if (screen is MainMenu)
- menuScreen.LoadToSolo();
-
- // we might even already be at the song
- if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true))
- return;
-
// Find beatmaps that match our predicate.
var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList();
@@ -381,9 +373,16 @@ namespace osu.Game
?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value))
?? beatmaps.First();
- Ruleset.Value = selection.Ruleset;
- Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
- }, validScreens: new[] { typeof(SongSelect) });
+ if (screen is IHandlePresentBeatmap presentableScreen)
+ {
+ presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset);
+ }
+ else
+ {
+ Ruleset.Value = selection.Ruleset;
+ Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
+ }
+ }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) });
}
///
@@ -441,7 +440,7 @@ namespace osu.Game
public override Task Import(params ImportTask[] imports)
{
// encapsulate task as we don't want to begin the import process until in a ready state.
- var importTask = new Task(async () => await base.Import(imports));
+ var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
waitForReady(() => this, _ => importTask.Start());
@@ -832,7 +831,7 @@ namespace osu.Game
asyncLoadStream = Task.Run(async () =>
{
if (previousLoadStream != null)
- await previousLoadStream;
+ await previousLoadStream.ConfigureAwait(false);
try
{
@@ -846,7 +845,7 @@ namespace osu.Game
// The delegate won't complete if OsuGame has been disposed in the meantime
while (!IsDisposed && !del.Completed)
- await Task.Delay(10);
+ await Task.Delay(10).ConfigureAwait(false);
// Either we're disposed or the load process has started successfully
if (IsDisposed)
@@ -854,7 +853,7 @@ namespace osu.Game
Debug.Assert(task != null);
- await task;
+ await task.ConfigureAwait(false);
Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
}
@@ -881,13 +880,8 @@ namespace osu.Game
switch (action)
{
case GlobalAction.ResetInputSettings:
- var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity);
-
- sensitivity.Disabled = false;
- sensitivity.Value = 1;
- sensitivity.Disabled = true;
-
- frameworkConfig.Set(FrameworkSetting.IgnoredInputHandlers, string.Empty);
+ frameworkConfig.GetBindable(FrameworkSetting.IgnoredInputHandlers).SetDefault();
+ frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity).SetDefault();
frameworkConfig.GetBindable(FrameworkSetting.ConfineMouseMode).SetDefault();
return true;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 3d24f245f9..e1c7b67a8c 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -434,7 +434,7 @@ namespace osu.Game
foreach (var importer in fileImporters)
{
if (importer.HandledExtensions.Contains(extension))
- await importer.Import(paths);
+ await importer.Import(paths).ConfigureAwait(false);
}
}
@@ -445,7 +445,7 @@ namespace osu.Game
{
var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
- }));
+ })).ConfigureAwait(false);
}
public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 537dd00727..2da5be5e6c 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -160,9 +160,9 @@ namespace osu.Game.Overlays
tcs.SetException(e);
};
- await API.PerformAsync(req);
+ await API.PerformAsync(req).ConfigureAwait(false);
- await tcs.Task;
+ return tcs.Task;
});
}
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index e6fe6ac749..0922ce5ecc 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -244,7 +244,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
return unsorted.OrderByDescending(u => u.LastVisit).ToList();
case UserSortCriteria.Rank:
- return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList();
+ return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList();
case UserSortCriteria.Username:
return unsorted.OrderBy(u => u.Username).ToList();
diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
new file mode 100644
index 0000000000..a87c06ffdf
--- /dev/null
+++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics.Sprites;
+
+namespace osu.Game.Overlays.Dialog
+{
+ ///
+ /// A dialog which confirms a user action.
+ ///
+ public class ConfirmDialog : PopupDialog
+ {
+ ///
+ /// Construct a new confirmation dialog.
+ ///
+ /// The description of the action to be displayed to the user.
+ /// An action to perform on confirmation.
+ /// An optional action to perform on cancel.
+ public ConfirmDialog(string message, Action onConfirm, Action onCancel = null)
+ {
+ HeaderText = message;
+ BodyText = "Last chance to turn back";
+
+ Icon = FontAwesome.Solid.ExclamationTriangle;
+
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogOkButton
+ {
+ Text = @"Yes",
+ Action = onConfirm
+ },
+ new PopupDialogCancelButton
+ {
+ Text = @"Cancel",
+ Action = onCancel
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 455e13711d..3a78cff890 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -17,7 +17,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
protected override string Header => "Mouse";
private readonly BindableBool rawInputToggle = new BindableBool();
- private Bindable sensitivityBindable = new BindableDouble();
+
+ private Bindable configSensitivity;
+
+ private Bindable localSensitivity;
+
private Bindable ignoredInputHandlers;
private Bindable windowMode;
@@ -26,12 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig, FrameworkConfigManager config)
{
- var configSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity);
-
// use local bindable to avoid changing enabled state of game host's bindable.
- sensitivityBindable = configSensitivity.GetUnboundCopy();
- configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue);
- sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue);
+ configSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity);
+ localSensitivity = configSensitivity.GetUnboundCopy();
+
+ windowMode = config.GetBindable(FrameworkSetting.WindowMode);
+ ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers);
Children = new Drawable[]
{
@@ -43,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new SensitivitySetting
{
LabelText = "Cursor sensitivity",
- Current = sensitivityBindable
+ Current = localSensitivity
},
new SettingsCheckbox
{
@@ -66,14 +70,43 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons)
},
};
+ }
- windowMode = config.GetBindable(FrameworkSetting.WindowMode);
- windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1, true);
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ configSensitivity.BindValueChanged(val =>
+ {
+ var disabled = localSensitivity.Disabled;
+
+ localSensitivity.Disabled = false;
+ localSensitivity.Value = val.NewValue;
+ localSensitivity.Disabled = disabled;
+ }, true);
+
+ localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue);
+
+ windowMode.BindValueChanged(mode =>
+ {
+ var isFullscreen = mode.NewValue == WindowMode.Fullscreen;
+
+ if (isFullscreen)
+ {
+ confineMouseModeSetting.Current.Disabled = true;
+ confineMouseModeSetting.TooltipText = "Not applicable in full screen mode";
+ }
+ else
+ {
+ confineMouseModeSetting.Current.Disabled = false;
+ confineMouseModeSetting.TooltipText = string.Empty;
+ }
+ }, true);
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
{
rawInputToggle.Disabled = true;
- sensitivityBindable.Disabled = true;
+ localSensitivity.Disabled = true;
}
else
{
@@ -86,12 +119,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler;
};
- ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers);
ignoredInputHandlers.ValueChanged += handler =>
{
bool raw = !handler.NewValue.Contains("Raw");
rawInputToggle.Value = raw;
- sensitivityBindable.Disabled = !raw;
+ localSensitivity.Disabled = !raw;
};
ignoredInputHandlers.TriggerChange();
diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index 4cb8d7f83c..85765bf991 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -121,8 +121,10 @@ namespace osu.Game.Overlays.Settings
labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
}
- private class RestoreDefaultValueButton : Container, IHasTooltip
+ protected internal class RestoreDefaultValueButton : Container, IHasTooltip
{
+ public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
+
private Bindable bindable;
public Bindable Bindable
diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index fe75a3a607..6f979b8dc8 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -5,11 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Threading;
-using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications;
@@ -30,9 +28,6 @@ namespace osu.Game
[Resolved]
private DialogOverlay dialogOverlay { get; set; }
- [Resolved]
- private IBindable beatmap { get; set; }
-
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
@@ -90,7 +85,7 @@ namespace osu.Game
var type = current.GetType();
// check if we are already at a valid target screen.
- if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled)
+ if (validScreens.Any(t => t.IsAssignableFrom(type)))
{
finalAction(current);
Cancel();
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index f15e5e1df0..a25dc3e6db 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Difficulty
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{
- var skills = CreateSkills(beatmap);
+ var skills = CreateSkills(beatmap, mods);
if (!beatmap.HitObjects.Any())
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
@@ -202,7 +202,8 @@ namespace osu.Game.Rulesets.Difficulty
/// Creates the s to calculate the difficulty of an .
///
/// The whose difficulty will be calculated.
+ /// Mods to calculate difficulty with.
/// The s.
- protected abstract Skill[] CreateSkills(IBeatmap beatmap);
+ protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods);
}
}
diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
index 1063a24b27..95117be073 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty.Skills
{
@@ -46,10 +47,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
///
protected double CurrentStrain { get; private set; } = 1;
+ ///
+ /// Mods for use in skill calculations.
+ ///
+ protected IReadOnlyList Mods => mods;
+
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List strainPeaks = new List();
+ private readonly Mod[] mods;
+
+ protected Skill(Mod[] mods)
+ {
+ this.mods = mods;
+ }
+
///
/// Process a and update current strain values accordingly.
///
diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
new file mode 100644
index 0000000000..13cc41f8e0
--- /dev/null
+++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Filter;
+
+namespace osu.Game.Rulesets.Filter
+{
+ ///
+ /// Allows for extending the beatmap filtering capabilities of song select (as implemented in )
+ /// with ruleset-specific criteria.
+ ///
+ public interface IRulesetFilterCriteria
+ {
+ ///
+ /// Checks whether the supplied satisfies ruleset-specific custom criteria,
+ /// in addition to the ones mandated by song select.
+ ///
+ /// The beatmap to test the criteria against.
+ ///
+ /// true if the beatmap matches the ruleset-specific custom filtering criteria,
+ /// false otherwise.
+ ///
+ bool Matches(BeatmapInfo beatmap);
+
+ ///
+ /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box.
+ /// The format of the criterion is:
+ ///
+ /// {key}{op}{value}
+ ///
+ ///
+ ///
+ ///
+ /// For adding optional string criteria, can be used for matching,
+ /// along with for parsing.
+ ///
+ ///
+ /// For adding numerical-type range criteria, can be used for matching,
+ /// along with
+ /// and - and -typed overloads for parsing.
+ ///
+ ///
+ /// The key (name) of the criterion.
+ /// The operator in the criterion.
+ /// The value of the criterion.
+ ///
+ /// true if the keyword criterion is valid, false if it has been ignored.
+ /// Valid criteria are stripped from ,
+ /// while ignored criteria are included in .
+ ///
+ bool TryParseCustomKeywordCriteria(string key, Operator op, string value);
+ }
+}
diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index da9bb8a09d..feeafb7151 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -150,17 +150,13 @@ namespace osu.Game.Rulesets.Judgements
}
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
- {
- var drawableAnimation = (Drawable)animatable;
-
animatable.PlayAnimation();
- // a derived version of DrawableJudgement may be proposing a lifetime.
- // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
- double lastTransformTime = drawableAnimation.LatestTransformEndTime;
- if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
- LifetimeEnd = lastTransformTime;
- }
+ // a derived version of DrawableJudgement may be proposing a lifetime.
+ // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
+ double lastTransformTime = JudgementBody.Drawable.LatestTransformEndTime;
+ if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
+ LifetimeEnd = lastTransformTime;
}
}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index dbc2bd4d01..38d30a2e31 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -26,6 +26,7 @@ using JetBrains.Annotations;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Testing;
+using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets
@@ -306,5 +307,11 @@ namespace osu.Game.Rulesets
/// The result type to get the name for.
/// The display name.
public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
+
+ ///
+ /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
+ ///
+ [CanBeNull]
+ public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null;
}
}
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index b31884d246..14aa3fe99a 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.UI
~DrawableRulesetDependencies()
{
+ // required to potentially clean up sample store from audio hierarchy.
Dispose(false);
}
diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs
index 5f66c13d2f..bb15983de3 100644
--- a/osu.Game/Scoring/ScorePerformanceCache.cs
+++ b/osu.Game/Scoring/ScorePerformanceCache.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Scoring
{
var score = lookup.ScoreInfo;
- var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token);
+ var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false);
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
if (attributes.Attributes == null)
diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs
index 48c5523883..a6fb94b151 100644
--- a/osu.Game/Screens/BackgroundScreen.cs
+++ b/osu.Game/Screens/BackgroundScreen.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Screens
{
private readonly bool animateOnEnter;
+ public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
+
protected BackgroundScreen(bool animateOnEnter = true)
{
this.animateOnEnter = animateOnEnter;
diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs
new file mode 100644
index 0000000000..60801fb3eb
--- /dev/null
+++ b/osu.Game/Screens/IHandlePresentBeatmap.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+
+namespace osu.Game.Screens
+{
+ ///
+ /// Denotes a screen which can handle beatmap / ruleset selection via local logic.
+ /// This is used in the flow to handle cases which require custom logic,
+ /// for instance, if a lease is held on the Beatmap.
+ ///
+ public interface IHandlePresentBeatmap
+ {
+ ///
+ /// Invoked with a requested beatmap / ruleset for selection.
+ ///
+ /// The beatmap to be selected.
+ /// The ruleset to be selected.
+ void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset);
+ }
+}
diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs
index 329623e03a..ee8ef6926d 100644
--- a/osu.Game/Screens/Import/FileImportScreen.cs
+++ b/osu.Game/Screens/Import/FileImportScreen.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Screens.Import
Task.Factory.StartNew(async () =>
{
- await game.Import(path);
+ await game.Import(path).ConfigureAwait(false);
// some files will be deleted after successful import, so we want to refresh the view.
Schedule(() =>
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index f93bfd7705..81b1cb0bf1 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -172,18 +172,6 @@ namespace osu.Game.Screens.Menu
return;
}
- // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
- if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
- {
- notifications?.Post(new SimpleNotification
- {
- Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.",
- Icon = FontAwesome.Solid.AppleAlt,
- });
-
- return;
- }
-
OnMultiplayer?.Invoke();
}
diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs
index d120eb21a8..6488a2fd63 100644
--- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs
+++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs
@@ -9,10 +9,15 @@ namespace osu.Game.Screens.Menu
{
public class ConfirmExitDialog : PopupDialog
{
- public ConfirmExitDialog(Action confirm, Action cancel)
+ ///
+ /// Construct a new exit confirmation dialog.
+ ///
+ /// An action to perform on confirmation.
+ /// An optional action to perform on cancel.
+ public ConfirmExitDialog(Action onConfirm, Action onCancel = null)
{
- HeaderText = "Are you sure you want to exit?";
- BodyText = "Last chance to back out.";
+ HeaderText = "Are you sure you want to exit osu!?";
+ BodyText = "Last chance to turn back";
Icon = FontAwesome.Solid.ExclamationTriangle;
@@ -20,13 +25,13 @@ namespace osu.Game.Screens.Menu
{
new PopupDialogOkButton
{
- Text = @"Goodbye",
- Action = confirm
+ Text = @"Let me out!",
+ Action = onConfirm
},
new PopupDialogCancelButton
{
- Text = @"Just a little more",
- Action = cancel
+ Text = @"Just a little more...",
+ Action = onCancel
},
};
}
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index ffe6882a72..abe6c62461 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Screens.Menu
rulesets.Hide();
lazerLogo.Hide();
- background.Hide();
+ background.ApplyToBackground(b => b.Hide());
using (BeginAbsoluteSequence(0, true))
{
@@ -231,7 +231,8 @@ namespace osu.Game.Screens.Menu
lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run.
logo.FadeIn();
- background.FadeIn();
+
+ background.ApplyToBackground(b => b.Show());
game.Add(new GameWideFlash());
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 424e6d2cd5..baeb86c976 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -9,12 +9,14 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Screens;
+using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.IO;
using osu.Game.Online.API;
using osu.Game.Overlays;
+using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.OnlinePlay.Multiplayer;
@@ -23,7 +25,7 @@ using osu.Game.Screens.Select;
namespace osu.Game.Screens.Menu
{
- public class MainMenu : OsuScreen
+ public class MainMenu : OsuScreen, IHandlePresentBeatmap
{
public const float FADE_IN_DURATION = 300;
@@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu
Beatmap.SetDefault();
this.Push(new Editor());
},
- OnSolo = onSolo,
+ OnSolo = loadSoloSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()),
OnPlaylists = () => this.Push(new Playlists()),
OnExit = confirmAndExit,
@@ -160,9 +162,7 @@ namespace osu.Game.Screens.Menu
LoadComponentAsync(songSelect = new PlaySongSelect());
}
- public void LoadToSolo() => Schedule(onSolo);
-
- private void onSolo() => this.Push(consumeSongSelect());
+ private void loadSoloSongSelect() => this.Push(consumeSongSelect());
private Screen consumeSongSelect()
{
@@ -289,5 +289,13 @@ namespace osu.Game.Screens.Menu
this.FadeOut(3000);
return base.OnExiting(next);
}
+
+ public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
+ {
+ Beatmap.Value = beatmap;
+ Ruleset.Value = ruleset;
+
+ Schedule(loadSoloSongSelect);
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
index f17d97c3fd..c9f0f6de90 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
@@ -4,9 +4,11 @@
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
@@ -19,6 +21,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private LoadingLayer loadingLayer;
+ ///
+ /// Construct a new instance of multiplayer song select.
+ ///
+ /// An optional initial beatmap selection to perform.
+ /// An optional initial ruleset selection to perform.
+ public MultiplayerMatchSongSelect(WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
+ {
+ if (beatmap != null || ruleset != null)
+ {
+ Schedule(() =>
+ {
+ if (beatmap != null) Beatmap.Value = beatmap;
+ if (ruleset != null) Ruleset.Value = ruleset;
+ });
+ }
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 4fbea4e3be..ceeee67806 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -12,9 +12,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Threading;
+using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Dialog;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
@@ -29,7 +33,7 @@ using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.Pa
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
[Cached]
- public class MultiplayerMatchSubScreen : RoomSubScreen
+ public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap
{
public override string Title { get; }
@@ -279,14 +283,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList();
}
+ [Resolved(canBeNull: true)]
+ private DialogOverlay dialogOverlay { get; set; }
+
+ private bool exitConfirmed;
+
public override bool OnBackButton()
{
- if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible)
+ if (client.Room == null)
+ {
+ // room has not been created yet; exit immediately.
+ return base.OnBackButton();
+ }
+
+ if (settingsOverlay.State.Value == Visibility.Visible)
{
settingsOverlay.Hide();
return true;
}
+ if (!exitConfirmed && dialogOverlay != null)
+ {
+ dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
+ {
+ exitConfirmed = true;
+ this.Exit();
+ }));
+
+ return true;
+ }
+
return base.OnBackButton();
}
@@ -394,5 +420,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
modSettingChangeTracker?.Dispose();
}
+
+ public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
+ {
+ if (!this.IsCurrentScreen())
+ return;
+
+ if (!client.IsHost)
+ {
+ // todo: should handle this when the request queue is implemented.
+ // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap
+ // flow may need to change to support an "unable to present" return value.
+ return;
+ }
+
+ this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset));
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index ffcf248575..b3cd44d55a 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -137,13 +137,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override async Task SubmitScore(Score score)
{
- await base.SubmitScore(score);
+ await base.SubmitScore(score).ConfigureAwait(false);
- await client.ChangeState(MultiplayerUserState.FinishedPlay);
+ await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false);
// Await up to 60 seconds for results to become available (6 api request timeouts).
// This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur.
- await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60)));
+ await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false);
}
protected override ResultsScreen CreateResults(ScoreInfo score)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index ddc88261f7..a75e4bdc07 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override async Task SubmitScore(Score score)
{
- await base.SubmitScore(score);
+ await base.SubmitScore(score).ConfigureAwait(false);
Debug.Assert(Token != null);
@@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
};
api.Queue(request);
- await tcs.Task;
+ await tcs.Task.ConfigureAwait(false);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
index 4784bca7dd..81183a425a 100644
--- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
@@ -84,14 +84,15 @@ namespace osu.Game.Screens.Play.HUD
{
InternalChildren = new[]
{
- displayedCountSpriteText = createSpriteText().With(s =>
- {
- s.Alpha = 0;
- }),
popOutCount = createSpriteText().With(s =>
{
s.Alpha = 0;
s.Margin = new MarginPadding(0.05f);
+ s.Blending = BlendingParameters.Additive;
+ }),
+ displayedCountSpriteText = createSpriteText().With(s =>
+ {
+ s.Alpha = 0;
})
};
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index e81efdac78..0e221351aa 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -592,7 +592,7 @@ namespace osu.Game.Screens.Play
try
{
- await SubmitScore(score);
+ await SubmitScore(score).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -601,7 +601,7 @@ namespace osu.Game.Screens.Play
try
{
- await ImportScore(score);
+ await ImportScore(score).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 1aab50037a..521b90202d 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel
if (match)
match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
+ if (match && criteria.RulesetCriteria != null)
+ match &= criteria.RulesetCriteria.Matches(Beatmap);
+
Filtered.Value = !match;
}
diff --git a/osu.Game/Screens/Select/Filter/Operator.cs b/osu.Game/Screens/Select/Filter/Operator.cs
new file mode 100644
index 0000000000..706daf631f
--- /dev/null
+++ b/osu.Game/Screens/Select/Filter/Operator.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Screens.Select.Filter
+{
+ ///
+ /// Defines logical operators that can be used in the song select search box keyword filters.
+ ///
+ public enum Operator
+ {
+ Less,
+ LessOrEqual,
+ Equal,
+ GreaterOrEqual,
+ Greater
+ }
+}
diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index eafd8a87d1..298b6e49bd 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -36,6 +37,8 @@ namespace osu.Game.Screens.Select
public FilterCriteria CreateCriteria()
{
+ Debug.Assert(ruleset.Value.ID != null);
+
var query = searchTextBox.Text;
var criteria = new FilterCriteria
@@ -53,6 +56,8 @@ namespace osu.Game.Screens.Select
if (!maximumStars.IsDefault)
criteria.UserStarDifficulty.Max = maximumStars.Value;
+ criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
+
FilterQueryParser.ApplyQueries(criteria, query);
return criteria;
}
diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index 7bddb3e51b..208048380a 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -8,6 +8,7 @@ using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select
@@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select
[CanBeNull]
public BeatmapCollection Collection;
+ [CanBeNull]
+ public IRulesetFilterCriteria RulesetCriteria { get; set; }
+
public struct OptionalRange : IEquatable>
where T : struct
{
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index 4b6b3be45c..ea7f233bea 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -5,13 +5,17 @@ using System;
using System.Globalization;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
+using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select
{
- internal static class FilterQueryParser
+ ///
+ /// Utility class used for parsing song select filter queries entered via the search box.
+ ///
+ public static class FilterQueryParser
{
private static readonly Regex query_syntax_regex = new Regex(
- @"\b(?stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))",
+ @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*"")|(\S*))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static void ApplyQueries(FilterCriteria criteria, string query)
@@ -19,62 +23,81 @@ namespace osu.Game.Screens.Select
foreach (Match match in query_syntax_regex.Matches(query))
{
var key = match.Groups["key"].Value.ToLower();
- var op = match.Groups["op"].Value;
+ var op = parseOperator(match.Groups["op"].Value);
var value = match.Groups["value"].Value;
- parseKeywordCriteria(criteria, key, value, op);
-
- query = query.Replace(match.ToString(), "");
+ if (tryParseKeywordCriteria(criteria, key, value, op))
+ query = query.Replace(match.ToString(), "");
}
criteria.SearchText = query;
}
- private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
+ private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op)
{
switch (key)
{
- case "stars" when parseFloatWithPoint(value, out var stars):
- updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
- break;
+ case "stars":
+ return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
- case "ar" when parseFloatWithPoint(value, out var ar):
- updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
- break;
+ case "ar":
+ return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
- case "dr" when parseFloatWithPoint(value, out var dr):
- case "hp" when parseFloatWithPoint(value, out dr):
- updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
- break;
+ case "dr":
+ case "hp":
+ return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
- case "cs" when parseFloatWithPoint(value, out var cs):
- updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
- break;
+ case "cs":
+ return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
- case "bpm" when parseDoubleWithPoint(value, out var bpm):
- updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
- break;
+ case "bpm":
+ return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
- case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
- var scale = getLengthScale(value);
- updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
- break;
+ case "length":
+ return tryUpdateLengthRange(criteria, op, value);
- case "divisor" when parseInt(value, out var divisor):
- updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
- break;
+ case "divisor":
+ return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
- case "status" when Enum.TryParse(value, true, out var statusValue):
- updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
- break;
+ case "status":
+ return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
+ (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
case "creator":
- updateCriteriaText(ref criteria.Creator, op, value);
- break;
+ return TryUpdateCriteriaText(ref criteria.Creator, op, value);
case "artist":
- updateCriteriaText(ref criteria.Artist, op, value);
- break;
+ return TryUpdateCriteriaText(ref criteria.Artist, op, value);
+
+ default:
+ return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
+ }
+ }
+
+ private static Operator parseOperator(string value)
+ {
+ switch (value)
+ {
+ case "=":
+ case ":":
+ return Operator.Equal;
+
+ case "<":
+ return Operator.Less;
+
+ case "<=":
+ case "<:":
+ return Operator.LessOrEqual;
+
+ case ">":
+ return Operator.Greater;
+
+ case ">=":
+ case ">:":
+ return Operator.GreaterOrEqual;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}");
}
}
@@ -84,129 +107,203 @@ namespace osu.Game.Screens.Select
value.EndsWith('m') ? 60000 :
value.EndsWith('h') ? 3600000 : 1000;
- private static bool parseFloatWithPoint(string value, out float result) =>
+ private static bool tryParseFloatWithPoint(string value, out float result) =>
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
- private static bool parseDoubleWithPoint(string value, out double result) =>
+ private static bool tryParseDoubleWithPoint(string value, out double result) =>
double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
- private static bool parseInt(string value, out int result) =>
+ private static bool tryParseInt(string value, out int result) =>
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
- private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
+ ///
+ /// Attempts to parse a keyword filter with the specified and textual .
+ /// If the value indicates a valid textual filter, the function returns true and the resulting data is stored into
+ /// .
+ ///
+ /// The to store the parsed data into, if successful.
+ ///
+ /// The operator for the keyword filter.
+ /// Only is valid for textual filters.
+ ///
+ /// The value of the keyword filter.
+ public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
{
switch (op)
{
- case "=":
- case ":":
+ case Operator.Equal:
textFilter.SearchTerm = value.Trim('"');
- break;
+ return true;
+
+ default:
+ return false;
}
}
- private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, float value, float tolerance = 0.05f)
+ ///
+ /// Attempts to parse a keyword filter of type
+ /// from the specified and .
+ /// If can be parsed as a , the function returns true
+ /// and the resulting range constraint is stored into .
+ ///
+ ///
+ /// The -typed
+ /// to store the parsed data into, if successful.
+ ///
+ /// The operator for the keyword filter.
+ /// The value of the keyword filter.
+ /// Allowed tolerance of the parsed range boundary value.
+ public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, float tolerance = 0.05f)
+ => tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
+
+ private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, float value, float tolerance = 0.05f)
{
switch (op)
{
default:
- return;
+ return false;
- case "=":
- case ":":
+ case Operator.Equal:
range.Min = value - tolerance;
range.Max = value + tolerance;
break;
- case ">":
+ case Operator.Greater:
range.Min = value + tolerance;
break;
- case ">=":
- case ">:":
+ case Operator.GreaterOrEqual:
range.Min = value - tolerance;
break;
- case "<":
+ case Operator.Less:
range.Max = value - tolerance;
break;
- case "<=":
- case "<:":
+ case Operator.LessOrEqual:
range.Max = value + tolerance;
break;
}
+
+ return true;
}
- private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, double value, double tolerance = 0.05)
+ ///
+ /// Attempts to parse a keyword filter of type
+ /// from the specified and .
+ /// If can be parsed as a , the function returns true
+ /// and the resulting range constraint is stored into .
+ ///
+ ///
+ /// The -typed
+ /// to store the parsed data into, if successful.
+ ///
+ /// The operator for the keyword filter.
+ /// The value of the keyword filter.
+ /// Allowed tolerance of the parsed range boundary value.
+ public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, double tolerance = 0.05)
+ => tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
+
+ private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, double value, double tolerance = 0.05)
{
switch (op)
{
default:
- return;
+ return false;
- case "=":
- case ":":
+ case Operator.Equal:
range.Min = value - tolerance;
range.Max = value + tolerance;
break;
- case ">":
+ case Operator.Greater:
range.Min = value + tolerance;
break;
- case ">=":
- case ">:":
+ case Operator.GreaterOrEqual:
range.Min = value - tolerance;
break;
- case "<":
+ case Operator.Less:
range.Max = value - tolerance;
break;
- case "<=":
- case "<:":
+ case Operator.LessOrEqual:
range.Max = value + tolerance;
break;
}
+
+ return true;
}
- private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, T value)
+ ///
+ /// Used to determine whether the string value can be converted to type .
+ /// If conversion can be performed, the delegate returns true
+ /// and the conversion result is returned in the out parameter .
+ ///
+ /// The string value to attempt parsing for.
+ /// The parsed value, if conversion is possible.
+ public delegate bool TryParseFunction(string val, out T parsed);
+
+ ///
+ /// Attempts to parse a keyword filter of type ,
+ /// from the specified and .
+ /// If can be parsed into using , the function returns true
+ /// and the resulting range constraint is stored into .
+ ///
+ /// The to store the parsed data into, if successful.
+ /// The operator for the keyword filter.
+ /// The value of the keyword filter.
+ /// Function used to determine if can be converted to type .
+ public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, TryParseFunction parseFunction)
+ where T : struct
+ => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
+
+ private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value)
where T : struct
{
switch (op)
{
default:
- return;
+ return false;
- case "=":
- case ":":
+ case Operator.Equal:
range.IsLowerInclusive = range.IsUpperInclusive = true;
range.Min = value;
range.Max = value;
break;
- case ">":
+ case Operator.Greater:
range.IsLowerInclusive = false;
range.Min = value;
break;
- case ">=":
- case ">:":
+ case Operator.GreaterOrEqual:
range.IsLowerInclusive = true;
range.Min = value;
break;
- case "<":
+ case Operator.Less:
range.IsUpperInclusive = false;
range.Max = value;
break;
- case "<=":
- case "<:":
+ case Operator.LessOrEqual:
range.IsUpperInclusive = true;
range.Max = value;
break;
}
+
+ return true;
+ }
+
+ private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val)
+ {
+ if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length))
+ return false;
+
+ var scale = getLengthScale(val);
+ return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
}
}
}
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index e8d84b49f9..6b435cff0f 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -36,6 +36,7 @@ namespace osu.Game.Skinning
~Skin()
{
+ // required to potentially clean up sample store from audio hierarchy.
Dispose(false);
}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 2826c826a5..fcde9f041b 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Skinning
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
- await base.Populate(model, archive, cancellationToken);
+ await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
if (model.Name?.Contains(".osk") == true)
populateMetadata(model);
diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs
index 6ce3b617e9..c478b91c22 100644
--- a/osu.Game/Storyboards/CommandTimelineGroup.cs
+++ b/osu.Game/Storyboards/CommandTimelineGroup.cs
@@ -45,11 +45,30 @@ namespace osu.Game.Storyboards
};
}
+ ///
+ /// Returns the earliest visible time. Will be null unless this group's first command has a start value of zero.
+ ///
+ public double? EarliestDisplayedTime
+ {
+ get
+ {
+ var first = Alpha.Commands.FirstOrDefault();
+
+ return first?.StartValue == 0 ? first.StartTime : (double?)null;
+ }
+ }
+
[JsonIgnore]
public double CommandsStartTime
{
get
{
+ // if the first alpha command starts at zero it should be given priority over anything else.
+ // this is due to it creating a state where the target is not present before that time, causing any other events to not be visible.
+ var earliestDisplay = EarliestDisplayedTime;
+ if (earliestDisplay != null)
+ return earliestDisplay.Value;
+
double min = double.MaxValue;
for (int i = 0; i < timelines.Length; i++)
diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs
index f411ad04f3..fdaa59d7d9 100644
--- a/osu.Game/Storyboards/StoryboardSprite.cs
+++ b/osu.Game/Storyboards/StoryboardSprite.cs
@@ -24,13 +24,46 @@ namespace osu.Game.Storyboards
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup();
- public double StartTime => Math.Min(
- TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue,
- loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue);
+ public double StartTime
+ {
+ get
+ {
+ // check for presence affecting commands as an initial pass.
+ double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue;
- public double EndTime => Math.Max(
- TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue,
- loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue);
+ foreach (var l in loops)
+ {
+ if (!(l.EarliestDisplayedTime is double lEarliest))
+ continue;
+
+ earliestStartTime = Math.Min(earliestStartTime, lEarliest);
+ }
+
+ if (earliestStartTime < double.MaxValue)
+ return earliestStartTime;
+
+ // if an alpha-affecting command was not found, use the earliest of any command.
+ earliestStartTime = TimelineGroup.StartTime;
+
+ foreach (var l in loops)
+ earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
+
+ return earliestStartTime;
+ }
+ }
+
+ public double EndTime
+ {
+ get
+ {
+ double latestEndTime = TimelineGroup.EndTime;
+
+ foreach (var l in loops)
+ latestEndTime = Math.Max(latestEndTime, l.EndTime);
+
+ return latestEndTime;
+ }
+ }
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index 2e8c834c65..7775c2bd24 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -7,8 +7,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -48,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
RoomManager.Schedule(() => RoomManager.PartRoom());
if (joinRoom)
+ {
+ Room.Name.Value = "test name";
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
+ Ruleset = { Value = Ruleset.Value }
+ });
+
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
+ }
});
public override void SetUpSteps()
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 379bb758c5..09fcc1ff47 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -3,12 +3,15 @@
#nullable enable
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@@ -25,6 +28,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; } = null!;
+
+ private readonly TestMultiplayerRoomManager roomManager;
+
+ public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
+ {
+ this.roomManager = roomManager;
+ }
+
public void Connect() => isConnected.Value = true;
public void Disconnect() => isConnected.Value = false;
@@ -89,13 +102,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected override Task JoinRoom(long roomId)
{
- var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
+ var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId);
- var room = new MultiplayerRoom(roomId);
- room.Users.Add(user);
+ var user = new MultiplayerRoomUser(api.LocalUser.Value.Id)
+ {
+ User = api.LocalUser.Value
+ };
- if (room.Users.Count == 1)
- room.Host = user;
+ var room = new MultiplayerRoom(roomId)
+ {
+ Settings =
+ {
+ Name = apiRoom.Name.Value,
+ BeatmapID = apiRoom.Playlist.Last().BeatmapID,
+ RulesetID = apiRoom.Playlist.Last().RulesetID,
+ BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash,
+ RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(),
+ AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(),
+ PlaylistItemId = apiRoom.Playlist.Last().ID
+ },
+ Users = { user },
+ Host = user
+ };
return Task.FromResult(room);
}
@@ -108,7 +136,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Debug.Assert(Room != null);
- await ((IMultiplayerClient)this).SettingsChanged(settings);
+ await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
@@ -150,5 +178,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
return ((IMultiplayerClient)this).LoadRequested();
}
+
+ protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
+ {
+ Debug.Assert(Room != null);
+
+ var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID);
+ var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet
+ ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet;
+
+ if (set == null)
+ throw new InvalidOperationException("Beatmap not found.");
+
+ return Task.FromResult(set);
+ }
}
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
index 860caef071..e57411d04d 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
@@ -32,11 +32,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
RelativeSizeAxes = Axes.Both;
+ RoomManager = new TestMultiplayerRoomManager();
+ Client = new TestMultiplayerClient(RoomManager);
+ OngoingOperationTracker = new OngoingOperationTracker();
+
AddRangeInternal(new Drawable[]
{
- Client = new TestMultiplayerClient(),
- RoomManager = new TestMultiplayerRoomManager(),
- OngoingOperationTracker = new OngoingOperationTracker(),
+ Client,
+ RoomManager,
+ OngoingOperationTracker,
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index 5e12156f3c..7e824c4d7c 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached]
public readonly Bindable Filter = new Bindable(new FilterCriteria());
- private readonly List rooms = new List();
+ public new readonly List Rooms = new List();
protected override void LoadComplete()
{
@@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
int currentScoreId = 0;
int currentRoomId = 0;
+ int currentPlaylistItemId = 0;
((DummyAPIAccess)api).HandleRequest = req =>
{
@@ -46,7 +47,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
createdRoom.CopyFrom(createRoomRequest.Room);
createdRoom.RoomID.Value ??= currentRoomId++;
- rooms.Add(createdRoom);
+ for (int i = 0; i < createdRoom.Playlist.Count; i++)
+ createdRoom.Playlist[i].ID = currentPlaylistItemId++;
+
+ Rooms.Add(createdRoom);
createRoomRequest.TriggerSuccess(createdRoom);
break;
@@ -61,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
case GetRoomsRequest getRoomsRequest:
var roomsWithoutParticipants = new List();
- foreach (var r in rooms)
+ foreach (var r in Rooms)
{
var newRoom = new Room();
@@ -75,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
break;
case GetRoomRequest getRoomRequest:
- getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
+ getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
break;
case GetBeatmapSetRequest getBeatmapSetRequest:
diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs
index 4ebf2a7368..50572a7867 100644
--- a/osu.Game/Updater/SimpleUpdateManager.cs
+++ b/osu.Game/Updater/SimpleUpdateManager.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Updater
{
var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest");
- await releases.PerformAsync();
+ await releases.PerformAsync().ConfigureAwait(false);
var latest = releases.ResponseObject;
@@ -77,7 +77,7 @@ namespace osu.Game.Updater
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal));
break;
- case RuntimeInfo.Platform.MacOsx:
+ case RuntimeInfo.Platform.macOS:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal));
break;
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
index f772c6d282..9a0454bc95 100644
--- a/osu.Game/Updater/UpdateManager.cs
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Updater
lock (updateTaskLock)
waitTask = (updateCheckTask ??= PerformUpdateCheck());
- bool hasUpdates = await waitTask;
+ bool hasUpdates = await waitTask.ConfigureAwait(false);
lock (updateTaskLock)
updateCheckTask = null;
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 4a6fd540c7..4d537b91bd 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -72,9 +72,6 @@ namespace osu.Game.Users
[JsonProperty(@"support_level")]
public int SupportLevel;
- [JsonProperty(@"current_mode_rank")]
- public int? CurrentModeRank;
-
[JsonProperty(@"is_gmt")]
public bool IsGMT;
@@ -182,7 +179,7 @@ namespace osu.Game.Users
private UserStatistics statistics;
///
- /// User statistics for the requested ruleset (in the case of a response).
+ /// User statistics for the requested ruleset (in the case of a or response).
/// Otherwise empty.
///
[JsonProperty(@"statistics")]
diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs
index 78e6f5a05a..dc926898fc 100644
--- a/osu.Game/Users/UserStatistics.cs
+++ b/osu.Game/Users/UserStatistics.cs
@@ -29,16 +29,9 @@ namespace osu.Game.Users
[JsonProperty(@"global_rank")]
public int? GlobalRank;
+ [JsonProperty(@"country_rank")]
public int? CountryRank;
- [JsonProperty(@"rank")]
- private UserRanks ranks
- {
- // eventually that will also become an own json property instead of reading from a `rank` object.
- // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53.
- set => CountryRank = value.Country;
- }
-
// populated via User model, as that's where the data currently lives.
public RankHistoryData RankHistory;
@@ -119,13 +112,5 @@ namespace osu.Game.Users
}
}
}
-
-#pragma warning disable 649
- private struct UserRanks
- {
- [JsonProperty(@"country")]
- public int? Country;
- }
-#pragma warning restore 649
}
}
diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs
index be9d01cde6..8f12760a6b 100644
--- a/osu.Game/Utils/SentryLogger.cs
+++ b/osu.Game/Utils/SentryLogger.cs
@@ -86,11 +86,6 @@ namespace osu.Game.Utils
#region Disposal
- ~SentryLogger()
- {
- Dispose(false);
- }
-
public void Dispose()
{
Dispose(true);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 84a74502c2..90c8b98f42 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -22,17 +22,17 @@
-
+
-
+
-
+
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 2cea2e4b13..ccd33bf88c 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,19 +70,21 @@
-
+
$(NoWarn);NU1605
+
-
-
-
-
-
+
+ none
+
+
+ none
+
@@ -91,8 +93,8 @@
-
-
+
+