1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:07:23 +08:00

Merge branch 'master' into catch-combo-counter

This commit is contained in:
Dean Herbert 2020-09-21 17:53:00 +09:00 committed by GitHub
commit 1e09d8fd1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 548 additions and 315 deletions

View File

@ -38,7 +38,13 @@ If you are looking to install or test osu! without setting up a development envi
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Developing or debugging
## Developing a custom ruleset
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
## Developing osu!
Please make sure you have the following prerequisites:

View File

@ -5,6 +5,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo;

View File

@ -5,6 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
@ -68,14 +69,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
return base.ConvertBeatmap(original);
return base.ConvertBeatmap(original, cancellationToken);
}
protected override Beatmap<ManiaHitObject> CreateBeatmap()
@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return beatmap;
}
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
if (original is ManiaHitObject maniaOriginal)
{

View File

@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Types;
using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@ -48,14 +49,16 @@ namespace osu.Game.Rulesets.Osu.Objects
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
protected override void CreateNestedHitObjects()
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects();
base.CreateNestedHitObjects(cancellationToken);
int totalSpins = MaximumBonusSpins + SpinsRequired;
for (int i = 0; i < totalSpins; i++)
{
cancellationToken.ThrowIfCancellationRequested();
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired

View File

@ -8,6 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Utils;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@ -48,14 +50,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
public override bool CanConvert() => true;
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original)
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
if (original.BeatmapInfo.RulesetID == 3)
{
@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
return converted;
}
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
// Old osu! used hit sounding to determine various hit type information
IList<HitSampleInfo> samples = obj.Samples;
@ -104,6 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
};
i = (i + 1) % allSamples.Count;
if (Precision.AlmostEquals(0, tickSpacing))
break;
}
}
else

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Objects;
@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
// as that index can be simply subtracted from the current index to get the number of elements in between
// without off-by-one errors
int indexBeforeLastRepeat = -1;
int lastMarkEnd = 0;
for (int i = 0; i < hitObjects.Count; i++)
{
@ -87,7 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
if (repeatedLength < roll_min_repetitions)
continue;
markObjectsAsCheese(i, repeatedLength);
markObjectsAsCheese(Math.Max(lastMarkEnd, i - repeatedLength + 1), i);
lastMarkEnd = i;
}
}
@ -113,6 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
private void findTlTap(int parity, HitType type)
{
int tlLength = -2;
int lastMarkEnd = 0;
for (int i = parity; i < hitObjects.Count; i += 2)
{
@ -124,17 +128,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
if (tlLength < tl_min_repetitions)
continue;
markObjectsAsCheese(i, tlLength);
markObjectsAsCheese(Math.Max(lastMarkEnd, i - tlLength + 1), i);
lastMarkEnd = i;
}
}
/// <summary>
/// Marks <paramref name="count"/> elements counting backwards from <paramref name="end"/> as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
/// Marks all objects from <paramref name="start"/> to <paramref name="end"/> (inclusive) as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
/// </summary>
private void markObjectsAsCheese(int end, int count)
private void markObjectsAsCheese(int start, int end)
{
for (int i = 0; i < count; ++i)
hitObjects[end - i].StaminaCheese = true;
for (int i = start; i <= end; i++)
hitObjects[i].StaminaCheese = true;
}
}
}

View File

