1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge branch 'master' into beatmap-refactor/uncontested

This commit is contained in:
Dean Herbert 2021-10-28 16:37:06 +09:00
commit 6f5040722a
81 changed files with 903 additions and 567 deletions

View File

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

View File

@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch.Objects
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; }
}

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
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)
{

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo)
=> 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)
.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"/>.
/// </summary>
/// <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))
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>
/// 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;
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;
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;
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>(() =>
{
string keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";

View File

@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
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]
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 osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
@ -158,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
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)
// 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))
{
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;

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
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;
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.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using static osu.Game.Skinning.LegacySkinConfiguration;
using static osu.Game.Skinning.SkinConfiguration;
namespace osu.Game.Tests.Gameplay
{

View File

@ -4,10 +4,12 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
using SharpCompress.Archives.Zip;
@ -16,155 +18,179 @@ namespace osu.Game.Tests.Skins.IO
{
public class ImportSkinTest : ImportTest
{
[Test]
public async Task TestBasicImport()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk"));
Assert.That(imported.Name, Is.EqualTo("test skin"));
Assert.That(imported.Creator, Is.EqualTo("skinner"));
}
finally
{
host.Exit();
}
}
}
#region Testing filename metadata inclusion
[Test]
public async Task TestImportTwiceWithSameMetadata()
public Task TestSingleImportDifferentFilename() => runSkinTest(async osu =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(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();
}
}
}
// When the import filename doesn't match, it should be appended (and update the skin.ini).
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
});
[Test]
public async Task TestImportTwiceWithNoMetadata()
public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "test skin.osk"));
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(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();
}
}
}
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "skinner", osu);
});
[Test]
public async Task TestImportTwiceWithDifferentMetadata()
public Task TestSingleImportNoIniFile() => runSkinTest(async osu =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithNonIniFile(), "test skin.osk"));
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(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();
}
}
}
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
});
[Test]
public async Task TestImportUpperCasedOskArchive()
public Task TestEmptyImportImportsWithFilename() => runSkinTest(async osu =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createEmptyOsk(), "test skin.osk"));
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.OsK"));
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
});
Assert.That(imported.Name, Is.EqualTo("name 1"));
Assert.That(imported.Creator, Is.EqualTo("author 1"));
#endregion
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.oSK"));
Assert.That(imported2.Hash, Is.EqualTo(imported.Hash));
}
finally
{
host.Exit();
}
}
}
#region Cases where imports should match existing
[Test]
public async Task TestSameMetadataNameDifferentFolderName()
public Task TestImportTwiceWithSameMetadataAndFilename() => runSkinTest(async osu =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
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"));
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 1"));
Assert.That(imported.Name, Is.EqualTo("name 1 [my custom skin 1]"));
Assert.That(imported.Creator, Is.EqualTo("author 1"));
assertImportedOnce(import1, import2);
});
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 2"));
Assert.That(imported2.Name, Is.EqualTo("name 1 [my custom skin 2]"));
Assert.That(imported2.Creator, Is.EqualTo("author 1"));
[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.
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), "download.osk"));
Assert.That(imported2.Hash, Is.Not.EqualTo(imported.Hash));
}
finally
{
host.Exit();
}
}
assertImportedOnce(import1, import2);
});
[Test]
public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.OsK"));
assertCorrectMetadata(import1, "name 1", "author 1", osu);
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.oSK"));
assertImportedOnce(import1, import2);
});
[Test]
public Task TestSameMetadataNameSameFolderName() => 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 1"));
assertImportedOnce(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
});
#endregion
#region Cases where imports should be uniquely imported
[Test]
public Task TestImportTwiceWithSameMetadataButDifferentFilename() => 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"), "skin2.osk"));
assertImportedBoth(import1, import2);
});
[Test]
public Task TestImportTwiceWithNoMetadataDifferentDownloadFilename() => 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.
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();
using var zip = ZipArchive.Create();
@ -193,6 +219,22 @@ namespace osu.Game.Tests.Skins.IO
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)
{
var skinManager = osu.Dependencies.Get<SkinManager>();

View File

@ -106,7 +106,7 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin-latest.ini"))
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]

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 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]
@ -160,7 +160,7 @@ namespace osu.Game.Tests.Skins
// completely ignoring beatmap versions for simplicity.
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
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]
@ -169,14 +169,14 @@ namespace osu.Game.Tests.Skins
AddStep("Set user skin version 2.3", () => userSource.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 == LegacySkinConfiguration.LATEST_VERSION);
() => requester.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value == SkinConfiguration.LATEST_VERSION);
}
[Test]
public void TestIniWithNoVersionFallsBackTo1()
{
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

View File

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

View File

@ -17,6 +17,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
@ -33,6 +34,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osuTK.Input;
@ -48,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client;
private TestRequestHandlingMultiplayerRoomManager roomManager => multiplayerScreen.RoomManager;
private TestMultiplayerRoomManager roomManager => multiplayerScreen.RoomManager;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@ -616,17 +618,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
[Cached]
public readonly TestRoomRequestsHandler RequestsHandler = new TestRoomRequestsHandler();
public DependenciesScreen(TestMultiplayerClient client)
{
Client = client;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{
((DummyAPIAccess)api).HandleRequest = request => RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestRequestHandlingMultiplayerRoomManager RoomManager { get; private set; }
public new TestMultiplayerRoomManager RoomManager { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestRequestHandlingMultiplayerRoomManager();
protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
@ -23,6 +24,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@ -176,17 +178,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
[Cached]
public readonly TestRoomRequestsHandler RequestsHandler = new TestRoomRequestsHandler();
public DependenciesScreen(TestMultiplayerClient client)
{
Client = client;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{
((DummyAPIAccess)api).HandleRequest = request => RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestRequestHandlingMultiplayerRoomManager RoomManager { get; private set; }
public new TestMultiplayerRoomManager RoomManager { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestRequestHandlingMultiplayerRoomManager();
protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager();
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -24,6 +25,7 @@ using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK;
using osuTK.Input;
@ -459,12 +461,22 @@ namespace osu.Game.Tests.Visual.Navigation
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
[Cached]
public readonly TestRoomRequestsHandler RequestsHandler;
public TestMultiplayer()
{
Client = new TestMultiplayerClient((TestRequestHandlingMultiplayerRoomManager)RoomManager);
Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
RequestsHandler = new TestRoomRequestsHandler();
}
protected override RoomManager CreateRoomManager() => new TestRequestHandlingMultiplayerRoomManager();
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{
((DummyAPIAccess)api).HandleRequest = request => RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
}
}
}

View File

@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
{
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));

View File

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

View File

@ -11,24 +11,26 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
public class TestScenePlaylistsRoomCreation : OnlinePlayTestScene
{
private BeatmapManager manager;
private RulesetStore rulesets;
private TestPlaylistsRoomSubScreen match;
private ILive<BeatmapSetInfo> importedBeatmap;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
@ -40,7 +42,9 @@ namespace osu.Game.Tests.Visual.Playlists
public void SetupSteps()
{
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)));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@ -48,16 +52,15 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestLoadSimpleMatch()
{
AddStep("set room properties", () =>
setupAndCreateRoom(room =>
{
SelectedRoom.Value.RoomID.Value = 1;
SelectedRoom.Value.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = API.LocalUser.Value;
SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value);
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
SelectedRoom.Value.Playlist.Add(new PlaylistItem
room.Name.Value = "my awesome room";
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 }
});
});
@ -69,24 +72,17 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestPlaylistItemSelectedOnCreate()
{
AddStep("set room properties", () =>
setupAndCreateRoom(room =>
{
SelectedRoom.Value.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = API.LocalUser.Value;
SelectedRoom.Value.Playlist.Add(new PlaylistItem
room.Name.Value = "my awesome room";
room.Host.Value = API.LocalUser.Value;
room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
});
});
AddStep("move mouse to create button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PlaylistsRoomSettingsOverlay.CreateRoomButton>().Single());
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]);
}
@ -94,15 +90,12 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestBeatmapUpdatedOnReImport()
{
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();
var existing = manager.QueryBeatmapSets(s => s.OnlineBeatmapSetID == importedBeatmap.Value.OnlineBeatmapSetID).ToList();
foreach (var s in existing)
{
@ -115,35 +108,47 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("import altered beatmap", () =>
{
IBeatmap beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value;
});
AddStep("load room", () =>
setupAndCreateRoom(room =>
{
SelectedRoom.Value.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = API.LocalUser.Value;
SelectedRoom.Value.Playlist.Add(new PlaylistItem
room.Name.Value = "my awesome room";
room.Host.Value = API.LocalUser.Value;
room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = importedSet.Beatmaps[0] },
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);
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);
}
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
{
public new Bindable<PlaylistItem> SelectedItem => base.SelectedItem;

View File

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

View File

@ -4,12 +4,12 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Tournament.Components;
using osuTK;
namespace osu.Game.Tournament.Tests.Components
{
@ -23,12 +23,10 @@ namespace osu.Game.Tournament.Tests.Components
private FillFlowContainer<TournamentBeatmapPanel> fillFlow;
private BeatmapInfo beatmapInfo;
[BackgroundDependencyLoader]
private void load()
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 490154 });
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = 490154 });
req.Success += success;
api.Queue(req);
@ -38,18 +36,17 @@ namespace osu.Game.Tournament.Tests.Components
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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;
foreach (var mod in mods)
{
fillFlow.Add(new TournamentBeatmapPanel(beatmapInfo, mod.Acronym)
fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre

View File

@ -44,8 +44,8 @@ namespace osu.Game.Tournament.Tests.NonVisual
{
Beatmaps =
{
new RoundBeatmap { BeatmapInfo = TournamentTestScene.CreateSampleBeatmapInfo() },
new RoundBeatmap { BeatmapInfo = TournamentTestScene.CreateSampleBeatmapInfo() },
new RoundBeatmap { Beatmap = TournamentTestScene.CreateSampleBeatmap() },
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
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Beatmap = CreateSampleBeatmap(),
Mods = mods
});
}

View File

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

View File

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

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.Models;
using osuTK.Graphics;
@ -20,7 +21,7 @@ namespace osu.Game.Tournament.Components
{
public class TournamentBeatmapPanel : CompositeDrawable
{
public readonly IBeatmapInfo BeatmapInfo;
public readonly APIBeatmap Beatmap;
private readonly string mod;
@ -32,11 +33,11 @@ namespace osu.Game.Tournament.Components
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
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;
Width = 400;
@ -62,7 +63,7 @@ namespace osu.Game.Tournament.Components
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f),
OnlineInfo = BeatmapInfo.BeatmapSet as IBeatmapSetOnlineInfo,
OnlineInfo = Beatmap.BeatmapSet,
},
new FillFlowContainer
{
@ -75,7 +76,7 @@ namespace osu.Game.Tournament.Components
{
new TournamentSpriteText
{
Text = BeatmapInfo.GetDisplayTitleRomanisable(false),
Text = Beatmap.GetDisplayTitleRomanisable(false),
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
},
new FillFlowContainer
@ -92,7 +93,7 @@ namespace osu.Game.Tournament.Components
},
new TournamentSpriteText
{
Text = BeatmapInfo.Metadata?.Author,
Text = Beatmap.Metadata.Author,
Padding = new MarginPadding { Right = 20 },
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
},
@ -104,7 +105,7 @@ namespace osu.Game.Tournament.Components
},
new TournamentSpriteText
{
Text = BeatmapInfo.DifficultyName,
Text = Beatmap.DifficultyName,
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
},
}
@ -148,7 +149,7 @@ namespace osu.Game.Tournament.Components
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;
choice = found;

View File

@ -11,10 +11,10 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Tournament.Models;
@ -87,14 +87,14 @@ namespace osu.Game.Tournament.IPC
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)
Beatmap.Value = existing.BeatmapInfo;
Beatmap.Value = existing.Beatmap;
else
{
beatmapLookupRequest = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId });
beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmapInfo(Rulesets);
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
beatmapLookupRequest.Success += b => Beatmap.Value = b;
API.Queue(beatmapLookupRequest);
}
}

