Merge branch 'master' into beatmap-refactor/download-tracker

This commit is contained in:
Dean Herbert 2021-10-29 11:50:54 +09:00
commit 3b095e1626
98 changed files with 1169 additions and 726 deletions

View File

@ -15,7 +15,7 @@
] ]
}, },
"smoogipoo.nvika": { "smoogipoo.nvika": {
"version": "1.0.1", "version": "1.0.3",
"commands": [ "commands": [
"nvika" "nvika"
] ]

View File

@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Distance => Path.Distance; public double Distance => Path.Distance;
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>(); public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
public double? LegacyLastTickOffset { get; set; } public double? LegacyLastTickOffset { get; set; }
} }

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null; return null;
case CatchSkinComponents.Catcher: case CatchSkinComponents.Catcher:
decimal version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1; decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
if (version < 2.3m) if (version < 2.3m)
{ {

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo) private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo)
=> hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList(); => hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
private IList<IList<string>> getNodeSampleNames(List<IList<HitSampleInfo>> hitSampleInfo) private IList<IList<string>> getNodeSampleNames(IList<IList<HitSampleInfo>> hitSampleInfo)
=> hitSampleInfo?.Select(getSampleNames) => hitSampleInfo?.Select(getSampleNames)
.ToList(); .ToList();

View File

@ -488,7 +488,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>. /// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary> /// </summary>
/// <param name="time">The time to retrieve node samples at.</param> /// <param name="time">The time to retrieve node samples at.</param>
private List<IList<HitSampleInfo>> nodeSamplesAt(int time) private IList<IList<HitSampleInfo>> nodeSamplesAt(int time)
{ {
if (!(HitObject is IHasPathWithRepeats curveData)) if (!(HitObject is IHasPathWithRepeats curveData))
return null; return null;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.Objects
} }
} }
public List<IList<HitSampleInfo>> NodeSamples { get; set; } public IList<IList<HitSampleInfo>> NodeSamples { get; set; }
/// <summary> /// <summary>
/// The head note of the hold. /// The head note of the hold.

View File

@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
float rightLineWidth = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1; float rightLineWidth = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1;
bool hasLeftLine = leftLineWidth > 0; bool hasLeftLine = leftLineWidth > 0;
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|| isLastColumn; || isLastColumn;
Color4 lineColour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White; Color4 lineColour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;

View File

@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
this.beatmap = (ManiaBeatmap)beatmap; this.beatmap = (ManiaBeatmap)beatmap;
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null); isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() => hasKeyTexture = new Lazy<bool>(() =>
{ {
string keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1"; string keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";

View File

@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
internal float LazyTravelDistance; internal float LazyTravelDistance;
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>(); public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
[JsonIgnore] [JsonIgnore]
public IList<HitSampleInfo> TailSamples { get; private set; } public IList<HitSampleInfo> TailSamples { get; private set; }

View File

@ -14,7 +14,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
@ -158,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (hasNumber) if (hasNumber)
{ {
decimal? legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value; decimal? legacyVersion = skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value;
if (legacyVersion >= 2.0m) if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece. // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.

View File

@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
if (shouldConvertSliderToHits(obj, beatmap, distanceData, out int taikoDuration, out double tickSpacing)) if (shouldConvertSliderToHits(obj, beatmap, distanceData, out int taikoDuration, out double tickSpacing))
{ {
List<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples }); IList<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
int i = 0; int i = 0;

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position // because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
float negativeScaleAdjust = content.Width / ratio; float negativeScaleAdjust = content.Width / ratio;
if (skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m) if (skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
{ {
left.Centre.Position = new Vector2(0, taiko_bar_y) * ratio; left.Centre.Position = new Vector2(0, taiko_bar_y) * ratio;
right.Centre.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio; right.Centre.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using static osu.Game.Skinning.LegacySkinConfiguration; using static osu.Game.Skinning.SkinConfiguration;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Gameplay
{ {

View File

@ -4,10 +4,12 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Skinning; using osu.Game.Skinning;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
@ -16,155 +18,179 @@ namespace osu.Game.Tests.Skins.IO
{ {
public class ImportSkinTest : ImportTest public class ImportSkinTest : ImportTest
{ {
[Test] #region Testing filename metadata inclusion
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] [Test]
public async Task TestImportTwiceWithSameMetadata() public Task TestSingleImportDifferentFilename() => runSkinTest(async osu =>
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
{
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini).
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk")); assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
});
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Count, Is.EqualTo(1));
// the first should be overwritten by the second import.
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
host.Exit();
}
}
}
[Test] [Test]
public async Task TestImportTwiceWithNoMetadata() public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "test skin.osk"));
{
try
{
var osu = LoadOsuIntoHost(host);
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "skinner", osu);
});
[Test]
public Task TestSingleImportNoIniFile() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithNonIniFile(), "test skin.osk"));
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
});
[Test]
public Task TestEmptyImportImportsWithFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createEmptyOsk(), "test skin.osk"));
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
});
#endregion
#region Cases where imports should match existing
[Test]
public Task TestImportTwiceWithSameMetadataAndFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
assertImportedOnce(import1, import2);
});
[Test]
public Task TestImportTwiceWithNoMetadataSameDownloadFilename() => runSkinTest(async osu =>
{
// 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. // 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 import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); assertImportedOnce(import1, import2);
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Count, Is.EqualTo(2)); });
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
host.Exit();
}
}
}
[Test] [Test]
public async Task TestImportTwiceWithDifferentMetadata() public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu =>
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.OsK"));
{ assertCorrectMetadata(import1, "name 1", "author 1", osu);
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk")); var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.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)); assertImportedOnce(import1, import2);
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Count, Is.EqualTo(2)); });
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
host.Exit();
}
}
}
[Test] [Test]
public async Task TestImportUpperCasedOskArchive() public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
{ var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.OsK")); assertImportedOnce(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
});
Assert.That(imported.Name, Is.EqualTo("name 1")); #endregion
Assert.That(imported.Creator, Is.EqualTo("author 1"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.oSK")); #region Cases where imports should be uniquely imported
Assert.That(imported2.Hash, Is.EqualTo(imported.Hash));
}
finally
{
host.Exit();
}
}
}
[Test] [Test]
public async Task TestSameMetadataNameDifferentFolderName() public Task TestImportTwiceWithSameMetadataButDifferentFilename() => runSkinTest(async osu =>
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
{ var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin2.osk"));
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 1")); assertImportedBoth(import1, import2);
Assert.That(imported.Name, Is.EqualTo("name 1 [my custom skin 1]")); });
Assert.That(imported.Creator, Is.EqualTo("author 1"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 2")); [Test]
Assert.That(imported2.Name, Is.EqualTo("name 1 [my custom skin 2]")); public Task TestImportTwiceWithNoMetadataDifferentDownloadFilename() => runSkinTest(async osu =>
Assert.That(imported2.Creator, Is.EqualTo("author 1"));
Assert.That(imported2.Hash, Is.Not.EqualTo(imported.Hash));
}
finally
{ {
host.Exit(); // 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 import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk"));
} var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download2.osk"));
assertImportedBoth(import1, import2);
});
[Test]
public Task TestImportTwiceWithSameFilenameDifferentMetadata() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin v2", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin v2.1", "skinner"), "skin.osk"));
assertImportedBoth(import1, import2);
assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", osu);
assertCorrectMetadata(import2, "test skin v2.1 [skin]", "skinner", osu);
});
[Test]
public Task TestSameMetadataNameDifferentFolderName() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 2"));
assertImportedBoth(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu);
});
#endregion
private void assertCorrectMetadata(SkinInfo import1, string name, string creator, OsuGameBase osu)
{
Assert.That(import1.Name, Is.EqualTo(name));
Assert.That(import1.Creator, Is.EqualTo(creator));
// for extra safety let's reconstruct the skin, reading from the skin.ini.
var instance = import1.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
} }
private MemoryStream createOsk(string name, string author, bool makeUnique = true) private void assertImportedBoth(SkinInfo import1, SkinInfo import2)
{
Assert.That(import2.ID, Is.Not.EqualTo(import1.ID));
Assert.That(import2.Hash, Is.Not.EqualTo(import1.Hash));
Assert.That(import2.Files.Select(f => f.FileInfoID), Is.Not.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
}
private void assertImportedOnce(SkinInfo import1, SkinInfo import2)
{
Assert.That(import2.ID, Is.EqualTo(import1.ID));
Assert.That(import2.Hash, Is.EqualTo(import1.Hash));
Assert.That(import2.Files.Select(f => f.FileInfoID), Is.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
}
private MemoryStream createEmptyOsk()
{
var zipStream = new MemoryStream();
using var zip = ZipArchive.Create();
zip.SaveTo(zipStream);
return zipStream;
}
private MemoryStream createOskWithNonIniFile()
{
var zipStream = new MemoryStream();
using var zip = ZipArchive.Create();
zip.AddEntry("hitcircle.png", new MemoryStream(new byte[] { 0, 1, 2, 3 }));
zip.SaveTo(zipStream);
return zipStream;
}
private MemoryStream createOskWithIni(string name, string author, bool makeUnique = false)
{ {
var zipStream = new MemoryStream(); var zipStream = new MemoryStream();
using var zip = ZipArchive.Create(); using var zip = ZipArchive.Create();
@ -193,6 +219,22 @@ namespace osu.Game.Tests.Skins.IO
return stream; return stream;
} }
private async Task runSkinTest(Func<OsuGameBase, Task> action, [CallerMemberName] string callingMethodName = @"")
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName))
{
try
{
var osu = LoadOsuIntoHost(host);
await action(osu);
}
finally
{
host.Exit();
}
}
}
private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{ {
var skinManager = osu.Dependencies.Get<SkinManager>(); var skinManager = osu.Dependencies.Get<SkinManager>();

View File

@ -106,7 +106,7 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder(); var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin-latest.ini")) using (var resStream = TestResources.OpenResource("skin-latest.ini"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
Assert.AreEqual(LegacySkinConfiguration.LATEST_VERSION, decoder.Decode(stream).LegacyVersion); Assert.AreEqual(SkinConfiguration.LATEST_VERSION, decoder.Decode(stream).LegacyVersion);
} }
[Test] [Test]

View File

@ -151,7 +151,7 @@ namespace osu.Game.Tests.Skins
{ {
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null); AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m); AddAssert("Check legacy version lookup", () => requester.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value == 2.3m);
} }
[Test] [Test]
@ -160,7 +160,7 @@ namespace osu.Game.Tests.Skins
// completely ignoring beatmap versions for simplicity. // completely ignoring beatmap versions for simplicity.
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m); AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m);
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m); AddAssert("Check legacy version lookup", () => requester.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value == 2.3m);
} }
[Test] [Test]
@ -169,14 +169,14 @@ namespace osu.Game.Tests.Skins
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = null); AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = null);
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null); AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
AddAssert("Check legacy version lookup", AddAssert("Check legacy version lookup",
() => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == LegacySkinConfiguration.LATEST_VERSION); () => requester.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value == SkinConfiguration.LATEST_VERSION);
} }
[Test] [Test]
public void TestIniWithNoVersionFallsBackTo1() public void TestIniWithNoVersionFallsBackTo1()
{ {
AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream()))); AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream())));
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m); AddAssert("Check legacy version lookup", () => requester.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value == 1.0m);
} }
public enum LookupType public enum LookupType

View File

