diff --git a/osu.Android.props b/osu.Android.props
index 90d131b117..57550cfb93 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index 17e7fb81f6..0d0fd136a7 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -14,10 +14,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
private readonly HitPiece piece;
- private static Hit hit;
+ public new Hit HitObject => (Hit)base.HitObject;
public HitPlacementBlueprint()
- : base(hit = new Hit())
+ : base(new Hit())
{
InternalChild = piece = new HitPiece
{
@@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
switch (e.Button)
{
case MouseButton.Left:
- hit.Type = HitType.Centre;
+ HitObject.Type = HitType.Centre;
EndPlacement(true);
return true;
case MouseButton.Right:
- hit.Type = HitType.Rim;
+ HitObject.Type = HitType.Rim;
EndPlacement(true);
return true;
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index 38cda69a46..1e9fc187eb 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -52,23 +52,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply()
{
type.BindTo(HitObject.TypeBindable);
- type.BindValueChanged(_ =>
- {
- updateActionsFromType();
-
- // will overwrite samples, should only be called on subsequent changes
- // after the initial application.
- updateSamplesFromTypeChange();
-
- RecreatePieces();
- });
-
- // action update also has to happen immediately on application.
- updateActionsFromType();
+ // this doesn't need to be run inline as RecreatePieces is called by the base call below.
+ type.BindValueChanged(_ => Scheduler.AddOnce(RecreatePieces));
base.OnApply();
}
+ protected override void RecreatePieces()
+ {
+ updateActionsFromType();
+ base.RecreatePieces();
+ }
+
protected override void OnFree()
{
base.OnFree();
@@ -83,33 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
validActionPressed = pressHandledThisFrame = false;
}
- private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
-
- protected override void LoadSamples()
- {
- base.LoadSamples();
-
- type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre;
- }
-
- private void updateSamplesFromTypeChange()
- {
- var rimSamples = getRimSamples();
-
- bool isRimType = HitObject.Type == HitType.Rim;
-
- if (isRimType != rimSamples.Any())
- {
- if (isRimType)
- HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
- else
- {
- foreach (var sample in rimSamples)
- HitObject.Samples.Remove(sample);
- }
- }
- }
-
private void updateActionsFromType()
{
HitActions =
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 6041eccb51..6a8d8a611c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -137,7 +137,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
- MainPiece?.Expire();
+ if (MainPiece != null)
+ Content.Remove(MainPiece);
+
Content.Add(MainPiece = CreateMainPiece());
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs
index 4f1523eb3f..70d4371e99 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs
@@ -1,11 +1,9 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
-using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
@@ -29,14 +27,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply()
{
isStrong.BindTo(HitObject.IsStrongBindable);
- isStrong.BindValueChanged(_ =>
- {
- // will overwrite samples, should only be called on subsequent changes
- // after the initial application.
- updateSamplesFromStrong();
-
- RecreatePieces();
- });
+ // this doesn't need to be run inline as RecreatePieces is called by the base call below.
+ isStrong.BindValueChanged(_ => Scheduler.AddOnce(RecreatePieces));
base.OnApply();
}
@@ -50,30 +42,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
isStrong.UnbindEvents();
}
- private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
-
- protected override void LoadSamples()
- {
- base.LoadSamples();
- isStrong.Value = getStrongSamples().Any();
- }
-
- private void updateSamplesFromStrong()
- {
- var strongSamples = getStrongSamples();
-
- if (isStrong.Value != strongSamples.Any())
- {
- if (isStrong.Value)
- HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
- else
- {
- foreach (var sample in strongSamples)
- HitObject.Samples.Remove(sample);
- }
- }
- }
-
protected override void RecreatePieces()
{
base.RecreatePieces();
diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
index 1b51288605..f4a66c39a8 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
@@ -1,7 +1,9 @@
// 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 osu.Framework.Bindables;
+using osu.Game.Audio;
namespace osu.Game.Rulesets.Taiko.Objects
{
@@ -15,9 +17,36 @@ namespace osu.Game.Rulesets.Taiko.Objects
public HitType Type
{
get => TypeBindable.Value;
- set => TypeBindable.Value = value;
+ set
+ {
+ TypeBindable.Value = value;
+ updateSamplesFromType();
+ }
}
+ private void updateSamplesFromType()
+ {
+ var rimSamples = getRimSamples();
+
+ bool isRimType = Type == HitType.Rim;
+
+ if (isRimType != rimSamples.Any())
+ {
+ if (isRimType)
+ Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ else
+ {
+ foreach (var sample in rimSamples)
+ Samples.Remove(sample);
+ }
+ }
+ }
+
+ ///
+ /// Returns an array of any samples which would cause this object to be a "rim" type hit.
+ ///
+ private HitSampleInfo[] getRimSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
+
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
index fcd055bcec..cac56d1269 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
@@ -1,8 +1,10 @@
// 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 System.Threading;
using osu.Framework.Bindables;
+using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects
@@ -31,9 +33,31 @@ namespace osu.Game.Rulesets.Taiko.Objects
public bool IsStrong
{
get => IsStrongBindable.Value;
- set => IsStrongBindable.Value = value;
+ set
+ {
+ IsStrongBindable.Value = value;
+ updateSamplesFromStrong();
+ }
}
+ private void updateSamplesFromStrong()
+ {
+ var strongSamples = getStrongSamples();
+
+ if (IsStrongBindable.Value != strongSamples.Any())
+ {
+ if (IsStrongBindable.Value)
+ Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+ else
+ {
+ foreach (var sample in strongSamples)
+ Samples.Remove(sample);
+ }
+ }
+ }
+
+ private HitSampleInfo[] getStrongSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
+
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
diff --git a/osu.Game.Tests/Gameplay/TestSceneProxyContainer.cs b/osu.Game.Tests/Gameplay/TestSceneProxyContainer.cs
new file mode 100644
index 0000000000..1264d575a4
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneProxyContainer.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneProxyContainer : OsuTestScene
+ {
+ private HitObjectContainer hitObjectContainer;
+ private ProxyContainer proxyContainer;
+ private readonly ManualClock clock = new ManualClock();
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = new Container
+ {
+ Children = new Drawable[]
+ {
+ hitObjectContainer = new HitObjectContainer(),
+ proxyContainer = new ProxyContainer()
+ },
+ Clock = new FramedClock(clock)
+ };
+ clock.CurrentTime = 0;
+ });
+
+ [Test]
+ public void TestProxyLifetimeManagement()
+ {
+ AddStep("Add proxy drawables", () =>
+ {
+ addProxy(new TestDrawableHitObject(1000));
+ addProxy(new TestDrawableHitObject(3000));
+ addProxy(new TestDrawableHitObject(5000));
+ });
+
+ AddStep("time = 1000", () => clock.CurrentTime = 1000);
+ AddAssert("One proxy is alive", () => proxyContainer.AliveChildren.Count == 1);
+ AddStep("time = 5000", () => clock.CurrentTime = 5000);
+ AddAssert("One proxy is alive", () => proxyContainer.AliveChildren.Count == 1);
+ AddStep("time = 6000", () => clock.CurrentTime = 6000);
+ AddAssert("No proxy is alive", () => proxyContainer.AliveChildren.Count == 0);
+ }
+
+ private void addProxy(DrawableHitObject drawableHitObject)
+ {
+ hitObjectContainer.Add(drawableHitObject);
+ proxyContainer.AddProxy(drawableHitObject);
+ }
+
+ private class ProxyContainer : LifetimeManagementContainer
+ {
+ public IReadOnlyList AliveChildren => AliveInternalChildren;
+
+ public void AddProxy(Drawable d) => AddInternal(d.CreateProxy());
+ }
+
+ private class TestDrawableHitObject : DrawableHitObject
+ {
+ protected override double InitialLifetimeOffset => 100;
+
+ public TestDrawableHitObject(double startTime)
+ : base(new HitObject { StartTime = startTime })
+ {
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ LifetimeEnd = LifetimeStart + 500;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index 2791952b66..3cedaf9d45 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -11,8 +11,8 @@ using osu.Game.Overlays;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
-using osu.Game.Screens.Select;
using osuTK.Input;
+using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -37,17 +37,17 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtSongSelect()
{
- PushAndConfirm(() => new PlaySongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
- AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) }));
+ AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddAssert("did perform", () => actionPerformed);
- AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
+ AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
}
[Test]
public void TestPerformAtMenuFromSongSelect()
{
- PushAndConfirm(() => new PlaySongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -57,18 +57,18 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtSongSelectFromPlayerLoader()
{
- PushAndConfirm(() => new PlaySongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
- AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) }));
- AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
+ AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
+ AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddAssert("did perform", () => actionPerformed);
}
[Test]
public void TestPerformAtMenuFromPlayerLoader()
{
- PushAndConfirm(() => new PlaySongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 859cefe3a9..253e448bb4 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -34,9 +34,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithEscape()
{
- TestSongSelect songSelect = null;
+ TestPlaySongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestSongSelect());
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
pushEscape();
@@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestOpenModSelectOverlayUsingAction()
{
- TestSongSelect songSelect = null;
+ TestPlaySongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestSongSelect());
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
}
@@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
- PushAndConfirm(() => new TestSongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Navigation
WorkingBeatmap beatmap() => Game.Beatmap.Value;
- PushAndConfirm(() => new TestSongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
@@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Navigation
WorkingBeatmap beatmap() => Game.Beatmap.Value;
- PushAndConfirm(() => new TestSongSelect());
+ PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
@@ -139,9 +139,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestMenuMakesMusic()
{
- TestSongSelect songSelect = null;
+ TestPlaySongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestSongSelect());
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
@@ -153,9 +153,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithClick()
{
- TestSongSelect songSelect = null;
+ TestPlaySongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestSongSelect());
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
@@ -213,9 +213,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestModSelectInput()
{
- TestSongSelect songSelect = null;
+ TestPlaySongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestSongSelect());
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
@@ -234,9 +234,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestBeatmapOptionsInput()
{
- TestSongSelect songSelect = null;
+ TestPlaySongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestSongSelect());
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
@@ -312,11 +312,13 @@ namespace osu.Game.Tests.Visual.Navigation
ConfirmAtMainMenu();
}
- private class TestSongSelect : PlaySongSelect
+ public class TestPlaySongSelect : PlaySongSelect
{
public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
+
+ protected override bool DisplayStableImportPrompt => false;
}
}
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 2af15923a0..f305b7255e 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -22,82 +22,17 @@ namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneAccuracyCircle : OsuTestScene
{
- [Test]
- public void TestLowDRank()
+ [TestCase(0.2, ScoreRank.D)]
+ [TestCase(0.5, ScoreRank.D)]
+ [TestCase(0.75, ScoreRank.C)]
+ [TestCase(0.85, ScoreRank.B)]
+ [TestCase(0.925, ScoreRank.A)]
+ [TestCase(0.975, ScoreRank.S)]
+ [TestCase(0.9999, ScoreRank.S)]
+ [TestCase(1, ScoreRank.X)]
+ public void TestRank(double accuracy, ScoreRank rank)
{
- var score = createScore();
- score.Accuracy = 0.2;
- score.Rank = ScoreRank.D;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestDRank()
- {
- var score = createScore();
- score.Accuracy = 0.5;
- score.Rank = ScoreRank.D;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestCRank()
- {
- var score = createScore();
- score.Accuracy = 0.75;
- score.Rank = ScoreRank.C;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestBRank()
- {
- var score = createScore();
- score.Accuracy = 0.85;
- score.Rank = ScoreRank.B;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestARank()
- {
- var score = createScore();
- score.Accuracy = 0.925;
- score.Rank = ScoreRank.A;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestSRank()
- {
- var score = createScore();
- score.Accuracy = 0.975;
- score.Rank = ScoreRank.S;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestAlmostSSRank()
- {
- var score = createScore();
- score.Accuracy = 0.9999;
- score.Rank = ScoreRank.S;
-
- addCircleStep(score);
- }
-
- [Test]
- public void TestSSRank()
- {
- var score = createScore();
- score.Accuracy = 1;
- score.Rank = ScoreRank.X;
+ var score = createScore(accuracy, rank);
addCircleStep(score);
}
@@ -120,7 +55,7 @@ namespace osu.Game.Tests.Visual.Ranking
}
}
},
- new AccuracyCircle(score, true)
+ new AccuracyCircle(score)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -129,7 +64,7 @@ namespace osu.Game.Tests.Visual.Ranking
};
});
- private ScoreInfo createScore() => new ScoreInfo
+ private ScoreInfo createScore(double accuracy, ScoreRank rank) => new ScoreInfo
{
User = new User
{
@@ -139,9 +74,9 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
TotalScore = 2845370,
- Accuracy = 0.95,
+ Accuracy = accuracy,
MaxCombo = 999,
- Rank = ScoreRank.S,
+ Rank = rank,
Date = DateTimeOffset.Now,
Statistics =
{
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index b2be7cdf88..ba6b6bd529 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -29,13 +29,8 @@ namespace osu.Game.Tests.Visual.Ranking
[TestFixture]
public class TestSceneResultsScreen : OsuManualInputManagerTestScene
{
- private BeatmapManager beatmaps;
-
- [BackgroundDependencyLoader]
- private void load(BeatmapManager beatmaps)
- {
- this.beatmaps = beatmaps;
- }
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
protected override void LoadComplete()
{
@@ -46,10 +41,6 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}
- private TestResultsScreen createResultsScreen() => new TestResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
-
- private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
-
[Test]
public void TestResultsWithoutPlayer()
{
@@ -69,12 +60,25 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
}
- [Test]
- public void TestResultsWithPlayer()
+ [TestCase(0.2, ScoreRank.D)]
+ [TestCase(0.5, ScoreRank.D)]
+ [TestCase(0.75, ScoreRank.C)]
+ [TestCase(0.85, ScoreRank.B)]
+ [TestCase(0.925, ScoreRank.A)]
+ [TestCase(0.975, ScoreRank.S)]
+ [TestCase(0.9999, ScoreRank.S)]
+ [TestCase(1, ScoreRank.X)]
+ public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
{
TestResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
+ {
+ Accuracy = accuracy,
+ Rank = rank
+ };
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
@@ -232,6 +236,10 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value);
}
+ private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? new TestScoreInfo(new OsuRuleset().RulesetInfo));
+
+ private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index 9723409c79..3a63587b30 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -8,13 +8,13 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
+using osu.Game.IO;
using osu.Game.IO.Legacy;
using osu.Game.Overlays.Notifications;
@@ -38,8 +38,6 @@ namespace osu.Game.Collections
public readonly BindableList Collections = new BindableList();
- public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
-
[Resolved]
private GameHost host { get; set; }
@@ -96,25 +94,12 @@ namespace osu.Game.Collections
///
public Action PostNotification { protected get; set; }
- ///
- /// Set a storage with access to an osu-stable install for import purposes.
- ///
- public Func GetStableStorage { private get; set; }
-
///
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
///
- public Task ImportFromStableAsync()
+ public Task ImportFromStableAsync(StableStorage stableStorage)
{
- var stable = GetStableStorage?.Invoke();
-
- if (stable == null)
- {
- Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
- return Task.CompletedTask;
- }
-
- if (!stable.Exists(database_name))
+ if (!stableStorage.Exists(database_name))
{
// This handles situations like when the user does not have a collections.db file
Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
@@ -123,7 +108,7 @@ namespace osu.Game.Collections
return Task.Run(async () =>
{
- using (var stream = stable.GetStream(database_name))
+ using (var stream = stableStorage.GetStream(database_name))
await Import(stream).ConfigureAwait(false);
});
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index e0f80d2743..8efd451857 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Humanizer;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
-using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -81,8 +80,6 @@ namespace osu.Game.Database
public virtual IEnumerable HandledExtensions => new[] { ".zip" };
- public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
-
protected readonly FileStore Files;
protected readonly IDatabaseContextFactory ContextFactory;
@@ -669,16 +666,6 @@ namespace osu.Game.Database
#region osu-stable import
- ///
- /// Set a storage with access to an osu-stable install for import purposes.
- ///
- public Func GetStableStorage { private get; set; }
-
- ///
- /// Denotes whether an osu-stable installation is present to perform automated imports from.
- ///
- public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
-
///
/// The relative path from osu-stable's data directory to import items from.
///
@@ -700,22 +687,16 @@ namespace osu.Game.Database
///
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
///
- public Task ImportFromStableAsync()
+ public Task ImportFromStableAsync(StableStorage stableStorage)
{
- var stableStorage = GetStableStorage?.Invoke();
-
- if (stableStorage == null)
- {
- Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
- return Task.CompletedTask;
- }
-
var storage = PrepareStableStorage(stableStorage);
+ // Handle situations like when the user does not have a Skins folder.
if (!storage.ExistsDirectory(ImportFromStablePath))
{
- // This handles situations like when the user does not have a Skins folder
- Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
+ string fullPath = storage.GetFullPath(ImportFromStablePath);
+
+ Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
diff --git a/osu.Game/Database/StableImportManager.cs b/osu.Game/Database/StableImportManager.cs
new file mode 100644
index 0000000000..63a6db35c0
--- /dev/null
+++ b/osu.Game/Database/StableImportManager.cs
@@ -0,0 +1,96 @@
+// 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 System.Collections.Generic;
+using System.Threading.Tasks;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Game.Beatmaps;
+using osu.Game.Collections;
+using osu.Game.IO;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings.Sections.Maintenance;
+using osu.Game.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Database
+{
+ public class StableImportManager : Component
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
+ [Resolved]
+ private ScoreManager scores { get; set; }
+
+ [Resolved]
+ private CollectionManager collections { get; set; }
+
+ [Resolved]
+ private OsuGame game { get; set; }
+
+ [Resolved]
+ private DialogOverlay dialogOverlay { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private DesktopGameHost desktopGameHost { get; set; }
+
+ private StableStorage cachedStorage;
+
+ public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
+
+ public async Task ImportFromStableAsync(StableContent content)
+ {
+ var stableStorage = await getStableStorage().ConfigureAwait(false);
+ var importTasks = new List();
+
+ Task beatmapImportTask = Task.CompletedTask;
+ if (content.HasFlagFast(StableContent.Beatmaps))
+ importTasks.Add(beatmapImportTask = beatmaps.ImportFromStableAsync(stableStorage));
+
+ if (content.HasFlagFast(StableContent.Skins))
+ importTasks.Add(skins.ImportFromStableAsync(stableStorage));
+
+ if (content.HasFlagFast(StableContent.Collections))
+ importTasks.Add(beatmapImportTask.ContinueWith(_ => collections.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
+
+ if (content.HasFlagFast(StableContent.Scores))
+ importTasks.Add(beatmapImportTask.ContinueWith(_ => scores.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
+
+ await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
+ }
+
+ private async Task getStableStorage()
+ {
+ if (cachedStorage != null)
+ return cachedStorage;
+
+ var stableStorage = game.GetStorageForStableInstall();
+ if (stableStorage != null)
+ return cachedStorage = stableStorage;
+
+ var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
+ var stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
+
+ return cachedStorage = new StableStorage(stablePath, desktopGameHost);
+ }
+ }
+
+ [Flags]
+ public enum StableContent
+ {
+ Beatmaps = 1 << 0,
+ Scores = 1 << 1,
+ Skins = 1 << 2,
+ Collections = 1 << 3,
+ All = Beatmaps | Scores | Skins | Collections
+ }
+}
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 0067a55fd8..a4fc963328 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -40,10 +40,10 @@ namespace osu.Game.Online.Spectator
private readonly List watchingUsers = new List();
public IBindableList PlayingUsers => playingUsers;
-
private readonly BindableList playingUsers = new BindableList();
- private readonly Dictionary playingUserStates = new Dictionary();
+ public IBindableDictionary PlayingUserStates => playingUserStates;
+ private readonly BindableDictionary playingUserStates = new BindableDictionary();
private IBeatmap? currentBeatmap;
@@ -200,6 +200,7 @@ namespace osu.Game.Online.Spectator
Schedule(() =>
{
watchingUsers.Remove(userId);
+ playingUserStates.Remove(userId);
StopWatchingUserInternal(userId);
});
}
@@ -256,33 +257,5 @@ namespace osu.Game.Online.Spectator
lastSendTime = Time.Current;
}
-
- ///
- /// Attempts to retrieve the for a currently-playing user.
- ///
- /// The user.
- /// The current for the user, if they're playing. null if the user is not playing.
- /// true if successful (the user is playing), false otherwise.
- public bool TryGetPlayingUserState(int userId, out SpectatorState state)
- {
- return playingUserStates.TryGetValue(userId, out state);
- }
-
- ///
- /// Bind an action to with the option of running the bound action once immediately.
- ///
- /// The action to perform when a user begins playing.
- /// Whether the action provided in should be run once immediately for all users currently playing.
- public void BindUserBeganPlaying(Action callback, bool runOnceImmediately = false)
- {
- // The lock is taken before the event is subscribed to to prevent doubling of events.
- OnUserBeganPlaying += callback;
-
- if (!runOnceImmediately)
- return;
-
- foreach (var (userId, state) in playingUserStates)
- callback(userId, state);
- }
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index f860cd8dd2..06e0b6e9bf 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -100,6 +100,9 @@ namespace osu.Game
[Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
+ [Cached]
+ private readonly StableImportManager stableImportManager = new StableImportManager();
+
[Cached]
private readonly ScreenshotManager screenshotManager = new ScreenshotManager();
@@ -566,14 +569,11 @@ namespace osu.Game
// todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => notifications.Post(n);
- SkinManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PostNotification = n => notifications.Post(n);
- BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
ScoreManager.PostNotification = n => notifications.Post(n);
- ScoreManager.GetStableStorage = GetStorageForStableInstall;
ScoreManager.PresentImport = items => PresentScore(items.First());
// make config aware of how to lookup skins for on-screen display purposes.
@@ -690,10 +690,10 @@ namespace osu.Game
loadComponentSingleFile(new CollectionManager(Storage)
{
PostNotification = n => notifications.Post(n),
- GetStableStorage = GetStorageForStableInstall
}, Add, true);
loadComponentSingleFile(difficultyRecommender, Add);
+ loadComponentSingleFile(stableImportManager, Add);
loadComponentSingleFile(screenshotManager, Add);
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 754b260bf0..e31e307d4d 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -96,7 +96,8 @@ namespace osu.Game.Overlays.Mods
Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774");
Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e");
- RelativeSizeAxes = Axes.Both;
+ RelativeSizeAxes = Axes.X;
+ Height = HEIGHT;
Padding = new MarginPadding { Horizontal = -OsuScreen.HORIZONTAL_OVERFLOW_PADDING };
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs
index e7c69e89fe..349a112477 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs
@@ -11,9 +11,9 @@ using osuTK;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
-using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Screens;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
@@ -69,20 +69,24 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
+ new Dimension(GridSizeMode.AutoSize),
new Dimension(),
- new Dimension(GridSizeMode.Relative, 0.8f),
- new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
- new OsuSpriteText
+ new OsuTextFlowContainer(cp =>
{
- Text = HeaderText,
- Font = OsuFont.Default.With(size: 40),
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
+ cp.Font = OsuFont.Default.With(size: 24);
+ })
+ {
+ Text = HeaderText.ToString(),
+ TextAnchor = Anchor.TopCentre,
+ Margin = new MarginPadding(10),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
}
},
new Drawable[]
@@ -99,6 +103,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 300,
+ Margin = new MarginPadding(10),
Text = "Select directory",
Action = () => OnSelection(directorySelector.CurrentPath.Value)
},
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index 848ce381a9..a38ca81e23 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Collections;
+using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
using osu.Game.Skinning;
@@ -29,9 +30,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private TriangleButton undeleteButton;
[BackgroundDependencyLoader(permitNulls: true)]
- private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, DialogOverlay dialogOverlay)
+ private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay)
{
- if (beatmaps.SupportsImportFromStable)
+ if (stableImportManager?.SupportsImportFromStable == true)
{
Add(importBeatmapsButton = new SettingsButton
{
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () =>
{
importBeatmapsButton.Enabled.Value = false;
- beatmaps.ImportFromStableAsync().ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true));
+ stableImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true));
}
});
}
@@ -57,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}
});
- if (scores.SupportsImportFromStable)
+ if (stableImportManager?.SupportsImportFromStable == true)
{
Add(importScoresButton = new SettingsButton
{
@@ -65,7 +66,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () =>
{
importScoresButton.Enabled.Value = false;
- scores.ImportFromStableAsync().ContinueWith(t => Schedule(() => importScoresButton.Enabled.Value = true));
+ stableImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(t => Schedule(() => importScoresButton.Enabled.Value = true));
}
});
}
@@ -83,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}
});
- if (skins.SupportsImportFromStable)
+ if (stableImportManager?.SupportsImportFromStable == true)
{
Add(importSkinsButton = new SettingsButton
{
@@ -91,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () =>
{
importSkinsButton.Enabled.Value = false;
- skins.ImportFromStableAsync().ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true));
+ stableImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true));
}
});
}
@@ -111,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
if (collectionManager != null)
{
- if (collectionManager.SupportsImportFromStable)
+ if (stableImportManager?.SupportsImportFromStable == true)
{
Add(importCollectionsButton = new SettingsButton
{
@@ -119,7 +120,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () =>
{
importCollectionsButton.Enabled.Value = false;
- collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true));
+ stableImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true));
}
});
}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs
new file mode 100644
index 0000000000..904c9deaae
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Screens;
+using osu.Game.Overlays.Dialog;
+
+namespace osu.Game.Overlays.Settings.Sections.Maintenance
+{
+ public class StableDirectoryLocationDialog : PopupDialog
+ {
+ [Resolved]
+ private OsuGame game { get; set; }
+
+ public StableDirectoryLocationDialog(TaskCompletionSource taskCompletionSource)
+ {
+ HeaderText = "Failed to automatically locate an osu!stable installation.";
+ BodyText = "An existing install could not be located. If you know where it is, you can help locate it.";
+ Icon = FontAwesome.Solid.QuestionCircle;
+
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogOkButton
+ {
+ Text = "Sure! I know where it is located!",
+ Action = () => Schedule(() => game.PerformFromScreen(screen => screen.Push(new StableDirectorySelectScreen(taskCompletionSource))))
+ },
+ new PopupDialogCancelButton
+ {
+ Text = "Actually I don't have osu!stable installed.",
+ Action = () => taskCompletionSource.TrySetCanceled()
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs
new file mode 100644
index 0000000000..4aea05fb14
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework.Localisation;
+using osu.Framework.Screens;
+
+namespace osu.Game.Overlays.Settings.Sections.Maintenance
+{
+ public class StableDirectorySelectScreen : DirectorySelectScreen
+ {
+ private readonly TaskCompletionSource taskCompletionSource;
+
+ protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled;
+
+ protected override bool IsValidDirectory(DirectoryInfo info) => info?.GetFiles("osu!.*.cfg").Any() ?? false;
+
+ public override LocalisableString HeaderText => "Please select your osu!stable install location";
+
+ public StableDirectorySelectScreen(TaskCompletionSource taskCompletionSource)
+ {
+ this.taskCompletionSource = taskCompletionSource;
+ }
+
+ protected override void OnSelection(DirectoryInfo directory)
+ {
+ taskCompletionSource.TrySetResult(directory.FullName);
+ this.Exit();
+ }
+
+ public override bool OnExiting(IScreen next)
+ {
+ taskCompletionSource.TrySetCanceled();
+ return base.OnExiting(next);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs
index ed0430012a..64e1ac16bd 100644
--- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs
+++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs
@@ -3,7 +3,6 @@
#nullable enable
-using System;
using System.Diagnostics;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling;
@@ -27,13 +26,14 @@ namespace osu.Game.Rulesets.Objects.Pooling
///
protected bool HasEntryApplied { get; private set; }
+ // Drawable's lifetime gets out of sync with entry's lifetime if entry's lifetime is modified.
+ // We cannot delegate getter to `Entry.LifetimeStart` because it is incompatible with `LifetimeManagementContainer` due to how lifetime change is detected.
public override double LifetimeStart
{
- get => Entry?.LifetimeStart ?? double.MinValue;
+ get => base.LifetimeStart;
set
{
- if (Entry == null && LifetimeStart != value)
- throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime)} when entry is not set");
+ base.LifetimeStart = value;
if (Entry != null)
Entry.LifetimeStart = value;
@@ -42,11 +42,10 @@ namespace osu.Game.Rulesets.Objects.Pooling
public override double LifetimeEnd
{
- get => Entry?.LifetimeEnd ?? double.MaxValue;
+ get => base.LifetimeEnd;
set
{
- if (Entry == null && LifetimeEnd != value)
- throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime)} when entry is not set");
+ base.LifetimeEnd = value;
if (Entry != null)
Entry.LifetimeEnd = value;
@@ -80,7 +79,12 @@ namespace osu.Game.Rulesets.Objects.Pooling
free();
Entry = entry;
+
+ base.LifetimeStart = entry.LifetimeStart;
+ base.LifetimeEnd = entry.LifetimeEnd;
+
OnApply(entry);
+
HasEntryApplied = true;
}
@@ -112,7 +116,11 @@ namespace osu.Game.Rulesets.Objects.Pooling
Debug.Assert(Entry != null && HasEntryApplied);
OnFree(Entry);
+
Entry = null;
+ base.LifetimeStart = double.MinValue;
+ base.LifetimeEnd = double.MaxValue;
+
HasEntryApplied = false;
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 375aac729d..a53e253581 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -72,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Depth = float.MinValue,
- RelativeSizeAxes = Axes.Both,
- Height = 0.5f,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index bca3a07fa6..c70b4dd35b 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -10,11 +10,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
-using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
-using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Ranking.Expanded.Accuracy
@@ -76,19 +74,14 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
private readonly ScoreInfo score;
- private readonly bool withFlair;
-
private SmoothCircularProgress accuracyCircle;
private SmoothCircularProgress innerMask;
private Container badges;
private RankText rankText;
- private SkinnableSound applauseSound;
-
- public AccuracyCircle(ScoreInfo score, bool withFlair)
+ public AccuracyCircle(ScoreInfo score)
{
this.score = score;
- this.withFlair = withFlair;
}
[BackgroundDependencyLoader]
@@ -211,13 +204,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
},
rankText = new RankText(score.Rank)
};
-
- if (withFlair)
- {
- AddInternal(applauseSound = score.Rank >= ScoreRank.A
- ? new SkinnableSound(new SampleInfo("Results/rankpass", "applause"))
- : new SkinnableSound(new SampleInfo("Results/rankfail")));
- }
}
private ScoreRank getRank(ScoreRank rank)
@@ -256,7 +242,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true))
{
- this.Delay(-1440).Schedule(() => applauseSound?.Play());
rankText.Appear();
}
}
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 6a6b39b61c..4895240314 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -122,7 +122,7 @@ namespace osu.Game.Screens.Ranking.Expanded
Margin = new MarginPadding { Top = 40 },
RelativeSizeAxes = Axes.X,
Height = 230,
- Child = new AccuracyCircle(score, withFlair)
+ Child = new AccuracyCircle(score)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index c1f5d92d17..a0ea27b640 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Screens;
+using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@@ -19,13 +20,20 @@ using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Ranking
{
public abstract class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler
{
+ ///
+ /// Delay before the default applause sound should be played, in order to match the grade display timing in .
+ ///
+ public const double APPLAUSE_DELAY = AccuracyCircle.ACCURACY_TRANSFORM_DELAY + AccuracyCircle.TEXT_APPEAR_DELAY + ScorePanel.RESIZE_DURATION + ScorePanel.TOP_LAYER_EXPAND_DELAY - 1440;
+
protected const float BACKGROUND_BLUR = 20;
private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y;
@@ -56,6 +64,8 @@ namespace osu.Game.Screens.Ranking
private readonly bool allowRetry;
private readonly bool allowWatchingReplay;
+ private SkinnableSound applauseSound;
+
protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
@@ -146,6 +156,13 @@ namespace osu.Game.Screens.Ranking
bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay);
ScorePanelList.AddScore(Score, shouldFlair);
+
+ if (shouldFlair)
+ {
+ AddInternal(applauseSound = Score.Rank >= ScoreRank.A
+ ? new SkinnableSound(new SampleInfo("Results/rankpass", "applause"))
+ : new SkinnableSound(new SampleInfo("Results/rankfail")));
+ }
}
if (allowWatchingReplay)
@@ -183,6 +200,9 @@ namespace osu.Game.Screens.Ranking
api.Queue(req);
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
+
+ using (BeginDelayedSequence(APPLAUSE_DELAY))
+ Schedule(() => applauseSound?.Play());
}
protected override void Update()
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index df710e4eb8..f66a998db6 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -54,12 +54,12 @@ namespace osu.Game.Screens.Ranking
///
/// Duration for the panel to resize into its expanded/contracted size.
///
- private const double resize_duration = 200;
+ public const double RESIZE_DURATION = 200;
///
- /// Delay after before the top layer is expanded.
+ /// Delay after before the top layer is expanded.
///
- private const double top_layer_expand_delay = 100;
+ public const double TOP_LAYER_EXPAND_DELAY = 100;
///
/// Duration for the top layer expansion.
@@ -208,8 +208,8 @@ namespace osu.Game.Screens.Ranking
case PanelState.Expanded:
Size = new Vector2(EXPANDED_WIDTH, expanded_height);
- topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint);
- middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
+ topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
+ middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0));
@@ -221,20 +221,20 @@ namespace osu.Game.Screens.Ranking
case PanelState.Contracted:
Size = new Vector2(CONTRACTED_WIDTH, contracted_height);
- topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint);
- middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint);
+ topLayerBackground.FadeColour(contracted_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
+ middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0));
middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
break;
}
- content.ResizeTo(Size, resize_duration, Easing.OutQuint);
+ content.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
bool topLayerExpanded = topLayerContainer.Y < 0;
// If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state.
- using (BeginDelayedSequence(topLayerExpanded ? 0 : resize_duration + top_layer_expand_delay, true))
+ using (BeginDelayedSequence(topLayerExpanded ? 0 : RESIZE_DURATION + TOP_LAYER_EXPAND_DELAY, true))
{
topLayerContainer.FadeIn();
diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs
index 8dab83b24c..d8137432bd 100644
--- a/osu.Game/Screens/Select/ImportFromStablePopup.cs
+++ b/osu.Game/Screens/Select/ImportFromStablePopup.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select
public ImportFromStablePopup(Action importFromStable)
{
HeaderText = @"You have no beatmaps!";
- BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins, collections and scores?\nThis will create a second copy of all files on disk.";
+ BodyText = "Would you like to import your beatmaps, skins, collections and scores from an existing osu!stable installation?\nThis will create a second copy of all files on disk.";
Icon = FontAwesome.Solid.Plane;
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 215700d87c..74e10037ab 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -22,7 +22,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select.Options;
-using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -35,9 +34,9 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Scoring;
using System.Diagnostics;
using osu.Game.Screens.Play;
+using osu.Game.Database;
namespace osu.Game.Screens.Select
{
@@ -52,6 +51,8 @@ namespace osu.Game.Screens.Select
protected virtual bool ShowFooter => true;
+ protected virtual bool DisplayStableImportPrompt => stableImportManager?.SupportsImportFromStable == true;
+
///
/// Can be null if is false.
///
@@ -84,6 +85,9 @@ namespace osu.Game.Screens.Select
[Resolved]
private BeatmapManager beatmaps { get; set; }
+ [Resolved(CanBeNull = true)]
+ private StableImportManager stableImportManager { get; set; }
+
protected ModSelectOverlay ModSelect { get; private set; }
protected Sample SampleConfirm { get; private set; }
@@ -101,7 +105,7 @@ namespace osu.Game.Screens.Select
private MusicController music { get; set; }
[BackgroundDependencyLoader(true)]
- private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
+ private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
{
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
@@ -282,18 +286,12 @@ namespace osu.Game.Screens.Select
{
Schedule(() =>
{
- // if we have no beatmaps but osu-stable is found, let's prompt the user to import.
- if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && beatmaps.StableInstallationAvailable)
+ // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one.
+ if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && DisplayStableImportPrompt)
{
dialogOverlay.Push(new ImportFromStablePopup(() =>
{
- Task.Run(beatmaps.ImportFromStableAsync)
- .ContinueWith(_ =>
- {
- Task.Run(scores.ImportFromStableAsync);
- Task.Run(collections.ImportFromStableAsync);
- }, TaskContinuationOptions.OnlyOnRanToCompletion);
- Task.Run(skins.ImportFromStableAsync);
+ Task.Run(() => stableImportManager.ImportFromStableAsync(StableContent.All));
}));
}
});
diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs
index e6c9a0acd4..9a20bb58b8 100644
--- a/osu.Game/Screens/Spectate/SpectatorScreen.cs
+++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Spectator;
@@ -42,6 +43,8 @@ namespace osu.Game.Screens.Spectate
[Resolved]
private UserLookupCache userLookupCache { get; set; }
+ private readonly IBindableDictionary playingUserStates = new BindableDictionary();
+
private readonly Dictionary userMap = new Dictionary();
private readonly Dictionary gameplayStates = new Dictionary();
@@ -65,8 +68,9 @@ namespace osu.Game.Screens.Spectate
foreach (var u in users.Result)
userMap[u.Id] = u;
- spectatorClient.BindUserBeganPlaying(userBeganPlaying, true);
- spectatorClient.OnUserFinishedPlaying += userFinishedPlaying;
+ playingUserStates.BindTo(spectatorClient.PlayingUserStates);
+ playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
+
spectatorClient.OnNewFrames += userSentFrames;
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
@@ -102,7 +106,7 @@ namespace osu.Game.Screens.Spectate
foreach (var (userId, _) in userMap)
{
- if (!spectatorClient.TryGetPlayingUserState(userId, out var userState))
+ if (!playingUserStates.TryGetValue(userId, out var userState))
continue;
if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID))
@@ -110,7 +114,31 @@ namespace osu.Game.Screens.Spectate
}
}
- private void userBeganPlaying(int userId, SpectatorState state)
+ private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyDictionaryChangedAction.Add:
+ foreach (var (userId, state) in e.NewItems.AsNonNull())
+ onUserStateAdded(userId, state);
+ break;
+
+ case NotifyDictionaryChangedAction.Remove:
+ foreach (var (userId, _) in e.OldItems.AsNonNull())
+ onUserStateRemoved(userId);
+ break;
+
+ case NotifyDictionaryChangedAction.Replace:
+ foreach (var (userId, _) in e.OldItems.AsNonNull())
+ onUserStateRemoved(userId);
+
+ foreach (var (userId, state) in e.NewItems.AsNonNull())
+ onUserStateAdded(userId, state);
+ break;
+ }
+ }
+
+ private void onUserStateAdded(int userId, SpectatorState state)
{
if (state.RulesetID == null || state.BeatmapID == null)
return;
@@ -118,24 +146,30 @@ namespace osu.Game.Screens.Spectate
if (!userMap.ContainsKey(userId))
return;
- // The user may have stopped playing.
- if (!spectatorClient.TryGetPlayingUserState(userId, out _))
+ Schedule(() => OnUserStateChanged(userId, state));
+ updateGameplayState(userId);
+ }
+
+ private void onUserStateRemoved(int userId)
+ {
+ if (!userMap.ContainsKey(userId))
return;
- Schedule(() => OnUserStateChanged(userId, state));
+ if (!gameplayStates.TryGetValue(userId, out var gameplayState))
+ return;
- updateGameplayState(userId);
+ gameplayState.Score.Replay.HasReceivedAllFrames = true;
+
+ gameplayStates.Remove(userId);
+ Schedule(() => EndGameplay(userId));
}
private void updateGameplayState(int userId)
{
Debug.Assert(userMap.ContainsKey(userId));
- // The user may have stopped playing.
- if (!spectatorClient.TryGetPlayingUserState(userId, out var spectatorState))
- return;
-
var user = userMap[userId];
+ var spectatorState = playingUserStates[userId];
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance();
if (resolvedRuleset == null)
@@ -186,20 +220,6 @@ namespace osu.Game.Screens.Spectate
}
}
- private void userFinishedPlaying(int userId, SpectatorState state)
- {
- if (!userMap.ContainsKey(userId))
- return;
-
- if (!gameplayStates.TryGetValue(userId, out var gameplayState))
- return;
-
- gameplayState.Score.Replay.HasReceivedAllFrames = true;
-
- gameplayStates.Remove(userId);
- Schedule(() => EndGameplay(userId));
- }
-
///
/// Invoked when a spectated user's state has changed.
///
@@ -226,7 +246,7 @@ namespace osu.Game.Screens.Spectate
/// The user to stop spectating.
protected void RemoveUser(int userId)
{
- userFinishedPlaying(userId, null);
+ onUserStateRemoved(userId);
userIds.Remove(userId);
userMap.Remove(userId);
@@ -240,8 +260,6 @@ namespace osu.Game.Screens.Spectate
if (spectatorClient != null)
{
- spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
- spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying;
spectatorClient.OnNewFrames -= userSentFrames;
foreach (var (userId, _) in userMap)
diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs
index b7af1c0fcd..7f04252c6b 100644
--- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs
+++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@@ -63,39 +64,43 @@ namespace osu.Game.Skinning.Editor
adjustScaleFromAnchor(ref scale, anchor);
- var selectionQuad = getSelectionQuad();
+ // the selection quad is always upright, so use an AABB rect to make mutating the values easier.
+ var selectionRect = getSelectionQuad().AABBFloat;
- // the selection quad is always upright, so use a rect to make mutating the values easier.
- var adjustedRect = selectionQuad.AABBFloat;
+ // copy to mutate, as we will need to compare to the original later on.
+ var adjustedRect = selectionRect;
- // for now aspect lock scale adjustments that occur at corners.
- if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
- scale.Y = scale.X / selectionQuad.Width * selectionQuad.Height;
+ // first, remove any scale axis we are not interested in.
+ if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0;
+ if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0;
- if (anchor.HasFlagFast(Anchor.x0))
+ bool shouldAspectLock =
+ // for now aspect lock scale adjustments that occur at corners..
+ (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
+ // ..or if any of the selection have been rotated.
+ // this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
+ || SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0));
+
+ if (shouldAspectLock)
{
- adjustedRect.X -= scale.X;
- adjustedRect.Width += scale.X;
- }
- else if (anchor.HasFlagFast(Anchor.x2))
- {
- adjustedRect.Width += scale.X;
+ if (anchor.HasFlagFast(Anchor.x1))
+ // if dragging from the horizontal centre, only a vertical component is available.
+ scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
+ else
+ // in all other cases (arbitrarily) use the horizontal component for aspect lock.
+ scale.Y = scale.X / selectionRect.Width * selectionRect.Height;
}
- if (anchor.HasFlagFast(Anchor.y0))
- {
- adjustedRect.Y -= scale.Y;
- adjustedRect.Height += scale.Y;
- }
- else if (anchor.HasFlagFast(Anchor.y2))
- {
- adjustedRect.Height += scale.Y;
- }
+ if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
+ if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
- // scale adjust should match that of the quad itself.
+ adjustedRect.Width += scale.X;
+ adjustedRect.Height += scale.Y;
+
+ // scale adjust applied to each individual item should match that of the quad itself.
var scaledDelta = new Vector2(
- adjustedRect.Width / selectionQuad.Width,
- adjustedRect.Height / selectionQuad.Height
+ adjustedRect.Width / selectionRect.Width,
+ adjustedRect.Height / selectionRect.Height
);
foreach (var b in SelectedBlueprints)
@@ -107,8 +112,8 @@ namespace osu.Game.Skinning.Editor
var relativePositionInOriginal =
new Vector2(
- (screenPosition.X - selectionQuad.TopLeft.X) / selectionQuad.Width,
- (screenPosition.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height
+ (screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width,
+ (screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height
);
var newPositionInAdjusted = new Vector2(
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 587bdaf622..1e3b77cd70 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7ba7a554d6..a2a9ac35fc 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+