@ -28,17 +28,17 @@ using FileInfo = System.IO.FileInfo;
namespace osu.Game.Tests.Beatmaps.IO
{
[TestFixture]
public class ImportBeatmapTest
public class ImportBeatmapTest : ImportTest
{
[Test]
public async Task TestImportWhenClosed()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
await LoadOszIntoOsu(loadOsu(host));
await LoadOszIntoOsu(LoadOsuIntoHost(host));
}
finally
{
@ -51,11 +51,11 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDelete()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var imported = await LoadOszIntoOsu(osu);
@ -72,11 +72,11 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var imported = await LoadOszIntoOsu(osu);
var importedSecondTime = await LoadOszIntoOsu(osu);
@ -98,11 +98,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithReZip()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -156,11 +156,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithChangedFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -207,11 +207,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithDifferentFilename()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -259,11 +259,11 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportCorruptThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var imported = await LoadOszIntoOsu(osu);
@ -301,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestRollbackOnFailure()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
@ -314,7 +314,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Interlocked.Increment(ref loggedExceptionCount);
};
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
// ReSharper disable once AccessToModifiedClosure
@ -378,11 +378,11 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var imported = await LoadOszIntoOsu(osu);
@ -406,11 +406,11 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(set.ToString()))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var imported = await LoadOszIntoOsu(osu);
@ -440,11 +440,11 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWithDuplicateBeatmapIDs()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var metadata = new BeatmapMetadata
{
@ -496,15 +496,15 @@ namespace osu.Game.Tests.Beatmaps.IO
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
public void TestImportOverIPC()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true))
using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true))
using (HeadlessGameHost client = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-client", true))
{
try
{
Assert.IsTrue(host.IsPrimaryInstance);
Assert.IsFalse(client.IsPrimaryInstance);
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -526,11 +526,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWhenFileOpen()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
using (File.OpenRead(temp))
await osu.Dependencies.Get<BeatmapManager>().Import(temp);
@ -548,11 +548,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithDuplicateHashes()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -590,11 +590,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportNestedStructure()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -635,11 +635,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithIgnoredDirectoryInArchive()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
@ -689,11 +689,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestUpdateBeatmapInfo()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var temp = TestResources.GetTestBeatmapForImport();
@ -719,11 +719,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestUpdateBeatmapFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var temp = TestResources.GetTestBeatmapForImport();
@ -761,11 +761,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public void TestCreateNewEmptyBeatmap()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
@ -788,11 +788,11 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public void TestCreateNewBeatmapWithObject()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
@ -863,14 +863,6 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
}
private OsuGameBase loadOsu(GameHost host)
{
var osu = new OsuGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
{
IEnumerable<BeatmapSetInfo> resultSets = null;

View File

@ -4,18 +4,15 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO
{
[TestFixture]
public class ImportCollectionsTest
public class ImportCollectionsTest : ImportTest
{
[Test]
public async Task TestImportEmptyDatabase()
@ -24,7 +21,7 @@ namespace osu.Game.Tests.Collections.IO
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(new MemoryStream());
@ -44,7 +41,7 @@ namespace osu.Game.Tests.Collections.IO
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
@ -70,7 +67,7 @@ namespace osu.Game.Tests.Collections.IO
{
try
{
var osu = loadOsu(host, true);
var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
@ -101,7 +98,7 @@ namespace osu.Game.Tests.Collections.IO
{
AppDomain.CurrentDomain.UnhandledException += setException;
var osu = loadOsu(host, true);
var osu = LoadOsuIntoHost(host, true);
using (var ms = new MemoryStream())
{
@ -135,7 +132,7 @@ namespace osu.Game.Tests.Collections.IO
{
try
{
var osu = loadOsu(host, true);
var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
@ -156,7 +153,7 @@ namespace osu.Game.Tests.Collections.IO
{
try
{
var osu = loadOsu(host, true);
var osu = LoadOsuIntoHost(host, true);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -172,50 +169,5 @@ namespace osu.Game.Tests.Collections.IO
}
}
}
private TestOsuGameBase loadOsu(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
#pragma warning disable 4014
Task.Run(() => host.Run(osu));
#pragma warning restore 4014
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private class TestOsuGameBase : OsuGameBase
{
public CollectionManager CollectionManager { get; private set; }
private readonly bool withBeatmap;
public TestOsuGameBase(bool withBeatmap)
{
this.withBeatmap = withBeatmap;
}
[BackgroundDependencyLoader]
private void load()
{
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
AddInternal(CollectionManager = new CollectionManager(Storage));
}
}
}
}

View File

@ -0,0 +1,66 @@
// 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.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests
{
public abstract class ImportTest
{
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
bool ready = false;
// wait for two update frames to be executed. this ensures that all components have had a change to run LoadComplete and hopefully avoid
// database access (GlobalActionContainer is one to do this).
host.UpdateThread.Scheduler.Add(() => host.UpdateThread.Scheduler.Add(() => ready = true));
waitForOrAssert(() => ready, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
public class TestOsuGameBase : OsuGameBase
{
public CollectionManager CollectionManager { get; private set; }
private readonly bool withBeatmap;
public TestOsuGameBase(bool withBeatmap)
{
this.withBeatmap = withBeatmap;
}
[BackgroundDependencyLoader]
private void load()
{
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
AddInternal(CollectionManager = new CollectionManager(Storage));
}
}
}
}

View File

@ -4,8 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation;
@ -17,18 +16,18 @@ using osu.Game.IO;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class CustomDataDirectoryTest
public class CustomDataDirectoryTest : ImportTest
{
[Test]
public void TestDefaultDirectory()
{
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory)))
using (var host = new CustomTestHeadlessGameHost())
{
try
{
string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory));
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
@ -45,14 +44,14 @@ namespace osu.Game.Tests.NonVisual
{
string customPath = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory)))
using (var host = new CustomTestHeadlessGameHost())
{
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
// switch to DI'd storage
var storage = osu.Dependencies.Get<Storage>();
@ -71,14 +70,14 @@ namespace osu.Game.Tests.NonVisual
{
string customPath = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup)))
using (var host = new CustomTestHeadlessGameHost())
{
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
// switch to DI'd storage
var storage = osu.Dependencies.Get<Storage>();
@ -104,13 +103,13 @@ namespace osu.Game.Tests.NonVisual
{
string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration)))
using (var host = new CustomTestHeadlessGameHost())
{
try
{
string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration));
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
@ -165,11 +164,11 @@ namespace osu.Game.Tests.NonVisual
string customPath = prepareCustomPath();
string customPath2 = prepareCustomPath("-2");
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
using (var host = new CustomTestHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
const string database_filename = "client.db";
@ -194,11 +193,11 @@ namespace osu.Game.Tests.NonVisual
{
string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
using (var host = new CustomTestHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
@ -215,11 +214,11 @@ namespace osu.Game.Tests.NonVisual
{
string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
using (var host = new CustomTestHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
@ -244,11 +243,11 @@ namespace osu.Game.Tests.NonVisual
{
string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
using (var host = new CustomTestHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
@ -268,25 +267,6 @@ namespace osu.Game.Tests.NonVisual
}
}
private OsuGameBase loadOsu(GameHost host)
{
var osu = new OsuGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private static string getDefaultLocationFor(string testTypeName)
{
string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
@ -307,14 +287,14 @@ namespace osu.Game.Tests.NonVisual
return path;
}
public class CustomTestHeadlessGameHost : HeadlessGameHost
public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost
{
public Storage InitialStorage { get; }
public CustomTestHeadlessGameHost(string name)
: base(name)
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
: base(callingMethodName: callingMethodName)
{
string defaultStorageLocation = getDefaultLocationFor(name);
string defaultStorageLocation = getDefaultLocationFor(callingMethodName);
InitialStorage = new DesktopStorage(defaultStorageLocation, this);
InitialStorage.DeleteDirectory(string.Empty);

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -17,12 +16,11 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Scores.IO
{
public class ImportScoreTest
public class ImportScoreTest : ImportTest
{
[Test]
public async Task TestBasicImport()
@ -31,7 +29,7 @@ namespace osu.Game.Tests.Scores.IO
{
try
{
var osu = await loadOsu(host);
var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo
{
@ -45,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
OnlineScoreID = 12345,
};
var imported = await loadIntoOsu(osu, toImport);
var imported = await loadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
@ -70,14 +68,14 @@ namespace osu.Game.Tests.Scores.IO
{
try
{
var osu = await loadOsu(host);
var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo
{
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
};
var imported = await loadIntoOsu(osu, toImport);
var imported = await loadScoreIntoOsu(osu, toImport);
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
@ -96,7 +94,7 @@ namespace osu.Game.Tests.Scores.IO
{
try
{
var osu = await loadOsu(host);
var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo
{
@ -107,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO
}
};
var imported = await loadIntoOsu(osu, toImport);
var imported = await loadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
@ -126,7 +124,7 @@ namespace osu.Game.Tests.Scores.IO
{
try
{
var osu = await loadOsu(host);
var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo
{
@ -138,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO
}
};
var imported = await loadIntoOsu(osu, toImport);
var imported = await loadScoreIntoOsu(osu, toImport);
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
var scoreManager = osu.Dependencies.Get<ScoreManager>();
@ -146,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID)));
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
var secondImport = await loadIntoOsu(osu, imported);
var secondImport = await loadScoreIntoOsu(osu, imported);
Assert.That(secondImport, Is.Null);
}
finally
@ -163,9 +161,9 @@ namespace osu.Game.Tests.Scores.IO
{
try
{
var osu = await loadOsu(host);
var osu = LoadOsuIntoHost(host, true);
await loadIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
var scoreManager = osu.Dependencies.Get<ScoreManager>();
@ -179,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO
}
}
private async Task<ScoreInfo> loadIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
private async Task<ScoreInfo> loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
@ -192,33 +190,6 @@ namespace osu.Game.Tests.Scores.IO
return scoreManager.GetAllUsableScores().FirstOrDefault();
}
private async Task<OsuGameBase> loadOsu(GameHost host)
{
var osu = new OsuGameBase();
#pragma warning disable 4014
Task.Run(() => host.Run(osu));
#pragma warning restore 4014
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
var beatmapFile = TestResources.GetTestBeatmapForImport();
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
await beatmapManager.Import(beatmapFile);
return osu;
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private class TestArchiveReader : ArchiveReader
{
public TestArchiveReader()

View File

@ -0,0 +1,147 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Skins.IO
{
public class ImportSkinTest : ImportTest
{
[Test]
public async Task TestBasicImport()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk"));
Assert.That(imported.Name, Is.EqualTo("test skin"));
Assert.That(imported.Creator, Is.EqualTo("skinner"));
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportTwiceWithSameMetadata()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(1));
// the first should be overwritten by the second import.
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportTwiceWithNoMetadata()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(2));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportTwiceWithDifferentMetadata()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(2));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
host.Exit();
}
}
}
private MemoryStream createOsk(string name, string author)
{
var zipStream = new MemoryStream();
using var zip = ZipArchive.Create();
zip.AddEntry("skin.ini", generateSkinIni(name, author));
zip.SaveTo(zipStream);
return zipStream;
}
private MemoryStream generateSkinIni(string name, string author)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.WriteLine("[General]");
writer.WriteLine($"Name: {name}");
writer.WriteLine($"Author: {author}");
writer.WriteLine();
writer.WriteLine($"# unique {Guid.NewGuid()}");
writer.Flush();
return stream;
}
private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{
var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(archive);
}
}
}

View File

@ -22,27 +22,16 @@ namespace osu.Game.Tests.Visual.Collections
{
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content => content;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private readonly Container content;
private readonly DialogOverlay dialogOverlay;
private readonly CollectionManager manager;
private DialogOverlay dialogOverlay;
private CollectionManager manager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private ManageCollectionsDialog dialog;
public TestSceneManageCollectionsDialog()
{
base.Content.AddRange(new Drawable[]
{
manager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both },
dialogOverlay = new DialogOverlay()
});
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
@ -50,14 +39,16 @@ namespace osu.Game.Tests.Visual.Collections
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(manager);
dependencies.Cache(dialogOverlay);
return dependencies;
base.Content.AddRange(new Drawable[]
{
manager = new CollectionManager(LocalStorage),
Content,
dialogOverlay = new DialogOverlay()
});
Dependencies.Cache(manager);
Dependencies.Cache(dialogOverlay);
}
[SetUp]

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -276,7 +277,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override bool CanConvert() => true;
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
yield return new TestHitObject
{

View File

@ -54,7 +54,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Ruleset = new OsuRuleset().RulesetInfo,
OnlineBeatmapID = beatmapId,
Path = "normal.osu",
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
BPM = bpm,

View File

@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Navigation
protected TestOsuGame Game;
protected override bool UseFreshStoragePerRun => true;
[BackgroundDependencyLoader]
private void load(GameHost host)
{

View File

@ -74,6 +74,13 @@ namespace osu.Game.Tests.Visual.Navigation
private void returnToMenu()
{
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
AddStep("pause audio", () =>
{
if (Game.MusicController.IsPlaying)
Game.MusicController.TogglePause();
});
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
}

View File

@ -110,6 +110,13 @@ namespace osu.Game.Tests.Visual.Navigation
private void returnToMenu()
{
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
AddStep("pause audio", () =>
{
if (Game.MusicController.IsPlaying)
Game.MusicController.TogglePause();
});
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
}

View File

@ -839,7 +839,6 @@ namespace osu.Game.Tests.Visual.SongSelect
new BeatmapInfo
{
OnlineBeatmapID = id * 10,
Path = "normal.osu",
Version = "Normal",
StarDifficulty = 2,
BaseDifficulty = new BeatmapDifficulty
@ -850,7 +849,6 @@ namespace osu.Game.Tests.Visual.SongSelect
new BeatmapInfo
{
OnlineBeatmapID = id * 10 + 1,
Path = "hard.osu",
Version = "Hard",
StarDifficulty = 5,
BaseDifficulty = new BeatmapDifficulty
@ -861,7 +859,6 @@ namespace osu.Game.Tests.Visual.SongSelect
new BeatmapInfo
{
OnlineBeatmapID = id * 10 + 2,
Path = "insane.osu",
Version = "Insane",
StarDifficulty = 6,
BaseDifficulty = new BeatmapDifficulty

View File

@ -23,25 +23,15 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneFilterControl : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content => content;
private readonly Container content;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private readonly CollectionManager collectionManager;
private CollectionManager collectionManager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private FilterControl control;
public TestSceneFilterControl()
{
base.Content.AddRange(new Drawable[]
{
collectionManager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
@ -49,13 +39,14 @@ namespace osu.Game.Tests.Visual.SongSelect
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(collectionManager);
return dependencies;
base.Content.AddRange(new Drawable[]
{
collectionManager = new CollectionManager(LocalStorage),
Content
});
Dependencies.Cache(collectionManager);
}
[SetUp]

View File

@ -879,7 +879,6 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Ruleset = getRuleset(),
OnlineBeatmapID = beatmapId,
Path = "normal.osu",
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
BPM = bpm,

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
@ -26,6 +27,8 @@ namespace osu.Game.Beatmaps
public IBeatmap Beatmap { get; }
private CancellationToken cancellationToken;
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
{
Beatmap = beatmap;
@ -36,14 +39,25 @@ namespace osu.Game.Beatmaps
/// </summary>
public abstract bool CanConvert();
/// <summary>
/// Converts <see cref="Beatmap"/>.
/// </summary>
/// <returns>The converted Beatmap.</returns>
public IBeatmap Convert()
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
this.cancellationToken = cancellationToken;
// We always operate on a clone of the original beatmap, to not modify it game-wide
return ConvertBeatmap(Beatmap.Clone());
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
}
/// <summary>
/// Performs the conversion of a Beatmap using this Beatmap Converter.
/// </summary>
/// <param name="original">The un-converted Beatmap.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The converted Beatmap.</returns>
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
#pragma warning disable 618
return ConvertBeatmap(original);
#pragma warning restore 618
}
/// <summary>
@ -51,19 +65,20 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="original">The un-converted Beatmap.</param>
/// <returns>The converted Beatmap.</returns>
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
{
var beatmap = CreateBeatmap();
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList();
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks;
return beatmap;
}
private List<T> convertHitObjects(IReadOnlyList<HitObject> hitObjects, IBeatmap beatmap)
private List<T> convertHitObjects(IReadOnlyList<HitObject> hitObjects, IBeatmap beatmap, CancellationToken cancellationToken)
{
var result = new List<T>(hitObjects.Count);
@ -75,7 +90,7 @@ namespace osu.Game.Beatmaps
continue;
}
var converted = ConvertHitObject(obj, beatmap);
var converted = ConvertHitObject(obj, beatmap, cancellationToken);
if (ObjectConverted != null)
{
@ -104,7 +119,23 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The converted hit object.</returns>
protected abstract IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap);
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
#pragma warning disable 618
return ConvertHitObject(original, beatmap);
#pragma warning restore 618
}
/// <summary>
/// Performs the conversion of a hit object.
/// This method is generally executed sequentially for all objects in a beatmap.
/// </summary>
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <returns>The converted hit object.</returns>
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty<T>();
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
@ -76,7 +77,7 @@ namespace osu.Game.Beatmaps
public bool CanConvert() => true;
public IBeatmap Convert()
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
foreach (var obj in Beatmap.HitObjects)
ObjectConverted?.Invoke(obj, obj.Yield());

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
@ -30,6 +31,8 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Converts <see cref="Beatmap"/>.
/// </summary>
IBeatmap Convert();
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The converted Beatmap.</returns>
IBeatmap Convert(CancellationToken cancellationToken = default);
}
}

View File

@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps
}
// Convert
IBeatmap converted = converter.Convert();
IBeatmap converted = converter.Convert(cancellationSource.Token);
// Apply conversion mods to the result
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())

View File

@ -230,7 +230,7 @@ namespace osu.Game.Collections
public void DeleteAll()
{
Collections.Clear();
PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" });
PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" });
}
private readonly object saveLock = new object();

View File

@ -24,6 +24,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.Skin, 0, -1, int.MaxValue);
Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
Set(OsuSetting.BeatmapDetailModsFilter, false);
Set(OsuSetting.ShowConvertedBeatmaps, true);
Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
@ -200,6 +201,7 @@ namespace osu.Game.Configuration
CursorRotation,
MenuParallax,
BeatmapDetailTab,
BeatmapDetailModsFilter,
Username,
ReleaseStream,
SavePassword,

View File

@ -253,6 +253,9 @@ namespace osu.Game.Database
/// Generally should include all file types which determine the file's uniqueness.
/// Large files should be avoided if possible.
/// </summary>
/// <remarks>
/// This is only used by the default hash implementation. If <see cref="ComputeHash"/> is overridden, it will not be used.
/// </remarks>
protected abstract string[] HashableFileTypes { get; }
internal static void LogForModel(TModel model, string message, Exception e = null)
@ -271,7 +274,7 @@ namespace osu.Game.Database
/// <remarks>
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
/// </remarks>
private string computeHash(TModel item, ArchiveReader reader = null)
protected virtual string ComputeHash(TModel item, ArchiveReader reader = null)
{
// for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
@ -318,7 +321,7 @@ namespace osu.Game.Database
LogForModel(item, "Beginning import...");
item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
item.Hash = computeHash(item, archive);
item.Hash = ComputeHash(item, archive);
await Populate(item, archive, cancellationToken);
@ -437,7 +440,7 @@ namespace osu.Game.Database
{
using (ContextFactory.GetForWrite())
{
item.Hash = computeHash(item);
item.Hash = ComputeHash(item);
ModelStore.Update(item);
}
}

View File

@ -123,8 +123,8 @@ namespace osu.Game.Graphics.UserInterface
protected void FadeUnhovered()
{
Bar.FadeOut(transition_length, Easing.OutQuint);
Text.FadeColour(AccentColour, transition_length, Easing.OutQuint);
Bar.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
Text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)

View File

@ -211,7 +211,7 @@ namespace osu.Game.Overlays.Chat.Tabs
TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH);
box.FadeColour(BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint);
box.FadeColour(IsHovered ? backgroundHover : BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint);
highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
Text.Font = Text.Font.With(weight: FontWeight.Medium);

View File

@ -23,6 +23,7 @@ using osu.Game.Overlays.Chat.Selection;
using osu.Game.Overlays.Chat.Tabs;
using osuTK.Input;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Overlays
{
@ -78,7 +79,7 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, OsuColour colours)
private void load(OsuConfigManager config, OsuColour colours, TextureStore textures)
{
const float padding = 5;
@ -163,13 +164,13 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
new SpriteIcon
new Sprite
{
Icon = FontAwesome.Solid.Comments,
Texture = textures.Get(IconTexture),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(20),
Margin = new MarginPadding(10),
Size = new Vector2(OverlayTitle.ICON_SIZE),
Margin = new MarginPadding { Left = 10 },
},
ChannelTabControl = CreateChannelTabControl().With(d =>
{

View File

@ -14,6 +14,8 @@ namespace osu.Game.Overlays
{
public abstract class OverlayTitle : CompositeDrawable, INamedOverlayComponent
{
public const float ICON_SIZE = 30;
private readonly OsuSpriteText titleText;
private readonly Container icon;
@ -51,7 +53,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 5 }, // compensates for osu-web sprites having around 5px of whitespace on each side
Size = new Vector2(30)
Size = new Vector2(ICON_SIZE)
},
titleText = new OsuSpriteText
{

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile;
@ -176,6 +177,10 @@ namespace osu.Game.Overlays
AccentColour = colourProvider.Highlight1;
}
protected override bool OnClick(ClickEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
private class ProfileSectionTabItem : OverlayTabItem
{
public ProfileSectionTabItem(ProfileSection value)

View File

@ -145,6 +145,7 @@ namespace osu.Game.Rulesets.Objects
#pragma warning restore 618
}
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual void CreateNestedHitObjects()
{
}

View File

@ -207,14 +207,15 @@ namespace osu.Game.Screens.Edit
beatmapProcessor?.PreProcess();
foreach (var hitObject in pendingUpdates)
{
processHitObject(hitObject);
HitObjectUpdated?.Invoke(hitObject);
}
pendingUpdates.Clear();
beatmapProcessor?.PostProcess();
// explicitly needs to be fired after PostProcess
foreach (var hitObject in pendingUpdates)
HitObjectUpdated?.Invoke(hitObject);
pendingUpdates.Clear();
}
}

View File

@ -30,6 +30,8 @@ namespace osu.Game.Screens.Select
protected Bindable<BeatmapDetailAreaTabItem> CurrentTab => tabControl.Current;
protected Bindable<bool> CurrentModsFilter => tabControl.CurrentModsFilter;
private readonly Container content;
protected override Container<Drawable> Content => content;

View File

@ -26,6 +26,12 @@ namespace osu.Game.Screens.Select
set => tabs.Current = value;
}
public Bindable<bool> CurrentModsFilter
{
get => modsCheckbox.Current;
set => modsCheckbox.Current = value;
}
public Action<BeatmapDetailAreaTabItem, bool> OnFilter; // passed the selected tab and if mods is checked
public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems

View File

@ -66,24 +66,9 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config)
{
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
showConverted.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
minimumStars.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
maximumStars.ValueChanged += _ => updateCriteria();
ruleset.BindTo(parentRuleset);
ruleset.BindValueChanged(_ => updateCriteria());
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
groupMode.BindValueChanged(_ => updateCriteria());
sortMode.BindValueChanged(_ => updateCriteria());
Children = new Drawable[]
{
new Box
@ -182,6 +167,21 @@ namespace osu.Game.Screens.Select
}
};
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
showConverted.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
minimumStars.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
maximumStars.ValueChanged += _ => updateCriteria();
ruleset.BindTo(parentRuleset);
ruleset.BindValueChanged(_ => updateCriteria());
groupMode.BindValueChanged(_ => updateCriteria());
sortMode.BindValueChanged(_ => updateCriteria());
collectionDropdown.Current.ValueChanged += val =>
{
if (val.NewValue == null)

View File

@ -29,6 +29,8 @@ namespace osu.Game.Screens.Select
private Bindable<TabType> selectedTab;
private Bindable<bool> selectedModsFilter;
public PlayBeatmapDetailArea()
{
Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both });
@ -38,8 +40,13 @@ namespace osu.Game.Screens.Select
private void load(OsuConfigManager config)
{
selectedTab = config.GetBindable<TabType>(OsuSetting.BeatmapDetailTab);
selectedModsFilter = config.GetBindable<bool>(OsuSetting.BeatmapDetailModsFilter);
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
CurrentTab.BindValueChanged(tab => selectedTab.Value = getTabTypeFromTabItem(tab.NewValue));
selectedModsFilter.BindValueChanged(checkbox => CurrentModsFilter.Value = checkbox.NewValue, true);
CurrentModsFilter.BindValueChanged(checkbox => selectedModsFilter.Value = checkbox.NewValue);
}
public override void Refresh()

View File

@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
@ -86,21 +87,46 @@ namespace osu.Game.Skinning
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name };
private const string unknown_creator_string = "Unknown";
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
{
// we need to populate early to create a hash based off skin.ini contents
if (item.Name?.Contains(".osk") == true)
populateMetadata(item);
if (item.Creator != null && item.Creator != unknown_creator_string)
{
// this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation.
// likely, the skin should expose a real version (ie. the version of the skin, not the skin.ini version it's targeting).
return item.ToString().ComputeSHA2Hash();
}
// if there was no creator, the ToString above would give the filename, which alone isn't really enough to base any decisions on.
return base.ComputeHash(item, reader);
}
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
await base.Populate(model, archive, cancellationToken);
Skin reference = GetSkin(model);
if (model.Name?.Contains(".osk") == true)
populateMetadata(model);
}
private void populateMetadata(SkinInfo item)
{
Skin reference = GetSkin(item);
if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name))
{
model.Name = reference.Configuration.SkinInfo.Name;
model.Creator = reference.Configuration.SkinInfo.Creator;
item.Name = reference.Configuration.SkinInfo.Name;
item.Creator = reference.Configuration.SkinInfo.Creator;
}
else
{
model.Name = model.Name.Replace(".osk", "");
model.Creator ??= "Unknown";
item.Name = item.Name.Replace(".osk", "");
item.Creator ??= unknown_creator_string;
}
}

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual
private Lazy<Storage> localStorage;
protected Storage LocalStorage => localStorage.Value;
private readonly Lazy<DatabaseContextFactory> contextFactory;
private Lazy<DatabaseContextFactory> contextFactory;
protected IAPIProvider API
{
@ -69,8 +69,33 @@ namespace osu.Game.Tests.Visual
/// </summary>
protected virtual bool UseOnlineAPI => false;
/// <summary>
/// When running headless, there is an opportunity to use the host storage rather than creating a second isolated one.
/// This is because the host is recycled per TestScene execution in headless at an nunit level.
/// </summary>
private Storage isolatedHostStorage;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
if (!UseFreshStoragePerRun)
isolatedHostStorage = (parent.Get<GameHost>() as HeadlessGameHost)?.Storage;
contextFactory = new Lazy<DatabaseContextFactory>(() =>
{
var factory = new DatabaseContextFactory(LocalStorage);
// only reset the database if not using the host storage.
// if we reset the host storage, it will delete global key bindings.
if (isolatedHostStorage == null)
factory.ResetDatabase();
using (var usage = factory.Get())
usage.Migrate();
return factory;
});
RecycleLocalStorage();
var baseDependencies = base.CreateChildDependencies(parent);
var providedRuleset = CreateRuleset();
@ -104,19 +129,11 @@ namespace osu.Game.Tests.Visual
protected OsuTestScene()
{
RecycleLocalStorage();
contextFactory = new Lazy<DatabaseContextFactory>(() =>
{
var factory = new DatabaseContextFactory(LocalStorage);
factory.ResetDatabase();
using (var usage = factory.Get())
usage.Migrate();
return factory;
});
base.Content.Add(content = new DrawSizePreservingFillContainer());
}
protected virtual bool UseFreshStoragePerRun => false;
public virtual void RecycleLocalStorage()
{
if (localStorage?.IsValueCreated == true)
@ -131,7 +148,8 @@ namespace osu.Game.Tests.Visual
}
}
localStorage = new Lazy<Storage>(() => new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}")));
localStorage =
new Lazy<Storage>(() => isolatedHostStorage ?? new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}")));
}
[Resolved]
@ -172,7 +190,7 @@ namespace osu.Game.Tests.Visual
if (MusicController?.TrackLoaded == true)
MusicController.CurrentTrack.Stop();
if (contextFactory.IsValueCreated)
if (contextFactory?.IsValueCreated == true)
contextFactory.Value.ResetDatabase();
RecycleLocalStorage();