1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:28:00 +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. 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: 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 osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions; 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); 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 positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo; var comboData = obj as IHasCombo;

View File

@ -5,6 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; 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); 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; 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); int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed); Random = new FastRandom(seed);
return base.ConvertBeatmap(original); return base.ConvertBeatmap(original, cancellationToken);
} }
protected override Beatmap<ManiaHitObject> CreateBeatmap() protected override Beatmap<ManiaHitObject> CreateBeatmap()
@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return beatmap; 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) if (original is ManiaHitObject maniaOriginal)
{ {

View File

@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Linq; using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions; 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); 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 positionData = original as IHasPosition;
var comboData = original as IHasCombo; var comboData = original as IHasCombo;

View File

@ -1,6 +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.
using System.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -48,14 +49,16 @@ namespace osu.Game.Rulesets.Osu.Objects
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); 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; int totalSpins = MaximumBonusSpins + SpinsRequired;
for (int i = 0; i < totalSpins; i++) for (int i = 0; i < totalSpins; i++)
{ {
cancellationToken.ThrowIfCancellationRequested();
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired AddNested(i < SpinsRequired

View File

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

View File

@ -1,6 +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.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Objects; 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 // as that index can be simply subtracted from the current index to get the number of elements in between
// without off-by-one errors // without off-by-one errors
int indexBeforeLastRepeat = -1; int indexBeforeLastRepeat = -1;
int lastMarkEnd = 0;
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {
@ -87,7 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
if (repeatedLength < roll_min_repetitions) if (repeatedLength < roll_min_repetitions)
continue; 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) private void findTlTap(int parity, HitType type)
{ {
int tlLength = -2; int tlLength = -2;
int lastMarkEnd = 0;
for (int i = parity; i < hitObjects.Count; i += 2) 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) if (tlLength < tl_min_repetitions)
continue; continue;
markObjectsAsCheese(i, tlLength); markObjectsAsCheese(Math.Max(lastMarkEnd, i - tlLength + 1), i);
lastMarkEnd = i;
} }
} }
/// <summary> /// <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> /// </summary>
private void markObjectsAsCheese(int end, int count) private void markObjectsAsCheese(int start, int end)
{ {
for (int i = 0; i < count; ++i) for (int i = start; i <= end; i++)
hitObjects[end - i].StaminaCheese = true; hitObjects[i].StaminaCheese = true;
} }
} }
} }

View File

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

View File

