1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-27 17:53:15 +08:00

Merge branch 'master' into localisation-proof-of-concept

This commit is contained in:
Dean Herbert 2021-05-22 17:07:23 +09:00
commit b13a68592f
33 changed files with 544 additions and 375 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.513.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.521.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -14,10 +14,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{ {
private readonly HitPiece piece; private readonly HitPiece piece;
private static Hit hit; public new Hit HitObject => (Hit)base.HitObject;
public HitPlacementBlueprint() public HitPlacementBlueprint()
: base(hit = new Hit()) : base(new Hit())
{ {
InternalChild = piece = new HitPiece InternalChild = piece = new HitPiece
{ {
@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
switch (e.Button) switch (e.Button)
{ {
case MouseButton.Left: case MouseButton.Left:
hit.Type = HitType.Centre; HitObject.Type = HitType.Centre;
EndPlacement(true); EndPlacement(true);
return true; return true;
case MouseButton.Right: case MouseButton.Right:
hit.Type = HitType.Rim; HitObject.Type = HitType.Rim;
EndPlacement(true); EndPlacement(true);
return true; return true;
} }

View File

@ -52,23 +52,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply() protected override void OnApply()
{ {
type.BindTo(HitObject.TypeBindable); type.BindTo(HitObject.TypeBindable);
type.BindValueChanged(_ => // this doesn't need to be run inline as RecreatePieces is called by the base call below.
{ type.BindValueChanged(_ => Scheduler.AddOnce(RecreatePieces));
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();
base.OnApply(); base.OnApply();
} }
protected override void RecreatePieces()
{
updateActionsFromType();
base.RecreatePieces();
}
protected override void OnFree() protected override void OnFree()
{ {
base.OnFree(); base.OnFree();
@ -83,33 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
validActionPressed = pressHandledThisFrame = false; 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() private void updateActionsFromType()
{ {
HitActions = HitActions =

View File

@ -137,7 +137,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE); Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
MainPiece?.Expire(); if (MainPiece != null)
Content.Remove(MainPiece);
Content.Add(MainPiece = CreateMainPiece()); Content.Add(MainPiece = CreateMainPiece());
} }

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
@ -29,14 +27,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply() protected override void OnApply()
{ {
isStrong.BindTo(HitObject.IsStrongBindable); isStrong.BindTo(HitObject.IsStrongBindable);
isStrong.BindValueChanged(_ => // this doesn't need to be run inline as RecreatePieces is called by the base call below.
{ isStrong.BindValueChanged(_ => Scheduler.AddOnce(RecreatePieces));
// will overwrite samples, should only be called on subsequent changes
// after the initial application.
updateSamplesFromStrong();
RecreatePieces();
});
base.OnApply(); base.OnApply();
} }
@ -50,30 +42,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
isStrong.UnbindEvents(); 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() protected override void RecreatePieces()
{ {
base.RecreatePieces(); base.RecreatePieces();

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
@ -15,9 +17,36 @@ namespace osu.Game.Rulesets.Taiko.Objects
public HitType Type public HitType Type
{ {
get => TypeBindable.Value; 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);
}
}
}
/// <summary>
/// Returns an array of any samples which would cause this object to be a "rim" type hit.
/// </summary>
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 }; protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject

View File

@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
@ -31,9 +33,31 @@ namespace osu.Game.Rulesets.Taiko.Objects
public bool IsStrong public bool IsStrong
{ {
get => IsStrongBindable.Value; 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) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(cancellationToken); base.CreateNestedHitObjects(cancellationToken);

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using 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<Drawable> 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;
}
}
}
}

View File

@ -11,8 +11,8 @@ using osu.Game.Overlays;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osuTK.Input; using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
@ -37,17 +37,17 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestPerformAtSongSelect() 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("did perform", () => actionPerformed);
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
} }
[Test] [Test]
public void TestPerformAtMenuFromSongSelect() public void TestPerformAtMenuFromSongSelect()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@ -57,18 +57,18 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestPerformAtSongSelectFromPlayerLoader() public void TestPerformAtSongSelectFromPlayerLoader()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) })); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddAssert("did perform", () => actionPerformed); AddAssert("did perform", () => actionPerformed);
} }
[Test] [Test]
public void TestPerformAtMenuFromPlayerLoader() public void TestPerformAtMenuFromPlayerLoader()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));