View File

@ -3,14 +3,14 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tournament.IPC
{
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<TourneyState> State { get; } = new Bindable<TourneyState>();
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.
// 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
{
@ -10,6 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID;
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.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tournament.Models
{
@ -10,7 +11,8 @@ namespace osu.Game.Tournament.Models
{
public int ID;
public BeatmapInfo BeatmapInfo;
[JsonProperty("BeatmapInfo")]
public APIBeatmap Beatmap;
public long Score;

View File

@ -4,8 +4,8 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC;
@ -37,10 +37,10 @@ namespace osu.Game.Tournament.Screens
SongBar.Mods = mods.NewValue;
}
private void beatmapChanged(ValueChangedEvent<BeatmapInfo> beatmap)
private void beatmapChanged(ValueChangedEvent<APIBeatmap> beatmap)
{
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.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Tournament.Components;
@ -226,25 +226,25 @@ namespace osu.Game.Tournament.Screens.Editors
Model.ID = id.NewValue ?? 0;
if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
Model.Beatmap = null;
if (Model.BeatmapInfo != null)
if (Model.Beatmap != null)
{
updatePanel();
return;
}
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = Model.ID });
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
req.Success += res =>
{
Model.BeatmapInfo = res.ToBeatmapInfo(rulesets);
Model.Beatmap = res;
updatePanel();
};
req.Failure += _ =>
{
Model.BeatmapInfo = null;
Model.Beatmap = null;
updatePanel();
};
@ -259,9 +259,9 @@ namespace osu.Game.Tournament.Screens.Editors
{
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,
Origin = Anchor.CentreLeft,

View File

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

View File

@ -8,8 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
@ -105,14 +105,14 @@ namespace osu.Game.Tournament.Screens.MapPool
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)
return;
// if bans have already been placed, beatmap changes result in a selection being made autoamtically
if (beatmap.NewValue.OnlineBeatmapID != null)
addForBeatmap(beatmap.NewValue.OnlineBeatmapID.Value);
if (beatmap.NewValue.OnlineID > 0)
addForBeatmap(beatmap.NewValue.OnlineID);
}
private void setMode(TeamColour colour, ChoiceType choiceType)
@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool
if (map != null)
{
if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineID > 0)
addForBeatmap(map.BeatmapInfo.OnlineID);
if (e.Button == MouseButton.Left && map.Beatmap.OnlineID > 0)
addForBeatmap(map.Beatmap.OnlineID);
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)
{
@ -179,7 +179,7 @@ namespace osu.Game.Tournament.Screens.MapPool
if (CurrentMatch.Value == null)
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
return;
@ -245,7 +245,7 @@ namespace osu.Game.Tournament.Screens.MapPool
flowCount = 1;
}
currentFlow.Add(new TournamentBeatmapPanel(b.BeatmapInfo, b.Mods)
currentFlow.Add(new TournamentBeatmapPanel(b.Beatmap, b.Mods)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,

View File

@ -141,9 +141,9 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Spacing = new Vector2(5),
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 = 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

View File

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

View File

@ -32,7 +32,7 @@ namespace osu.Game.Database
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
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;
@ -315,25 +315,29 @@ namespace osu.Game.Database
/// <remarks>
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
/// </remarks>
protected virtual string ComputeHash(TModel item, ArchiveReader reader = null)
protected virtual string ComputeHash(TModel item)
{
if (reader != null)
// fast hashing for cases where the item's files may not be populated.
return computeHashFast(reader);
var hashableFiles = item.Files
.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
.OrderBy(f => f.Filename)
.ToArray();
// for now, concatenate all hashable files in the set to create a unique hash.
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))
if (hashableFiles.Length > 0)
{
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable);
// for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
foreach (TFileModel file in hashableFiles)
{
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable);
}
if (hashable.Length > 0)
return hashable.ComputeSHA2Hash();
}
if (hashable.Length > 0)
return hashable.ComputeSHA2Hash();
return item.Hash;
return generateFallbackHash();
}
/// <summary>
@ -393,7 +397,7 @@ namespace osu.Game.Database
LogForModel(item, @"Beginning import...");
item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
item.Hash = ComputeHash(item, archive);
item.Hash = ComputeHash(item);
await Populate(item, archive, cancellationToken).ConfigureAwait(false);
@ -516,9 +520,12 @@ namespace osu.Game.Database
{
Files.Dereference(file.FileInfo);
// 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.
usage.Context.Set<TFileModel>().Remove(file);
if (file.ID > 0)
{
// 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.
usage.Context.Set<TFileModel>().Remove(file);
}
}
model.Files.Remove(file);
@ -540,9 +547,10 @@ namespace osu.Game.Database
Filename = filename,
FileInfo = Files.Add(contents)
});
Update(model);
}
if (model.ID > 0)
Update(model);
}
/// <summary>
@ -684,7 +692,7 @@ namespace osu.Game.Database
if (hashable.Length > 0)
return hashable.ComputeSHA2Hash();
return reader.Name.ComputeSHA2Hash();
return generateFallbackHash();
}
/// <summary>
@ -897,6 +905,14 @@ namespace osu.Game.Database
#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)
{
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")]
private float overallDifficulty { get; set; }
public double Length => TimeSpan.FromSeconds(lengthInSeconds).TotalMilliseconds;
[JsonIgnore]
public double Length { get; set; }
[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")]
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();
double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException();
double IBeatmapSetInfo.MaxLength => throw new NotImplementedException();
double IBeatmapSetInfo.MaxBPM => throw new NotImplementedException();
double IBeatmapSetInfo.MaxBPM => BPM;
#endregion
}

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,
List<IList<HitSampleInfo>> nodeSamples)
IList<IList<HitSampleInfo>> nodeSamples)
{
newCombo |= forceNewCombo;
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>
/// <returns>The hit object.</returns>
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>
/// Creates a legacy Spinner-type hit object.
@ -481,7 +481,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary>
/// <remarks>
/// 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>
public readonly bool IsLayered;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Distance => Path.Distance;
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
public IList<IList<HitSampleInfo>> NodeSamples { get; set; }
public int RepeatCount { get; set; }
[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,
List<IList<HitSampleInfo>> nodeSamples)
IList<IList<HitSampleInfo>> nodeSamples)
{
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,
List<IList<HitSampleInfo>> nodeSamples)
IList<IList<HitSampleInfo>> nodeSamples)
{
newCombo |= forceNewCombo;
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,
List<IList<HitSampleInfo>> nodeSamples)
IList<IList<HitSampleInfo>> nodeSamples)
{
return new ConvertSlider
{

View File

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

View File

@ -3,13 +3,15 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
{
public abstract class ReadyButton : TriangleButton
public abstract class ReadyButton : TriangleButton, IHasTooltip
{
public new readonly BindableBool Enabled = new BindableBool();
@ -24,6 +26,18 @@ namespace osu.Game.Screens.OnlinePlay.Components
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;
[Resolved]
private OsuColour colours { get; set; }
public RoomLocalUserInfo()
{
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);
attemptDisplay.Text += $" ({remaining} remaining)";
if (remaining == 0)
attemptDisplay.Colour = colours.RedLight;
}
}
else

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
@ -16,6 +18,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(typeof(Room), nameof(Room.EndDate))]
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]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; }
@ -32,11 +40,49 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
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()
{
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 SelectionPollingComponent selectionPollingComponent;
private FillFlowContainer progressSection;
public PlaylistsRoomSubScreen(Room room)
: base(room, false) // Editing is temporarily not allowed.
{
@ -67,6 +69,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Schedule(() => SelectedItem.Value = Room.Playlist.FirstOrDefault());
}
}, true);
Room.MaxAttempts.BindValueChanged(attempts => progressSection.Alpha = Room.MaxAttempts.Value != null ? 1 : 0, true);
}
protected override Drawable CreateMainContent() => new GridContainer
@ -153,6 +157,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
},
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")
},
@ -162,6 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),