@ -38,8 +38,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
} }
[Test] [Test]
@ -204,7 +202,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestDownloadButtonHiddenWhenBeatmapExists() public void TestDownloadButtonHiddenWhenBeatmapExists()
{ {
createPlaylist(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo); var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).Wait());
createPlaylist(beatmap);
assertDownloadButtonVisible(false); assertDownloadButtonVisible(false);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{ {
protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private RoomsContainer container; private RoomsContainer container;

View File

@ -23,8 +23,6 @@ using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
@ -32,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Input; using osuTK.Input;
@ -44,11 +43,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets; private RulesetStore rulesets;
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private DependenciesScreen dependenciesScreen; private TestMultiplayerScreenStack multiplayerScreenStack;
private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client;
private TestRequestHandlingMultiplayerRoomManager roomManager => multiplayerScreen.RoomManager; private TestMultiplayerClient client => multiplayerScreenStack.Client;
private TestMultiplayerRoomManager roomManager => multiplayerScreenStack.RoomManager;
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -70,22 +68,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}); });
AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
AddStep("load dependencies", () =>
{
client = new TestMultiplayerClient(roomManager);
// The screen gets suspended so it stops receiving updates.
Child = client;
LoadScreen(dependenciesScreen = new DependenciesScreen(client));
});
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
} }
@ -441,7 +425,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match externally", () => client.StartMatch()); AddStep("start match externally", () => client.StartMatch());
AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen()); AddAssert("play not started", () => multiplayerScreenStack.IsCurrentScreen());
} }
[Test] [Test]
@ -485,7 +469,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}); });
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen()); AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is SpectatorScreen);
} }
[Test] [Test]
@ -527,16 +511,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("open mod overlay", () => this.ChildrenOfType<RoomSubScreen.UserModSelectButton>().Single().TriggerClick()); AddStep("open mod overlay", () => this.ChildrenOfType<RoomSubScreen.UserModSelectButton>().Single().TriggerClick());
AddStep("invoke on back button", () => multiplayerScreen.OnBackButton()); AddStep("invoke on back button", () => multiplayerScreenStack.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden); AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
testLeave("back button", () => multiplayerScreen.OnBackButton()); testLeave("back button", () => multiplayerScreenStack.OnBackButton());
// mimics home button and OS window close // mimics home button and OS window close
testLeave("forced exit", () => multiplayerScreen.Exit()); testLeave("forced exit", () => multiplayerScreenStack.Exit());
void testLeave(string actionName, Action action) void testLeave(string actionName, Action action)
{ {
@ -577,7 +561,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click start button", () => InputManager.Click(MouseButton.Left)); AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out. // Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
@ -586,15 +570,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time); AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time);
} }
AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => multiplayerScreenStack.CurrentScreen is ResultsScreen);
} }
private MultiplayerReadyButton readyButton => this.ChildrenOfType<MultiplayerReadyButton>().Single(); private MultiplayerReadyButton readyButton => this.ChildrenOfType<MultiplayerReadyButton>().Single();
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room())); AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);
@ -607,26 +591,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => client.Room != null); AddUntilStep("wait for join", () => client.Room != null);
} }
/// <summary>
/// Used for the sole purpose of adding <see cref="TestMultiplayerClient"/> as a resolvable dependency.
/// </summary>
private class DependenciesScreen : OsuScreen
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
public DependenciesScreen(TestMultiplayerClient client)
{
Client = client;
}
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestRequestHandlingMultiplayerRoomManager RoomManager { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestRequestHandlingMultiplayerRoomManager();
}
} }
} }

View File

@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -40,9 +39,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
} }
[SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps();
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result); AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -44,9 +43,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
return room; return room;
} }
[SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps();
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result); AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
{ {
protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen; private LoungeSubScreen loungeScreen;

View File

@ -4,7 +4,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
@ -14,8 +13,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp] [SetUp]
public new void Setup() => Schedule(() => public new void Setup() => Schedule(() =>
{ {
SelectedRoom.Value = new Room();
Child = new Container Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -6,18 +6,14 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -33,9 +29,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets; private RulesetStore rulesets;
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private DependenciesScreen dependenciesScreen; private TestMultiplayerScreenStack multiplayerScreenStack;
private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client; private TestMultiplayerClient client => multiplayerScreenStack.Client;
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -57,24 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}); });
AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
AddStep("load dependencies", () =>
{
client = new TestMultiplayerClient(multiplayerScreen.RoomManager);
// The screen gets suspended so it stops receiving updates.
Child = client;
LoadScreen(dependenciesScreen = new DependenciesScreen(client));
});
AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen);
AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.NotLoaded);
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
} }
@ -120,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press button", () => AddStep("press button", () =>
{ {
InputManager.MoveMouseTo(multiplayerScreen.ChildrenOfType<TeamDisplay>().First()); InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
@ -154,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddStep("open room", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room())); AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);
@ -167,26 +147,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => client.Room != null); AddUntilStep("wait for join", () => client.Room != null);
} }
/// <summary>
/// Used for the sole purpose of adding <see cref="TestMultiplayerClient"/> as a resolvable dependency.
/// </summary>
private class DependenciesScreen : OsuScreen
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
public DependenciesScreen(TestMultiplayerClient client)
{
Client = client;
}
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestRequestHandlingMultiplayerRoomManager RoomManager { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestRequestHandlingMultiplayerRoomManager();
}
} }
} }

View File

@ -9,21 +9,18 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Multiplayer;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -333,12 +330,12 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestPushMatchSubScreenAndPressBackButtonImmediately() public void TestPushMatchSubScreenAndPressBackButtonImmediately()
{ {
TestMultiplayer multiplayer = null; TestMultiplayerScreenStack multiplayerScreenStack = null;
PushAndConfirm(() => multiplayer = new TestMultiplayer()); PushAndConfirm(() => multiplayerScreenStack = new TestMultiplayerScreenStack());
AddUntilStep("wait for lounge", () => multiplayer.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayer.ChildrenOfType<LoungeSubScreen>().Single().Open()); AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2); AddWaitStep("wait two frames", 2);
} }
@ -453,18 +450,5 @@ namespace osu.Game.Tests.Visual.Navigation
protected override bool DisplayStableImportPrompt => false; protected override bool DisplayStableImportPrompt => false;
} }
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
public TestMultiplayer()
{
Client = new TestMultiplayerClient((TestRequestHandlingMultiplayerRoomManager)RoomManager);
}
protected override RoomManager CreateRoomManager() => new TestRequestHandlingMultiplayerRoomManager();
}
} }
} }

View File