View File

@ -34,9 +34,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestExitSongSelectWithEscape() public void TestExitSongSelectWithEscape()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
pushEscape(); pushEscape();
@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestOpenModSelectOverlayUsingAction() 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)); AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
} }
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
Player player = null; Player player = null;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Navigation
WorkingBeatmap beatmap() => Game.Beatmap.Value; WorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Navigation
WorkingBeatmap beatmap() => Game.Beatmap.Value; WorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
@ -139,9 +139,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestMenuMakesMusic() 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); AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
@ -153,9 +153,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestExitSongSelectWithClick() public void TestExitSongSelectWithClick()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition)); AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
@ -213,9 +213,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestModSelectInput() public void TestModSelectInput()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
@ -234,9 +234,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestBeatmapOptionsInput() public void TestBeatmapOptionsInput()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show()); AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
@ -312,11 +312,13 @@ namespace osu.Game.Tests.Visual.Navigation
ConfirmAtMainMenu(); ConfirmAtMainMenu();
} }
private class TestSongSelect : PlaySongSelect public class TestPlaySongSelect : PlaySongSelect
{ {
public ModSelectOverlay ModSelectOverlay => ModSelect; public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
protected override bool DisplayStableImportPrompt => false;
} }
} }
} }

View File

@ -22,82 +22,17 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
public class TestSceneAccuracyCircle : OsuTestScene public class TestSceneAccuracyCircle : OsuTestScene
{ {
[Test] [TestCase(0.2, ScoreRank.D)]
public void TestLowDRank() [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(); var score = createScore(accuracy, rank);
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;
addCircleStep(score); addCircleStep(score);
} }
@ -120,7 +55,7 @@ namespace osu.Game.Tests.Visual.Ranking
} }
} }
}, },
new AccuracyCircle(score, true) new AccuracyCircle(score)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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 User = new User
{ {
@ -139,9 +74,9 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
TotalScore = 2845370, TotalScore = 2845370,
Accuracy = 0.95, Accuracy = accuracy,
MaxCombo = 999, MaxCombo = 999,
Rank = ScoreRank.S, Rank = rank,
Date = DateTimeOffset.Now, Date = DateTimeOffset.Now,
Statistics = Statistics =
{ {

View File

@ -29,13 +29,8 @@ namespace osu.Game.Tests.Visual.Ranking
[TestFixture] [TestFixture]
public class TestSceneResultsScreen : OsuManualInputManagerTestScene public class TestSceneResultsScreen : OsuManualInputManagerTestScene
{ {
private BeatmapManager beatmaps; [Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps)
{
this.beatmaps = beatmaps;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -46,10 +41,6 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); 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] [Test]
public void TestResultsWithoutPlayer() public void TestResultsWithoutPlayer()
{ {
@ -69,12 +60,25 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("retry overlay not present", () => screen.RetryOverlay == null); AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
} }
[Test] [TestCase(0.2, ScoreRank.D)]
public void TestResultsWithPlayer() [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; 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); AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null); AddAssert("retry overlay present", () => screen.RetryOverlay != null);
} }
@ -232,6 +236,10 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value); AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().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 private class TestResultsContainer : Container
{ {
[Cached(typeof(Player))] [Cached(typeof(Player))]

View File

@ -8,13 +8,13 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -38,8 +38,6 @@ namespace osu.Game.Collections
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>(); public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
@ -96,25 +94,12 @@ namespace osu.Game.Collections
/// </summary> /// </summary>
public Action<Notification> PostNotification { protected get; set; } public Action<Notification> PostNotification { protected get; set; }
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>
public Task ImportFromStableAsync() public Task ImportFromStableAsync(StableStorage stableStorage)
{ {
var stable = GetStableStorage?.Invoke(); if (!stableStorage.Exists(database_name))
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
if (!stable.Exists(database_name))
{ {
// This handles situations like when the user does not have a collections.db file // 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); 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 () => return Task.Run(async () =>
{ {
using (var stream = stable.GetStream(database_name)) using (var stream = stableStorage.GetStream(database_name))
await Import(stream).ConfigureAwait(false); await Import(stream).ConfigureAwait(false);
}); });
} }

View File

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Humanizer; using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
@ -81,8 +80,6 @@ namespace osu.Game.Database
public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" }; public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" };
public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
protected readonly FileStore Files; protected readonly FileStore Files;
protected readonly IDatabaseContextFactory ContextFactory; protected readonly IDatabaseContextFactory ContextFactory;
@ -669,16 +666,6 @@ namespace osu.Game.Database
#region osu-stable import #region osu-stable import
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<StableStorage> GetStableStorage { private get; set; }
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
/// </summary>
public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
/// <summary> /// <summary>
/// The relative path from osu-stable's data directory to import items from. /// The relative path from osu-stable's data directory to import items from.
/// </summary> /// </summary>
@ -700,22 +687,16 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>
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); var storage = PrepareStableStorage(stableStorage);
// Handle situations like when the user does not have a Skins folder.
if (!storage.ExistsDirectory(ImportFromStablePath)) if (!storage.ExistsDirectory(ImportFromStablePath))
{ {
// This handles situations like when the user does not have a Skins folder string fullPath = storage.GetFullPath(ImportFromStablePath);
Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -0,0 +1,96 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using 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>();
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<StableStorage> getStableStorage()
{
if (cachedStorage != null)
return cachedStorage;
var stableStorage = game.GetStorageForStableInstall();
if (stableStorage != null)
return cachedStorage = stableStorage;
var taskCompletionSource = new TaskCompletionSource<string>(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
}
}

View File

@ -40,10 +40,10 @@ namespace osu.Game.Online.Spectator
private readonly List<int> watchingUsers = new List<int>(); private readonly List<int> watchingUsers = new List<int>();
public IBindableList<int> PlayingUsers => playingUsers; public IBindableList<int> PlayingUsers => playingUsers;
private readonly BindableList<int> playingUsers = new BindableList<int>(); private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly Dictionary<int, SpectatorState> playingUserStates = new Dictionary<int, SpectatorState>(); public IBindableDictionary<int, SpectatorState> PlayingUserStates => playingUserStates;
private readonly BindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
private IBeatmap? currentBeatmap; private IBeatmap? currentBeatmap;
@ -200,6 +200,7 @@ namespace osu.Game.Online.Spectator
Schedule(() => Schedule(() =>
{ {
watchingUsers.Remove(userId); watchingUsers.Remove(userId);
playingUserStates.Remove(userId);
StopWatchingUserInternal(userId); StopWatchingUserInternal(userId);
}); });
} }
@ -256,33 +257,5 @@ namespace osu.Game.Online.Spectator
lastSendTime = Time.Current; lastSendTime = Time.Current;
} }
/// <summary>
/// Attempts to retrieve the <see cref="SpectatorState"/> for a currently-playing user.
/// </summary>
/// <param name="userId">The user.</param>
/// <param name="state">The current <see cref="SpectatorState"/> for the user, if they're playing. <c>null</c> if the user is not playing.</param>
/// <returns><c>true</c> if successful (the user is playing), <c>false</c> otherwise.</returns>
public bool TryGetPlayingUserState(int userId, out SpectatorState state)
{
return playingUserStates.TryGetValue(userId, out state);
}
/// <summary>
/// Bind an action to <see cref="OnUserBeganPlaying"/> with the option of running the bound action once immediately.
/// </summary>
/// <param name="callback">The action to perform when a user begins playing.</param>
/// <param name="runOnceImmediately">Whether the action provided in <paramref name="callback"/> should be run once immediately for all users currently playing.</param>
public void BindUserBeganPlaying(Action<int, SpectatorState> 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);
}
} }
} }