View File

@ -19,7 +19,14 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
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.CustomComboColours = new List<Color4>

View File

@ -35,7 +35,6 @@ namespace osu.Game.Skinning
: base(skin, resources)
{
this.resources = resources;
Configuration = new DefaultSkinConfiguration();
}
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)
{
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.
// 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>
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>();
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
@ -69,29 +63,20 @@ namespace osu.Game.Skinning
/// <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>
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)
{
var samples = resources?.AudioManager?.GetSampleStore(storage);
@ -110,6 +95,21 @@ namespace osu.Game.Skinning
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)
{
switch (lookup)
@ -146,7 +146,7 @@ namespace osu.Game.Skinning
break;
case LegacySkinConfiguration.LegacySetting legacy:
case SkinConfiguration.LegacySetting legacy:
return legacySettingLookup<TValue>(legacy);
default:
@ -189,7 +189,7 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.ExplosionScale:
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));
if (existing.ExplosionWidth[maniaLookup.TargetColumn.Value] != 0)
@ -236,7 +236,7 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale:
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));
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;
[CanBeNull]
private IBindable<TValue> legacySettingLookup<TValue>(LegacySkinConfiguration.LegacySetting legacySetting)
private IBindable<TValue> legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
{
switch (legacySetting)
{
case LegacySkinConfiguration.LegacySetting.Version:
return SkinUtils.As<TValue>(new Bindable<decimal>(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION));
case SkinConfiguration.LegacySetting.Version:
return SkinUtils.As<TValue>(new Bindable<decimal>(Configuration.LegacyVersion ?? SkinConfiguration.LATEST_VERSION));
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
{
public class LegacySkinDecoder : LegacyDecoder<LegacySkinConfiguration>
public class LegacySkinDecoder : LegacyDecoder<SkinConfiguration>
{
public LegacySkinDecoder()
: 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)
{
@ -34,7 +34,7 @@ namespace osu.Game.Skinning
case @"Version":
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))
skin.LegacyVersion = version;
@ -57,7 +57,7 @@ namespace osu.Game.Skinning
base.ParseLine(skin, section, line);
}
protected override LegacySkinConfiguration CreateTemplateObject()
protected override SkinConfiguration CreateTemplateObject()
{
var config = base.CreateTemplateObject();
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.Sprites;
using osu.Framework.Graphics.Textures;
using static osu.Game.Skinning.LegacySkinConfiguration;
using static osu.Game.Skinning.SkinConfiguration;
namespace osu.Game.Skinning
{

View File

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

View File

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
@ -21,8 +23,9 @@ namespace osu.Game.Skinning
public abstract class Skin : IDisposable, ISkin
{
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;
@ -36,9 +39,18 @@ namespace osu.Game.Skinning
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;
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.
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>
/// Remove all stored customisations for the provided target.
/// </summary>

View File

@ -14,11 +14,31 @@ namespace osu.Game.Skinning
{
public readonly SkinInfo SkinInfo = new SkinInfo();
public const decimal LATEST_VERSION = 2.7m;
/// <summary>
/// Whether to allow <see cref="DefaultComboColours"/> as a fallback list for when no combo colours are provided.
/// </summary>
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>
{
new Color4(255, 192, 0, 255),

View File

@ -18,12 +18,12 @@ namespace osu.Game.Skinning
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 Creator { get; set; }
public string InstantiationInfo { get; set; }
public virtual Skin CreateInstance(IStorageResourceProvider resources)

View File

@ -14,11 +14,11 @@ using Newtonsoft.Json;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
@ -51,7 +51,7 @@ namespace osu.Game.Skinning
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";
@ -85,6 +85,27 @@ namespace osu.Game.Skinning
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";
@ -128,28 +149,118 @@ namespace osu.Game.Skinning
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";
protected override bool HasCustomHashFunction => true;
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
protected override string ComputeHash(SkinInfo 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.
// 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);
// 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.
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)
@ -158,32 +269,12 @@ namespace osu.Game.Skinning
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;
}
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>
/// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/>
/// </summary>

View File

@ -10,7 +10,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Rulesets;
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;
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// <summary>
/// The cached <see cref="IRoomManager"/>.
/// </summary>
new TestRequestHandlingMultiplayerRoomManager RoomManager { get; }
new TestMultiplayerRoomManager RoomManager { get; }
/// <summary>
/// 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 TestMultiplayerClient Client => OnlinePlayDependencies.Client;
public new TestRequestHandlingMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache;
public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient;
@ -35,12 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new void Setup() => Schedule(() =>
{
if (joinRoom)
{
var room = CreateRoom();
RoomManager.CreateRoom(room);
SelectedRoom.Value = room;
}
SelectedRoom.Value = CreateRoom();
});
protected virtual Room CreateRoom()
@ -64,7 +59,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
base.SetUpSteps();
if (joinRoom)
{
AddStep("join room", () => RoomManager.CreateRoom(SelectedRoom.Value));
AddUntilStep("wait for room join", () => Client.Room != null);
}
}
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();

View File

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

View File

@ -39,9 +39,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Resolved]
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;
}

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
@ -12,25 +11,20 @@ using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
/// <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"/>.
/// </summary>
public class TestRequestHandlingMultiplayerRoomManager : MultiplayerRoomManager
public class TestMultiplayerRoomManager : MultiplayerRoomManager
{
public IReadOnlyList<Room> ServerSideRooms => handler.ServerSideRooms;
[Resolved]
private TestRoomRequestsHandler requestsHandler { get; set; }
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 IReadOnlyList<Room> ServerSideRooms => requestsHandler.ServerSideRooms;
/// <summary>
/// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired.
/// </summary>
/// <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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
@ -27,11 +28,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay
/// </summary>
protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies;
private DelegatedDependencyContainer dependencies;
protected override Container<Drawable> Content => content;
[Resolved]
private OsuGameBase game { get; set; }
private readonly Container content;
private readonly Container drawableDependenciesContainer;
private DelegatedDependencyContainer dependencies;
protected OnlinePlayTestScene()
{
@ -57,6 +61,12 @@ namespace osu.Game.Tests.Visual.OnlinePlay
drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents);
});
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("setup API", () => ((DummyAPIAccess)API).HandleRequest = request => OnlinePlayDependencies.RequestsHandler.HandleRequest(request, API.LocalUser.Value, game));
}
/// <summary>
/// Creates the room dependencies. Called every <see cref="Setup"/>.
/// </summary>

View File

@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public IRoomManager RoomManager { get; }
public OngoingOperationTracker OngoingOperationTracker { get; }
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; }
public TestRoomRequestsHandler RequestsHandler { get; }
/// <summary>
/// All cached dependencies which are also <see cref="Drawable"/> components.
@ -36,9 +37,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay
RoomManager = CreateRoomManager();
OngoingOperationTracker = new OngoingOperationTracker();
AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
RequestsHandler = new TestRoomRequestsHandler();
dependencies = new DependencyContainer(new CachedModelDependencyContainer<Room>(null) { Model = { BindTarget = SelectedRoom } });
CacheAs(RequestsHandler);
CacheAs(SelectedRoom);
CacheAs(RoomManager);
CacheAs(OngoingOperationTracker);
@ -71,6 +74,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
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.
using System;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Components;
@ -15,20 +13,12 @@ namespace osu.Game.Tests.Visual.OnlinePlay
/// <summary>
/// A very simple <see cref="RoomManager"/> for use in online play test scenes.
/// </summary>
public class TestRequestHandlingRoomManager : RoomManager
public class TestRoomManager : RoomManager
{
public Action<Room, string> JoinRoomRequested;
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)
{
JoinRoomRequested?.Invoke(room, password);