mirror of
https://github.com/osukey/osukey.git
synced 2025-05-17 03:27:21 +09:00
Merge branch 'master' into pie-chart-progress
This commit is contained in:
commit
3abf44da91
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Rulesets.Catch.Mods;
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Replays;
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
@ -37,9 +36,9 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
|
|
||||||
public override float DefaultFlashlightSize => 350;
|
public override float DefaultFlashlightSize => 350;
|
||||||
|
|
||||||
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
|
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield.AsNonNull());
|
||||||
|
|
||||||
private CatchPlayfield playfield;
|
private CatchPlayfield? playfield;
|
||||||
|
|
||||||
public override void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
public override void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
using System.Diagnostics;
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
{
|
{
|
||||||
public override string Description => @"Use the mouse to control the catcher.";
|
public override string Description => @"Use the mouse to control the catcher.";
|
||||||
|
|
||||||
private DrawableRuleset<CatchHitObject> drawableRuleset;
|
private DrawableRuleset<CatchHitObject>? drawableRuleset;
|
||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
@ -29,6 +28,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
|
|
||||||
public void ApplyToPlayer(Player player)
|
public void ApplyToPlayer(Player player)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(drawableRuleset != null);
|
||||||
|
|
||||||
if (!drawableRuleset.HasReplayLoaded.Value)
|
if (!drawableRuleset.HasReplayLoaded.Value)
|
||||||
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
|
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Mods
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
@ -142,7 +142,6 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
// ReSharper disable once AccessToDisposedClosure
|
|
||||||
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
|
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
|
||||||
|
|
||||||
Assert.NotNull(beatmapSet);
|
Assert.NotNull(beatmapSet);
|
||||||
@ -311,6 +310,7 @@ namespace osu.Game.Tests.Database
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
File.Delete(temp);
|
||||||
Directory.Delete(extractedFolder, true);
|
Directory.Delete(extractedFolder, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -670,6 +670,61 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenReimportWithNewDifficulty()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var store = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
string? pathOriginal = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string pathMissingOneBeatmap = pathOriginal.Replace(".osz", "_missing_difficulty.osz");
|
||||||
|
|
||||||
|
string extractedFolder = $"{pathOriginal}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var zip = ZipArchive.Open(pathOriginal))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
// remove one difficulty before first import
|
||||||
|
new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).Delete();
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(pathMissingOneBeatmap, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstImport = await importer.Import(new ImportTask(pathMissingOneBeatmap));
|
||||||
|
Assert.That(firstImport, Is.Not.Null);
|
||||||
|
|
||||||
|
Assert.That(realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), Has.Count.EqualTo(1));
|
||||||
|
Assert.That(realm.Realm.All<BeatmapSetInfo>().First(s => !s.DeletePending).Beatmaps, Has.Count.EqualTo(11));
|
||||||
|
|
||||||
|
// Second import matches first but contains one extra .osu file.
|
||||||
|
var secondImport = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
Assert.That(secondImport, Is.Not.Null);
|
||||||
|
|
||||||
|
Assert.That(realm.Realm.All<BeatmapInfo>(), Has.Count.EqualTo(23));
|
||||||
|
Assert.That(realm.Realm.All<BeatmapSetInfo>(), Has.Count.EqualTo(2));
|
||||||
|
|
||||||
|
Assert.That(realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), Has.Count.EqualTo(1));
|
||||||
|
Assert.That(realm.Realm.All<BeatmapSetInfo>().First(s => !s.DeletePending).Beatmaps, Has.Count.EqualTo(12));
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.That(firstImport?.ID, Is.Not.EqualTo(secondImport?.ID));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestImportThenReimportAfterMissingFiles()
|
public void TestImportThenReimportAfterMissingFiles()
|
||||||
{
|
{
|
||||||
@ -742,7 +797,7 @@ namespace osu.Game.Tests.Database
|
|||||||
await realm.Realm.WriteAsync(() =>
|
await realm.Realm.WriteAsync(() =>
|
||||||
{
|
{
|
||||||
foreach (var b in imported.Beatmaps)
|
foreach (var b in imported.Beatmaps)
|
||||||
b.OnlineID = -1;
|
b.ResetOnlineInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteBeatmapSet(imported, realm.Realm);
|
deleteBeatmapSet(imported, realm.Realm);
|
||||||
|
447
osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs
Normal file
447
osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using Realms;
|
||||||
|
using SharpCompress.Archives;
|
||||||
|
using SharpCompress.Archives.Zip;
|
||||||
|
using SharpCompress.Common;
|
||||||
|
using SharpCompress.Writers.Zip;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tests the flow where a beatmap is already loaded and an update is applied.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class BeatmapImporterUpdateTests : RealmTest
|
||||||
|
{
|
||||||
|
private const int count_beatmaps = 12;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNewDifficultyAdded()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
|
||||||
|
{
|
||||||
|
// remove one difficulty before first import
|
||||||
|
directory.GetFiles("*.osu").First().Delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathMissingOneBeatmap));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps - 1));
|
||||||
|
|
||||||
|
// Second import matches first but contains one extra .osu file.
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginal), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
|
||||||
|
|
||||||
|
// Previous beatmap set has no beatmaps so will be completely purged on the spot.
|
||||||
|
Assert.That(importBeforeUpdate.Value.IsValid, Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regression test covering https://github.com/ppy/osu/issues/19369 (import potentially duplicating if original has no <see cref="BeatmapInfo.OnlineID"/>).
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestNewDifficultyAddedNoOnlineID()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
|
||||||
|
{
|
||||||
|
// remove one difficulty before first import
|
||||||
|
directory.GetFiles("*.osu").First().Delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathMissingOneBeatmap));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
// This test is the same as TestNewDifficultyAdded except for this block.
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
s.OnlineID = -1;
|
||||||
|
foreach (var beatmap in s.Beatmaps)
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps - 1));
|
||||||
|
|
||||||
|
// Second import matches first but contains one extra .osu file.
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginal), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
|
||||||
|
|
||||||
|
// Previous beatmap set has no beatmaps so will be completely purged on the spot.
|
||||||
|
Assert.That(importBeforeUpdate.Value.IsValid, Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExistingDifficultyModified()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathModified, directory =>
|
||||||
|
{
|
||||||
|
// Modify one .osu file with different content.
|
||||||
|
var firstOsuFile = directory.GetFiles("*.osu").First();
|
||||||
|
|
||||||
|
string existingContent = File.ReadAllText(firstOsuFile.FullName);
|
||||||
|
|
||||||
|
File.WriteAllText(firstOsuFile.FullName, existingContent + "\n# I am new content");
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
|
||||||
|
|
||||||
|
// Second import matches first but contains one extra .osu file.
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathModified), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
// should only contain the modified beatmap (others purged).
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
|
||||||
|
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
|
||||||
|
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1, s => s.DeletePending);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExistingDifficultyRemoved()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
|
||||||
|
{
|
||||||
|
// remove one difficulty before first import
|
||||||
|
directory.GetFiles("*.osu").First().Delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
|
||||||
|
|
||||||
|
// Second import matches first but contains one extra .osu file.
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathMissingOneBeatmap), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 2);
|
||||||
|
|
||||||
|
// previous set should contain the removed beatmap still.
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.EqualTo(-1));
|
||||||
|
|
||||||
|
// Previous beatmap set has no beatmaps so will be completely purged on the spot.
|
||||||
|
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps - 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdatedImportContainsNothing()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathEmpty, directory =>
|
||||||
|
{
|
||||||
|
foreach (var file in directory.GetFiles())
|
||||||
|
file.Delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathEmpty), importBeforeUpdate.Value);
|
||||||
|
Assert.That(importAfterUpdate, Is.Null);
|
||||||
|
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1);
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps);
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate.Value.IsValid, Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoChanges()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchive(out string pathOriginalSecond);
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 1);
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps);
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
|
||||||
|
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreTransferredOnUnchanged()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
|
||||||
|
{
|
||||||
|
// arbitrary beatmap removal
|
||||||
|
directory.GetFiles("*.osu").First().Delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
string scoreTargetBeatmapHash = string.Empty;
|
||||||
|
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
var beatmapInfo = s.Beatmaps.Last();
|
||||||
|
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
||||||
|
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
|
});
|
||||||
|
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathMissingOneBeatmap), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 2);
|
||||||
|
|
||||||
|
// score is transferred across to the new set
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreLostOnModification()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
string? scoreTargetFilename = string.Empty;
|
||||||
|
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
var beatmapInfo = s.Beatmaps.Last();
|
||||||
|
scoreTargetFilename = beatmapInfo.File?.Filename;
|
||||||
|
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
|
});
|
||||||
|
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathModified, directory =>
|
||||||
|
{
|
||||||
|
// Modify one .osu file with different content.
|
||||||
|
var firstOsuFile = directory.GetFiles(scoreTargetFilename).First();
|
||||||
|
|
||||||
|
string existingContent = File.ReadAllText(firstOsuFile.FullName);
|
||||||
|
|
||||||
|
File.WriteAllText(firstOsuFile.FullName, existingContent + "\n# I am new content");
|
||||||
|
});
|
||||||
|
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathModified), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 2);
|
||||||
|
|
||||||
|
// score is not transferred due to modifications.
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps.AsEnumerable().First(b => b.File?.Filename == scoreTargetFilename).Scores, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(importAfterUpdate.Value.Beatmaps.AsEnumerable().First(b => b.File?.Filename == scoreTargetFilename).Scores, Has.Count.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMetadataTransferred()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
|
||||||
|
{
|
||||||
|
// arbitrary beatmap removal
|
||||||
|
directory.GetFiles("*.osu").First().Delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathMissingOneBeatmap), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
|
||||||
|
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkCount<T>(RealmAccess realm, int expected, Expression<Func<T, bool>>? condition = null) where T : RealmObject
|
||||||
|
{
|
||||||
|
var query = realm.Realm.All<T>();
|
||||||
|
|
||||||
|
if (condition != null)
|
||||||
|
query = query.Where(condition);
|
||||||
|
|
||||||
|
Assert.That(query, Has.Count.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDisposable getBeatmapArchiveWithModifications(out string path, Action<DirectoryInfo> applyModifications)
|
||||||
|
{
|
||||||
|
var cleanup = getBeatmapArchive(out path);
|
||||||
|
|
||||||
|
string extractedFolder = $"{path}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Open(path))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
applyModifications(new DirectoryInfo(extractedFolder));
|
||||||
|
|
||||||
|
File.Delete(path);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(path, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDisposable getBeatmapArchive(out string path, bool quick = true)
|
||||||
|
{
|
||||||
|
string beatmapPath = TestResources.GetTestBeatmapForImport(quick);
|
||||||
|
|
||||||
|
path = beatmapPath;
|
||||||
|
|
||||||
|
return new InvokeOnDisposal(() => File.Delete(beatmapPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,31 +32,29 @@ namespace osu.Game.Tests.Database
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestAccessAfterStorageMigrate()
|
public void TestAccessAfterStorageMigrate()
|
||||||
{
|
{
|
||||||
RunTestWithRealm((realm, storage) =>
|
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
|
||||||
{
|
{
|
||||||
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
|
RunTestWithRealm((realm, storage) =>
|
||||||
|
|
||||||
Live<BeatmapInfo>? liveBeatmap = null;
|
|
||||||
|
|
||||||
realm.Run(r =>
|
|
||||||
{
|
{
|
||||||
r.Write(_ => r.Add(beatmap));
|
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
|
||||||
|
|
||||||
liveBeatmap = beatmap.ToLive(realm);
|
Live<BeatmapInfo>? liveBeatmap = null;
|
||||||
});
|
|
||||||
|
realm.Run(r =>
|
||||||
|
{
|
||||||
|
r.Write(_ => r.Add(beatmap));
|
||||||
|
|
||||||
|
liveBeatmap = beatmap.ToLive(realm);
|
||||||
|
});
|
||||||
|
|
||||||
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
|
|
||||||
{
|
|
||||||
migratedStorage.DeleteDirectory(string.Empty);
|
migratedStorage.DeleteDirectory(string.Empty);
|
||||||
|
|
||||||
using (realm.BlockAllOperations("testing"))
|
using (realm.BlockAllOperations("testing"))
|
||||||
{
|
|
||||||
storage.Migrate(migratedStorage);
|
storage.Migrate(migratedStorage);
|
||||||
}
|
|
||||||
|
|
||||||
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
|
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -341,14 +339,12 @@ namespace osu.Game.Tests.Database
|
|||||||
liveBeatmap.PerformRead(resolved =>
|
liveBeatmap.PerformRead(resolved =>
|
||||||
{
|
{
|
||||||
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||||
// ReSharper disable once AccessToDisposedClosure
|
|
||||||
Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count());
|
Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count());
|
||||||
Assert.AreEqual(1, changesTriggered);
|
Assert.AreEqual(1, changesTriggered);
|
||||||
|
|
||||||
// can access properties without a crash.
|
// can access properties without a crash.
|
||||||
Assert.IsFalse(resolved.Hidden);
|
Assert.IsFalse(resolved.Hidden);
|
||||||
|
|
||||||
// ReSharper disable once AccessToDisposedClosure
|
|
||||||
outerRealm.Write(r =>
|
outerRealm.Write(r =>
|
||||||
{
|
{
|
||||||
// can use with the main context.
|
// can use with the main context.
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
@ -20,22 +20,15 @@ namespace osu.Game.Tests.Database
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public abstract class RealmTest
|
public abstract class RealmTest
|
||||||
{
|
{
|
||||||
private static readonly TemporaryNativeStorage storage;
|
protected void RunTestWithRealm([InstantHandle] Action<RealmAccess, OsuStorage> testAction, [CallerMemberName] string caller = "")
|
||||||
|
|
||||||
static RealmTest()
|
|
||||||
{
|
|
||||||
storage = new TemporaryNativeStorage("realm-test");
|
|
||||||
storage.DeleteDirectory(string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void RunTestWithRealm(Action<RealmAccess, OsuStorage> testAction, [CallerMemberName] string caller = "")
|
|
||||||
{
|
{
|
||||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller))
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller))
|
||||||
{
|
{
|
||||||
host.Run(new RealmTestGame(() =>
|
host.Run(new RealmTestGame(() =>
|
||||||
{
|
{
|
||||||
// ReSharper disable once AccessToDisposedClosure
|
var defaultStorage = host.Storage;
|
||||||
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
|
|
||||||
|
var testStorage = new OsuStorage(host, defaultStorage);
|
||||||
|
|
||||||
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
|
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
|
||||||
{
|
{
|
||||||
@ -58,7 +51,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
host.Run(new RealmTestGame(async () =>
|
host.Run(new RealmTestGame(async () =>
|
||||||
{
|
{
|
||||||
var testStorage = storage.GetStorageForDirectory(caller);
|
var testStorage = host.Storage;
|
||||||
|
|
||||||
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
|
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
|
||||||
{
|
{
|
||||||
@ -116,7 +109,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
private class RealmTestGame : Framework.Game
|
private class RealmTestGame : Framework.Game
|
||||||
{
|
{
|
||||||
public RealmTestGame(Func<Task> work)
|
public RealmTestGame([InstantHandle] Func<Task> work)
|
||||||
{
|
{
|
||||||
// ReSharper disable once AsyncVoidLambda
|
// ReSharper disable once AsyncVoidLambda
|
||||||
Scheduler.Add(async () =>
|
Scheduler.Add(async () =>
|
||||||
@ -126,7 +119,7 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public RealmTestGame(Action work)
|
public RealmTestGame([InstantHandle] Action work)
|
||||||
{
|
{
|
||||||
Scheduler.Add(() =>
|
Scheduler.Add(() =>
|
||||||
{
|
{
|
||||||
|
@ -8,26 +8,17 @@ using osu.Framework.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
[Ignore("Timeline initialisation is kinda broken.")] // Initial work to rectify this was done in https://github.com/ppy/osu/pull/19297, but needs more massaging to work.
|
|
||||||
public class TestSceneTimelineZoom : TimelineTestScene
|
public class TestSceneTimelineZoom : TimelineTestScene
|
||||||
{
|
{
|
||||||
public override Drawable CreateTestComponent() => Empty();
|
public override Drawable CreateTestComponent() => Empty();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[FlakyTest]
|
|
||||||
/*
|
|
||||||
* Fail rate around 0.3%
|
|
||||||
*
|
|
||||||
* TearDown : osu.Framework.Testing.Drawables.Steps.AssertButton+TracedException : range halved
|
|
||||||
* --TearDown
|
|
||||||
* at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
|
|
||||||
* at osu.Framework.Threading.Scheduler.Update()
|
|
||||||
* at osu.Framework.Graphics.Drawable.UpdateSubTree()
|
|
||||||
*/
|
|
||||||
public void TestVisibleRangeUpdatesOnZoomChange()
|
public void TestVisibleRangeUpdatesOnZoomChange()
|
||||||
{
|
{
|
||||||
double initialVisibleRange = 0;
|
double initialVisibleRange = 0;
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => MusicController.TrackLoaded);
|
||||||
|
|
||||||
AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
|
AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
|
||||||
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
|
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
|
||||||
|
|
||||||
@ -45,6 +36,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
double initialVisibleRange = 0;
|
double initialVisibleRange = 0;
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => MusicController.TrackLoaded);
|
||||||
|
|
||||||
AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
|
AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
|
||||||
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
|
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = OsuColour.Gray(30)
|
Colour = OsuColour.Gray(30)
|
||||||
},
|
},
|
||||||
scrollContainer = new ZoomableScrollContainer
|
scrollContainer = new ZoomableScrollContainer(1, 60, 1)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -80,21 +80,6 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
|
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestZoomRangeUpdate()
|
|
||||||
{
|
|
||||||
AddStep("set zoom to 2", () => scrollContainer.Zoom = 2);
|
|
||||||
AddStep("set min zoom to 5", () => scrollContainer.MinZoom = 5);
|
|
||||||
AddAssert("zoom = 5", () => scrollContainer.Zoom == 5);
|
|
||||||
|
|
||||||
AddStep("set max zoom to 10", () => scrollContainer.MaxZoom = 10);
|
|
||||||
AddAssert("zoom = 5", () => scrollContainer.Zoom == 5);
|
|
||||||
|
|
||||||
AddStep("set min zoom to 20", () => scrollContainer.MinZoom = 20);
|
|
||||||
AddStep("set max zoom to 40", () => scrollContainer.MaxZoom = 40);
|
|
||||||
AddAssert("zoom = 20", () => scrollContainer.Zoom == 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestZoom0()
|
public void TestZoom0()
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public TestScenePause()
|
public TestScenePause()
|
||||||
{
|
{
|
||||||
base.Content.Add(content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both });
|
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
|
@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
createPlayerTest(false, r =>
|
createPlayerTest(false, r =>
|
||||||
{
|
{
|
||||||
var beatmap = createTestBeatmap(r);
|
var beatmap = createTestBeatmap(r);
|
||||||
beatmap.BeatmapInfo.OnlineID = -1;
|
beatmap.BeatmapInfo.ResetOnlineInfo();
|
||||||
return beatmap;
|
return beatmap;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -522,15 +522,15 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
var sets = new List<BeatmapSetInfo>();
|
var sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
for (int i = 0; i < 20; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo();
|
var set = TestResources.CreateTestBeatmapSetInfo();
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
|
|
||||||
beatmap.Metadata.Artist = "same artist";
|
beatmap.Metadata.Artist = $"artist {i / 2}";
|
||||||
beatmap.Metadata.Title = "same title";
|
beatmap.Metadata.Title = $"title {9 - i}";
|
||||||
|
|
||||||
sets.Add(set);
|
sets.Add(set);
|
||||||
}
|
}
|
||||||
@ -540,10 +540,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
loadBeatmaps(sets);
|
loadBeatmaps(sets);
|
||||||
|
|
||||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||||
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == index + idOffset).All(b => b));
|
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
|
||||||
|
|
||||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||||
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == index + idOffset).All(b => b));
|
AddAssert("Items are in reverse order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + sets.Count - index - 1).All(b => b));
|
||||||
|
|
||||||
|
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||||
|
AddAssert("Items reset to original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -21,12 +21,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneCursors : OsuManualInputManagerTestScene
|
public class TestSceneCursors : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private readonly MenuCursorContainer menuCursorContainer;
|
private readonly GlobalCursorDisplay globalCursorDisplay;
|
||||||
private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6];
|
private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6];
|
||||||
|
|
||||||
public TestSceneCursors()
|
public TestSceneCursors()
|
||||||
{
|
{
|
||||||
Child = menuCursorContainer = new MenuCursorContainer
|
Child = globalCursorDisplay = new GlobalCursorDisplay
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new[]
|
Children = new[]
|
||||||
@ -96,11 +96,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private void testUserCursor()
|
private void testUserCursor()
|
||||||
{
|
{
|
||||||
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
|
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
|
||||||
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
|
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].MenuCursor));
|
||||||
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
|
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].MenuCursor));
|
||||||
AddStep("Move out", moveOut);
|
AddStep("Move out", moveOut);
|
||||||
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
|
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
|
||||||
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor));
|
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -111,13 +111,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private void testLocalCursor()
|
private void testLocalCursor()
|
||||||
{
|
{
|
||||||
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
|
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
|
||||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
|
||||||
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
|
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
|
||||||
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor));
|
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
|
||||||
AddAssert("Check global cursor at mouse", () => checkAtMouse(menuCursorContainer.Cursor));
|
AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
|
||||||
AddStep("Move out", moveOut);
|
AddStep("Move out", moveOut);
|
||||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
|
||||||
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor));
|
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private void testUserCursorOverride()
|
private void testUserCursorOverride()
|
||||||
{
|
{
|
||||||
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
|
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
|
||||||
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
|
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
|
||||||
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
|
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
|
||||||
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
|
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
|
||||||
AddStep("Move out", moveOut);
|
AddStep("Move out", moveOut);
|
||||||
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor));
|
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor));
|
||||||
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor));
|
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -143,13 +143,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private void testMultipleLocalCursors()
|
private void testMultipleLocalCursors()
|
||||||
{
|
{
|
||||||
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
|
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
|
||||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
|
||||||
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
|
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
|
||||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||||
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
|
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
|
||||||
AddStep("Move out", moveOut);
|
AddStep("Move out", moveOut);
|
||||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
|
||||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -159,13 +159,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private void testUserOverrideWithLocal()
|
private void testUserOverrideWithLocal()
|
||||||
{
|
{
|
||||||
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
|
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
|
||||||
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
|
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
|
||||||
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
|
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
|
||||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||||
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
|
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
|
||||||
AddStep("Move out", moveOut);
|
AddStep("Move out", moveOut);
|
||||||
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor));
|
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor));
|
||||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
public bool SmoothTransition;
|
public bool SmoothTransition;
|
||||||
|
|
||||||
public CursorContainer Cursor { get; }
|
public CursorContainer MenuCursor { get; }
|
||||||
public bool ProvidingUserCursor { get; }
|
public bool ProvidingUserCursor { get; }
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
|
||||||
@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Text = providesUserCursor ? "User cursor" : "Local cursor"
|
Text = providesUserCursor ? "User cursor" : "Local cursor"
|
||||||
},
|
},
|
||||||
Cursor = new TestCursorContainer
|
MenuCursor = new TestCursorContainer
|
||||||
{
|
{
|
||||||
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
|
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -27,7 +28,6 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var osu = new TestTournament(runOnLoadComplete: () =>
|
var osu = new TestTournament(runOnLoadComplete: () =>
|
||||||
{
|
{
|
||||||
// ReSharper disable once AccessToDisposedClosure
|
|
||||||
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
|
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
|
||||||
|
|
||||||
using (var stream = storage.CreateFileSafely("bracket.json"))
|
using (var stream = storage.CreateFileSafely("bracket.json"))
|
||||||
@ -85,7 +85,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
|
|
||||||
public new Task BracketLoadTask => base.BracketLoadTask;
|
public new Task BracketLoadTask => base.BracketLoadTask;
|
||||||
|
|
||||||
public TestTournament(bool resetRuleset = false, Action runOnLoadComplete = null)
|
public TestTournament(bool resetRuleset = false, [InstantHandle] Action runOnLoadComplete = null)
|
||||||
{
|
{
|
||||||
this.resetRuleset = resetRuleset;
|
this.resetRuleset = resetRuleset;
|
||||||
this.runOnLoadComplete = runOnLoadComplete;
|
this.runOnLoadComplete = runOnLoadComplete;
|
||||||
|
@ -70,10 +70,10 @@ namespace osu.Game.Tournament
|
|||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display
|
GlobalCursorDisplay.MenuCursor.AlwaysPresent = true; // required for tooltip display
|
||||||
|
|
||||||
// we don't want to show the menu cursor as it would appear on stream output.
|
// we don't want to show the menu cursor as it would appear on stream output.
|
||||||
MenuCursorContainer.Cursor.Alpha = 0;
|
GlobalCursorDisplay.MenuCursor.Alpha = 0;
|
||||||
|
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
@ -16,6 +18,7 @@ using osu.Game.Database;
|
|||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
@ -38,6 +41,77 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
|
||||||
|
{
|
||||||
|
var imported = await Import(notification, importTask);
|
||||||
|
|
||||||
|
if (!imported.Any())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Debug.Assert(imported.Count() == 1);
|
||||||
|
|
||||||
|
var first = imported.First();
|
||||||
|
|
||||||
|
// If there were no changes, ensure we don't accidentally nuke ourselves.
|
||||||
|
if (first.ID == original.ID)
|
||||||
|
return first;
|
||||||
|
|
||||||
|
first.PerformWrite(updated =>
|
||||||
|
{
|
||||||
|
var realm = updated.Realm;
|
||||||
|
|
||||||
|
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
||||||
|
|
||||||
|
original = realm.Find<BeatmapSetInfo>(original.ID);
|
||||||
|
|
||||||
|
// Generally the import process will do this for us if the OnlineIDs match,
|
||||||
|
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
||||||
|
original.DeletePending = true;
|
||||||
|
|
||||||
|
// Transfer local values which should be persisted across a beatmap update.
|
||||||
|
updated.DateAdded = original.DateAdded;
|
||||||
|
|
||||||
|
foreach (var beatmap in original.Beatmaps.ToArray())
|
||||||
|
{
|
||||||
|
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
|
||||||
|
|
||||||
|
if (updatedBeatmap != null)
|
||||||
|
{
|
||||||
|
// If the updated beatmap matches an existing one, transfer any user data across..
|
||||||
|
if (beatmap.Scores.Any())
|
||||||
|
{
|
||||||
|
Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database);
|
||||||
|
|
||||||
|
foreach (var score in beatmap.Scores)
|
||||||
|
score.BeatmapInfo = updatedBeatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..then nuke the old beatmap completely.
|
||||||
|
// this is done instead of a soft deletion to avoid a user potentially creating weird
|
||||||
|
// interactions, like restoring the outdated beatmap then updating a second time
|
||||||
|
// (causing user data to be wiped).
|
||||||
|
original.Beatmaps.Remove(beatmap);
|
||||||
|
|
||||||
|
realm.Remove(beatmap.Metadata);
|
||||||
|
realm.Remove(beatmap);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If the beatmap differs in the original, leave it in a soft-deleted state but reset online info.
|
||||||
|
// This caters to the case where a user has made modifications they potentially want to restore,
|
||||||
|
// but after restoring we want to ensure it can't be used to trigger an update of the beatmap.
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the original has no beatmaps left, delete the set as well.
|
||||||
|
if (!original.Beatmaps.Any())
|
||||||
|
realm.Remove(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
|
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
|
||||||
|
|
||||||
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
|
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
|
||||||
@ -87,7 +161,7 @@ namespace osu.Game.Beatmaps
|
|||||||
existingSetWithSameOnlineID.OnlineID = -1;
|
existingSetWithSameOnlineID.OnlineID = -1;
|
||||||
|
|
||||||
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
|
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
|
||||||
b.OnlineID = -1;
|
b.ResetOnlineInfo();
|
||||||
|
|
||||||
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
|
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
|
||||||
}
|
}
|
||||||
@ -133,7 +207,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1);
|
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.ResetOnlineInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
|
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||||
|
@ -109,6 +109,17 @@ namespace osu.Game.Beatmaps
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool Hidden { get; set; }
|
public bool Hidden { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset any fetched online linking information (and history).
|
||||||
|
/// </summary>
|
||||||
|
public void ResetOnlineInfo()
|
||||||
|
{
|
||||||
|
OnlineID = -1;
|
||||||
|
LastOnlineUpdate = null;
|
||||||
|
OnlineMD5Hash = string.Empty;
|
||||||
|
Status = BeatmapOnlineStatus.None;
|
||||||
|
}
|
||||||
|
|
||||||
#region Properties we may not want persisted (but also maybe no harm?)
|
#region Properties we may not want persisted (but also maybe no harm?)
|
||||||
|
|
||||||
public double AudioLeadIn { get; set; }
|
public double AudioLeadIn { get; set; }
|
||||||
|
@ -164,8 +164,7 @@ namespace osu.Game.Beatmaps
|
|||||||
// clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
|
// clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
|
||||||
newBeatmapInfo.Hash = string.Empty;
|
newBeatmapInfo.Hash = string.Empty;
|
||||||
// clear online properties.
|
// clear online properties.
|
||||||
newBeatmapInfo.OnlineID = -1;
|
newBeatmapInfo.ResetOnlineInfo();
|
||||||
newBeatmapInfo.Status = BeatmapOnlineStatus.None;
|
|
||||||
|
|
||||||
return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin);
|
return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin);
|
||||||
}
|
}
|
||||||
@ -409,6 +408,9 @@ namespace osu.Game.Beatmaps
|
|||||||
Realm.Run(r => Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
|
Realm.Run(r => Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
|
||||||
|
beatmapImporter.ImportAsUpdate(notification, importTask, original);
|
||||||
|
|
||||||
#region Implementation of ICanAcceptFiles
|
#region Implementation of ICanAcceptFiles
|
||||||
|
|
||||||
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
|
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
|
||||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps
|
|||||||
if (req.CompletionState == APIRequestCompletionState.Failed)
|
if (req.CompletionState == APIRequestCompletionState.Failed)
|
||||||
{
|
{
|
||||||
logForModel(set, $"Online retrieval failed for {beatmapInfo}");
|
logForModel(set, $"Online retrieval failed for {beatmapInfo}");
|
||||||
beatmapInfo.OnlineID = -1;
|
beatmapInfo.ResetOnlineInfo();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ namespace osu.Game.Beatmaps
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
||||||
beatmapInfo.OnlineID = -1;
|
beatmapInfo.ResetOnlineInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ namespace osu.Game.Configuration
|
|||||||
// Input
|
// Input
|
||||||
SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f);
|
SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f);
|
||||||
SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f);
|
SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f);
|
||||||
|
SetDefault(OsuSetting.GameplayCursorDuringTouch, false);
|
||||||
SetDefault(OsuSetting.AutoCursorSize, false);
|
SetDefault(OsuSetting.AutoCursorSize, false);
|
||||||
|
|
||||||
SetDefault(OsuSetting.MouseDisableButtons, false);
|
SetDefault(OsuSetting.MouseDisableButtons, false);
|
||||||
@ -292,6 +293,7 @@ namespace osu.Game.Configuration
|
|||||||
MenuCursorSize,
|
MenuCursorSize,
|
||||||
GameplayCursorSize,
|
GameplayCursorSize,
|
||||||
AutoCursorSize,
|
AutoCursorSize,
|
||||||
|
GameplayCursorDuringTouch,
|
||||||
DimLevel,
|
DimLevel,
|
||||||
BlurLevel,
|
BlurLevel,
|
||||||
LightenDuringBreaks,
|
LightenDuringBreaks,
|
||||||
|
@ -23,6 +23,16 @@ namespace osu.Game.Database
|
|||||||
/// <returns>The imported models.</returns>
|
/// <returns>The imported models.</returns>
|
||||||
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a single import as an update for an existing model.
|
||||||
|
/// This will still run a full import, but perform any post-processing required to make it feel like an update to the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification">The notification to update.</param>
|
||||||
|
/// <param name="task">The import task.</param>
|
||||||
|
/// <param name="original">The original model which is being updated.</param>
|
||||||
|
/// <returns>The imported model.</returns>
|
||||||
|
Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user displayable name for the model type associated with this manager.
|
/// A user displayable name for the model type associated with this manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -42,7 +42,11 @@ namespace osu.Game.Database
|
|||||||
/// <returns>The request object.</returns>
|
/// <returns>The request object.</returns>
|
||||||
protected abstract ArchiveDownloadRequest<T> CreateDownloadRequest(T model, bool minimiseDownloadSize);
|
protected abstract ArchiveDownloadRequest<T> CreateDownloadRequest(T model, bool minimiseDownloadSize);
|
||||||
|
|
||||||
public bool Download(T model, bool minimiseDownloadSize = false)
|
public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null);
|
||||||
|
|
||||||
|
public void DownloadAsUpdate(TModel originalModel) => Download(originalModel, false, originalModel);
|
||||||
|
|
||||||
|
protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel)
|
||||||
{
|
{
|
||||||
if (!canDownload(model)) return false;
|
if (!canDownload(model)) return false;
|
||||||
|
|
||||||
@ -63,11 +67,15 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
Task.Factory.StartNew(async () =>
|
Task.Factory.StartNew(async () =>
|
||||||
{
|
{
|
||||||
// This gets scheduled back to the update thread, but we want the import to run in the background.
|
bool importSuccessful;
|
||||||
var imported = await importer.Import(notification, new ImportTask(filename)).ConfigureAwait(false);
|
|
||||||
|
if (originalModel != null)
|
||||||
|
importSuccessful = (await importer.ImportAsUpdate(notification, new ImportTask(filename), originalModel)) != null;
|
||||||
|
else
|
||||||
|
importSuccessful = (await importer.Import(notification, new ImportTask(filename))).Any();
|
||||||
|
|
||||||
// for now a failed import will be marked as a failed download for simplicity.
|
// for now a failed import will be marked as a failed download for simplicity.
|
||||||
if (!imported.Any())
|
if (!importSuccessful)
|
||||||
DownloadFailed?.Invoke(request);
|
DownloadFailed?.Invoke(request);
|
||||||
|
|
||||||
CurrentDownloads.Remove(request);
|
CurrentDownloads.Remove(request);
|
||||||
|
@ -278,7 +278,6 @@ namespace osu.Game.Database
|
|||||||
realm.Remove(score);
|
realm.Remove(score);
|
||||||
|
|
||||||
realm.Remove(beatmap.Metadata);
|
realm.Remove(beatmap.Metadata);
|
||||||
|
|
||||||
realm.Remove(beatmap);
|
realm.Remove(beatmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +174,8 @@ namespace osu.Game.Database
|
|||||||
return imported;
|
return imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
||||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||||
|
92
osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
Normal file
92
osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.StateChanges;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.Cursor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A container which provides the main <see cref="Cursor.MenuCursor"/>.
|
||||||
|
/// Also handles cases where a more localised cursor is provided by another component (via <see cref="IProvideCursor"/>).
|
||||||
|
/// </summary>
|
||||||
|
public class GlobalCursorDisplay : Container, IProvideCursor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Control whether any cursor should be displayed.
|
||||||
|
/// </summary>
|
||||||
|
internal bool ShowCursor = true;
|
||||||
|
|
||||||
|
public CursorContainer MenuCursor { get; }
|
||||||
|
|
||||||
|
public bool ProvidingUserCursor => true;
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
|
private Bindable<bool> showDuringTouch = null!;
|
||||||
|
|
||||||
|
private InputManager inputManager = null!;
|
||||||
|
|
||||||
|
private IProvideCursor? currentOverrideProvider;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
public GlobalCursorDisplay()
|
||||||
|
{
|
||||||
|
AddRangeInternal(new Drawable[]
|
||||||
|
{
|
||||||
|
MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } },
|
||||||
|
Content = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
inputManager = GetContainingInputManager();
|
||||||
|
showDuringTouch = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
var lastMouseSource = inputManager.CurrentState.Mouse.LastSource;
|
||||||
|
bool hasValidInput = lastMouseSource != null && (showDuringTouch.Value || lastMouseSource is not ISourcedFromTouch);
|
||||||
|
|
||||||
|
if (!hasValidInput || !ShowCursor)
|
||||||
|
{
|
||||||
|
currentOverrideProvider?.MenuCursor?.Hide();
|
||||||
|
currentOverrideProvider = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IProvideCursor newOverrideProvider = this;
|
||||||
|
|
||||||
|
foreach (var d in inputManager.HoveredDrawables)
|
||||||
|
{
|
||||||
|
if (d is IProvideCursor p && p.ProvidingUserCursor)
|
||||||
|
{
|
||||||
|
newOverrideProvider = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentOverrideProvider == newOverrideProvider)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentOverrideProvider?.MenuCursor?.Hide();
|
||||||
|
newOverrideProvider.MenuCursor?.Show();
|
||||||
|
|
||||||
|
currentOverrideProvider = newOverrideProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
|
|||||||
/// The cursor provided by this <see cref="IDrawable"/>.
|
/// The cursor provided by this <see cref="IDrawable"/>.
|
||||||
/// May be null if no cursor should be visible.
|
/// May be null if no cursor should be visible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CursorContainer Cursor { get; }
|
CursorContainer MenuCursor { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether <see cref="Cursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
|
/// Whether <see cref="MenuCursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
|
||||||
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
|
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool ProvidingUserCursor { get; }
|
bool ProvidingUserCursor { get; }
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Cursor;
|
|
||||||
using osu.Framework.Input;
|
|
||||||
using osu.Framework.Input.StateChanges;
|
|
||||||
|
|
||||||
namespace osu.Game.Graphics.Cursor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A container which provides a <see cref="MenuCursor"/> which can be overridden by hovered <see cref="Drawable"/>s.
|
|
||||||
/// </summary>
|
|
||||||
public class MenuCursorContainer : Container, IProvideCursor
|
|
||||||
{
|
|
||||||
protected override Container<Drawable> Content => content;
|
|
||||||
private readonly Container content;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether any cursors can be displayed.
|
|
||||||
/// </summary>
|
|
||||||
internal bool CanShowCursor = true;
|
|
||||||
|
|
||||||
public CursorContainer Cursor { get; }
|
|
||||||
public bool ProvidingUserCursor => true;
|
|
||||||
|
|
||||||
public MenuCursorContainer()
|
|
||||||
{
|
|
||||||
AddRangeInternal(new Drawable[]
|
|
||||||
{
|
|
||||||
Cursor = new MenuCursor { State = { Value = Visibility.Hidden } },
|
|
||||||
content = new Container { RelativeSizeAxes = Axes.Both }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private InputManager inputManager;
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
inputManager = GetContainingInputManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IProvideCursor currentTarget;
|
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
var lastMouseSource = inputManager.CurrentState.Mouse.LastSource;
|
|
||||||
bool hasValidInput = lastMouseSource != null && !(lastMouseSource is ISourcedFromTouch);
|
|
||||||
|
|
||||||
if (!hasValidInput || !CanShowCursor)
|
|
||||||
{
|
|
||||||
currentTarget?.Cursor?.Hide();
|
|
||||||
currentTarget = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IProvideCursor newTarget = this;
|
|
||||||
|
|
||||||
foreach (var d in inputManager.HoveredDrawables)
|
|
||||||
{
|
|
||||||
if (d is IProvideCursor p && p.ProvidingUserCursor)
|
|
||||||
{
|
|
||||||
newTarget = p;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTarget == newTarget)
|
|
||||||
return;
|
|
||||||
|
|
||||||
currentTarget?.Cursor?.Hide();
|
|
||||||
newTarget.Cursor?.Show();
|
|
||||||
|
|
||||||
currentTarget = newTarget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -16,7 +18,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public class ExternalLinkButton : CompositeDrawable, IHasTooltip
|
public class ExternalLinkButton : CompositeDrawable, IHasTooltip, IHasContextMenu
|
||||||
{
|
{
|
||||||
public string Link { get; set; }
|
public string Link { get; set; }
|
||||||
|
|
||||||
@ -42,6 +44,22 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MenuItem[] ContextMenuItems
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
List<MenuItem> items = new List<MenuItem>();
|
||||||
|
|
||||||
|
if (Link != null)
|
||||||
|
{
|
||||||
|
items.Add(new OsuMenuItem("Open", MenuItemType.Standard, () => host.OpenUrlExternally(Link)));
|
||||||
|
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, () => host.GetClipboard()?.SetText(Link)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
|
@ -34,6 +34,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString AutoCursorSize => new TranslatableString(getKey(@"auto_cursor_size"), @"Adjust gameplay cursor size based on current beatmap");
|
public static LocalisableString AutoCursorSize => new TranslatableString(getKey(@"auto_cursor_size"), @"Adjust gameplay cursor size based on current beatmap");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Show gameplay cursor during touch input"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString GameplayCursorDuringTouch => new TranslatableString(getKey(@"gameplay_cursor_during_touch"), @"Show gameplay cursor during touch input");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Beatmap skins"
|
/// "Beatmap skins"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -716,7 +716,7 @@ namespace osu.Game
|
|||||||
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
||||||
// in the cursor being shown for a few frames during the intro.
|
// in the cursor being shown for a few frames during the intro.
|
||||||
// This prevents the cursor from showing until we have a screen with CursorVisible = true
|
// This prevents the cursor from showing until we have a screen with CursorVisible = true
|
||||||
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
|
GlobalCursorDisplay.ShowCursor = menuScreen?.CursorVisible ?? false;
|
||||||
|
|
||||||
// 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);
|
||||||
@ -1231,7 +1231,7 @@ namespace osu.Game
|
|||||||
ScreenOffsetContainer.X = horizontalOffset;
|
ScreenOffsetContainer.X = horizontalOffset;
|
||||||
overlayContent.X = horizontalOffset * 1.2f;
|
overlayContent.X = horizontalOffset * 1.2f;
|
||||||
|
|
||||||
MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
|
GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void screenChanged(IScreen current, IScreen newScreen)
|
private void screenChanged(IScreen current, IScreen newScreen)
|
||||||
|
@ -138,7 +138,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
protected RealmKeyBindingStore KeyBindingStore { get; private set; }
|
protected RealmKeyBindingStore KeyBindingStore { get; private set; }
|
||||||
|
|
||||||
protected MenuCursorContainer MenuCursorContainer { get; private set; }
|
protected GlobalCursorDisplay GlobalCursorDisplay { get; private set; }
|
||||||
|
|
||||||
protected MusicController MusicController { get; private set; }
|
protected MusicController MusicController { get; private set; }
|
||||||
|
|
||||||
@ -340,10 +340,10 @@ namespace osu.Game
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = CreateScalingContainer().WithChildren(new Drawable[]
|
Child = CreateScalingContainer().WithChildren(new Drawable[]
|
||||||
{
|
{
|
||||||
(MenuCursorContainer = new MenuCursorContainer
|
(GlobalCursorDisplay = new GlobalCursorDisplay
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}).WithChild(content = new OsuTooltipContainer(MenuCursorContainer.Cursor)
|
}).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}),
|
}),
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
|
using osu.Game.Graphics.Cursor;
|
||||||
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;
|
||||||
@ -90,113 +91,118 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new Container
|
new OsuContextMenuContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Padding = new MarginPadding
|
Child = new Container
|
||||||
{
|
{
|
||||||
Vertical = BeatmapSetOverlay.Y_PADDING,
|
RelativeSizeAxes = Axes.X,
|
||||||
Left = BeatmapSetOverlay.X_PADDING,
|
AutoSizeAxes = Axes.Y,
|
||||||
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
|
Padding = new MarginPadding
|
||||||
},
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
fadeContent = new FillFlowContainer
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
Vertical = BeatmapSetOverlay.Y_PADDING,
|
||||||
AutoSizeAxes = Axes.Y,
|
Left = BeatmapSetOverlay.X_PADDING,
|
||||||
Direction = FillDirection.Vertical,
|
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
|
||||||
Children = new Drawable[]
|
},
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
fadeContent = new FillFlowContainer
|
||||||
{
|
{
|
||||||
new Container
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
new Container
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Child = Picker = new BeatmapPicker(),
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Margin = new MarginPadding { Top = 15 },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
title = new OsuSpriteText
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Child = Picker = new BeatmapPicker(),
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Margin = new MarginPadding { Top = 15 },
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true)
|
title = new OsuSpriteText
|
||||||
},
|
{
|
||||||
externalLink = new ExternalLinkButton
|
Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true)
|
||||||
{
|
},
|
||||||
Anchor = Anchor.BottomLeft,
|
externalLink = new ExternalLinkButton
|
||||||
Origin = Anchor.BottomLeft,
|
{
|
||||||
Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font
|
Anchor = Anchor.BottomLeft,
|
||||||
},
|
Origin = Anchor.BottomLeft,
|
||||||
explicitContent = new ExplicitContentBeatmapBadge
|
Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font
|
||||||
{
|
},
|
||||||
Alpha = 0f,
|
explicitContent = new ExplicitContentBeatmapBadge
|
||||||
Anchor = Anchor.BottomLeft,
|
{
|
||||||
Origin = Anchor.BottomLeft,
|
Alpha = 0f,
|
||||||
Margin = new MarginPadding { Left = 10, Bottom = 4 },
|
Anchor = Anchor.BottomLeft,
|
||||||
},
|
Origin = Anchor.BottomLeft,
|
||||||
spotlight = new SpotlightBeatmapBadge
|
Margin = new MarginPadding { Left = 10, Bottom = 4 },
|
||||||
{
|
},
|
||||||
Alpha = 0f,
|
spotlight = new SpotlightBeatmapBadge
|
||||||
Anchor = Anchor.BottomLeft,
|
{
|
||||||
Origin = Anchor.BottomLeft,
|
Alpha = 0f,
|
||||||
Margin = new MarginPadding { Left = 10, Bottom = 4 },
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Margin = new MarginPadding { Left = 10, Bottom = 4 },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
new FillFlowContainer
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Margin = new MarginPadding { Bottom = 20 },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
artist = new OsuSpriteText
|
Direction = FillDirection.Horizontal,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Margin = new MarginPadding { Bottom = 20 },
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
|
artist = new OsuSpriteText
|
||||||
},
|
{
|
||||||
featuredArtist = new FeaturedArtistBeatmapBadge
|
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
|
||||||
{
|
},
|
||||||
Alpha = 0f,
|
featuredArtist = new FeaturedArtistBeatmapBadge
|
||||||
Anchor = Anchor.BottomLeft,
|
{
|
||||||
Origin = Anchor.BottomLeft,
|
Alpha = 0f,
|
||||||
Margin = new MarginPadding { Left = 10 }
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Margin = new MarginPadding { Left = 10 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
new Container
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Child = author = new AuthorInfo(),
|
|
||||||
},
|
|
||||||
beatmapAvailability = new BeatmapAvailability(),
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Height = buttons_height,
|
|
||||||
Margin = new MarginPadding { Top = 10 },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
favouriteButton = new FavouriteButton
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Child = author = new AuthorInfo(),
|
||||||
|
},
|
||||||
|
beatmapAvailability = new BeatmapAvailability(),
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = buttons_height,
|
||||||
|
Margin = new MarginPadding { Top = 10 },
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
BeatmapSet = { BindTarget = BeatmapSet }
|
favouriteButton = new FavouriteButton
|
||||||
},
|
{
|
||||||
downloadButtonsContainer = new FillFlowContainer
|
BeatmapSet = { BindTarget = BeatmapSet }
|
||||||
{
|
},
|
||||||
RelativeSizeAxes = Axes.Both,
|
downloadButtonsContainer = new FillFlowContainer
|
||||||
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
|
{
|
||||||
Spacing = new Vector2(buttons_spacing),
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
|
||||||
|
Spacing = new Vector2(buttons_spacing),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
loading = new LoadingSpinner
|
loading = new LoadingSpinner
|
||||||
{
|
{
|
||||||
|
@ -121,7 +121,11 @@ namespace osu.Game.Overlays
|
|||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.Select:
|
case GlobalAction.Select:
|
||||||
CurrentDialog?.Buttons.OfType<PopupDialogOkButton>().FirstOrDefault()?.TriggerClick();
|
var clickableButton =
|
||||||
|
CurrentDialog?.Buttons.OfType<PopupDialogOkButton>().FirstOrDefault() ??
|
||||||
|
CurrentDialog?.Buttons.First();
|
||||||
|
|
||||||
|
clickableButton?.TriggerClick();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
LabelText = SkinSettingsStrings.AutoCursorSize,
|
LabelText = SkinSettingsStrings.AutoCursorSize,
|
||||||
Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
|
Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
|
||||||
},
|
},
|
||||||
|
new SettingsCheckbox
|
||||||
|
{
|
||||||
|
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
|
||||||
|
Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
||||||
|
@ -380,7 +380,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
// only show the cursor when within the playfield, by default.
|
// only show the cursor when within the playfield, by default.
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
CursorContainer IProvideCursor.Cursor => Playfield.Cursor;
|
CursorContainer IProvideCursor.MenuCursor => Playfield.Cursor;
|
||||||
|
|
||||||
public override GameplayCursorContainer Cursor => Playfield.Cursor;
|
public override GameplayCursorContainer Cursor => Playfield.Cursor;
|
||||||
|
|
||||||
|
@ -268,6 +268,8 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
|
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
|
||||||
|
|
||||||
|
public Task<Live<ScoreInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
|
||||||
|
|
||||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||||
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
|
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
|
||||||
|
|
||||||
|
@ -146,13 +146,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
waveform.Waveform = b.NewValue.Waveform;
|
waveform.Waveform = b.NewValue.Waveform;
|
||||||
track = b.NewValue.Track;
|
track = b.NewValue.Track;
|
||||||
|
|
||||||
// todo: i don't think this is safe, the track may not be loaded yet.
|
setupTimelineZoom();
|
||||||
if (track.Length > 0)
|
|
||||||
{
|
|
||||||
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
|
|
||||||
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
|
|
||||||
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
|
|
||||||
}
|
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
|
Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
|
||||||
@ -205,6 +199,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
scrollToTrackTime();
|
scrollToTrackTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupTimelineZoom()
|
||||||
|
{
|
||||||
|
if (!track.IsLoaded)
|
||||||
|
{
|
||||||
|
Scheduler.AddOnce(setupTimelineZoom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
|
||||||
|
|
||||||
|
float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
|
||||||
|
SetupZoom(initialZoom, getZoomLevelForVisibleMilliseconds(10000), getZoomLevelForVisibleMilliseconds(500));
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool OnScroll(ScrollEvent e)
|
protected override bool OnScroll(ScrollEvent e)
|
||||||
{
|
{
|
||||||
// if this is not a precision scroll event, let the editor handle the seek itself (for snapping support)
|
// if this is not a precision scroll event, let the editor handle the seek itself (for snapping support)
|
||||||
|
@ -32,20 +32,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
private readonly Container zoomedContent;
|
private readonly Container zoomedContent;
|
||||||
protected override Container<Drawable> Content => zoomedContent;
|
protected override Container<Drawable> Content => zoomedContent;
|
||||||
private float currentZoom = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current zoom level of <see cref="ZoomableScrollContainer" />.
|
/// The current zoom level of <see cref="ZoomableScrollContainer"/>.
|
||||||
/// It may differ from <see cref="Zoom" /> during transitions.
|
/// It may differ from <see cref="Zoom"/> during transitions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float CurrentZoom => currentZoom;
|
public float CurrentZoom { get; private set; } = 1;
|
||||||
|
|
||||||
|
private bool isZoomSetUp;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private IFrameBasedClock editorClock { get; set; }
|
private IFrameBasedClock editorClock { get; set; }
|
||||||
|
|
||||||
private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
|
private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
|
||||||
|
|
||||||
public ZoomableScrollContainer()
|
private float minZoom;
|
||||||
|
private float maxZoom;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="ZoomableScrollContainer"/> with no zoom range.
|
||||||
|
/// Functionality will be disabled until zoom is set up via <see cref="SetupZoom"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected ZoomableScrollContainer()
|
||||||
: base(Direction.Horizontal)
|
: base(Direction.Horizontal)
|
||||||
{
|
{
|
||||||
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
|
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
|
||||||
@ -53,46 +61,36 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
AddLayout(zoomedContentWidthCache);
|
AddLayout(zoomedContentWidthCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float minZoom = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minimum zoom level allowed.
|
/// Creates a <see cref="ZoomableScrollContainer"/> with a defined zoom range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float MinZoom
|
public ZoomableScrollContainer(float minimum, float maximum, float initial)
|
||||||
|
: this()
|
||||||
{
|
{
|
||||||
get => minZoom;
|
SetupZoom(initial, minimum, maximum);
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value < 1)
|
|
||||||
throw new ArgumentException($"{nameof(MinZoom)} must be >= 1.", nameof(value));
|
|
||||||
|
|
||||||
minZoom = value;
|
|
||||||
|
|
||||||
// ensure zoom range is in valid state before updating zoom.
|
|
||||||
if (MinZoom < MaxZoom)
|
|
||||||
updateZoom();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float maxZoom = 60;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum zoom level allowed.
|
/// Sets up the minimum and maximum range of this zoomable scroll container, along with the initial zoom value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float MaxZoom
|
/// <param name="initial">The initial zoom value, applied immediately.</param>
|
||||||
|
/// <param name="minimum">The minimum zoom value.</param>
|
||||||
|
/// <param name="maximum">The maximum zoom value.</param>
|
||||||
|
protected void SetupZoom(float initial, float minimum, float maximum)
|
||||||
{
|
{
|
||||||
get => maxZoom;
|
if (minimum < 1)
|
||||||
set
|
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be >= 1.", nameof(maximum));
|
||||||
{
|
|
||||||
if (value < 1)
|
|
||||||
throw new ArgumentException($"{nameof(MaxZoom)} must be >= 1.", nameof(value));
|
|
||||||
|
|
||||||
maxZoom = value;
|
if (maximum < 1)
|
||||||
|
throw new ArgumentException($"{nameof(maximum)} ({maximum}) must be >= 1.", nameof(maximum));
|
||||||
|
|
||||||
// ensure zoom range is in valid state before updating zoom.
|
if (minimum > maximum)
|
||||||
if (MaxZoom > MinZoom)
|
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
|
||||||
updateZoom();
|
|
||||||
}
|
minZoom = minimum;
|
||||||
|
maxZoom = maximum;
|
||||||
|
CurrentZoom = zoomTarget = initial;
|
||||||
|
isZoomSetUp = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -104,14 +102,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
set => updateZoom(value);
|
set => updateZoom(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateZoom(float? value = null)
|
private void updateZoom(float value)
|
||||||
{
|
{
|
||||||
float newZoom = Math.Clamp(value ?? Zoom, MinZoom, MaxZoom);
|
if (!isZoomSetUp)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float newZoom = Math.Clamp(value, minZoom, maxZoom);
|
||||||
|
|
||||||
if (IsLoaded)
|
if (IsLoaded)
|
||||||
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
|
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
|
||||||
else
|
else
|
||||||
currentZoom = zoomTarget = newZoom;
|
CurrentZoom = zoomTarget = newZoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -141,22 +142,25 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
private void updateZoomedContentWidth()
|
private void updateZoomedContentWidth()
|
||||||
{
|
{
|
||||||
zoomedContent.Width = DrawWidth * currentZoom;
|
zoomedContent.Width = DrawWidth * CurrentZoom;
|
||||||
zoomedContentWidthCache.Validate();
|
zoomedContentWidthCache.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AdjustZoomRelatively(float change, float? focusPoint = null)
|
public void AdjustZoomRelatively(float change, float? focusPoint = null)
|
||||||
{
|
{
|
||||||
|
if (!isZoomSetUp)
|
||||||
|
return;
|
||||||
|
|
||||||
const float zoom_change_sensitivity = 0.02f;
|
const float zoom_change_sensitivity = 0.02f;
|
||||||
|
|
||||||
setZoomTarget(zoomTarget + change * (MaxZoom - minZoom) * zoom_change_sensitivity, focusPoint);
|
setZoomTarget(zoomTarget + change * (maxZoom - minZoom) * zoom_change_sensitivity, focusPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float zoomTarget = 1;
|
private float zoomTarget = 1;
|
||||||
|
|
||||||
private void setZoomTarget(float newZoom, float? focusPoint = null)
|
private void setZoomTarget(float newZoom, float? focusPoint = null)
|
||||||
{
|
{
|
||||||
zoomTarget = Math.Clamp(newZoom, MinZoom, MaxZoom);
|
zoomTarget = Math.Clamp(newZoom, minZoom, maxZoom);
|
||||||
focusPoint ??= zoomedContent.ToLocalSpace(ToScreenSpace(new Vector2(DrawWidth / 2, 0))).X;
|
focusPoint ??= zoomedContent.ToLocalSpace(ToScreenSpace(new Vector2(DrawWidth / 2, 0))).X;
|
||||||
|
|
||||||
transformZoomTo(zoomTarget, focusPoint.Value, ZoomDuration, ZoomEasing);
|
transformZoomTo(zoomTarget, focusPoint.Value, ZoomDuration, ZoomEasing);
|
||||||
@ -192,7 +196,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
private readonly float scrollOffset;
|
private readonly float scrollOffset;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Transforms <see cref="ZoomableScrollContainer.currentZoom"/> to a new value.
|
/// Transforms <see cref="ZoomableScrollContainer.CurrentZoom"/> to a new value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="focusPoint">The focus point in absolute coordinates local to the content.</param>
|
/// <param name="focusPoint">The focus point in absolute coordinates local to the content.</param>
|
||||||
/// <param name="contentSize">The size of the content.</param>
|
/// <param name="contentSize">The size of the content.</param>
|
||||||
@ -204,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
this.scrollOffset = scrollOffset;
|
this.scrollOffset = scrollOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string TargetMember => nameof(currentZoom);
|
public override string TargetMember => nameof(CurrentZoom);
|
||||||
|
|
||||||
private float valueAt(double time)
|
private float valueAt(double time)
|
||||||
{
|
{
|
||||||
@ -222,7 +226,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
float expectedWidth = d.DrawWidth * newZoom;
|
float expectedWidth = d.DrawWidth * newZoom;
|
||||||
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
|
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
|
||||||
|
|
||||||
d.currentZoom = newZoom;
|
d.CurrentZoom = newZoom;
|
||||||
d.updateZoomedContentWidth();
|
d.updateZoomedContentWidth();
|
||||||
|
|
||||||
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
|
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
|
||||||
@ -231,7 +235,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
d.ScrollTo(targetOffset, false);
|
d.ScrollTo(targetOffset, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.currentZoom;
|
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -195,10 +196,14 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
PrepareMenuLoad();
|
PrepareMenuLoad();
|
||||||
LoadMenu();
|
LoadMenu();
|
||||||
notifications.Post(new SimpleErrorNotification
|
|
||||||
|
if (!Debugger.IsAttached)
|
||||||
{
|
{
|
||||||
Text = "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."
|
notifications.Post(new SimpleErrorNotification
|
||||||
});
|
{
|
||||||
|
Text = "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."
|
||||||
|
});
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
@ -90,18 +91,16 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
spinner.Show();
|
spinner.Show();
|
||||||
|
|
||||||
var localCancellationSource = loadCancellation = new CancellationTokenSource();
|
var localCancellationSource = loadCancellation = new CancellationTokenSource();
|
||||||
IBeatmap playableBeatmap = null;
|
|
||||||
|
var workingBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo);
|
||||||
|
|
||||||
// Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events.
|
// Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events.
|
||||||
Task.Run(() =>
|
Task.Run(() => workingBeatmap.GetPlayableBeatmap(newScore.Ruleset, newScore.Mods), loadCancellation.Token).ContinueWith(task => Schedule(() =>
|
||||||
{
|
|
||||||
playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
|
|
||||||
}, loadCancellation.Token).ContinueWith(_ => Schedule(() =>
|
|
||||||
{
|
{
|
||||||
bool hitEventsAvailable = newScore.HitEvents.Count != 0;
|
bool hitEventsAvailable = newScore.HitEvents.Count != 0;
|
||||||
Container<Drawable> container;
|
Container<Drawable> container;
|
||||||
|
|
||||||
var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap);
|
var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, task.GetResultSafely());
|
||||||
|
|
||||||
if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents))
|
if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents))
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,7 @@ using System.Linq;
|
|||||||
namespace osu.Game.Screens.Select.Carousel
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group which ensures only one child is selected.
|
/// A group which ensures only one item is selected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CarouselGroup : CarouselItem
|
public class CarouselGroup : CarouselItem
|
||||||
{
|
{
|
||||||
@ -18,13 +18,15 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
private List<CarouselItem> items = new List<CarouselItem>();
|
private List<CarouselItem> items = new List<CarouselItem>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to assign a monotonically increasing ID to children as they are added. This member is
|
/// Used to assign a monotonically increasing ID to items as they are added. This member is
|
||||||
/// incremented whenever a child is added.
|
/// incremented whenever an item is added.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private ulong currentChildID;
|
private ulong currentItemID;
|
||||||
|
|
||||||
private Comparer<CarouselItem>? criteriaComparer;
|
private Comparer<CarouselItem>? criteriaComparer;
|
||||||
|
|
||||||
|
private static readonly Comparer<CarouselItem> item_id_comparer = Comparer<CarouselItem>.Create((x, y) => x.ItemID.CompareTo(y.ItemID));
|
||||||
|
|
||||||
private FilterCriteria? lastCriteria;
|
private FilterCriteria? lastCriteria;
|
||||||
|
|
||||||
protected int GetIndexOfItem(CarouselItem lastSelected) => items.IndexOf(lastSelected);
|
protected int GetIndexOfItem(CarouselItem lastSelected) => items.IndexOf(lastSelected);
|
||||||
@ -41,7 +43,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
public virtual void AddItem(CarouselItem i)
|
public virtual void AddItem(CarouselItem i)
|
||||||
{
|
{
|
||||||
i.State.ValueChanged += state => ChildItemStateChanged(i, state.NewValue);
|
i.State.ValueChanged += state => ChildItemStateChanged(i, state.NewValue);
|
||||||
i.ChildID = ++currentChildID;
|
i.ItemID = ++currentItemID;
|
||||||
|
|
||||||
if (lastCriteria != null)
|
if (lastCriteria != null)
|
||||||
{
|
{
|
||||||
@ -90,7 +92,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
// IEnumerable<T>.OrderBy() is used instead of List<T>.Sort() to ensure sorting stability
|
// IEnumerable<T>.OrderBy() is used instead of List<T>.Sort() to ensure sorting stability
|
||||||
criteriaComparer = Comparer<CarouselItem>.Create((x, y) => x.CompareTo(criteria, y));
|
criteriaComparer = Comparer<CarouselItem>.Create((x, y) => x.CompareTo(criteria, y));
|
||||||
items = items.OrderBy(c => c, criteriaComparer).ToList();
|
items = items.OrderBy(c => c, criteriaComparer).ThenBy(c => c, item_id_comparer).ToList();
|
||||||
|
|
||||||
lastCriteria = criteria;
|
lastCriteria = criteria;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using System.Linq;
|
|||||||
namespace osu.Game.Screens.Select.Carousel
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group which ensures at least one child is selected (if the group itself is selected).
|
/// A group which ensures at least one item is selected (if the group itself is selected).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CarouselGroupEagerSelect : CarouselGroup
|
public class CarouselGroupEagerSelect : CarouselGroup
|
||||||
{
|
{
|
||||||
@ -35,16 +35,16 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// To avoid overhead during filter operations, we don't attempt any selections until after all
|
/// To avoid overhead during filter operations, we don't attempt any selections until after all
|
||||||
/// children have been filtered. This bool will be true during the base <see cref="Filter(FilterCriteria)"/>
|
/// items have been filtered. This bool will be true during the base <see cref="Filter(FilterCriteria)"/>
|
||||||
/// operation.
|
/// operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool filteringChildren;
|
private bool filteringItems;
|
||||||
|
|
||||||
public override void Filter(FilterCriteria criteria)
|
public override void Filter(FilterCriteria criteria)
|
||||||
{
|
{
|
||||||
filteringChildren = true;
|
filteringItems = true;
|
||||||
base.Filter(criteria);
|
base.Filter(criteria);
|
||||||
filteringChildren = false;
|
filteringItems = false;
|
||||||
|
|
||||||
attemptSelection();
|
attemptSelection();
|
||||||
}
|
}
|
||||||
@ -97,12 +97,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
private void attemptSelection()
|
private void attemptSelection()
|
||||||
{
|
{
|
||||||
if (filteringChildren) return;
|
if (filteringItems) return;
|
||||||
|
|
||||||
// we only perform eager selection if we are a currently selected group.
|
// we only perform eager selection if we are a currently selected group.
|
||||||
if (State.Value != CarouselItemState.Selected) return;
|
if (State.Value != CarouselItemState.Selected) return;
|
||||||
|
|
||||||
// we only perform eager selection if none of our children are in a selected state already.
|
// we only perform eager selection if none of our items are in a selected state already.
|
||||||
if (Items.Any(i => i.State.Value == CarouselItemState.Selected)) return;
|
if (Items.Any(i => i.State.Value == CarouselItemState.Selected)) return;
|
||||||
|
|
||||||
PerformSelection();
|
PerformSelection();
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used as a default sort method for <see cref="CarouselItem"/>s of differing types.
|
/// Used as a default sort method for <see cref="CarouselItem"/>s of differing types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal ulong ChildID;
|
internal ulong ItemID;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a fresh drawable version of this item.
|
/// Create a fresh drawable version of this item.
|
||||||
@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ChildID.CompareTo(other.ChildID);
|
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ItemID.CompareTo(other.ItemID);
|
||||||
|
|
||||||
public int CompareTo(CarouselItem other) => CarouselYPosition.CompareTo(other.CarouselYPosition);
|
public int CompareTo(CarouselItem other) => CarouselYPosition.CompareTo(other.CarouselYPosition);
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
Action = () =>
|
Action = () =>
|
||||||
{
|
{
|
||||||
beatmapDownloader.Download(beatmapSetInfo);
|
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo);
|
||||||
attachExistingDownload();
|
attachExistingDownload();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Screens.Utility
|
|||||||
|
|
||||||
public readonly Bindable<LatencyVisualMode> VisualMode = new Bindable<LatencyVisualMode>();
|
public readonly Bindable<LatencyVisualMode> VisualMode = new Bindable<LatencyVisualMode>();
|
||||||
|
|
||||||
public CursorContainer? Cursor { get; private set; }
|
public CursorContainer? MenuCursor { get; private set; }
|
||||||
|
|
||||||
public bool ProvidingUserCursor => IsActiveArea.Value;
|
public bool ProvidingUserCursor => IsActiveArea.Value;
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ namespace osu.Game.Screens.Utility
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
Cursor = new LatencyCursorContainer
|
MenuCursor = new LatencyCursorContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
@ -105,7 +105,7 @@ namespace osu.Game.Screens.Utility
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
Cursor = new LatencyCursorContainer
|
MenuCursor = new LatencyCursorContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
@ -119,7 +119,7 @@ namespace osu.Game.Screens.Utility
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
Cursor = new LatencyCursorContainer
|
MenuCursor = new LatencyCursorContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
|
@ -272,6 +272,8 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinImporter.Import(notification, tasks);
|
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinImporter.Import(notification, tasks);
|
||||||
|
|
||||||
|
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) => skinImporter.ImportAsUpdate(notification, task, original);
|
||||||
|
|
||||||
public Task<Live<SkinInfo>> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken);
|
public Task<Live<SkinInfo>> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -38,11 +38,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
protected OsuManualInputManagerTestScene()
|
protected OsuManualInputManagerTestScene()
|
||||||
{
|
{
|
||||||
MenuCursorContainer cursorContainer;
|
GlobalCursorDisplay cursorDisplay;
|
||||||
|
|
||||||
CompositeDrawable mainContent = cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both };
|
CompositeDrawable mainContent = cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
cursorContainer.Child = content = new OsuTooltipContainer(cursorContainer.Cursor)
|
cursorDisplay.Child = content = new OsuTooltipContainer(cursorDisplay.MenuCursor)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user