View File

@ -101,6 +101,9 @@ namespace osu.Game
[Cached] [Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
[Cached]
private readonly StableImportManager stableImportManager = new StableImportManager();
[Cached] [Cached]
private readonly ScreenshotManager screenshotManager = new ScreenshotManager(); private readonly ScreenshotManager screenshotManager = new ScreenshotManager();
@ -573,14 +576,11 @@ namespace osu.Game
// todo: all archive managers should be able to be looped here. // todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => notifications.Post(n); SkinManager.PostNotification = n => notifications.Post(n);
SkinManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PostNotification = n => notifications.Post(n); BeatmapManager.PostNotification = n => notifications.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
ScoreManager.PostNotification = n => notifications.Post(n); ScoreManager.PostNotification = n => notifications.Post(n);
ScoreManager.GetStableStorage = GetStorageForStableInstall;
ScoreManager.PresentImport = items => PresentScore(items.First()); ScoreManager.PresentImport = items => PresentScore(items.First());
// make config aware of how to lookup skins for on-screen display purposes. // make config aware of how to lookup skins for on-screen display purposes.
@ -697,10 +697,10 @@ namespace osu.Game
loadComponentSingleFile(new CollectionManager(Storage) loadComponentSingleFile(new CollectionManager(Storage)
{ {
PostNotification = n => notifications.Post(n), PostNotification = n => notifications.Post(n),
GetStableStorage = GetStorageForStableInstall
}, Add, true); }, Add, true);
loadComponentSingleFile(difficultyRecommender, Add); loadComponentSingleFile(difficultyRecommender, Add);
loadComponentSingleFile(stableImportManager, Add);
loadComponentSingleFile(screenshotManager, Add); loadComponentSingleFile(screenshotManager, Add);

View File

@ -96,7 +96,8 @@ namespace osu.Game.Overlays.Mods
Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774");
Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e"); Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e");
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.X;
Height = HEIGHT;
Padding = new MarginPadding { Horizontal = -OsuScreen.HORIZONTAL_OVERFLOW_PADDING }; Padding = new MarginPadding { Horizontal = -OsuScreen.HORIZONTAL_OVERFLOW_PADDING };