@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 10).ToArray())); AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent)); AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
{ {
protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private TestLoungeSubScreen loungeScreen; private TestLoungeSubScreen loungeScreen;

View File

@ -11,24 +11,27 @@ using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Playlists namespace osu.Game.Tests.Visual.Playlists
{ {
public class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene public class TestScenePlaylistsRoomCreation : OnlinePlayTestScene
{ {
private BeatmapManager manager; private BeatmapManager manager;
private RulesetStore rulesets; private RulesetStore rulesets;
private TestPlaylistsRoomSubScreen match; private TestPlaylistsRoomSubScreen match;
private ILive<BeatmapSetInfo> importedBeatmap;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
@ -40,7 +43,9 @@ namespace osu.Game.Tests.Visual.Playlists
public void SetupSteps() public void SetupSteps()
{ {
AddStep("set room", () => SelectedRoom.Value = new Room()); AddStep("set room", () => SelectedRoom.Value = new Room());
AddStep("ensure has beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait());
importBeatmap();
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value))); AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value)));
AddUntilStep("wait for load", () => match.IsCurrentScreen()); AddUntilStep("wait for load", () => match.IsCurrentScreen());
} }
@ -48,44 +53,58 @@ namespace osu.Game.Tests.Visual.Playlists
[Test] [Test]
public void TestLoadSimpleMatch() public void TestLoadSimpleMatch()
{ {
AddStep("set room properties", () => setupAndCreateRoom(room =>
{ {
SelectedRoom.Value.RoomID.Value = 1; room.Name.Value = "my awesome room";
SelectedRoom.Value.Name.Value = "my awesome room"; room.Host.Value = API.LocalUser.Value;
SelectedRoom.Value.Host.Value = API.LocalUser.Value; room.RecentParticipants.Add(room.Host.Value);
SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value); room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{ {
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
Ruleset = { Value = new OsuRuleset().RulesetInfo } Ruleset = { Value = new OsuRuleset().RulesetInfo }
}); });
}); });
AddUntilStep("Progress details are hidden", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent.Alpha == 0);
AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick()); AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick());
AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader); AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader);
} }
[Test] [Test]
public void TestPlaylistItemSelectedOnCreate() public void TestAttemptLimitedMatch()
{ {
AddStep("set room properties", () => setupAndCreateRoom(room =>
{ {
SelectedRoom.Value.Name.Value = "my awesome room"; room.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = API.LocalUser.Value; room.MaxAttempts.Value = 5;
SelectedRoom.Value.Playlist.Add(new PlaylistItem room.Host.Value = API.LocalUser.Value;
room.RecentParticipants.Add(room.Host.Value);
room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
room.Playlist.Add(new PlaylistItem
{ {
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
Ruleset = { Value = new OsuRuleset().RulesetInfo } Ruleset = { Value = new OsuRuleset().RulesetInfo }
}); });
}); });
AddStep("move mouse to create button", () => AddUntilStep("Progress details are visible", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent.Alpha == 1);
{ }
InputManager.MoveMouseTo(this.ChildrenOfType<PlaylistsRoomSettingsOverlay.CreateRoomButton>().Single());
});
AddStep("click", () => InputManager.Click(MouseButton.Left)); [Test]
public void TestPlaylistItemSelectedOnCreate()
{
setupAndCreateRoom(room =>
{
room.Name.Value = "my awesome room";
room.Host.Value = API.LocalUser.Value;
room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
});
});
AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]); AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]);
} }
@ -94,56 +113,51 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestBeatmapUpdatedOnReImport() public void TestBeatmapUpdatedOnReImport()
{ {
BeatmapSetInfo importedSet = null; BeatmapSetInfo importedSet = null;
TestBeatmap beatmap = null;
// this step is required to make sure the further imports actually get online IDs.
// all the playlist logic relies on online ID matching.
AddStep("remove all matching online IDs", () =>
{
beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo);
var existing = manager.QueryBeatmapSets(s => s.OnlineBeatmapSetID == beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID).ToList();
foreach (var s in existing)
{
s.OnlineBeatmapSetID = null;
foreach (var b in s.Beatmaps)
b.OnlineBeatmapID = null;
manager.Update(s);
}
});
AddStep("import altered beatmap", () => AddStep("import altered beatmap", () =>
{ {
IBeatmap beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
// intentionally increment online IDs to clash with import below.
beatmap.BeatmapInfo.OnlineBeatmapID++;
beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID++;
importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value; importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value;
}); });
AddStep("load room", () => setupAndCreateRoom(room =>
{ {
SelectedRoom.Value.Name.Value = "my awesome room"; room.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = API.LocalUser.Value; room.Host.Value = API.LocalUser.Value;
SelectedRoom.Value.Playlist.Add(new PlaylistItem room.Playlist.Add(new PlaylistItem
{ {
Beatmap = { Value = importedSet.Beatmaps[0] }, Beatmap = { Value = importedSet.Beatmaps[0] },
Ruleset = { Value = new OsuRuleset().RulesetInfo } Ruleset = { Value = new OsuRuleset().RulesetInfo }
}); });
}); });
AddStep("create room", () =>
{
InputManager.MoveMouseTo(match.ChildrenOfType<PlaylistsRoomSettingsOverlay.CreateRoomButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1); AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1);
AddStep("re-import original beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait()); importBeatmap();
AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1); AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1);
} }
private void setupAndCreateRoom(Action<Room> room)
{
AddStep("setup room", () => room(SelectedRoom.Value));
AddStep("click create button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PlaylistsRoomSettingsOverlay.CreateRoomButton>().Single());
InputManager.Click(MouseButton.Left);
});
}
private void importBeatmap() => AddStep("import beatmap", () => importedBeatmap = manager.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Result);
private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen
{ {
public new Bindable<PlaylistItem> SelectedItem => base.SelectedItem; public new Bindable<PlaylistItem> SelectedItem => base.SelectedItem;

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
@ -58,6 +59,13 @@ namespace osu.Game.Tests.Visual.Settings
Default = string.Empty, Default = string.Empty,
Value = "Sample text" Value = "Sample text"
}; };
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> IntTextboxBindable { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
} }
private enum TestEnum private enum TestEnum

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual
{
/// <summary>
/// An <see cref="OsuScreen"/> loadable into <see cref="ScreenTestScene"/>s via <see cref="ScreenTestScene.LoadScreen"/>,
/// which provides dependencies for and loads an isolated <see cref="Screens.OnlinePlay.Multiplayer.Multiplayer"/> screen.
/// <p>
/// This screen:
/// <list type="bullet">
/// <item>Provides a <see cref="TestMultiplayerClient"/> to be resolved as a dependency in the <see cref="Screens.OnlinePlay.Multiplayer.Multiplayer"/> screen,
/// which is typically a part of <see cref="OsuGameBase"/>.</item>
/// <item>Rebinds the <see cref="DummyAPIAccess"/> to handle requests via a <see cref="TestRoomRequestsHandler"/>.</item>
/// <item>Provides a <see cref="TestMultiplayerRoomManager"/> for the <see cref="Screens.OnlinePlay.Multiplayer.Multiplayer"/> screen.</item>
/// </list>
/// </p>
/// </summary>
public class TestMultiplayerScreenStack : OsuScreen
{
public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen;
public TestMultiplayerRoomManager RoomManager => multiplayerScreen.RoomManager;
public IScreen CurrentScreen => screenStack.CurrentScreen;
public new bool IsLoaded => base.IsLoaded && MultiplayerScreen.IsLoaded;
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
private readonly OsuScreenStack screenStack;
private readonly TestMultiplayer multiplayerScreen;
public TestMultiplayerScreenStack()
{
multiplayerScreen = new TestMultiplayer();
InternalChildren = new Drawable[]
{
Client = new TestMultiplayerClient(RoomManager),
screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }
};
screenStack.Push(multiplayerScreen);
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{
((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
public override bool OnBackButton() => multiplayerScreen.OnBackButton();
public override bool OnExiting(IScreen next)
{
if (screenStack.CurrentScreen == null)
return base.OnExiting(next);
screenStack.Exit();
return true;
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestMultiplayerRoomManager RoomManager { get; private set; }
public TestRoomRequestsHandler RequestsHandler { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(RequestsHandler = new TestRoomRequestsHandler());
}
}
}

View File

@ -41,6 +41,44 @@ namespace osu.Game.Tests.Visual.UserInterface
notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; }; notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };
}); });
[Test]
public void TestCompleteProgress()
{
ProgressNotification notification = null;
AddStep("add progress notification", () =>
{
notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
};
notificationOverlay.Post(notification);
progressingNotifications.Add(notification);
});
AddUntilStep("wait completion", () => notification.State == ProgressNotificationState.Completed);
}
[Test]
public void TestCancelProgress()
{
ProgressNotification notification = null;
AddStep("add progress notification", () =>
{
notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
};
notificationOverlay.Post(notification);
progressingNotifications.Add(notification);
});
AddWaitStep("wait 3", 3);
AddStep("cancel notification", () => notification.State = ProgressNotificationState.Cancelled);
}
[Test] [Test]
public void TestBasicFlow() public void TestBasicFlow()
{ {
@ -138,7 +176,7 @@ namespace osu.Game.Tests.Visual.UserInterface
foreach (var n in progressingNotifications.FindAll(n => n.State == ProgressNotificationState.Active)) foreach (var n in progressingNotifications.FindAll(n => n.State == ProgressNotificationState.Active))
{ {
if (n.Progress < 1) if (n.Progress < 1)
n.Progress += (float)(Time.Elapsed / 400) * RNG.NextSingle(); n.Progress += (float)(Time.Elapsed / 2000);
else else
n.State = ProgressNotificationState.Completed; n.State = ProgressNotificationState.Completed;
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("setup cover", () => Child = new UpdateableOnlineBeatmapSetCover(coverType) AddStep("setup cover", () => Child = new UpdateableOnlineBeatmapSetCover(coverType)
{ {
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, OnlineInfo = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet.OnlineInfo,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
}); });
@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var cover = new UpdateableOnlineBeatmapSetCover(coverType) var cover = new UpdateableOnlineBeatmapSetCover(coverType)
{ {
BeatmapSet = setInfo, OnlineInfo = setInfo.OnlineInfo,
Height = 100, Height = 100,
Masking = true, Masking = true,
}; };
@ -99,12 +99,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover
{ {
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, OnlineInfo = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet.OnlineInfo,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
}); });
AddStep("change model", () => updateableCover.BeatmapSet = null); AddStep("change model", () => updateableCover.OnlineInfo = null);
AddWaitStep("wait some", 5); AddWaitStep("wait some", 5);
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any()); AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any());
} }
@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(0) AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(0)
{ {
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"), OnlineInfo = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg").OnlineInfo,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
Alpha = 0.4f Alpha = 0.4f
@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1); AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
AddStep("switch beatmap", AddStep("switch beatmap",
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg")); () => updateableCover.OnlineInfo = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg").OnlineInfo);
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Except(new[] { initialCover }).Any()); AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Except(new[] { initialCover }).Any());
} }

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -23,14 +22,13 @@ namespace osu.Game.Tournament.Tests.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 1091460 }); var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = 1091460 });
req.Success += success; req.Success += success;
api.Queue(req); api.Queue(req);
} }
private void success(APIBeatmap apiBeatmap) private void success(APIBeatmap beatmap)
{ {
var beatmap = apiBeatmap.ToBeatmapInfo(rulesets);
Add(new TournamentBeatmapPanel(beatmap) Add(new TournamentBeatmapPanel(beatmap)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -4,12 +4,12 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osuTK;
namespace osu.Game.Tournament.Tests.Components namespace osu.Game.Tournament.Tests.Components
{ {
@ -23,12 +23,10 @@ namespace osu.Game.Tournament.Tests.Components
private FillFlowContainer<TournamentBeatmapPanel> fillFlow; private FillFlowContainer<TournamentBeatmapPanel> fillFlow;
private BeatmapInfo beatmapInfo;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 490154 }); var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = 490154 });
req.Success += success; req.Success += success;
api.Queue(req); api.Queue(req);
@ -38,18 +36,17 @@ namespace osu.Game.Tournament.Tests.Components
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Direction = FillDirection.Full, Direction = FillDirection.Full,
Spacing = new osuTK.Vector2(10) Spacing = new Vector2(10)
}); });
} }
private void success(APIBeatmap apiBeatmap) private void success(APIBeatmap beatmap)
{ {
beatmapInfo = apiBeatmap.ToBeatmapInfo(rulesets);
var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods; var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods;
foreach (var mod in mods) foreach (var mod in mods)
{ {
fillFlow.Add(new TournamentBeatmapPanel(beatmapInfo, mod.Acronym) fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre

View File

@ -44,8 +44,8 @@ namespace osu.Game.Tournament.Tests.NonVisual
{ {
Beatmaps = Beatmaps =
{ {
new RoundBeatmap { BeatmapInfo = TournamentTestScene.CreateSampleBeatmapInfo() }, new RoundBeatmap { Beatmap = TournamentTestScene.CreateSampleBeatmap() },
new RoundBeatmap { BeatmapInfo = TournamentTestScene.CreateSampleBeatmapInfo() }, new RoundBeatmap { Beatmap = TournamentTestScene.CreateSampleBeatmap() },
} }
} }
}, },

View File

@ -132,7 +132,7 @@ namespace osu.Game.Tournament.Tests.Screens
{ {
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Mods = mods Mods = mods
}); });
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -74,19 +73,19 @@ namespace osu.Game.Tournament.Tests
{ {
new SeedingBeatmap new SeedingBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Score = 12345672, Score = 12345672,
Seed = { Value = 24 }, Seed = { Value = 24 },
}, },
new SeedingBeatmap new SeedingBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Score = 1234567, Score = 1234567,
Seed = { Value = 12 }, Seed = { Value = 12 },
}, },
new SeedingBeatmap new SeedingBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Score = 1234567, Score = 1234567,
Seed = { Value = 16 }, Seed = { Value = 16 },
} }
@ -100,19 +99,19 @@ namespace osu.Game.Tournament.Tests
{ {
new SeedingBeatmap new SeedingBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Score = 234567, Score = 234567,
Seed = { Value = 3 }, Seed = { Value = 3 },
}, },
new SeedingBeatmap new SeedingBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Score = 234567, Score = 234567,
Seed = { Value = 6 }, Seed = { Value = 6 },
}, },
new SeedingBeatmap new SeedingBeatmap
{ {
BeatmapInfo = CreateSampleBeatmapInfo(), Beatmap = CreateSampleBeatmap(),
Score = 234567, Score = 234567,
Seed = { Value = 12 }, Seed = { Value = 12 },
} }
@ -152,16 +151,15 @@ namespace osu.Game.Tournament.Tests
} }
}; };
public static BeatmapInfo CreateSampleBeatmapInfo() => public static APIBeatmap CreateSampleBeatmap() =>
new BeatmapInfo new APIBeatmap
{ {
Metadata = new BeatmapMetadata BeatmapSet = new APIBeatmapSet
{ {
Title = "Test Title", Title = "Test Title",
Artist = "Test Artist", Artist = "Test Artist",
ID = RNG.Next(0, 1000000)
}, },
OnlineInfo = new APIBeatmap(), OnlineID = RNG.Next(0, 1000000),
}; };
protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner(); protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner();

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK; using osuTK;
@ -21,22 +22,21 @@ namespace osu.Game.Tournament.Components
{ {
public class SongBar : CompositeDrawable public class SongBar : CompositeDrawable
{ {
private BeatmapInfo beatmapInfo; private APIBeatmap beatmap;
public const float HEIGHT = 145 / 2f; public const float HEIGHT = 145 / 2f;
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private IBindable<RulesetInfo> ruleset { get; set; }
public BeatmapInfo BeatmapInfo public APIBeatmap Beatmap
{ {
get => beatmapInfo;
set set
{ {
if (beatmapInfo == value) if (beatmap == value)
return; return;
beatmapInfo = value; beatmap = value;
update(); update();
} }
} }
@ -95,18 +95,18 @@ namespace osu.Game.Tournament.Components
private void update() private void update()
{ {
if (beatmapInfo == null) if (beatmap == null)
{ {
flow.Clear(); flow.Clear();
return; return;
} }
double bpm = beatmapInfo.BeatmapSet.OnlineInfo.BPM; double bpm = beatmap.BPM;
double length = beatmapInfo.Length; double length = beatmap.Length;
string hardRockExtra = ""; string hardRockExtra = "";
string srExtra = ""; string srExtra = "";
float ar = beatmapInfo.BaseDifficulty.ApproachRate; float ar = beatmap.Difficulty.ApproachRate;
if ((mods & LegacyMods.HardRock) > 0) if ((mods & LegacyMods.HardRock) > 0)
{ {
@ -132,9 +132,9 @@ namespace osu.Game.Tournament.Components
default: default:
stats = new (string heading, string content)[] stats = new (string heading, string content)[]
{ {
("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("CS", $"{beatmap.Difficulty.CircleSize:0.#}{hardRockExtra}"),
("AR", $"{ar:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}{hardRockExtra}"),
("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), ("OD", $"{beatmap.Difficulty.OverallDifficulty:0.#}{hardRockExtra}"),
}; };
break; break;
@ -142,15 +142,15 @@ namespace osu.Game.Tournament.Components
case 3: case 3:
stats = new (string heading, string content)[] stats = new (string heading, string content)[]
{ {
("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), ("OD", $"{beatmap.Difficulty.OverallDifficulty:0.#}{hardRockExtra}"),
("HP", $"{beatmapInfo.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") ("HP", $"{beatmap.Difficulty.DrainRate:0.#}{hardRockExtra}")
}; };
break; break;
case 2: case 2:
stats = new (string heading, string content)[] stats = new (string heading, string content)[]
{ {
("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("CS", $"{beatmap.Difficulty.CircleSize:0.#}{hardRockExtra}"),
("AR", $"{ar:0.#}"), ("AR", $"{ar:0.#}"),
}; };
break; break;
@ -186,7 +186,7 @@ namespace osu.Game.Tournament.Components
Children = new Drawable[] Children = new Drawable[]
{ {
new DiffPiece(stats), new DiffPiece(stats),
new DiffPiece(("Star Rating", $"{beatmapInfo.StarDifficulty:0.#}{srExtra}")) new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.#}{srExtra}"))
} }
}, },
new FillFlowContainer new FillFlowContainer
@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components
} }
} }
}, },
new TournamentBeatmapPanel(beatmapInfo) new TournamentBeatmapPanel(beatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 0.5f, Width = 0.5f,

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK.Graphics; using osuTK.Graphics;
@ -20,7 +21,7 @@ namespace osu.Game.Tournament.Components
{ {
public class TournamentBeatmapPanel : CompositeDrawable public class TournamentBeatmapPanel : CompositeDrawable
{ {
public readonly IBeatmapInfo BeatmapInfo; public readonly APIBeatmap Beatmap;
private readonly string mod; private readonly string mod;
@ -32,11 +33,11 @@ namespace osu.Game.Tournament.Components
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>(); private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private Box flash; private Box flash;
public TournamentBeatmapPanel(IBeatmapInfo beatmapInfo, string mod = null) public TournamentBeatmapPanel(APIBeatmap beatmap, string mod = null)
{ {
if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo)); if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
BeatmapInfo = beatmapInfo; Beatmap = beatmap;
this.mod = mod; this.mod = mod;
Width = 400; Width = 400;
@ -62,7 +63,7 @@ namespace osu.Game.Tournament.Components
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f), Colour = OsuColour.Gray(0.5f),
BeatmapSet = BeatmapInfo.BeatmapSet as IBeatmapSetOnlineInfo, OnlineInfo = Beatmap.BeatmapSet,
}, },
new FillFlowContainer new FillFlowContainer
{ {
@ -75,7 +76,7 @@ namespace osu.Game.Tournament.Components
{ {
new TournamentSpriteText new TournamentSpriteText
{ {
Text = BeatmapInfo.GetDisplayTitleRomanisable(false), Text = Beatmap.GetDisplayTitleRomanisable(false),
Font = OsuFont.Torus.With(weight: FontWeight.Bold), Font = OsuFont.Torus.With(weight: FontWeight.Bold),
}, },
new FillFlowContainer new FillFlowContainer
@ -92,7 +93,7 @@ namespace osu.Game.Tournament.Components
}, },
new TournamentSpriteText new TournamentSpriteText
{ {
Text = BeatmapInfo.Metadata?.Author, Text = Beatmap.Metadata.Author,
Padding = new MarginPadding { Right = 20 }, Padding = new MarginPadding { Right = 20 },
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
}, },
@ -104,7 +105,7 @@ namespace osu.Game.Tournament.Components
}, },
new TournamentSpriteText new TournamentSpriteText
{ {
Text = BeatmapInfo.DifficultyName, Text = Beatmap.DifficultyName,
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
}, },
} }
@ -148,7 +149,7 @@ namespace osu.Game.Tournament.Components
private void updateState() private void updateState()
{ {
var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineID); var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineID);
bool doFlash = found != choice; bool doFlash = found != choice;
choice = found; choice = found;

View File

@ -11,10 +11,10 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
@ -87,14 +87,14 @@ namespace osu.Game.Tournament.IPC
lastBeatmapId = beatmapId; lastBeatmapId = beatmapId;
var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null); var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.Beatmap != null);
if (existing != null) if (existing != null)
Beatmap.Value = existing.BeatmapInfo; Beatmap.Value = existing.Beatmap;
else else
{ {
beatmapLookupRequest = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmapInfo(Rulesets); beatmapLookupRequest.Success += b => Beatmap.Value = b;
API.Queue(beatmapLookupRequest); API.Queue(beatmapLookupRequest);
} }
} }

View File

@ -3,14 +3,14 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tournament.IPC namespace osu.Game.Tournament.IPC
{ {
public class MatchIPCInfo : Component public class MatchIPCInfo : Component
{ {
public Bindable<BeatmapInfo> Beatmap { get; } = new Bindable<BeatmapInfo>(); public Bindable<APIBeatmap> Beatmap { get; } = new Bindable<APIBeatmap>();
public Bindable<LegacyMods> Mods { get; } = new Bindable<LegacyMods>(); public Bindable<LegacyMods> Mods { get; } = new Bindable<LegacyMods>();
public Bindable<TourneyState> State { get; } = new Bindable<TourneyState>(); public Bindable<TourneyState> State { get; } = new Bindable<TourneyState>();
public Bindable<string> ChatChannel { get; } = new Bindable<string>(); public Bindable<string> ChatChannel { get; } = new Bindable<string>();

View File

@ -1,7 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps; using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tournament.Models namespace osu.Game.Tournament.Models
{ {
@ -10,6 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID; public int ID;
public string Mods; public string Mods;
public BeatmapInfo BeatmapInfo; [JsonProperty("BeatmapInfo")]
public APIBeatmap Beatmap;
} }
} }

View File

@ -1,8 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tournament.Models namespace osu.Game.Tournament.Models
{ {
@ -10,7 +11,8 @@ namespace osu.Game.Tournament.Models
{ {
public int ID; public int ID;
public BeatmapInfo BeatmapInfo; [JsonProperty("BeatmapInfo")]
public APIBeatmap Beatmap;
public long Score; public long Score;

View File

@ -4,8 +4,8 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
@ -37,10 +37,10 @@ namespace osu.Game.Tournament.Screens
SongBar.Mods = mods.NewValue; SongBar.Mods = mods.NewValue;
} }
private void beatmapChanged(ValueChangedEvent<BeatmapInfo> beatmap) private void beatmapChanged(ValueChangedEvent<APIBeatmap> beatmap)
{ {
SongBar.FadeInFromZero(300, Easing.OutQuint); SongBar.FadeInFromZero(300, Easing.OutQuint);
SongBar.BeatmapInfo = beatmap.NewValue; SongBar.Beatmap = beatmap.NewValue;
} }
} }
} }

View File

@ -7,10 +7,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; 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.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
@ -226,25 +226,25 @@ namespace osu.Game.Tournament.Screens.Editors
Model.ID = id.NewValue ?? 0; Model.ID = id.NewValue ?? 0;
if (id.NewValue != id.OldValue) if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null; Model.Beatmap = null;
if (Model.BeatmapInfo != null) if (Model.Beatmap != null)
{ {
updatePanel(); updatePanel();
return; return;
} }
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = Model.ID }); var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
req.Success += res => req.Success += res =>
{ {
Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); Model.Beatmap = res;
updatePanel(); updatePanel();
}; };
req.Failure += _ => req.Failure += _ =>
{ {
Model.BeatmapInfo = null; Model.Beatmap = null;
updatePanel(); updatePanel();
}; };
@ -259,9 +259,9 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
drawableContainer.Clear(); drawableContainer.Clear();
if (Model.BeatmapInfo != null) if (Model.Beatmap != null)
{ {
drawableContainer.Child = new TournamentBeatmapPanel(Model.BeatmapInfo, Model.Mods) drawableContainer.Child = new TournamentBeatmapPanel(Model.Beatmap, Model.Mods)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,

View File

@ -7,10 +7,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; 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.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
@ -234,25 +234,25 @@ namespace osu.Game.Tournament.Screens.Editors
Model.ID = id.NewValue ?? 0; Model.ID = id.NewValue ?? 0;
if (id.NewValue != id.OldValue) if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null; Model.Beatmap = null;
if (Model.BeatmapInfo != null) if (Model.Beatmap != null)
{ {
updatePanel(); updatePanel();
return; return;
} }
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = Model.ID }); var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
req.Success += res => req.Success += res =>
{ {
Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); Model.Beatmap = res;
updatePanel(); updatePanel();
}; };
req.Failure += _ => req.Failure += _ =>
{ {
Model.BeatmapInfo = null; Model.Beatmap = null;
updatePanel(); updatePanel();
}; };
@ -267,9 +267,9 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
drawableContainer.Clear(); drawableContainer.Clear();
if (Model.BeatmapInfo != null) if (Model.Beatmap != null)
{ {
drawableContainer.Child = new TournamentBeatmapPanel(Model.BeatmapInfo, result.Mod.Value) drawableContainer.Child = new TournamentBeatmapPanel(Model.Beatmap, result.Mod.Value)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,

View File

@ -8,8 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
@ -105,14 +105,14 @@ namespace osu.Game.Tournament.Screens.MapPool
ipc.Beatmap.BindValueChanged(beatmapChanged); ipc.Beatmap.BindValueChanged(beatmapChanged);
} }
private void beatmapChanged(ValueChangedEvent<BeatmapInfo> beatmap) private void beatmapChanged(ValueChangedEvent<APIBeatmap> beatmap)
{ {
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
return; return;
// if bans have already been placed, beatmap changes result in a selection being made autoamtically // if bans have already been placed, beatmap changes result in a selection being made autoamtically
if (beatmap.NewValue.OnlineBeatmapID != null) if (beatmap.NewValue.OnlineID > 0)
addForBeatmap(beatmap.NewValue.OnlineBeatmapID.Value); addForBeatmap(beatmap.NewValue.OnlineID);
} }
private void setMode(TeamColour colour, ChoiceType choiceType) private void setMode(TeamColour colour, ChoiceType choiceType)
@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool
if (map != null) if (map != null)
{ {
if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineID > 0) if (e.Button == MouseButton.Left && map.Beatmap.OnlineID > 0)
addForBeatmap(map.BeatmapInfo.OnlineID); addForBeatmap(map.Beatmap.OnlineID);
else else
{ {
var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineID); var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineID);
if (existing != null) if (existing != null)
{ {
@ -179,7 +179,7 @@ namespace osu.Game.Tournament.Screens.MapPool
if (CurrentMatch.Value == null) if (CurrentMatch.Value == null)
return; return;
if (CurrentMatch.Value.Round.Value.Beatmaps.All(b => b.BeatmapInfo.OnlineBeatmapID != beatmapId)) if (CurrentMatch.Value.Round.Value.Beatmaps.All(b => b.Beatmap.OnlineID != beatmapId))
// don't attempt to add if the beatmap isn't in our pool // don't attempt to add if the beatmap isn't in our pool
return; return;
@ -245,7 +245,7 @@ namespace osu.Game.Tournament.Screens.MapPool
flowCount = 1; flowCount = 1;
} }
currentFlow.Add(new TournamentBeatmapPanel(b.BeatmapInfo, b.Mods) currentFlow.Add(new TournamentBeatmapPanel(b.Beatmap, b.Mods)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,

View File

@ -141,9 +141,9 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Spacing = new Vector2(5), Spacing = new Vector2(5),
Children = new Drawable[] Children = new Drawable[]
{ {
new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Title, Colour = TournamentGame.TEXT_COLOUR, }, new TournamentSpriteText { Text = beatmap.Beatmap.Metadata.Title, Colour = TournamentGame.TEXT_COLOUR, },
new TournamentSpriteText { Text = "by", Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, new TournamentSpriteText { Text = "by", Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Artist, Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, new TournamentSpriteText { Text = beatmap.Beatmap.Metadata.Artist, Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
} }
}, },
new FillFlowContainer new FillFlowContainer

View File

@ -7,12 +7,14 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Graphics;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.IO; using osu.Game.Tournament.IO;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
@ -39,9 +41,18 @@ namespace osu.Game.Tournament
return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); return dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
} }
private TournamentSpriteText initialisationText;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(Storage baseStorage) private void load(Storage baseStorage)
{ {
AddInternal(initialisationText = new TournamentSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Torus.With(size: 32),
});
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
dependencies.CacheAs<Storage>(storage = new TournamentStorage(baseStorage)); dependencies.CacheAs<Storage>(storage = new TournamentStorage(baseStorage));
@ -123,7 +134,8 @@ namespace osu.Game.Tournament
} }
addedInfo |= addPlayers(); addedInfo |= addPlayers();
addedInfo |= addBeatmaps(); addedInfo |= addRoundBeatmaps();
addedInfo |= addSeedingBeatmaps();
if (addedInfo) if (addedInfo)
SaveChanges(); SaveChanges();
@ -145,6 +157,8 @@ namespace osu.Game.Tournament
Add(ipc); Add(ipc);
taskCompletionSource.SetResult(true); taskCompletionSource.SetResult(true);
initialisationText.Expire();
}); });
} }
@ -153,81 +167,108 @@ namespace osu.Game.Tournament
/// </summary> /// </summary>
private bool addPlayers() private bool addPlayers()
{ {
bool addedInfo = false; var playersRequiringPopulation = ladder.Teams
.SelectMany(t => t.Players)
foreach (var t in ladder.Teams) .Where(p => string.IsNullOrEmpty(p.Username)
{
foreach (var p in t.Players)
{
if (string.IsNullOrEmpty(p.Username)
|| p.Statistics?.GlobalRank == null || p.Statistics?.GlobalRank == null
|| p.Statistics?.CountryRank == null) || p.Statistics?.CountryRank == null).ToList();
if (playersRequiringPopulation.Count == 0)
return false;
for (int i = 0; i < playersRequiringPopulation.Count; i++)
{ {
var p = playersRequiringPopulation[i];
PopulateUser(p, immediate: true); PopulateUser(p, immediate: true);
addedInfo = true; updateLoadProgressMessage($"Populating user stats ({i} / {playersRequiringPopulation.Count})");
}
}
} }
return addedInfo; return true;
} }
/// <summary> /// <summary>
/// Add missing beatmap info based on beatmap IDs /// Add missing beatmap info based on beatmap IDs
/// </summary> /// </summary>
private bool addBeatmaps() private bool addRoundBeatmaps()
{ {
bool addedInfo = false; var beatmapsRequiringPopulation = ladder.Rounds
.SelectMany(r => r.Beatmaps)
.Where(b => string.IsNullOrEmpty(b.Beatmap?.BeatmapSet?.Title) && b.ID > 0).ToList();
foreach (var r in ladder.Rounds) if (beatmapsRequiringPopulation.Count == 0)
{ return false;
foreach (var b in r.Beatmaps.ToList())
{
if (b.BeatmapInfo != null)
continue;
if (b.ID > 0) for (int i = 0; i < beatmapsRequiringPopulation.Count; i++)
{ {
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); var b = beatmapsRequiringPopulation[i];
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID });
API.Perform(req); API.Perform(req);
b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); b.Beatmap = req.Response ?? new APIBeatmap();
addedInfo = true; updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})");
} }
if (b.BeatmapInfo == null) return true;
// if online population couldn't be performed, ensure we don't leave a null value behind
r.Beatmaps.Remove(b);
}
} }
foreach (var t in ladder.Teams) /// <summary>
/// Add missing beatmap info based on beatmap IDs
/// </summary>
private bool addSeedingBeatmaps()
{ {
foreach (var s in t.SeedingResults) var beatmapsRequiringPopulation = ladder.Teams
{ .SelectMany(r => r.SeedingResults)
foreach (var b in s.Beatmaps) .SelectMany(r => r.Beatmaps)
{ .Where(b => string.IsNullOrEmpty(b.Beatmap?.BeatmapSet?.Title) && b.ID > 0).ToList();
if (b.BeatmapInfo == null && b.ID > 0)
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
req.Perform(API);
b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore);
addedInfo = true; if (beatmapsRequiringPopulation.Count == 0)
} return false;
}
} for (int i = 0; i < beatmapsRequiringPopulation.Count; i++)
{
var b = beatmapsRequiringPopulation[i];
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID });
API.Perform(req);
b.Beatmap = req.Response ?? new APIBeatmap();
updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})");
} }
return addedInfo; return true;
} }
private void updateLoadProgressMessage(string s) => Schedule(() => initialisationText.Text = s);
public void PopulateUser(User user, Action success = null, Action failure = null, bool immediate = false) public void PopulateUser(User user, Action success = null, Action failure = null, bool immediate = false)
{ {
var req = new GetUserRequest(user.Id, Ruleset.Value); var req = new GetUserRequest(user.Id, Ruleset.Value);
req.Success += res => if (immediate)
{ {
API.Perform(req);
populate();
}
else
{
req.Success += res => { populate(); };
req.Failure += _ =>
{
user.Id = 1;
failure?.Invoke();
};
API.Queue(req);
}
void populate()
{
var res = req.Response;
if (res == null)
return;
user.Id = res.Id; user.Id = res.Id;
user.Username = res.Username; user.Username = res.Username;
@ -236,18 +277,7 @@ namespace osu.Game.Tournament
user.Cover = res.Cover; user.Cover = res.Cover;
success?.Invoke(); success?.Invoke();
}; }
req.Failure += _ =>
{
user.Id = 1;
failure?.Invoke();
};
if (immediate)
API.Perform(req);
else
API.Queue(req);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -269,18 +299,19 @@ namespace osu.Game.Tournament
ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true))) ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true)))
.ToList(); .ToList();
using (var stream = storage.GetStream(bracket_filename, FileAccess.Write, FileMode.Create)) // Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
using (var sw = new StreamWriter(stream)) string serialisedLadder = JsonConvert.SerializeObject(ladder,
{
sw.Write(JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings new JsonSerializerSettings
{ {
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() } Converters = new JsonConverter[] { new JsonPointConverter() }
})); });
}
using (var stream = storage.GetStream(bracket_filename, FileAccess.Write, FileMode.Create))
using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder);
} }
protected override UserInputManager CreateUserInputManager() => new TournamentInputManager(); protected override UserInputManager CreateUserInputManager() => new TournamentInputManager();

View File

@ -14,7 +14,7 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo, IBeatmapSetOnlineInfo public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo
{ {
public int ID { get; set; } public int ID { get; set; }
@ -35,6 +35,7 @@ namespace osu.Game.Beatmaps
[NotNull] [NotNull]
public List<BeatmapSetFileInfo> Files { get; set; } = new List<BeatmapSetFileInfo>(); public List<BeatmapSetFileInfo> Files { get; set; } = new List<BeatmapSetFileInfo>();
// This field is temporary and only used by `APIBeatmapSet.ToBeatmapSet` (soon to be removed) and tests (to be updated to provide APIBeatmapSet instead).
[NotMapped] [NotMapped]
public APIBeatmapSet OnlineInfo { get; set; } public APIBeatmapSet OnlineInfo { get; set; }

View File

@ -12,9 +12,9 @@ namespace osu.Game.Beatmaps.Drawables
/// <summary> /// <summary>
/// Display a beatmap background from a local source, but fallback to online source if not available. /// Display a beatmap background from a local source, but fallback to online source if not available.
/// </summary> /// </summary>
public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo> public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<IBeatmapInfo>
{ {
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>(); public readonly Bindable<IBeatmapInfo> Beatmap = new Bindable<IBeatmapInfo>();
protected override double LoadDelay => 500; protected override double LoadDelay => 500;
@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps.Drawables
protected override double TransformDuration => 400; protected override double TransformDuration => 400;
protected override Drawable CreateDrawable(BeatmapInfo model) protected override Drawable CreateDrawable(IBeatmapInfo model)
{ {
var drawable = getDrawableForModel(model); var drawable = getDrawableForModel(model);
drawable.RelativeSizeAxes = Axes.Both; drawable.RelativeSizeAxes = Axes.Both;
@ -50,15 +50,21 @@ namespace osu.Game.Beatmaps.Drawables
return drawable; return drawable;
} }
private Drawable getDrawableForModel(BeatmapInfo model) private Drawable getDrawableForModel(IBeatmapInfo model)
{ {
// prefer online cover where available. // prefer online cover where available.
if (model?.BeatmapSet?.OnlineInfo != null) if (model?.BeatmapSet is IBeatmapSetOnlineInfo online)
return new OnlineBeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); return new OnlineBeatmapSetCover(online, beatmapSetCoverType);
return model?.ID > 0 if (model is BeatmapInfo localModel)
? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model)) {
: new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap); if (localModel.BeatmapSet?.OnlineInfo != null)
return new OnlineBeatmapSetCover(localModel.BeatmapSet.OnlineInfo, beatmapSetCoverType);
return new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(localModel));
}
return new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
} }
} }
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.Drawables
{ {
private readonly BeatmapSetCoverType coverType; private readonly BeatmapSetCoverType coverType;
public IBeatmapSetOnlineInfo BeatmapSet public IBeatmapSetOnlineInfo OnlineInfo
{ {
get => Model; get => Model;
set => Model = value; set => Model = value;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Database
/// <typeparam name="TFileModel">The associated file join type.</typeparam> /// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : IModelManager<TModel>, IModelFileManager<TModel, TFileModel> public abstract class ArchiveModelManager<TModel, TFileModel> : IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new() where TFileModel : class, INamedFileInfo, IHasPrimaryKey, new()
{ {
private const int import_queue_request_concurrency = 1; private const int import_queue_request_concurrency = 1;
@ -315,16 +315,19 @@ 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>
protected virtual string ComputeHash(TModel item, ArchiveReader reader = null) protected virtual string ComputeHash(TModel item)
{ {
if (reader != null) var hashableFiles = item.Files
// fast hashing for cases where the item's files may not be populated. .Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
return computeHashFast(reader); .OrderBy(f => f.Filename)
.ToArray();
if (hashableFiles.Length > 0)
{
// for now, concatenate all hashable files in the set to create a unique hash. // for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream(); MemoryStream hashable = new MemoryStream();
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename)) foreach (TFileModel file in hashableFiles)
{ {
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath)) using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable); s.CopyTo(hashable);
@ -332,8 +335,9 @@ namespace osu.Game.Database
if (hashable.Length > 0) if (hashable.Length > 0)
return hashable.ComputeSHA2Hash(); return hashable.ComputeSHA2Hash();
}
return item.Hash; return generateFallbackHash();
} }
/// <summary> /// <summary>
@ -393,7 +397,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);
await Populate(item, archive, cancellationToken).ConfigureAwait(false); await Populate(item, archive, cancellationToken).ConfigureAwait(false);
@ -516,10 +520,13 @@ namespace osu.Game.Database
{ {
Files.Dereference(file.FileInfo); Files.Dereference(file.FileInfo);
if (file.ID > 0)
{
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
// Definitely can be removed once we rework the database backend. // Definitely can be removed once we rework the database backend.
usage.Context.Set<TFileModel>().Remove(file); usage.Context.Set<TFileModel>().Remove(file);
} }
}
model.Files.Remove(file); model.Files.Remove(file);
} }
@ -540,9 +547,10 @@ namespace osu.Game.Database
Filename = filename, Filename = filename,
FileInfo = Files.Add(contents) FileInfo = Files.Add(contents)
}); });
Update(model);
} }
if (model.ID > 0)
Update(model);
} }
/// <summary> /// <summary>
@ -684,7 +692,7 @@ namespace osu.Game.Database
if (hashable.Length > 0) if (hashable.Length > 0)
return hashable.ComputeSHA2Hash(); return hashable.ComputeSHA2Hash();
return reader.Name.ComputeSHA2Hash(); return generateFallbackHash();
} }
/// <summary> /// <summary>
@ -897,6 +905,14 @@ namespace osu.Game.Database
#endregion #endregion
private static string generateFallbackHash()
{
// if a hash could no be generated from file content, presume a unique / new import.
// therefore, let's use a guaranteed unique hash.
// this doesn't follow the SHA2 hashing schema intentionally, so such entries on the data store can be identified.
return Guid.NewGuid().ToString();
}
private string getValidFilename(string filename) private string getValidFilename(string filename)
{ {
foreach (char c in Path.GetInvalidFileNameChars()) foreach (char c in Path.GetInvalidFileNameChars())

View File

@ -0,0 +1,23 @@
// 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 Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20211020081609_ResetSkinHashes")]
public partial class ResetSkinHashes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql($"UPDATE SkinInfo SET Hash = null");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -54,10 +54,15 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"accuracy")] [JsonProperty(@"accuracy")]
private float overallDifficulty { get; set; } private float overallDifficulty { get; set; }
public double Length => TimeSpan.FromSeconds(lengthInSeconds).TotalMilliseconds; [JsonIgnore]
public double Length { get; set; }
[JsonProperty(@"total_length")] [JsonProperty(@"total_length")]
private double lengthInSeconds { get; set; } private double lengthInSeconds
{
get => TimeSpan.FromMilliseconds(Length).TotalSeconds;
set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds;
}
[JsonProperty(@"count_circles")] [JsonProperty(@"count_circles")]
public int CircleCount { get; set; } public int CircleCount { get; set; }

View File

@ -164,7 +164,7 @@ namespace osu.Game.Online.API.Requests.Responses
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => throw new NotImplementedException(); IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => throw new NotImplementedException();
double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException(); double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException();
double IBeatmapSetInfo.MaxLength => throw new NotImplementedException(); double IBeatmapSetInfo.MaxLength => throw new NotImplementedException();
double IBeatmapSetInfo.MaxBPM => throw new NotImplementedException(); double IBeatmapSetInfo.MaxBPM => BPM;
#endregion #endregion
} }

View File

@ -158,6 +158,8 @@ namespace osu.Game
private Bindable<int> configRuleset; private Bindable<int> configRuleset;
private Bindable<float> uiScale;
private Bindable<int> configSkin; private Bindable<int> configSkin;
private readonly string[] args; private readonly string[] args;
@ -219,6 +221,7 @@ namespace osu.Game
// bind config int to database RulesetInfo // bind config int to database RulesetInfo
configRuleset = LocalConfig.GetBindable<int>(OsuSetting.Ruleset); configRuleset = LocalConfig.GetBindable<int>(OsuSetting.Ruleset);
uiScale = LocalConfig.GetBindable<float>(OsuSetting.UIScale);
var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value); var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value);
@ -1020,6 +1023,28 @@ namespace osu.Game
return false; return false;
} }
public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
const float adjustment_increment = 0.05f;
switch (e.Action)
{
case PlatformAction.ZoomIn:
uiScale.Value += adjustment_increment;
return true;
case PlatformAction.ZoomOut:
uiScale.Value -= adjustment_increment;
return true;
case PlatformAction.ZoomDefault:
uiScale.SetDefault();
return true;
}
return base.OnPressed(e);
}
#region Inactive audio dimming #region Inactive audio dimming
private readonly BindableDouble inactiveVolumeFade = new BindableDouble(); private readonly BindableDouble inactiveVolumeFade = new BindableDouble();

View File

@ -59,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapListing
return; return;
} }
beatmapCover.BeatmapSet = value; beatmapCover.OnlineInfo = value.OnlineInfo;
beatmapCover.FadeTo(0.1f, 200, Easing.OutQuint); beatmapCover.FadeTo(0.1f, 200, Easing.OutQuint);
} }
} }

View File

@ -163,7 +163,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
protected Drawable CreateBackground() => new UpdateableOnlineBeatmapSetCover protected Drawable CreateBackground() => new UpdateableOnlineBeatmapSetCover
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
BeatmapSet = SetInfo, OnlineInfo = SetInfo.OnlineInfo,
}; };
public class Statistic : FillFlowContainer public class Statistic : FillFlowContainer

View File

@ -231,7 +231,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.BindValueChanged(setInfo => BeatmapSet.BindValueChanged(setInfo =>
{ {
Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.BeatmapSet = setInfo.NewValue; cover.OnlineInfo = setInfo.NewValue?.OnlineInfo;
downloadTracker?.RemoveAndDisposeImmediately(); downloadTracker?.RemoveAndDisposeImmediately();

View File

@ -82,7 +82,7 @@ namespace osu.Game.Overlays.Dashboard.Home
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
BeatmapSet = SetInfo OnlineInfo = SetInfo.OnlineInfo
} }
}, },
new Container new Container

View File

@ -4,11 +4,15 @@
using System; using System;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -16,6 +20,8 @@ namespace osu.Game.Overlays.Notifications
{ {
public class ProgressNotification : Notification, IHasCompletionTarget public class ProgressNotification : Notification, IHasCompletionTarget
{ {
private const float loading_spinner_size = 22;
public string Text public string Text
{ {
set => Schedule(() => textDrawable.Text = value); set => Schedule(() => textDrawable.Text = value);
@ -65,29 +71,53 @@ namespace osu.Game.Overlays.Notifications
private void updateState() private void updateState()
{ {
const double colour_fade_duration = 200;
switch (state) switch (state)
{ {
case ProgressNotificationState.Queued: case ProgressNotificationState.Queued:
Light.Colour = colourQueued; Light.Colour = colourQueued;
Light.Pulsate = false; Light.Pulsate = false;
progressBar.Active = false; progressBar.Active = false;
iconBackground.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration);
loadingSpinner.Show();
break; break;
case ProgressNotificationState.Active: case ProgressNotificationState.Active:
Light.Colour = colourActive; Light.Colour = colourActive;
Light.Pulsate = true; Light.Pulsate = true;
progressBar.Active = true; progressBar.Active = true;
iconBackground.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration);
loadingSpinner.Show();
break; break;
case ProgressNotificationState.Cancelled: case ProgressNotificationState.Cancelled:
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
iconBackground.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration);
loadingSpinner.Hide();
var icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Ban,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(loading_spinner_size),
};
IconContent.Add(icon);
icon.FadeInFromZero(200, Easing.OutQuint);
Light.Colour = colourCancelled; Light.Colour = colourCancelled;
Light.Pulsate = false; Light.Pulsate = false;
progressBar.Active = false; progressBar.Active = false;
break; break;
case ProgressNotificationState.Completed: case ProgressNotificationState.Completed:
loadingSpinner.Hide();
NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint);
this.FadeOut(200).Finally(d => Completed()); this.FadeOut(200).Finally(d => Completed());
break; break;
@ -115,15 +145,13 @@ namespace osu.Game.Overlays.Notifications
private Color4 colourActive; private Color4 colourActive;
private Color4 colourCancelled; private Color4 colourCancelled;
private Box iconBackground;
private LoadingSpinner loadingSpinner;
private readonly TextFlowContainer textDrawable; private readonly TextFlowContainer textDrawable;
public ProgressNotification() public ProgressNotification()
{ {
IconContent.Add(new Box
{
RelativeSizeAxes = Axes.Both,
});
Content.Add(textDrawable = new OsuTextFlowContainer Content.Add(textDrawable = new OsuTextFlowContainer
{ {
Colour = OsuColour.Gray(128), Colour = OsuColour.Gray(128),
@ -138,6 +166,9 @@ namespace osu.Game.Overlays.Notifications
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}); });
// make some extra space for the progress bar.
IconContent.Margin = new MarginPadding { Bottom = 5 };
State = ProgressNotificationState.Queued; State = ProgressNotificationState.Queued;
// don't close on click by default. // don't close on click by default.
@ -150,6 +181,19 @@ namespace osu.Game.Overlays.Notifications
colourQueued = colours.YellowDark; colourQueued = colours.YellowDark;
colourActive = colours.Blue; colourActive = colours.Blue;
colourCancelled = colours.Red; colourCancelled = colours.Red;
IconContent.AddRange(new Drawable[]
{
iconBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
},
loadingSpinner = new LoadingSpinner
{
Size = new Vector2(loading_spinner_size),
}
});
} }
public override void Close() public override void Close()

View File

@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = cover_width, Width = cover_width,
BeatmapSet = mostPlayed.BeatmapSet, OnlineInfo = mostPlayed.BeatmapSet,
}, },
new Container new Container
{ {

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) IList<IList<HitSampleInfo>> nodeSamples)
{ {
newCombo |= forceNewCombo; newCombo |= forceNewCombo;
comboOffset += extraComboOffset; comboOffset += extraComboOffset;

View File

@ -408,7 +408,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param> /// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns> /// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples); IList<IList<HitSampleInfo>> nodeSamples);
/// <summary> /// <summary>
/// Creates a legacy Spinner-type hit object. /// Creates a legacy Spinner-type hit object.
@ -481,7 +481,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled /// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
/// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option. /// using the <see cref="SkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option.
/// </remarks> /// </remarks>
public readonly bool IsLayered; public readonly bool IsLayered;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Distance => Path.Distance; public double Distance => Path.Distance;
public List<IList<HitSampleInfo>> NodeSamples { get; set; } public IList<IList<HitSampleInfo>> NodeSamples { get; set; }
public int RepeatCount { get; set; } public int RepeatCount { get; set; }
[JsonIgnore] [JsonIgnore]

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) IList<IList<HitSampleInfo>> nodeSamples)
{ {
return new ConvertSlider return new ConvertSlider
{ {

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) IList<IList<HitSampleInfo>> nodeSamples)
{ {
newCombo |= forceNewCombo; newCombo |= forceNewCombo;
comboOffset += extraComboOffset; comboOffset += extraComboOffset;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) IList<IList<HitSampleInfo>> nodeSamples)
{ {
return new ConvertSlider return new ConvertSlider
{ {

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Objects.Types
/// n-1: The last repeat.<br /> /// n-1: The last repeat.<br />
/// n: The last node. /// n: The last node.
/// </summary> /// </summary>
List<IList<HitSampleInfo>> NodeSamples { get; } IList<IList<HitSampleInfo>> NodeSamples { get; }
} }
public static class HasRepeatsExtensions public static class HasRepeatsExtensions

View File

@ -3,13 +3,15 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components namespace osu.Game.Screens.OnlinePlay.Components
{ {
public abstract class ReadyButton : TriangleButton public abstract class ReadyButton : TriangleButton, IHasTooltip
{ {
public new readonly BindableBool Enabled = new BindableBool(); public new readonly BindableBool Enabled = new BindableBool();
@ -24,6 +26,18 @@ namespace osu.Game.Screens.OnlinePlay.Components
Enabled.BindValueChanged(_ => updateState(), true); Enabled.BindValueChanged(_ => updateState(), true);
} }
private void updateState() => base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; private void updateState() =>
base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
public virtual LocalisableString TooltipText
{
get
{
if (Enabled.Value)
return string.Empty;
return "Beatmap not downloaded";
}
}
} }
} }

View File

@ -14,6 +14,9 @@ namespace osu.Game.Screens.OnlinePlay.Components
{ {
private OsuSpriteText attemptDisplay; private OsuSpriteText attemptDisplay;
[Resolved]
private OsuColour colours { get; set; }
public RoomLocalUserInfo() public RoomLocalUserInfo()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -54,6 +57,9 @@ namespace osu.Game.Screens.OnlinePlay.Components
{ {
int remaining = MaxAttempts.Value.Value - UserScore.Value.PlaylistItemAttempts.Sum(a => a.Attempts); int remaining = MaxAttempts.Value.Value - UserScore.Value.PlaylistItemAttempts.Sum(a => a.Attempts);
attemptDisplay.Text += $" ({remaining} remaining)"; attemptDisplay.Text += $" ({remaining} remaining)";
if (remaining == 0)
attemptDisplay.Colour = colours.RedLight;
} }
} }
else else

View File

@ -332,13 +332,14 @@ namespace osu.Game.Screens.OnlinePlay
public PanelBackground() public PanelBackground()
{ {
UpdateableBeatmapBackgroundSprite backgroundSprite;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new UpdateableBeatmapBackgroundSprite backgroundSprite = new UpdateableBeatmapBackgroundSprite
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill, FillMode = FillMode.Fill,
Beatmap = { BindTarget = Beatmap }
}, },
new FillFlowContainer new FillFlowContainer
{ {
@ -373,6 +374,10 @@ namespace osu.Game.Screens.OnlinePlay
} }
} }
}; };
// manual binding required as playlists don't expose IBeatmapInfo currently.
// may be removed in the future if this changes.
Beatmap.BindValueChanged(beatmap => backgroundSprite.Beatmap.Value = beatmap.NewValue);
} }
} }
} }

View File

@ -2,8 +2,10 @@
// 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;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -16,6 +18,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(typeof(Room), nameof(Room.EndDate))] [Resolved(typeof(Room), nameof(Room.EndDate))]
private Bindable<DateTimeOffset?> endDate { get; set; } private Bindable<DateTimeOffset?> endDate { get; set; }
[Resolved(typeof(Room), nameof(Room.MaxAttempts))]
private Bindable<int?> maxAttempts { get; set; }
[Resolved(typeof(Room), nameof(Room.UserScore))]
private Bindable<PlaylistAggregateScore> userScore { get; set; }
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } private IBindable<WorkingBeatmap> gameBeatmap { get; set; }
@ -32,11 +40,49 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Triangles.ColourLight = colours.GreenLight; Triangles.ColourLight = colours.GreenLight;
} }
private bool hasRemainingAttempts = true;
protected override void LoadComplete()
{
base.LoadComplete();
userScore.BindValueChanged(aggregate =>
{
if (maxAttempts.Value == null)
return;
int remaining = maxAttempts.Value.Value - aggregate.NewValue.PlaylistItemAttempts.Sum(a => a.Attempts);
hasRemainingAttempts = remaining > 0;
});
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; Enabled.Value = hasRemainingAttempts && enoughTimeLeft;
}
public override LocalisableString TooltipText
{
get
{
if (Enabled.Value)
return string.Empty;
if (!enoughTimeLeft)
return "No time left!";
if (!hasRemainingAttempts)
return "Attempts exhausted!";
return base.TooltipText;
} }
} }
private bool enoughTimeLeft =>
// This should probably consider the length of the currently selected item, rather than a constant 30 seconds.
endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value;
}
} }

View File

@ -37,6 +37,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private MatchLeaderboard leaderboard; private MatchLeaderboard leaderboard;
private SelectionPollingComponent selectionPollingComponent; private SelectionPollingComponent selectionPollingComponent;
private FillFlowContainer progressSection;
public PlaylistsRoomSubScreen(Room room) public PlaylistsRoomSubScreen(Room room)
: base(room, false) // Editing is temporarily not allowed. : base(room, false) // Editing is temporarily not allowed.
{ {
@ -67,6 +69,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Schedule(() => SelectedItem.Value = Room.Playlist.FirstOrDefault()); Schedule(() => SelectedItem.Value = Room.Playlist.FirstOrDefault());
} }
}, true); }, true);
Room.MaxAttempts.BindValueChanged(attempts => progressSection.Alpha = Room.MaxAttempts.Value != null ? 1 : 0, true);
} }
protected override Drawable CreateMainContent() => new GridContainer protected override Drawable CreateMainContent() => new GridContainer
@ -153,6 +157,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
}, },
new Drawable[] new Drawable[]
{
progressSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OverlinedHeader("Progress"),
new RoomLocalUserInfo(),
}
},
},
new Drawable[]
{ {
new OverlinedHeader("Leaderboard") new OverlinedHeader("Leaderboard")
}, },
@ -162,6 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension(), new Dimension(),

View File

@ -19,7 +19,14 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources) public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: base(skin, new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"), resources, string.Empty) : base(
skin,
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"),
resources,
// A default legacy skin may still have a skin.ini if it is modified by the user.
// We must specify the stream directly as we are redirecting storage to the osu-resources location for other files.
new LegacySkinResourceStore<SkinFileInfo>(skin, resources.Files).GetStream("skin.ini")
)
{ {
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
Configuration.CustomComboColours = new List<Color4> Configuration.CustomComboColours = new List<Color4>

View File

@ -35,7 +35,6 @@ namespace osu.Game.Skinning
: base(skin, resources) : base(skin, resources)
{ {
this.resources = resources; this.resources = resources;
Configuration = new DefaultSkinConfiguration();
} }
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;

View File

@ -1,12 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Skinning
{
/// <summary>
/// A skin configuration pre-populated with sane defaults.
/// </summary>
public class DefaultSkinConfiguration : SkinConfiguration
{
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Skinning
{ {
switch (lookup) switch (lookup)
{ {
case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version: case SkinConfiguration.LegacySetting s when s == SkinConfiguration.LegacySetting.Version:
// For lookup simplicity, ignore beatmap-level versioning completely. // For lookup simplicity, ignore beatmap-level versioning completely.
// If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject) // If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject)

View File

@ -47,12 +47,6 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
protected virtual bool UseCustomSampleBanks => false; protected virtual bool UseCustomSampleBanks => false;
public new LegacySkinConfiguration Configuration
{
get => base.Configuration as LegacySkinConfiguration;
set => base.Configuration = value;
}
private readonly Dictionary<int, LegacyManiaSkinConfiguration> maniaConfigurations = new Dictionary<int, LegacyManiaSkinConfiguration>(); private readonly Dictionary<int, LegacyManiaSkinConfiguration> maniaConfigurations = new Dictionary<int, LegacyManiaSkinConfiguration>();
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
@ -69,29 +63,20 @@ namespace osu.Game.Skinning
/// <param name="resources">Access to raw game resources.</param> /// <param name="resources">Access to raw game resources.</param>
/// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param> /// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
: base(skin, resources) : this(skin, storage, resources, storage?.GetStream(configurationFilename))
{ {
using (var stream = storage?.GetStream(configurationFilename))
{
if (stream != null)
{
using (LineBufferedReader reader = new LineBufferedReader(stream, true))
Configuration = new LegacySkinDecoder().Decode(reader);
stream.Seek(0, SeekOrigin.Begin);
using (LineBufferedReader reader = new LineBufferedReader(stream))
{
var maniaList = new LegacyManiaSkinDecoder().Decode(reader);
foreach (var config in maniaList)
maniaConfigurations[config.Keys] = config;
}
}
else
Configuration = new LegacySkinConfiguration();
} }
/// <summary>
/// Construct a new legacy skin instance.
/// </summary>
/// <param name="skin">The model for this skin.</param>
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
/// <param name="resources">Access to raw game resources.</param>
/// <param name="configurationStream">An optional stream containing the contents of a skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream)
: base(skin, resources, configurationStream)
{
if (storage != null) if (storage != null)
{ {
var samples = resources?.AudioManager?.GetSampleStore(storage); var samples = resources?.AudioManager?.GetSampleStore(storage);
@ -110,6 +95,21 @@ namespace osu.Game.Skinning
true) != null); true) != null);
} }
protected override void ParseConfigurationStream(Stream stream)
{
base.ParseConfigurationStream(stream);
stream.Seek(0, SeekOrigin.Begin);
using (LineBufferedReader reader = new LineBufferedReader(stream))
{
var maniaList = new LegacyManiaSkinDecoder().Decode(reader);
foreach (var config in maniaList)
maniaConfigurations[config.Keys] = config;
}
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)
@ -146,7 +146,7 @@ namespace osu.Game.Skinning
break; break;
case LegacySkinConfiguration.LegacySetting legacy: case SkinConfiguration.LegacySetting legacy:
return legacySettingLookup<TValue>(legacy); return legacySettingLookup<TValue>(legacy);
default: default:
@ -189,7 +189,7 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.ExplosionScale: case LegacyManiaSkinConfigurationLookups.ExplosionScale:
Debug.Assert(maniaLookup.TargetColumn != null); Debug.Assert(maniaLookup.TargetColumn != null);
if (GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
return SkinUtils.As<TValue>(new Bindable<float>(1)); return SkinUtils.As<TValue>(new Bindable<float>(1));
if (existing.ExplosionWidth[maniaLookup.TargetColumn.Value] != 0) if (existing.ExplosionWidth[maniaLookup.TargetColumn.Value] != 0)
@ -236,7 +236,7 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale:
Debug.Assert(maniaLookup.TargetColumn != null); Debug.Assert(maniaLookup.TargetColumn != null);
if (GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
return SkinUtils.As<TValue>(new Bindable<float>(1)); return SkinUtils.As<TValue>(new Bindable<float>(1));
if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0)
@ -309,15 +309,15 @@ namespace osu.Game.Skinning
=> source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable<string>(image) : null; => source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable<string>(image) : null;
[CanBeNull] [CanBeNull]
private IBindable<TValue> legacySettingLookup<TValue>(LegacySkinConfiguration.LegacySetting legacySetting) private IBindable<TValue> legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
{ {
switch (legacySetting) switch (legacySetting)
{ {
case LegacySkinConfiguration.LegacySetting.Version: case SkinConfiguration.LegacySetting.Version:
return SkinUtils.As<TValue>(new Bindable<decimal>(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); return SkinUtils.As<TValue>(new Bindable<decimal>(Configuration.LegacyVersion ?? SkinConfiguration.LATEST_VERSION));
default: default:
return genericLookup<LegacySkinConfiguration.LegacySetting, TValue>(legacySetting); return genericLookup<SkinConfiguration.LegacySetting, TValue>(legacySetting);
} }
} }

View File

@ -1,28 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Skinning
{
public class LegacySkinConfiguration : SkinConfiguration
{
public const decimal LATEST_VERSION = 2.7m;
/// <summary>
/// Legacy version of this skin.
/// </summary>
public decimal? LegacyVersion { get; internal set; }
public enum LegacySetting
{
Version,
ComboPrefix,
ComboOverlap,
ScorePrefix,
ScoreOverlap,
HitCirclePrefix,
HitCircleOverlap,
AnimationFramerate,
LayeredHitSounds
}
}
}

View File

@ -6,14 +6,14 @@ using osu.Game.Beatmaps.Formats;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacySkinDecoder : LegacyDecoder<LegacySkinConfiguration> public class LegacySkinDecoder : LegacyDecoder<SkinConfiguration>
{ {
public LegacySkinDecoder() public LegacySkinDecoder()
: base(1) : base(1)
{ {
} }
protected override void ParseLine(LegacySkinConfiguration skin, Section section, string line) protected override void ParseLine(SkinConfiguration skin, Section section, string line)
{ {
if (section != Section.Colours) if (section != Section.Colours)
{ {
@ -34,7 +34,7 @@ namespace osu.Game.Skinning
case @"Version": case @"Version":
if (pair.Value == "latest") if (pair.Value == "latest")
skin.LegacyVersion = LegacySkinConfiguration.LATEST_VERSION; skin.LegacyVersion = SkinConfiguration.LATEST_VERSION;
else if (decimal.TryParse(pair.Value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal version)) else if (decimal.TryParse(pair.Value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal version))
skin.LegacyVersion = version; skin.LegacyVersion = version;
@ -57,7 +57,7 @@ namespace osu.Game.Skinning
base.ParseLine(skin, section, line); base.ParseLine(skin, section, line);
} }
protected override LegacySkinConfiguration CreateTemplateObject() protected override SkinConfiguration CreateTemplateObject()
{ {
var config = base.CreateTemplateObject(); var config = base.CreateTemplateObject();
config.LegacyVersion = 1.0m; config.LegacyVersion = 1.0m;

View File

@ -12,7 +12,7 @@ using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using static osu.Game.Skinning.LegacySkinConfiguration; using static osu.Game.Skinning.SkinConfiguration;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using static osu.Game.Skinning.LegacySkinConfiguration; using static osu.Game.Skinning.SkinConfiguration;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {

View File

@ -3,8 +3,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -21,8 +23,9 @@ namespace osu.Game.Skinning
public abstract class Skin : IDisposable, ISkin public abstract class Skin : IDisposable, ISkin
{ {
public readonly SkinInfo SkinInfo; public readonly SkinInfo SkinInfo;
private readonly IStorageResourceProvider resources;
public SkinConfiguration Configuration { get; protected set; } public SkinConfiguration Configuration { get; set; }
public IDictionary<SkinnableTarget, SkinnableInfo[]> DrawableComponentInfo => drawableComponentInfo; public IDictionary<SkinnableTarget, SkinnableInfo[]> DrawableComponentInfo => drawableComponentInfo;
@ -36,9 +39,18 @@ namespace osu.Game.Skinning
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup); public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
protected Skin(SkinInfo skin, IStorageResourceProvider resources) protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null)
{ {
SkinInfo = skin; SkinInfo = skin;
this.resources = resources;
configurationStream ??= getConfigurationStream();
if (configurationStream != null)
// stream will be closed after use by LineBufferedReader.
ParseConfigurationStream(configurationStream);
else
Configuration = new SkinConfiguration();
// we may want to move this to some kind of async operation in the future. // we may want to move this to some kind of async operation in the future.
foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
@ -73,6 +85,22 @@ namespace osu.Game.Skinning
} }
} }
protected virtual void ParseConfigurationStream(Stream stream)
{
using (LineBufferedReader reader = new LineBufferedReader(stream, true))
Configuration = new LegacySkinDecoder().Decode(reader);
}
private Stream getConfigurationStream()
{
string path = SkinInfo.Files.SingleOrDefault(f => f.Filename == "skin.ini")?.FileInfo.StoragePath;
if (string.IsNullOrEmpty(path))
return null;
return resources?.Files.GetStream(path);
}
/// <summary> /// <summary>
/// Remove all stored customisations for the provided target. /// Remove all stored customisations for the provided target.
/// </summary> /// </summary>

View File

@ -14,11 +14,31 @@ namespace osu.Game.Skinning
{ {
public readonly SkinInfo SkinInfo = new SkinInfo(); public readonly SkinInfo SkinInfo = new SkinInfo();
public const decimal LATEST_VERSION = 2.7m;
/// <summary> /// <summary>
/// Whether to allow <see cref="DefaultComboColours"/> as a fallback list for when no combo colours are provided. /// Whether to allow <see cref="DefaultComboColours"/> as a fallback list for when no combo colours are provided.
/// </summary> /// </summary>
internal bool AllowDefaultComboColoursFallback = true; internal bool AllowDefaultComboColoursFallback = true;
/// <summary>
/// Legacy version of this skin.
/// </summary>
public decimal? LegacyVersion { get; internal set; }
public enum LegacySetting
{
Version,
ComboPrefix,
ComboOverlap,
ScorePrefix,
ScoreOverlap,
HitCirclePrefix,
HitCircleOverlap,
AnimationFramerate,
LayeredHitSounds
}
public static List<Color4> DefaultComboColours { get; } = new List<Color4> public static List<Color4> DefaultComboColours { get; } = new List<Color4>
{ {
new Color4(255, 192, 0, 255), new Color4(255, 192, 0, 255),

View File

@ -18,12 +18,12 @@ namespace osu.Game.Skinning
public int ID { get; set; } public int ID { get; set; }
public string Name { get; set; } public string Name { get; set; } = string.Empty;
public string Creator { get; set; } = string.Empty;
public string Hash { get; set; } public string Hash { get; set; }
public string Creator { get; set; }
public string InstantiationInfo { get; set; } public string InstantiationInfo { get; set; }
public virtual Skin CreateInstance(IStorageResourceProvider resources) public virtual Skin CreateInstance(IStorageResourceProvider resources)

View File

@ -14,11 +14,11 @@ using Newtonsoft.Json;
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;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -51,7 +51,7 @@ namespace osu.Game.Skinning
public override IEnumerable<string> HandledExtensions => new[] { ".osk" }; public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
protected override string[] HashableFileTypes => new[] { ".ini" }; protected override string[] HashableFileTypes => new[] { ".ini", ".json" };
protected override string ImportFromStablePath => "Skins"; protected override string ImportFromStablePath => "Skins";
@ -85,6 +85,27 @@ namespace osu.Game.Skinning
SourceChanged?.Invoke(); SourceChanged?.Invoke();
}; };
// can be removed 20220420.
populateMissingHashes();
}
private void populateMissingHashes()
{
var skinsWithoutHashes = ModelStore.ConsumableItems.Where(i => i.Hash == null).ToArray();
foreach (SkinInfo skin in skinsWithoutHashes)
{
try
{
Update(skin);
}
catch (Exception e)
{
Delete(skin);
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
}
}
} }
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osk"; protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osk";
@ -128,28 +149,118 @@ namespace osu.Game.Skinning
CurrentSkinInfo.Value = ModelStore.ConsumableItems.Single(i => i.ID == chosen.ID); CurrentSkinInfo.Value = ModelStore.ConsumableItems.Single(i => i.ID == chosen.ID);
} }
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name ?? "No name" };
private const string unknown_creator_string = "Unknown"; private const string unknown_creator_string = "Unknown";
protected override bool HasCustomHashFunction => true; protected override bool HasCustomHashFunction => true;
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) protected override string ComputeHash(SkinInfo item)
{ {
var instance = GetSkin(item); var instance = GetSkin(item);
// in the case the skin has a skin.ini file, we are going to create a hash based on that. // This function can be run on fresh import or save. The logic here ensures a skin.ini file is in a good state for both operations.
// we don't want to do this in the case we don't have a skin.ini, as it would match only on the filename portion,
// causing potentially unique skin imports to be considered as a duplicate.
if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name))
{
// we need to populate early to create a hash based off skin.ini contents
populateMetadata(item, instance, reader?.Name);
return item.ToString().ComputeSHA2Hash(); // `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above.
string skinIniSourcedName = instance.Configuration.SkinInfo.Name;
string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator;
string archiveName = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase);
bool isImport = item.ID == 0;
if (isImport)
{
item.Name = !string.IsNullOrEmpty(skinIniSourcedName) ? skinIniSourcedName : archiveName;
item.Creator = !string.IsNullOrEmpty(skinIniSourcedCreator) ? skinIniSourcedCreator : unknown_creator_string;
// For imports, we want to use the archive or folder name as part of the metadata, in addition to any existing skin.ini metadata.
// In an ideal world, skin.ini would be the only source of metadata, but a lot of skin creators and users don't update it when making modifications.
// In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin.
if (archiveName != item.Name)
item.Name = $"{item.Name} [{archiveName}]";
} }
return base.ComputeHash(item, reader); // By this point, the metadata in SkinInfo will be correct.
// Regardless of whether this is an import or not, let's write the skin.ini if non-existing or non-matching.
// This is (weirdly) done inside ComputeHash to avoid adding a new method to handle this case. After switching to realm it can be moved into another place.
if (skinIniSourcedName != item.Name)
updateSkinIniMetadata(item);
return base.ComputeHash(item);
}
private void updateSkinIniMetadata(SkinInfo item)
{
string nameLine = $"Name: {item.Name}";
string authorLine = $"Author: {item.Creator}";
var existingFile = item.Files.SingleOrDefault(f => f.Filename == "skin.ini");
if (existingFile != null)
{
List<string> outputLines = new List<string>();
bool addedName = false;
bool addedAuthor = false;
using (var stream = Files.Storage.GetStream(existingFile.FileInfo.StoragePath))
using (var sr = new StreamReader(stream))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (line.StartsWith("Name:", StringComparison.Ordinal))
{
outputLines.Add(nameLine);
addedName = true;
}
else if (line.StartsWith("Author:", StringComparison.Ordinal))
{
outputLines.Add(authorLine);
addedAuthor = true;
}
else
outputLines.Add(line);
}
}
if (!addedName || !addedAuthor)
{
outputLines.AddRange(new[]
{
"[General]",
nameLine,
authorLine,
});
}
using (Stream stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
foreach (string line in outputLines)
sw.WriteLine(line);
}
ReplaceFile(item, existingFile, stream);
}
}
else
{
using (Stream stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
sw.WriteLine("[General]");
sw.WriteLine(nameLine);
sw.WriteLine(authorLine);
sw.WriteLine("Version: latest");
}
AddFile(item, stream, "skin.ini");
}
}
} }
protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
@ -158,32 +269,12 @@ namespace osu.Game.Skinning
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
populateMetadata(model, instance, archive?.Name); model.Name = instance.Configuration.SkinInfo.Name;
model.Creator = instance.Configuration.SkinInfo.Creator;
return Task.CompletedTask; return Task.CompletedTask;
} }
private void populateMetadata(SkinInfo item, Skin instance, string archiveName)
{
if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name))
{
item.Name = instance.Configuration.SkinInfo.Name;
item.Creator = instance.Configuration.SkinInfo.Creator;
}
else
{
item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase);
item.Creator ??= unknown_creator_string;
}
// generally when importing from a folder, the ".osk" extension will not be present.
// if we ever need a more reliable method of determining this, the type of `ArchiveReader` can be checked.
bool isArchiveImport = archiveName?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true;
if (archiveName != null && !isArchiveImport && archiveName != item.Name)
item.Name = $"{item.Name} [{archiveName}]";
}
/// <summary> /// <summary>
/// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/> /// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/>
/// </summary> /// </summary>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO; using osu.Game.IO;
@ -16,6 +17,9 @@ namespace osu.Game.Tests.Beatmaps
{ {
public class TestBeatmap : Beatmap public class TestBeatmap : Beatmap
{ {
private static int onlineSetID;
private static int onlineBeatmapID;
public TestBeatmap(RulesetInfo ruleset, bool withHitObjects = true) public TestBeatmap(RulesetInfo ruleset, bool withHitObjects = true)
{ {
var baseBeatmap = CreateBeatmap(); var baseBeatmap = CreateBeatmap();
@ -31,8 +35,10 @@ namespace osu.Game.Tests.Beatmaps
BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.RulesetID = ruleset.ID ?? 0;
BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo }; BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
BeatmapInfo.BeatmapSet.OnlineBeatmapSetID = Interlocked.Increment(ref onlineSetID);
BeatmapInfo.Length = 75000; BeatmapInfo.Length = 75000;
BeatmapInfo.OnlineInfo = new APIBeatmap(); BeatmapInfo.OnlineInfo = new APIBeatmap();
BeatmapInfo.OnlineBeatmapID = Interlocked.Increment(ref onlineBeatmapID);
BeatmapInfo.BeatmapSet.OnlineInfo = new APIBeatmapSet BeatmapInfo.BeatmapSet.OnlineInfo = new APIBeatmapSet
{ {
Status = BeatmapSetOnlineStatus.Ranked, Status = BeatmapSetOnlineStatus.Ranked,

View File

@ -10,7 +10,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -154,7 +153,7 @@ namespace osu.Game.Tests.Visual
{ {
} }
protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) protected override string ComputeHash(BeatmapSetInfo item)
=> string.Empty; => string.Empty;
} }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// <summary> /// <summary>
/// The cached <see cref="IRoomManager"/>. /// The cached <see cref="IRoomManager"/>.
/// </summary> /// </summary>
new TestRequestHandlingMultiplayerRoomManager RoomManager { get; } new TestMultiplayerRoomManager RoomManager { get; }
/// <summary> /// <summary>
/// The cached <see cref="UserLookupCache"/>. /// The cached <see cref="UserLookupCache"/>.

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public const int PLAYER_2_ID = 56; public const int PLAYER_2_ID = 56;
public TestMultiplayerClient Client => OnlinePlayDependencies.Client; public TestMultiplayerClient Client => OnlinePlayDependencies.Client;
public new TestRequestHandlingMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache; public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache;
public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient;
@ -35,12 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new void Setup() => Schedule(() => public new void Setup() => Schedule(() =>
{ {
if (joinRoom) if (joinRoom)
{ SelectedRoom.Value = CreateRoom();
var room = CreateRoom();
RoomManager.CreateRoom(room);
SelectedRoom.Value = room;
}
}); });
protected virtual Room CreateRoom() protected virtual Room CreateRoom()
@ -64,8 +59,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
base.SetUpSteps(); base.SetUpSteps();
if (joinRoom) if (joinRoom)
{
AddStep("join room", () => RoomManager.CreateRoom(SelectedRoom.Value));
AddUntilStep("wait for room join", () => Client.Room != null); AddUntilStep("wait for room join", () => Client.Room != null);
} }
}
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
} }

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestMultiplayerClient Client { get; } public TestMultiplayerClient Client { get; }
public TestUserLookupCache LookupCache { get; } public TestUserLookupCache LookupCache { get; }
public TestSpectatorClient SpectatorClient { get; } public TestSpectatorClient SpectatorClient { get; }
public new TestRequestHandlingMultiplayerRoomManager RoomManager => (TestRequestHandlingMultiplayerRoomManager)base.RoomManager; public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager;
public MultiplayerTestSceneDependencies() public MultiplayerTestSceneDependencies()
{ {
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
CacheAs<SpectatorClient>(SpectatorClient); CacheAs<SpectatorClient>(SpectatorClient);
} }
protected override IRoomManager CreateRoomManager() => new TestRequestHandlingMultiplayerRoomManager(); protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(RequestsHandler);
protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient(); protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient();
} }

View File

@ -39,9 +39,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } = null!; private BeatmapManager beatmaps { get; set; } = null!;
private readonly TestRequestHandlingMultiplayerRoomManager roomManager; private readonly TestMultiplayerRoomManager roomManager;
public TestMultiplayerClient(TestRequestHandlingMultiplayerRoomManager roomManager) public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
{ {
this.roomManager = roomManager; this.roomManager = roomManager;
} }

View File

@ -2,8 +2,6 @@
// 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.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
@ -12,25 +10,24 @@ using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
/// <summary> /// <summary>
/// A <see cref="RoomManager"/> for use in multiplayer test scenes, backed by a <see cref="TestRoomRequestsHandler"/>. /// A <see cref="RoomManager"/> for use in multiplayer test scenes.
/// Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>. /// Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>.
/// </summary> /// </summary>
public class TestRequestHandlingMultiplayerRoomManager : MultiplayerRoomManager public class TestMultiplayerRoomManager : MultiplayerRoomManager
{ {
public IReadOnlyList<Room> ServerSideRooms => handler.ServerSideRooms; private readonly TestRoomRequestsHandler requestsHandler;
private readonly TestRoomRequestsHandler handler = new TestRoomRequestsHandler(); public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler)
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{ {
((DummyAPIAccess)api).HandleRequest = request => handler.HandleRequest(request, api.LocalUser.Value, game); this.requestsHandler = requestsHandler;
} }
public IReadOnlyList<Room> ServerSideRooms => requestsHandler.ServerSideRooms;
/// <summary> /// <summary>
/// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired. /// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired.
/// </summary> /// </summary>
/// <param name="room">The room.</param> /// <param name="room">The room.</param>
public void AddServerSideRoom(Room room) => handler.AddServerSideRoom(room); public void AddServerSideRoom(Room room) => requestsHandler.AddServerSideRoom(room);
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
@ -27,11 +28,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay
/// </summary> /// </summary>
protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies; protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies;
private DelegatedDependencyContainer dependencies;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
[Resolved]
private OsuGameBase game { get; set; }
private readonly Container content; private readonly Container content;
private readonly Container drawableDependenciesContainer; private readonly Container drawableDependenciesContainer;
private DelegatedDependencyContainer dependencies;
protected OnlinePlayTestScene() protected OnlinePlayTestScene()
{ {
@ -57,6 +61,17 @@ namespace osu.Game.Tests.Visual.OnlinePlay
drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents); drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents);
}); });
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("setup API", () =>
{
var handler = OnlinePlayDependencies.RequestsHandler;
((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, game);
});
}
/// <summary> /// <summary>
/// Creates the room dependencies. Called every <see cref="Setup"/>. /// Creates the room dependencies. Called every <see cref="Setup"/>.
/// </summary> /// </summary>

View File

@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public IRoomManager RoomManager { get; } public IRoomManager RoomManager { get; }
public OngoingOperationTracker OngoingOperationTracker { get; } public OngoingOperationTracker OngoingOperationTracker { get; }
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; }
public TestRoomRequestsHandler RequestsHandler { get; }
/// <summary> /// <summary>
/// All cached dependencies which are also <see cref="Drawable"/> components. /// All cached dependencies which are also <see cref="Drawable"/> components.
@ -33,12 +34,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public OnlinePlayTestSceneDependencies() public OnlinePlayTestSceneDependencies()
{ {
SelectedRoom = new Bindable<Room>(); SelectedRoom = new Bindable<Room>();
RoomManager = CreateRoomManager(); RequestsHandler = new TestRoomRequestsHandler();
OngoingOperationTracker = new OngoingOperationTracker(); OngoingOperationTracker = new OngoingOperationTracker();
AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
RoomManager = CreateRoomManager();
dependencies = new DependencyContainer(new CachedModelDependencyContainer<Room>(null) { Model = { BindTarget = SelectedRoom } }); dependencies = new DependencyContainer(new CachedModelDependencyContainer<Room>(null) { Model = { BindTarget = SelectedRoom } });
CacheAs(RequestsHandler);
CacheAs(SelectedRoom); CacheAs(SelectedRoom);
CacheAs(RoomManager); CacheAs(RoomManager);
CacheAs(OngoingOperationTracker); CacheAs(OngoingOperationTracker);
@ -71,6 +74,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
drawableComponents.Add(drawable); drawableComponents.Add(drawable);
} }
protected virtual IRoomManager CreateRoomManager() => new TestRequestHandlingRoomManager(); protected virtual IRoomManager CreateRoomManager() => new TestRoomManager();
} }
} }

View File

@ -2,9 +2,7 @@
// 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;
using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
@ -15,20 +13,12 @@ namespace osu.Game.Tests.Visual.OnlinePlay
/// <summary> /// <summary>
/// A very simple <see cref="RoomManager"/> for use in online play test scenes. /// A very simple <see cref="RoomManager"/> for use in online play test scenes.
/// </summary> /// </summary>
public class TestRequestHandlingRoomManager : RoomManager public class TestRoomManager : RoomManager
{ {
public Action<Room, string> JoinRoomRequested; public Action<Room, string> JoinRoomRequested;
private int currentRoomId; private int currentRoomId;
private readonly TestRoomRequestsHandler handler = new TestRoomRequestsHandler();
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{
((DummyAPIAccess)api).HandleRequest = request => handler.HandleRequest(request, api.LocalUser.Value, game);
}
public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null) public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{ {
JoinRoomRequested?.Invoke(room, password); JoinRoomRequested?.Invoke(room, password);