@ -4,18 +4,15 @@
using System; using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO namespace osu.Game.Tests.Collections.IO
{ {
[TestFixture] [TestFixture]
public class ImportCollectionsTest public class ImportCollectionsTest : ImportTest
{ {
[Test] [Test]
public async Task TestImportEmptyDatabase() public async Task TestImportEmptyDatabase()
@ -24,7 +21,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(new MemoryStream()); await osu.CollectionManager.Import(new MemoryStream());
@ -44,7 +41,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
@ -70,7 +67,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
try try
{ {
var osu = loadOsu(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
@ -101,7 +98,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
AppDomain.CurrentDomain.UnhandledException += setException; AppDomain.CurrentDomain.UnhandledException += setException;
var osu = loadOsu(host, true); var osu = LoadOsuIntoHost(host, true);
using (var ms = new MemoryStream()) using (var ms = new MemoryStream())
{ {
@ -135,7 +132,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
try try
{ {
var osu = loadOsu(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
@ -156,7 +153,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
try try
{ {
var osu = loadOsu(host, true); var osu = LoadOsuIntoHost(host, true);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); 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;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -17,18 +16,18 @@ using osu.Game.IO;
namespace osu.Game.Tests.NonVisual namespace osu.Game.Tests.NonVisual
{ {
[TestFixture] [TestFixture]
public class CustomDataDirectoryTest public class CustomDataDirectoryTest : ImportTest
{ {
[Test] [Test]
public void TestDefaultDirectory() public void TestDefaultDirectory()
{ {
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory))) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
{ {
string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory)); string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory));
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
@ -45,14 +44,14 @@ namespace osu.Game.Tests.NonVisual
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory))) using (var host = new CustomTestHeadlessGameHost())
{ {
using (var storageConfig = new StorageConfigManager(host.InitialStorage)) using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath); storageConfig.Set(StorageConfig.FullPath, customPath);
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
// switch to DI'd storage // switch to DI'd storage
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
@ -71,14 +70,14 @@ namespace osu.Game.Tests.NonVisual
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup))) using (var host = new CustomTestHeadlessGameHost())
{ {
using (var storageConfig = new StorageConfigManager(host.InitialStorage)) using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath); storageConfig.Set(StorageConfig.FullPath, customPath);
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
// switch to DI'd storage // switch to DI'd storage
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
@ -104,13 +103,13 @@ namespace osu.Game.Tests.NonVisual
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration))) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
{ {
string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration)); string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration));
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>(); 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. // 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 customPath = prepareCustomPath();
string customPath2 = prepareCustomPath("-2"); string customPath2 = prepareCustomPath("-2");
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets))) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
const string database_filename = "client.db"; const string database_filename = "client.db";
@ -194,11 +193,11 @@ namespace osu.Game.Tests.NonVisual
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath)); Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
@ -215,11 +214,11 @@ namespace osu.Game.Tests.NonVisual
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails))) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.DoesNotThrow(() => osu.Migrate(customPath));
@ -244,11 +243,11 @@ namespace osu.Game.Tests.NonVisual
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget))) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath)); 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) private static string getDefaultLocationFor(string testTypeName)
{ {
string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName); string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
@ -307,14 +287,14 @@ namespace osu.Game.Tests.NonVisual
return path; return path;
} }
public class CustomTestHeadlessGameHost : HeadlessGameHost public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost
{ {
public Storage InitialStorage { get; } public Storage InitialStorage { get; }
public CustomTestHeadlessGameHost(string name) public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
: base(name) : base(callingMethodName: callingMethodName)
{ {
string defaultStorageLocation = getDefaultLocationFor(name); string defaultStorageLocation = getDefaultLocationFor(callingMethodName);
InitialStorage = new DesktopStorage(defaultStorageLocation, this); InitialStorage = new DesktopStorage(defaultStorageLocation, this);
InitialStorage.DeleteDirectory(string.Empty); InitialStorage.DeleteDirectory(string.Empty);

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -17,12 +16,11 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Tests.Resources;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Tests.Scores.IO namespace osu.Game.Tests.Scores.IO
{ {
public class ImportScoreTest public class ImportScoreTest : ImportTest
{ {
[Test] [Test]
public async Task TestBasicImport() public async Task TestBasicImport()
@ -31,7 +29,7 @@ namespace osu.Game.Tests.Scores.IO
{ {
try try
{ {
var osu = await loadOsu(host); var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo var toImport = new ScoreInfo
{ {
@ -45,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
OnlineScoreID = 12345, OnlineScoreID = 12345,
}; };
var imported = await loadIntoOsu(osu, toImport); var imported = await loadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore); Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
@ -70,14 +68,14 @@ namespace osu.Game.Tests.Scores.IO
{ {
try try
{ {
var osu = await loadOsu(host); var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo var toImport = new ScoreInfo
{ {
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, 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 OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
@ -96,7 +94,7 @@ namespace osu.Game.Tests.Scores.IO
{ {
try try
{ {
var osu = await loadOsu(host); var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo 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.Perfect], imported.Statistics[HitResult.Perfect]);
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
@ -126,7 +124,7 @@ namespace osu.Game.Tests.Scores.IO
{ {
try try
{ {
var osu = await loadOsu(host); var osu = LoadOsuIntoHost(host, true);
var toImport = new ScoreInfo 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 beatmapManager = osu.Dependencies.Get<BeatmapManager>();
var scoreManager = osu.Dependencies.Get<ScoreManager>(); 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))); 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)); 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); Assert.That(secondImport, Is.Null);
} }
finally finally
@ -163,9 +161,9 @@ namespace osu.Game.Tests.Scores.IO
{ {
try 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>(); 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>(); var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
@ -192,33 +190,6 @@ namespace osu.Game.Tests.Scores.IO
return scoreManager.GetAllUsableScores().FirstOrDefault(); 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 private class TestArchiveReader : ArchiveReader
{ {
public TestArchiveReader() 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 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 DialogOverlay dialogOverlay;
private readonly DialogOverlay dialogOverlay; private CollectionManager manager;
private readonly CollectionManager manager;
private RulesetStore rulesets; private RulesetStore rulesets;
private BeatmapManager beatmapManager; private BeatmapManager beatmapManager;
private ManageCollectionsDialog dialog; private ManageCollectionsDialog dialog;
public TestSceneManageCollectionsDialog()
{
base.Content.AddRange(new Drawable[]
{
manager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both },
dialogOverlay = new DialogOverlay()
});
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host) 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)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) base.Content.AddRange(new Drawable[]
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); manager = new CollectionManager(LocalStorage),
dependencies.Cache(manager); Content,
dependencies.Cache(dialogOverlay); dialogOverlay = new DialogOverlay()
return dependencies; });
Dependencies.Cache(manager);
Dependencies.Cache(dialogOverlay);
} }
[SetUp] [SetUp]

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -276,7 +277,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override bool CanConvert() => true; 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 yield return new TestHitObject
{ {

View File

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

View File

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

View File

@ -74,6 +74,13 @@ namespace osu.Game.Tests.Visual.Navigation
private void returnToMenu() 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()); AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
} }

View File

@ -110,6 +110,13 @@ namespace osu.Game.Tests.Visual.Navigation
private void returnToMenu() 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()); AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
} }

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -26,6 +27,8 @@ namespace osu.Game.Beatmaps
public IBeatmap Beatmap { get; } public IBeatmap Beatmap { get; }
private CancellationToken cancellationToken;
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset) protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
{ {
Beatmap = beatmap; Beatmap = beatmap;
@ -36,14 +39,25 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public abstract bool CanConvert(); public abstract bool CanConvert();
/// <summary> public IBeatmap Convert(CancellationToken cancellationToken = default)
/// Converts <see cref="Beatmap"/>.
/// </summary>
/// <returns>The converted Beatmap.</returns>
public IBeatmap Convert()
{ {
this.cancellationToken = cancellationToken;
// We always operate on a clone of the original beatmap, to not modify it game-wide // 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> /// <summary>
@ -51,19 +65,20 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
/// <param name="original">The un-converted Beatmap.</param> /// <param name="original">The un-converted Beatmap.</param>
/// <returns>The converted Beatmap.</returns> /// <returns>The converted Beatmap.</returns>
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original) protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
{ {
var beatmap = CreateBeatmap(); var beatmap = CreateBeatmap();
beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo; 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; beatmap.Breaks = original.Breaks;
return beatmap; 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); var result = new List<T>(hitObjects.Count);
@ -75,7 +90,7 @@ namespace osu.Game.Beatmaps
continue; continue;
} }
var converted = ConvertHitObject(obj, beatmap); var converted = ConvertHitObject(obj, beatmap, cancellationToken);
if (ObjectConverted != null) if (ObjectConverted != null)
{ {
@ -104,7 +119,23 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
/// <param name="original">The hit object to convert.</param> /// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param> /// <param name="beatmap">The un-converted Beatmap.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The converted hit object.</returns> /// <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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -76,7 +77,7 @@ namespace osu.Game.Beatmaps
public bool CanConvert() => true; public bool CanConvert() => true;
public IBeatmap Convert() public IBeatmap Convert(CancellationToken cancellationToken = default)
{ {
foreach (var obj in Beatmap.HitObjects) foreach (var obj in Beatmap.HitObjects)
ObjectConverted?.Invoke(obj, obj.Yield()); ObjectConverted?.Invoke(obj, obj.Yield());

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -30,6 +31,8 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Converts <see cref="Beatmap"/>. /// Converts <see cref="Beatmap"/>.
/// </summary> /// </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 // Convert
IBeatmap converted = converter.Convert(); IBeatmap converted = converter.Convert(cancellationSource.Token);
// Apply conversion mods to the result // Apply conversion mods to the result
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>()) foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())

View File

@ -230,7 +230,7 @@ namespace osu.Game.Collections
public void DeleteAll() public void DeleteAll()
{ {
Collections.Clear(); Collections.Clear();
PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" }); PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" });
} }
private readonly object saveLock = new object(); 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.Skin, 0, -1, int.MaxValue);
Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
Set(OsuSetting.BeatmapDetailModsFilter, false);
Set(OsuSetting.ShowConvertedBeatmaps, true); Set(OsuSetting.ShowConvertedBeatmaps, true);
Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
@ -200,6 +201,7 @@ namespace osu.Game.Configuration
CursorRotation, CursorRotation,
MenuParallax, MenuParallax,
BeatmapDetailTab, BeatmapDetailTab,
BeatmapDetailModsFilter,
Username, Username,
ReleaseStream, ReleaseStream,
SavePassword, SavePassword,

View File

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

View File

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

View File

@ -211,7 +211,7 @@ namespace osu.Game.Overlays.Chat.Tabs
TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH); 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); highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
Text.Font = Text.Font.With(weight: FontWeight.Medium); 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 osu.Game.Overlays.Chat.Tabs;
using osuTK.Input; using osuTK.Input;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -78,7 +79,7 @@ namespace osu.Game.Overlays
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, OsuColour colours) private void load(OsuConfigManager config, OsuColour colours, TextureStore textures)
{ {
const float padding = 5; const float padding = 5;
@ -163,13 +164,13 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = Color4.Black,
}, },
new SpriteIcon new Sprite
{ {
Icon = FontAwesome.Solid.Comments, Texture = textures.Get(IconTexture),
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Size = new Vector2(20), Size = new Vector2(OverlayTitle.ICON_SIZE),
Margin = new MarginPadding(10), Margin = new MarginPadding { Left = 10 },
}, },
ChannelTabControl = CreateChannelTabControl().With(d => ChannelTabControl = CreateChannelTabControl().With(d =>
{ {

View File

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

View File

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

View File

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

View File

@ -207,14 +207,15 @@ namespace osu.Game.Screens.Edit
beatmapProcessor?.PreProcess(); beatmapProcessor?.PreProcess();
foreach (var hitObject in pendingUpdates) foreach (var hitObject in pendingUpdates)
{
processHitObject(hitObject); processHitObject(hitObject);
HitObjectUpdated?.Invoke(hitObject);
}
pendingUpdates.Clear();
beatmapProcessor?.PostProcess(); 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<BeatmapDetailAreaTabItem> CurrentTab => tabControl.Current;
protected Bindable<bool> CurrentModsFilter => tabControl.CurrentModsFilter;
private readonly Container content; private readonly Container content;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;

View File

@ -26,6 +26,12 @@ namespace osu.Game.Screens.Select
set => tabs.Current = value; 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 Action<BeatmapDetailAreaTabItem, bool> OnFilter; // passed the selected tab and if mods is checked
public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems

View File

@ -66,24 +66,9 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config) 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); sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode); groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
groupMode.BindValueChanged(_ => updateCriteria());
sortMode.BindValueChanged(_ => updateCriteria());
Children = new Drawable[] Children = new Drawable[]
{ {
new Box 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 => collectionDropdown.Current.ValueChanged += val =>
{ {
if (val.NewValue == null) if (val.NewValue == null)

View File

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

View File

@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore;
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.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.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 }; 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) protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
await base.Populate(model, archive, cancellationToken); 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)) if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name))
{ {
model.Name = reference.Configuration.SkinInfo.Name; item.Name = reference.Configuration.SkinInfo.Name;
model.Creator = reference.Configuration.SkinInfo.Creator; item.Creator = reference.Configuration.SkinInfo.Creator;
} }
else else
{ {
model.Name = model.Name.Replace(".osk", ""); item.Name = item.Name.Replace(".osk", "");
model.Creator ??= "Unknown"; item.Creator ??= unknown_creator_string;
} }
} }

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual
private Lazy<Storage> localStorage; private Lazy<Storage> localStorage;
protected Storage LocalStorage => localStorage.Value; protected Storage LocalStorage => localStorage.Value;
private readonly Lazy<DatabaseContextFactory> contextFactory; private Lazy<DatabaseContextFactory> contextFactory;
protected IAPIProvider API protected IAPIProvider API
{ {
@ -69,8 +69,33 @@ namespace osu.Game.Tests.Visual
/// </summary> /// </summary>
protected virtual bool UseOnlineAPI => false; 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) 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 baseDependencies = base.CreateChildDependencies(parent);
var providedRuleset = CreateRuleset(); var providedRuleset = CreateRuleset();
@ -104,19 +129,11 @@ namespace osu.Game.Tests.Visual
protected OsuTestScene() 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()); base.Content.Add(content = new DrawSizePreservingFillContainer());
} }
protected virtual bool UseFreshStoragePerRun => false;
public virtual void RecycleLocalStorage() public virtual void RecycleLocalStorage()
{ {
if (localStorage?.IsValueCreated == true) 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] [Resolved]
@ -172,7 +190,7 @@ namespace osu.Game.Tests.Visual
if (MusicController?.TrackLoaded == true) if (MusicController?.TrackLoaded == true)
MusicController.CurrentTrack.Stop(); MusicController.CurrentTrack.Stop();
if (contextFactory.IsValueCreated) if (contextFactory?.IsValueCreated == true)
contextFactory.Value.ResetDatabase(); contextFactory.Value.ResetDatabase();
RecycleLocalStorage(); RecycleLocalStorage();