diff --git a/osu.Android.props b/osu.Android.props
index ff6499631d..b6260fd1d4 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
index de1f61a0bd..5e46498aca 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
@@ -16,31 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAlternate : OsuModTestScene
{
- [Test]
- public void TestInputAtIntro() => CreateModTest(new ModTestData
- {
- Mod = new OsuModAlternate(),
- PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
- Autoplay = false,
- Beatmap = new Beatmap
- {
- HitObjects = new List
- {
- new HitCircle
- {
- StartTime = 1000,
- Position = new Vector2(100),
- },
- },
- },
- ReplayFrames = new List
- {
- new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
- new OsuReplayFrame(501, new Vector2(200)),
- new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
- }
- });
-
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
@@ -116,17 +91,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}
});
+ ///
+ /// Ensures alternation is reset before the first hitobject after intro.
+ ///
+ [Test]
+ public void TestInputSingularAtIntro() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAlternate(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ // first press during intro.
+ new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(200)),
+ // press same key at hitobject and ensure it has been hit.
+ new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
+ }
+ });
+
+ ///
+ /// Ensures alternation is reset before the first hitobject after a break.
+ ///
[Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
- PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List
{
- new BreakPeriod(500, 2250),
+ new BreakPeriod(500, 2000),
},
HitObjects = new List
{
@@ -138,16 +146,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new HitCircle
{
StartTime = 2500,
- Position = new Vector2(100),
- }
+ Position = new Vector2(500, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(500, 100),
+ },
}
},
ReplayFrames = new List
{
+ // first press to start alternate lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
- new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton),
- new OsuReplayFrame(2501, new Vector2(100)),
+ // press same key after break but before hit object.
+ new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2251, new Vector2(300, 100)),
+ // press same key at second hitobject and ensure it has been hit.
+ new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2501, new Vector2(500, 100)),
+ // press same key at third hitobject and ensure it has been missed.
+ new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(3001, new Vector2(500, 100)),
}
});
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
index 46b97dd23b..dfa28a537a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
@@ -2,21 +2,24 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer
+ public class OsuModAlternate : Mod, IApplicableToDrawableRuleset
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
@@ -26,9 +29,16 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
- private double firstObjectValidJudgementTime;
- private IBindable isBreakTime;
private const double flash_duration = 1000;
+
+ ///
+ /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
+ ///
+ ///
+ /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
+ ///
+ private PeriodTracker nonGameplayPeriods;
+
private OsuAction? lastActionPressed;
private DrawableRuleset ruleset;
@@ -39,29 +49,30 @@ namespace osu.Game.Rulesets.Osu.Mods
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
- var firstHitObject = ruleset.Objects.FirstOrDefault();
- firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
+ var periods = new List();
+
+ if (drawableRuleset.Objects.Any())
+ {
+ periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
+
+ foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
+ periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
+
+ static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
+ }
+
+ nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
- public void ApplyToPlayer(Player player)
- {
- isBreakTime = player.IsBreakTime.GetBoundCopy();
- isBreakTime.ValueChanged += e =>
- {
- if (e.NewValue)
- lastActionPressed = null;
- };
- }
-
private bool checkCorrectAction(OsuAction action)
{
- if (isBreakTime.Value)
- return true;
-
- if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
+ if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ {
+ lastActionPressed = null;
return true;
+ }
switch (action)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
index f46573c494..76ff361ce3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset
{
- public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index fea9246035..23500f5da6 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Description => "It never gets boring!";
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
+
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random? rng;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 9be0dc748a..d9ab749ad3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
- public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
index ee325db66a..ab45e5192d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious...";
public override double ScoreMultiplier => 1.0;
- public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
+ public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index 4fab9b6a5a..5b121f4673 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -42,7 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSuddenDeath) };
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
+ {
+ typeof(IRequiresApproachCircles),
+ typeof(OsuModRandom),
+ typeof(OsuModSpunOut),
+ typeof(OsuModStrictTracking),
+ typeof(OsuModSuddenDeath)
+ }).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable Seed { get; } = new Bindable
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
index 312281ac18..e05580fed6 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);
- AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).MatchStarted());
+ AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted());
}
[Test]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
index 63741451f3..c550c9afda 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
@@ -4,11 +4,11 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
-using osu.Game.Rulesets;
namespace osu.Game.Tests.Visual.Online
{
@@ -17,79 +17,86 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
- private readonly TestRulesetSelector selector;
+ private BeatmapRulesetSelector selector;
- public TestSceneBeatmapRulesetSelector()
+ [SetUp]
+ public void SetUp() => Schedule(() => Child = selector = new BeatmapRulesetSelector
{
- Add(selector = new TestRulesetSelector());
- }
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BeatmapSet = new APIBeatmapSet(),
+ });
- [Resolved]
- private IRulesetStore rulesets { get; set; }
+ [Test]
+ public void TestDisplay()
+ {
+ AddSliderStep("osu", 0, 100, 0, v => updateBeatmaps(0, v));
+ AddSliderStep("taiko", 0, 100, 0, v => updateBeatmaps(1, v));
+ AddSliderStep("fruits", 0, 100, 0, v => updateBeatmaps(2, v));
+ AddSliderStep("mania", 0, 100, 0, v => updateBeatmaps(3, v));
+
+ void updateBeatmaps(int ruleset, int count)
+ {
+ if (selector == null)
+ return;
+
+ selector.BeatmapSet = new APIBeatmapSet
+ {
+ Beatmaps = selector.BeatmapSet.Beatmaps
+ .Where(b => b.Ruleset.OnlineID != ruleset)
+ .Concat(Enumerable.Range(0, count).Select(_ => new APIBeatmap { RulesetID = ruleset }))
+ .ToArray(),
+ };
+ }
+ }
[Test]
public void TestMultipleRulesetsBeatmapSet()
{
- var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2);
-
AddStep("load multiple rulesets beatmapset", () =>
- {
- selector.BeatmapSet = new APIBeatmapSet
- {
- Beatmaps = enabledRulesets.Select(r => new APIBeatmap { RulesetID = r.OnlineID }).ToArray()
- };
- });
-
- var tabItems = selector.TabContainer.TabItems;
- AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value));
- AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value);
- }
-
- [Test]
- public void TestSingleRulesetBeatmapSet()
- {
- var enabledRuleset = rulesets.AvailableRulesets.Last();
-
- AddStep("load single ruleset beatmapset", () =>
{
selector.BeatmapSet = new APIBeatmapSet
{
Beatmaps = new[]
{
- new APIBeatmap
- {
- RulesetID = enabledRuleset.OnlineID
- }
+ new APIBeatmap { RulesetID = 1 },
+ new APIBeatmap { RulesetID = 2 },
}
};
});
- AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset));
+ AddAssert("osu disabled", () => !selector.ChildrenOfType().Single(t => t.Value.OnlineID == 0).Enabled.Value);
+ AddAssert("mania disabled", () => !selector.ChildrenOfType().Single(t => t.Value.OnlineID == 3).Enabled.Value);
+
+ AddAssert("taiko selected", () => selector.ChildrenOfType().Single(t => t.Active.Value).Value.OnlineID == 1);
+ }
+
+ [Test]
+ public void TestSingleRulesetBeatmapSet()
+ {
+ AddStep("load single ruleset beatmapset", () =>
+ {
+ selector.BeatmapSet = new APIBeatmapSet
+ {
+ Beatmaps = new[] { new APIBeatmap { RulesetID = 3 } }
+ };
+ });
+
+ AddAssert("single ruleset selected", () => selector.ChildrenOfType().Single(t => t.Active.Value).Value.OnlineID == 3);
}
[Test]
public void TestEmptyBeatmapSet()
{
AddStep("load empty beatmapset", () => selector.BeatmapSet = new APIBeatmapSet());
-
- AddAssert("no ruleset selected", () => selector.SelectedTab == null);
- AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
+ AddAssert("all rulesets disabled", () => selector.ChildrenOfType().All(t => !t.Active.Value && !t.Enabled.Value));
}
[Test]
public void TestNullBeatmapSet()
{
AddStep("load null beatmapset", () => selector.BeatmapSet = null);
-
- AddAssert("no ruleset selected", () => selector.SelectedTab == null);
- AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
- }
-
- private class TestRulesetSelector : BeatmapRulesetSelector
- {
- public new TabItem SelectedTab => base.SelectedTab;
-
- public new TabFillFlowContainer TabContainer => base.TabContainer;
+ AddAssert("all rulesets disabled", () => selector.ChildrenOfType().All(t => !t.Active.Value && !t.Enabled.Value));
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
index cbbe8b8eac..ae90872439 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Online
public class TestSceneProfileRulesetSelector : OsuTestScene
{
[Cached]
- private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
public TestSceneProfileRulesetSelector()
{
@@ -32,14 +32,14 @@ namespace osu.Game.Tests.Visual.Online
};
AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo));
- AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo));
AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo));
AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo));
+ AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo));
- AddStep("User with osu as default", () => user.Value = new APIUser { PlayMode = "osu" });
- AddStep("User with mania as default", () => user.Value = new APIUser { PlayMode = "mania" });
- AddStep("User with taiko as default", () => user.Value = new APIUser { PlayMode = "taiko" });
- AddStep("User with catch as default", () => user.Value = new APIUser { PlayMode = "fruits" });
+ AddStep("User with osu as default", () => user.Value = new APIUser { Id = 0, PlayMode = "osu" });
+ AddStep("User with taiko as default", () => user.Value = new APIUser { Id = 1, PlayMode = "taiko" });
+ AddStep("User with catch as default", () => user.Value = new APIUser { Id = 2, PlayMode = "fruits" });
+ AddStep("User with mania as default", () => user.Value = new APIUser { Id = 3, PlayMode = "mania" });
AddStep("null user", () => user.Value = null);
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs
index 0ac65b357c..f27615eea5 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs
@@ -1,35 +1,99 @@
// 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.Graphics;
using osu.Game.Screens.Select;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneSongSelectFooter : OsuManualInputManagerTestScene
{
- public TestSceneSongSelectFooter()
- {
- AddStep("Create footer", () =>
- {
- Footer footer;
- AddRange(new Drawable[]
- {
- footer = new Footer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- });
+ private FooterButtonRandom randomButton;
- footer.AddButton(new FooterButtonMods(), null);
- footer.AddButton(new FooterButtonRandom
- {
- NextRandom = () => { },
- PreviousRandom = () => { },
- }, null);
- footer.AddButton(new FooterButtonOptions(), null);
+ private bool nextRandomCalled;
+ private bool previousRandomCalled;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ nextRandomCalled = false;
+ previousRandomCalled = false;
+
+ Footer footer;
+
+ Child = footer = new Footer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+
+ footer.AddButton(new FooterButtonMods(), null);
+ footer.AddButton(randomButton = new FooterButtonRandom
+ {
+ NextRandom = () => nextRandomCalled = true,
+ PreviousRandom = () => previousRandomCalled = true,
+ }, null);
+ footer.AddButton(new FooterButtonOptions(), null);
+
+ InputManager.MoveMouseTo(Vector2.Zero);
+ });
+
+ [Test]
+ public void TestFooterRandom()
+ {
+ AddStep("press F2", () => InputManager.Key(Key.F2));
+ AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRandomViaMouse()
+ {
+ AddStep("click button", () =>
+ {
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Left);
});
+ AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewind()
+ {
+ AddStep("press Shift+F2", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.PressKey(Key.F2);
+ InputManager.ReleaseKey(Key.F2);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewindViaShiftMouseLeft()
+ {
+ AddStep("shift + click button", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewindViaMouseRight()
+ {
+ AddStep("right click button", () =>
+ {
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Right);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
index 836cf6caad..39298f56ba 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
@@ -65,6 +65,12 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
+ [Test]
+ public void TestBasic()
+ {
+ AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible);
+ }
+
[Test]
[Ignore("Enable when first run setup is being displayed on first run.")]
public void TestDoesntOpenOnSecondRun()
diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs
index c3c566782f..dea44e6d99 100644
--- a/osu.Game/Graphics/UserInterface/ShearedButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnMouseDown(MouseDownEvent e)
{
- Content.ScaleTo(0.8f, 2000, Easing.OutQuint);
+ Content.ScaleTo(0.9f, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
}
@@ -176,8 +176,8 @@ namespace osu.Game.Graphics.UserInterface
if (!Enabled.Value)
{
- colourDark = colourDark.Darken(0.3f);
- colourLight = colourLight.Darken(0.3f);
+ colourDark = colourDark.Darken(1f);
+ colourLight = colourLight.Darken(1f);
}
else if (IsHovered)
{
diff --git a/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs
new file mode 100644
index 0000000000..4ec5019a07
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.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.
+
+using MessagePack;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// A started by the server when clients being to load.
+ /// Indicates how long until gameplay will forcefully start, excluding any users which have not completed loading,
+ /// and forcing progression of any clients that are blocking load due to user interaction.
+ ///
+ [MessagePackObject]
+ public class ForceGameplayStartCountdown : MultiplayerCountdown
+ {
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
index 3e6821b1cd..2f454ea835 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
@@ -93,14 +93,20 @@ namespace osu.Game.Online.Multiplayer
Task UserModsChanged(int userId, IEnumerable mods);
///
- /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point.
+ /// Signals that the match is starting and the loading of gameplay should be started. This will *only* be sent to clients which are to begin loading at this point.
///
Task LoadRequested();
///
- /// Signals that a match has started. All users in the state should begin gameplay as soon as possible.
+ /// Signals that loading of gameplay is to be aborted.
///
- Task MatchStarted();
+ Task LoadAborted();
+
+ ///
+ /// Signals that gameplay has started.
+ /// All users in the or states should begin gameplay as soon as possible.
+ ///
+ Task GameplayStarted();
///
/// Signals that the match has ended, all players have finished and results are ready to be displayed.
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 967220abbf..cae675b406 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -69,10 +69,15 @@ namespace osu.Game.Online.Multiplayer
///
public virtual event Action? LoadRequested;
+ ///
+ /// Invoked when the multiplayer server requests loading of play to be aborted.
+ ///
+ public event Action? LoadAborted;
+
///
/// Invoked when the multiplayer server requests gameplay to be started.
///
- public event Action? MatchStarted;
+ public event Action? GameplayStarted;
///
/// Invoked when the multiplayer server has finished collating results.
@@ -604,14 +609,27 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
- Task IMultiplayerClient.MatchStarted()
+ Task IMultiplayerClient.LoadAborted()
{
Scheduler.Add(() =>
{
if (Room == null)
return;
- MatchStarted?.Invoke();
+ LoadAborted?.Invoke();
+ }, false);
+
+ return Task.CompletedTask;
+ }
+
+ Task IMultiplayerClient.GameplayStarted()
+ {
+ Scheduler.Add(() =>
+ {
+ if (Room == null)
+ return;
+
+ GameplayStarted?.Invoke();
}, false);
return Task.CompletedTask;
diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
index 81190e64c9..dbf2ab667b 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Online.Multiplayer
///
[MessagePackObject]
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
+ [Union(1, typeof(ForceGameplayStartCountdown))]
public abstract class MultiplayerCountdown
{
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
index f0b7dcbff8..50e539e8a6 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
@@ -65,5 +65,21 @@ namespace osu.Game.Online.Multiplayer
}
public override int GetHashCode() => UserID.GetHashCode();
+
+ ///
+ /// Whether this user has finished loading and can start gameplay.
+ ///
+ public bool CanStartGameplay()
+ {
+ switch (State)
+ {
+ case MultiplayerUserState.Loaded:
+ case MultiplayerUserState.ReadyForGameplay:
+ return true;
+
+ default:
+ return false;
+ }
+ }
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs
index c467ff84bb..d1369a7970 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs
@@ -29,10 +29,16 @@ namespace osu.Game.Online.Multiplayer
WaitingForLoad,
///
- /// The user's client has marked itself as loaded and ready to begin gameplay.
+ /// The user has marked itself as loaded, but may still be adjusting settings prior to being ready for gameplay.
+ /// Players remaining in this state for an extended period of time will be automatically transitioned to the state by the server.
///
Loaded,
+ ///
+ /// The user has finished adjusting settings and is ready to start gameplay.
+ ///
+ ReadyForGameplay,
+
///
/// The user is currently playing in a game. This is a reserved state, and is set by the server.
///
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 7e62908ecd..4dc23d8b85 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -54,7 +54,8 @@ namespace osu.Game.Online.Multiplayer
connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
- connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
+ connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
+ connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs
index c6ddc03564..e44dad1db5 100644
--- a/osu.Game/Online/ProductionEndpointConfiguration.cs
+++ b/osu.Game/Online/ProductionEndpointConfiguration.cs
@@ -10,8 +10,8 @@ namespace osu.Game.Online
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
- SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
- MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
+ SpectatorEndpointUrl = "https://spectator2.ppy.sh/spectator";
+ MultiplayerEndpointUrl = "https://spectator2.ppy.sh/multiplayer";
}
}
}
diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs
index 156f916cef..d1f0ba725f 100644
--- a/osu.Game/Online/SignalRWorkaroundTypes.cs
+++ b/osu.Game/Online/SignalRWorkaroundTypes.cs
@@ -24,7 +24,8 @@ namespace osu.Game.Online
(typeof(CountdownChangedEvent), typeof(MatchServerEvent)),
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)),
- (typeof(MatchStartCountdown), typeof(MultiplayerCountdown))
+ (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
+ (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown))
};
}
}
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index b9d3854066..bd63c997df 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -5,7 +5,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Input.Events;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet;
@@ -24,9 +23,6 @@ namespace osu.Game.Overlays
private readonly Bindable beatmapSet = new Bindable();
- // receive input outside our bounds so we can trigger a close event on ourselves.
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
-
public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue)
{
@@ -71,12 +67,6 @@ namespace osu.Game.Overlays
beatmapSet.Value = null;
}
- protected override bool OnClick(ClickEvent e)
- {
- Hide();
- return true;
- }
-
public void FetchAndShowBeatmap(int beatmapId)
{
beatmapSet.Value = null;
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
index 1bd82f6d99..862506add2 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
@@ -99,6 +99,8 @@ namespace osu.Game.Overlays.FirstRunSetup
private class NestedSongSelect : PlaySongSelect
{
protected override bool ControlGlobalMusic => false;
+
+ public override bool? AllowTrackAdjustments => false;
}
private class PinnedMainMenu : MainMenu
diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs
index 4277f0f2ba..75778e6c4d 100644
--- a/osu.Game/Overlays/FirstRunSetupOverlay.cs
+++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs
@@ -25,7 +25,6 @@ using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
-using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Overlays
{
@@ -45,8 +44,8 @@ namespace osu.Game.Overlays
private ScreenStack? stack;
- public PurpleTriangleButton NextButton = null!;
- public DangerousTriangleButton BackButton = null!;
+ public ShearedButton NextButton = null!;
+ public ShearedButton BackButton = null!;
private readonly Bindable showFirstRunSetup = new Bindable();
@@ -72,7 +71,7 @@ namespace osu.Game.Overlays
private Container content = null!;
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuColour colours)
{
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
@@ -84,7 +83,11 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Horizontal = 70 * 1.2f },
+ Padding = new MarginPadding
+ {
+ Horizontal = 70 * 1.2f,
+ Bottom = 20,
+ },
Child = new InputBlockingContainer
{
Masking = true,
@@ -117,14 +120,15 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Width = 0.98f,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Margin = new MarginPadding { Vertical = PADDING },
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
ColumnDimensions = new[]
{
- new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10),
+ new Dimension(GridSizeMode.AutoSize),
new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 10),
},
RowDimensions = new[]
{
@@ -134,21 +138,25 @@ namespace osu.Game.Overlays
{
new[]
{
- BackButton = new DangerousTriangleButton
+ Empty(),
+ BackButton = new ShearedButton(300)
{
- Width = 300,
Text = CommonStrings.Back,
Action = showPreviousStep,
Enabled = { Value = false },
+ DarkerColour = colours.Pink2,
+ LighterColour = colours.Pink1,
},
- Empty(),
- NextButton = new PurpleTriangleButton
+ NextButton = new ShearedButton(0)
{
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
+ DarkerColour = ColourProvider.Colour2,
+ LighterColour = ColourProvider.Colour1,
Action = showNextStep
- }
+ },
+ Empty(),
},
}
});
diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs
index 9d4afc94d1..1f11b98881 100644
--- a/osu.Game/Overlays/OverlayRulesetTabItem.cs
+++ b/osu.Game/Overlays/OverlayRulesetTabItem.cs
@@ -5,18 +5,18 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK.Graphics;
using osuTK;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays
{
- public class OverlayRulesetTabItem : TabItem
+ public class OverlayRulesetTabItem : TabItem, IHasTooltip
{
private Color4 accentColour;
@@ -26,7 +26,7 @@ namespace osu.Game.Overlays
set
{
accentColour = value;
- text.FadeColour(value, 120, Easing.OutQuint);
+ icon.FadeColour(value, 120, Easing.OutQuint);
}
}
@@ -35,7 +35,9 @@ namespace osu.Game.Overlays
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- private readonly OsuSpriteText text;
+ private readonly Drawable icon;
+
+ public LocalisableString TooltipText => Value.Name;
public OverlayRulesetTabItem(RulesetInfo value)
: base(value)
@@ -48,15 +50,14 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(3, 0),
- Child = text = new OsuSpriteText
+ Spacing = new Vector2(4, 0),
+ Child = icon = new ConstrainedIconContainer
{
- Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Text = value.Name,
- Font = OsuFont.GetFont(size: 14),
- ShadowColour = Color4.Black.Opacity(0.75f)
- }
+ Origin = Anchor.Centre,
+ Size = new Vector2(20f),
+ Icon = value.CreateInstance().CreateIcon(),
+ },
},
new HoverClickSounds()
});
@@ -70,7 +71,7 @@ namespace osu.Game.Overlays
Enabled.BindValueChanged(_ => updateState(), true);
}
- public override bool PropagatePositionalInputSubTree => Enabled.Value && !Active.Value && base.PropagatePositionalInputSubTree;
+ public override bool PropagatePositionalInputSubTree => Enabled.Value && base.PropagatePositionalInputSubTree;
protected override bool OnHover(HoverEvent e)
{
@@ -91,7 +92,6 @@ namespace osu.Game.Overlays
private void updateState()
{
- text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Medium);
AccentColour = Enabled.Value ? getActiveColour() : colourProvider.Foreground1;
}
diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs
index 3d20fba542..4a44e285bf 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs
@@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -23,7 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
isDefault = value;
- icon.FadeTo(isDefault ? 1 : 0, 200, Easing.OutQuint);
+ icon.Alpha = isDefault ? 1 : 0;
}
}
@@ -42,15 +45,20 @@ namespace osu.Game.Overlays.Profile.Header.Components
public ProfileRulesetTabItem(RulesetInfo value)
: base(value)
{
- Add(icon = new SpriteIcon
+ Add(icon = new DefaultRulesetIcon { Alpha = 0 });
+ }
+
+ public class DefaultRulesetIcon : SpriteIcon, IHasTooltip
+ {
+ public LocalisableString TooltipText => UsersStrings.ShowEditDefaultPlaymodeIsDefaultTooltip;
+
+ public DefaultRulesetIcon()
{
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Alpha = 0,
- AlwaysPresent = true,
- Icon = FontAwesome.Solid.Star,
- Size = new Vector2(12),
- });
+ Origin = Anchor.Centre;
+ Anchor = Anchor.Centre;
+ Icon = FontAwesome.Solid.Star;
+ Size = new Vector2(12);
+ }
}
}
}
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index fab2487c0d..44e0d9c37f 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -31,7 +31,9 @@ namespace osu.Game.Overlays.Profile
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
- TabControl.AddItem(LayoutStrings.HeaderUsersModding);
+
+ // todo: pending implementation.
+ // TabControl.AddItem(LayoutStrings.HeaderUsersModding);
centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true);
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs
index c84fcff11e..1a51aebb76 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
- bool countdownActive = multiplayerClient.Room?.Countdown != null;
+ bool countdownActive = multiplayerClient.Room?.Countdown is MatchStartCountdown;
if (countdownActive)
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
index 22a0243f8f..a7e18622dc 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
@@ -55,7 +55,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
- if (countdown != room?.Countdown)
+ MultiplayerCountdown newCountdown;
+
+ switch (room?.Countdown)
+ {
+ case MatchStartCountdown _:
+ newCountdown = room.Countdown;
+ break;
+
+ // Clear the countdown with any other (including non-null) countdown values.
+ default:
+ newCountdown = null;
+ break;
+ }
+
+ if (newCountdown != countdown)
{
countdown = room?.Countdown;
countdownChangeTime = Time.Current;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
index 66f6935bcc..53d081a108 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using osu.Framework.Allocation;
+using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Components;
@@ -20,6 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
client.RoomUpdated += onRoomUpdated;
+ client.LoadAborted += onLoadAborted;
onRoomUpdated();
}
@@ -35,6 +37,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
transitionFromResults();
}
+ private void onLoadAborted()
+ {
+ // If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens.
+ if (!this.IsCurrentScreen())
+ {
+ Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important);
+ this.MakeCurrent();
+ }
+ }
+
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
@@ -42,9 +54,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
+ Debug.Assert(client.LocalUser != null);
+
if (!(e.Last is MultiplayerPlayerLoader playerLoader))
return;
+ // Nothing needs to be done if already in the idle state (e.g. via load being aborted by the server).
+ if (client.LocalUser.State == MultiplayerUserState.Idle)
+ return;
+
// If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay.
if (!playerLoader.GameplayPassed)
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 70f8f1b752..02ff040a94 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!ValidForResume)
return; // token retrieval may have failed.
- client.MatchStarted += onMatchStarted;
+ client.GameplayStarted += onGameplayStarted;
client.ResultsReady += onResultsReady;
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
@@ -144,10 +144,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override void StartGameplay()
{
- // block base call, but let the server know we are ready to start.
- loadingDisplay.Show();
-
- client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion);
+ if (client.LocalUser?.State == MultiplayerUserState.Loaded)
+ {
+ // block base call, but let the server know we are ready to start.
+ loadingDisplay.Show();
+ client.ChangeState(MultiplayerUserState.ReadyForGameplay);
+ }
}
private void failAndBail(string message = null)
@@ -175,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
}
- private void onMatchStarted() => Scheduler.Add(() =>
+ private void onGameplayStarted() => Scheduler.Add(() =>
{
if (!this.IsCurrentScreen())
return;
@@ -223,7 +225,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client != null)
{
- client.MatchStarted -= onMatchStarted;
+ client.GameplayStarted -= onGameplayStarted;
client.ResultsReady -= onResultsReady;
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs
index 53dea83f18..7f01bd64ab 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs
@@ -2,7 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
using osu.Framework.Screens;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
@@ -11,6 +15,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public bool GameplayPassed => player?.GameplayState.HasPassed == true;
+ [Resolved]
+ private MultiplayerClient multiplayerClient { get; set; }
+
private Player player;
public MultiplayerPlayerLoader(Func createPlayer)
@@ -18,6 +25,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
}
+ protected override bool ReadyForGameplay =>
+ base.ReadyForGameplay
+ // The server is forcefully starting gameplay.
+ || multiplayerClient.LocalUser?.State == MultiplayerUserState.Playing;
+
+ protected override void OnPlayerLoaded()
+ {
+ base.OnPlayerLoaded();
+
+ multiplayerClient.ChangeState(MultiplayerUserState.Loaded)
+ .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion);
+ }
+
+ private void failAndBail(string message = null)
+ {
+ if (!string.IsNullOrEmpty(message))
+ Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important);
+
+ Schedule(() =>
+ {
+ if (this.IsCurrentScreen())
+ this.Exit();
+ });
+ }
+
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
index 2616b07c1f..658fc43e8d 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
@@ -112,6 +112,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
break;
case MultiplayerUserState.Loaded:
+ case MultiplayerUserState.ReadyForGameplay:
text.Text = "loaded";
icon.Icon = FontAwesome.Solid.DotCircle;
icon.Colour = colours.YellowLight;
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 494ab51a10..d75466764d 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -92,11 +92,15 @@ namespace osu.Game.Screens.Play
!playerConsumed
// don't push unless the player is completely loaded
&& CurrentPlayer?.LoadState == LoadState.Ready
- // don't push if the user is hovering one of the panes, unless they are idle.
- && (IsHovered || idleTracker.IsIdle.Value)
- // don't push if the user is dragging a slider or otherwise.
+ // don't push unless the player is ready to start gameplay
+ && ReadyForGameplay;
+
+ protected virtual bool ReadyForGameplay =>
+ // not ready if the user is hovering one of the panes, unless they are idle.
+ (IsHovered || idleTracker.IsIdle.Value)
+ // not ready if the user is dragging a slider or otherwise.
&& inputManager.DraggedDrawable == null
- // don't push if a focused overlay is visible, like settings.
+ // not ready if a focused overlay is visible, like settings.
&& inputManager.FocusedDrawable == null;
private readonly Func createPlayer;
@@ -364,7 +368,15 @@ namespace osu.Game.Screens.Play
CurrentPlayer.RestartCount = restartCount++;
CurrentPlayer.RestartRequested = restartRequested;
- LoadTask = LoadComponentAsync(CurrentPlayer, _ => MetadataInfo.Loading = false);
+ LoadTask = LoadComponentAsync(CurrentPlayer, _ =>
+ {
+ MetadataInfo.Loading = false;
+ OnPlayerLoaded();
+ });
+ }
+
+ protected virtual void OnPlayerLoaded()
+ {
}
private void restartRequested()
diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs
index 1ab54fa069..5dfa2a2664 100644
--- a/osu.Game/Screens/Select/Filter/SortMode.cs
+++ b/osu.Game/Screens/Select/Filter/SortMode.cs
@@ -27,8 +27,9 @@ namespace osu.Game.Screens.Select.Filter
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksLength))]
Length,
- [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))]
- RankAchieved,
+ // todo: pending support (https://github.com/ppy/osu/issues/4917)
+ // [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))]
+ // RankAchieved,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
Source,
diff --git a/osu.Game/Screens/Select/FooterButtonRandom.cs b/osu.Game/Screens/Select/FooterButtonRandom.cs
index 1d4722cf5d..f855b80f75 100644
--- a/osu.Game/Screens/Select/FooterButtonRandom.cs
+++ b/osu.Game/Screens/Select/FooterButtonRandom.cs
@@ -5,11 +5,13 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Screens.Select
{
@@ -18,6 +20,9 @@ namespace osu.Game.Screens.Select
public Action NextRandom { get; set; }
public Action PreviousRandom { get; set; }
+ private Container persistentText;
+ private OsuSpriteText randomSpriteText;
+ private OsuSpriteText rewindSpriteText;
private bool rewindSearch;
[BackgroundDependencyLoader]
@@ -25,7 +30,32 @@ namespace osu.Game.Screens.Select
{
SelectedColour = colours.Green;
DeselectedColour = SelectedColour.Opacity(0.5f);
- Text = @"random";
+
+ TextContainer.Add(persistentText = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AlwaysPresent = true,
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ randomSpriteText = new OsuSpriteText
+ {
+ AlwaysPresent = true,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "random",
+ },
+ rewindSpriteText = new OsuSpriteText
+ {
+ AlwaysPresent = true,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "rewind",
+ Alpha = 0f,
+ }
+ }
+ });
Action = () =>
{
@@ -33,22 +63,22 @@ namespace osu.Game.Screens.Select
{
const double fade_time = 500;
- OsuSpriteText rewindSpriteText;
+ OsuSpriteText fallingRewind;
- TextContainer.Add(rewindSpriteText = new OsuSpriteText
+ TextContainer.Add(fallingRewind = new OsuSpriteText
{
Alpha = 0,
- Text = @"rewind",
+ Text = rewindSpriteText.Text,
AlwaysPresent = true, // make sure the button is sized large enough to always show this
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
- rewindSpriteText.FadeOutFromOne(fade_time, Easing.In);
- rewindSpriteText.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In);
- rewindSpriteText.Expire();
+ fallingRewind.FadeOutFromOne(fade_time, Easing.In);
+ fallingRewind.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In);
+ fallingRewind.Expire();
- SpriteText.FadeInFromZero(fade_time, Easing.In);
+ persistentText.FadeInFromZero(fade_time, Easing.In);
PreviousRandom.Invoke();
}
@@ -59,6 +89,44 @@ namespace osu.Game.Screens.Select
};
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ updateText(e.ShiftPressed);
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ updateText(e.ShiftPressed);
+ base.OnKeyUp(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ try
+ {
+ // this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp).
+ rewindSearch |= e.ShiftPressed;
+ return base.OnClick(e);
+ }
+ finally
+ {
+ rewindSearch = false;
+ }
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ if (e.Button == MouseButton.Right)
+ {
+ rewindSearch = true;
+ TriggerClick();
+ return;
+ }
+
+ base.OnMouseUp(e);
+ }
+
public override bool OnPressed(KeyBindingPressEvent e)
{
rewindSearch = e.Action == GlobalAction.SelectPreviousRandom;
@@ -79,5 +147,11 @@ namespace osu.Game.Screens.Select
rewindSearch = false;
}
}
+
+ private void updateText(bool rewind = false)
+ {
+ randomSpriteText.Alpha = rewind ? 0 : 1;
+ rewindSpriteText.Alpha = rewind ? 1 : 0;
+ }
}
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 21774b73a0..725499d0e5 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
- ((IMultiplayerClient)this).MatchStarted();
+ ((IMultiplayerClient)this).GameplayStarted();
ChangeRoomState(MultiplayerRoomState.Playing);
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 26891ad978..fa7563da55 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index d261e13ade..e472b5f1a8 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,7 +84,7 @@
-
+