View File

@ -11,9 +11,9 @@ using osuTK;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings.Sections.Maintenance namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
@ -69,20 +69,24 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize),
new Dimension(), new Dimension(),
new Dimension(GridSizeMode.Relative, 0.8f), new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}, },
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
{ {
new OsuSpriteText new OsuTextFlowContainer(cp =>
{ {
Text = HeaderText, cp.Font = OsuFont.Default.With(size: 24);
Font = OsuFont.Default.With(size: 40), })
Origin = Anchor.Centre, {
Anchor = Anchor.Centre, Text = HeaderText.ToString(),
TextAnchor = Anchor.TopCentre,
Margin = new MarginPadding(10),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
} }
}, },
new Drawable[] new Drawable[]
@ -99,6 +103,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Width = 300, Width = 300,
Margin = new MarginPadding(10),
Text = "Select directory", Text = "Select directory",
Action = () => OnSelection(directorySelector.CurrentPath.Value) Action = () => OnSelection(directorySelector.CurrentPath.Value)
}, },

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -29,9 +30,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private TriangleButton undeleteButton; private TriangleButton undeleteButton;
[BackgroundDependencyLoader(permitNulls: true)] [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 Add(importBeatmapsButton = new SettingsButton
{ {
@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () => Action = () =>
{ {
importBeatmapsButton.Enabled.Value = false; 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 Add(importScoresButton = new SettingsButton
{ {
@ -65,7 +66,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () => Action = () =>
{ {
importScoresButton.Enabled.Value = false; 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 Add(importSkinsButton = new SettingsButton
{ {
@ -91,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () => Action = () =>
{ {
importSkinsButton.Enabled.Value = false; 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 != null)
{ {
if (collectionManager.SupportsImportFromStable) if (stableImportManager?.SupportsImportFromStable == true)
{ {
Add(importCollectionsButton = new SettingsButton Add(importCollectionsButton = new SettingsButton
{ {
@ -119,7 +120,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () => Action = () =>
{ {
importCollectionsButton.Enabled.Value = false; importCollectionsButton.Enabled.Value = false;
collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); stableImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true));
} }
}); });
} }

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using osu.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<string> 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()
}
};
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<string> 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<string> 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);
}
}
}

View File

@ -3,7 +3,6 @@
#nullable enable #nullable enable
using System;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
@ -27,13 +26,14 @@ namespace osu.Game.Rulesets.Objects.Pooling
/// </summary> /// </summary>
protected bool HasEntryApplied { get; private set; } 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 public override double LifetimeStart
{ {
get => Entry?.LifetimeStart ?? double.MinValue; get => base.LifetimeStart;
set set
{ {
if (Entry == null && LifetimeStart != value) base.LifetimeStart = value;
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
if (Entry != null) if (Entry != null)
Entry.LifetimeStart = value; Entry.LifetimeStart = value;
@ -42,11 +42,10 @@ namespace osu.Game.Rulesets.Objects.Pooling
public override double LifetimeEnd public override double LifetimeEnd
{ {
get => Entry?.LifetimeEnd ?? double.MaxValue; get => base.LifetimeEnd;
set set
{ {
if (Entry == null && LifetimeEnd != value) base.LifetimeEnd = value;
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
if (Entry != null) if (Entry != null)
Entry.LifetimeEnd = value; Entry.LifetimeEnd = value;
@ -80,7 +79,12 @@ namespace osu.Game.Rulesets.Objects.Pooling
free(); free();
Entry = entry; Entry = entry;
base.LifetimeStart = entry.LifetimeStart;
base.LifetimeEnd = entry.LifetimeEnd;
OnApply(entry); OnApply(entry);
HasEntryApplied = true; HasEntryApplied = true;
} }
@ -112,7 +116,11 @@ namespace osu.Game.Rulesets.Objects.Pooling
Debug.Assert(Entry != null && HasEntryApplied); Debug.Assert(Entry != null && HasEntryApplied);
OnFree(Entry); OnFree(Entry);
Entry = null; Entry = null;
base.LifetimeStart = double.MinValue;
base.LifetimeEnd = double.MaxValue;
HasEntryApplied = false; HasEntryApplied = false;
} }
} }

View File

@ -72,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Depth = float.MinValue, Depth = float.MinValue,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
Height = 0.5f, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }, Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay Child = userModsSelectOverlay = new UserModSelectOverlay
{ {

View File

@ -10,11 +10,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Screens.Ranking.Expanded.Accuracy namespace osu.Game.Screens.Ranking.Expanded.Accuracy
@ -76,19 +74,14 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly bool withFlair;
private SmoothCircularProgress accuracyCircle; private SmoothCircularProgress accuracyCircle;
private SmoothCircularProgress innerMask; private SmoothCircularProgress innerMask;
private Container<RankBadge> badges; private Container<RankBadge> badges;
private RankText rankText; private RankText rankText;
private SkinnableSound applauseSound; public AccuracyCircle(ScoreInfo score)
public AccuracyCircle(ScoreInfo score, bool withFlair)
{ {
this.score = score; this.score = score;
this.withFlair = withFlair;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -211,13 +204,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
}, },
rankText = new RankText(score.Rank) 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) private ScoreRank getRank(ScoreRank rank)
@ -256,7 +242,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true)) using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true))
{ {
this.Delay(-1440).Schedule(() => applauseSound?.Play());
rankText.Appear(); rankText.Appear();
} }
} }

View File

@ -122,7 +122,7 @@ namespace osu.Game.Screens.Ranking.Expanded
Margin = new MarginPadding { Top = 40 }, Margin = new MarginPadding { Top = 40 },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 230, Height = 230,
Child = new AccuracyCircle(score, withFlair) Child = new AccuracyCircle(score)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -19,13 +20,20 @@ using osu.Game.Online.API;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
public abstract class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction> public abstract class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>
{ {
/// <summary>
/// Delay before the default applause sound should be played, in order to match the grade display timing in <see cref="AccuracyCircle"/>.
/// </summary>
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; protected const float BACKGROUND_BLUR = 20;
private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y; 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 allowRetry;
private readonly bool allowWatchingReplay; private readonly bool allowWatchingReplay;
private SkinnableSound applauseSound;
protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true) protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{ {
Score = score; Score = score;
@ -146,6 +156,13 @@ namespace osu.Game.Screens.Ranking
bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay); bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay);
ScorePanelList.AddScore(Score, shouldFlair); 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) if (allowWatchingReplay)
@ -183,6 +200,9 @@ namespace osu.Game.Screens.Ranking
api.Queue(req); api.Queue(req);
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
using (BeginDelayedSequence(APPLAUSE_DELAY))
Schedule(() => applauseSound?.Play());
} }
protected override void Update() protected override void Update()

View File

@ -54,12 +54,12 @@ namespace osu.Game.Screens.Ranking
/// <summary> /// <summary>
/// Duration for the panel to resize into its expanded/contracted size. /// Duration for the panel to resize into its expanded/contracted size.
/// </summary> /// </summary>
private const double resize_duration = 200; public const double RESIZE_DURATION = 200;
/// <summary> /// <summary>
/// Delay after <see cref="resize_duration"/> before the top layer is expanded. /// Delay after <see cref="RESIZE_DURATION"/> before the top layer is expanded.
/// </summary> /// </summary>
private const double top_layer_expand_delay = 100; public const double TOP_LAYER_EXPAND_DELAY = 100;
/// <summary> /// <summary>
/// Duration for the top layer expansion. /// Duration for the top layer expansion.
@ -208,8 +208,8 @@ namespace osu.Game.Screens.Ranking
case PanelState.Expanded: case PanelState.Expanded:
Size = new Vector2(EXPANDED_WIDTH, expanded_height); Size = new Vector2(EXPANDED_WIDTH, expanded_height);
topLayerBackground.FadeColour(expanded_top_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); middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).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: case PanelState.Contracted:
Size = new Vector2(CONTRACTED_WIDTH, contracted_height); Size = new Vector2(CONTRACTED_WIDTH, contracted_height);
topLayerBackground.FadeColour(contracted_top_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); middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0)); topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0));
middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
break; break;
} }
content.ResizeTo(Size, resize_duration, Easing.OutQuint); content.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
bool topLayerExpanded = topLayerContainer.Y < 0; 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. // 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(); topLayerContainer.FadeIn();

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select
public ImportFromStablePopup(Action importFromStable) public ImportFromStablePopup(Action importFromStable)
{ {
HeaderText = @"You have no beatmaps!"; 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; Icon = FontAwesome.Solid.Plane;

View File

@ -22,7 +22,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -35,9 +34,9 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
using System.Diagnostics; using System.Diagnostics;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Database;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -52,6 +51,8 @@ namespace osu.Game.Screens.Select
protected virtual bool ShowFooter => true; protected virtual bool ShowFooter => true;
protected virtual bool DisplayStableImportPrompt => stableImportManager?.SupportsImportFromStable == true;
/// <summary> /// <summary>
/// Can be null if <see cref="ShowFooter"/> is false. /// Can be null if <see cref="ShowFooter"/> is false.
/// </summary> /// </summary>
@ -84,6 +85,9 @@ namespace osu.Game.Screens.Select
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
[Resolved(CanBeNull = true)]
private StableImportManager stableImportManager { get; set; }
protected ModSelectOverlay ModSelect { get; private set; } protected ModSelectOverlay ModSelect { get; private set; }
protected Sample SampleConfirm { get; private set; } protected Sample SampleConfirm { get; private set; }
@ -101,7 +105,7 @@ namespace osu.Game.Screens.Select
private MusicController music { get; set; } private MusicController music { get; set; }
[BackgroundDependencyLoader(true)] [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). // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue(); transferRulesetValue();
@ -282,18 +286,12 @@ namespace osu.Game.Screens.Select
{ {
Schedule(() => Schedule(() =>
{ {
// if we have no beatmaps but osu-stable is found, let's prompt the user to import. // 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() && beatmaps.StableInstallationAvailable) if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && DisplayStableImportPrompt)
{ {
dialogOverlay.Push(new ImportFromStablePopup(() => dialogOverlay.Push(new ImportFromStablePopup(() =>
{ {
Task.Run(beatmaps.ImportFromStableAsync) Task.Run(() => stableImportManager.ImportFromStableAsync(StableContent.All));
.ContinueWith(_ =>
{
Task.Run(scores.ImportFromStableAsync);
Task.Run(collections.ImportFromStableAsync);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Task.Run(skins.ImportFromStableAsync);
})); }));
} }
}); });

View File

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
@ -42,6 +43,8 @@ namespace osu.Game.Screens.Spectate
[Resolved] [Resolved]
private UserLookupCache userLookupCache { get; set; } private UserLookupCache userLookupCache { get; set; }
private readonly IBindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>(); private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>(); private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>();
@ -65,8 +68,9 @@ namespace osu.Game.Screens.Spectate
foreach (var u in users.Result) foreach (var u in users.Result)
userMap[u.Id] = u; userMap[u.Id] = u;
spectatorClient.BindUserBeganPlaying(userBeganPlaying, true); playingUserStates.BindTo(spectatorClient.PlayingUserStates);
spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
spectatorClient.OnNewFrames += userSentFrames; spectatorClient.OnNewFrames += userSentFrames;
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
@ -102,7 +106,7 @@ namespace osu.Game.Screens.Spectate
foreach (var (userId, _) in userMap) foreach (var (userId, _) in userMap)
{ {
if (!spectatorClient.TryGetPlayingUserState(userId, out var userState)) if (!playingUserStates.TryGetValue(userId, out var userState))
continue; continue;
if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID)) 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<int, SpectatorState> 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) if (state.RulesetID == null || state.BeatmapID == null)
return; return;
@ -118,24 +146,30 @@ namespace osu.Game.Screens.Spectate
if (!userMap.ContainsKey(userId)) if (!userMap.ContainsKey(userId))
return; return;
// The user may have stopped playing. Schedule(() => OnUserStateChanged(userId, state));
if (!spectatorClient.TryGetPlayingUserState(userId, out _)) updateGameplayState(userId);
}
private void onUserStateRemoved(int userId)
{
if (!userMap.ContainsKey(userId))
return; 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) private void updateGameplayState(int userId)
{ {
Debug.Assert(userMap.ContainsKey(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 user = userMap[userId];
var spectatorState = playingUserStates[userId];
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance(); var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance();
if (resolvedRuleset == null) 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));
}
/// <summary> /// <summary>
/// Invoked when a spectated user's state has changed. /// Invoked when a spectated user's state has changed.
/// </summary> /// </summary>
@ -226,7 +246,7 @@ namespace osu.Game.Screens.Spectate
/// <param name="userId">The user to stop spectating.</param> /// <param name="userId">The user to stop spectating.</param>
protected void RemoveUser(int userId) protected void RemoveUser(int userId)
{ {
userFinishedPlaying(userId, null); onUserStateRemoved(userId);
userIds.Remove(userId); userIds.Remove(userId);
userMap.Remove(userId); userMap.Remove(userId);
@ -240,8 +260,6 @@ namespace osu.Game.Screens.Spectate
if (spectatorClient != null) if (spectatorClient != null)
{ {
spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying;
spectatorClient.OnNewFrames -= userSentFrames; spectatorClient.OnNewFrames -= userSentFrames;
foreach (var (userId, _) in userMap) foreach (var (userId, _) in userMap)

View File

@ -33,7 +33,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="ppy.osu.Framework" Version="2021.513.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.521.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="Sentry" Version="3.3.4" /> <PackageReference Include="Sentry" Version="3.3.4" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.513.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.521.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.513.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.521.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />