diff --git a/osu.Android.props b/osu.Android.props
index 1b5461959a..6a3b113fa2 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
index e70def7f8b..bb3a724b91 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public TestLegacySkin(SkinInfo skin, IResourceStore storage)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
- : base(skin, storage, null, "skin.ini")
+ : base(skin, null, storage)
{
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index d19b3c71f1..0d436c1ef7 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore storage, string fileName)
- : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
+ : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
{
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index 2ba8c51a10..1474f2d277 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@@ -64,6 +65,62 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [TestCase(3, true)]
+ [TestCase(6, false)]
+ [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
+ public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
+ {
+ const double first_frame_time = 48;
+ const double second_frame_time = 65;
+
+ var decoder = new TestLegacyScoreDecoder(beatmapVersion);
+
+ using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
+ {
+ var score = decoder.Parse(resourceStream);
+
+ Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
+ Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
+ }
+ }
+
+ [TestCase(3)]
+ [TestCase(6)]
+ [TestCase(LegacyBeatmapDecoder.LATEST_VERSION)]
+ public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion)
+ {
+ const double first_frame_time = 2000;
+ const double second_frame_time = 3000;
+
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset)
+ {
+ BeatmapInfo =
+ {
+ BeatmapVersion = beatmapVersion
+ }
+ };
+
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
+ new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap);
+
+ Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time));
+ Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
+ }
+
[Test]
public void TestCultureInvariance()
{
@@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se");
- var encodeStream = new MemoryStream();
-
- var encoder = new LegacyScoreEncoder(score, beatmap);
- encoder.Encode(encodeStream);
-
- var decodeStream = new MemoryStream(encodeStream.GetBuffer());
-
- var decoder = new TestLegacyScoreDecoder();
- var decodedAfterEncode = decoder.Parse(decodeStream);
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
@@ -110,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
+ private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
+ {
+ var encodeStream = new MemoryStream();
+
+ var encoder = new LegacyScoreEncoder(score, beatmap);
+ encoder.Encode(encodeStream);
+
+ var decodeStream = new MemoryStream(encodeStream.GetBuffer());
+
+ var decoder = new TestLegacyScoreDecoder(beatmapVersion);
+ var decodedAfterEncode = decoder.Parse(decodeStream);
+ return decodedAfterEncode;
+ }
+
[TearDown]
public void TearDown()
{
@@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
+ private readonly int beatmapVersion;
+
private static readonly Dictionary rulesets = new Ruleset[]
{
new OsuRuleset(),
@@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
+ public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION)
+ {
+ this.beatmapVersion = beatmapVersion;
+ }
+
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
@@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
- Difficulty = new BeatmapDifficulty()
+ Difficulty = new BeatmapDifficulty(),
+ BeatmapVersion = beatmapVersion,
}
});
}
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index 9abd78039a..f9c13a8169 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database
Live? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
+ {
imported = await importer.Import(reader);
+ EnsureLoaded(realm.Realm);
+ }
Assert.AreEqual(1, realm.Realm.All().Count());
@@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database
new ImportTask(zipStream, string.Empty)
);
+ realm.Run(r => r.Refresh());
+
checkBeatmapSetCount(realm.Realm, 0);
checkBeatmapCount(realm.Realm, 0);
@@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database
{
}
+ EnsureLoaded(realm.Realm);
+
checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realm.Realm, 12);
@@ -726,6 +733,8 @@ namespace osu.Game.Tests.Database
var imported = importer.Import(toImport);
+ realm.Run(r => r.Refresh());
+
Assert.NotNull(imported);
Debug.Assert(imported != null);
@@ -891,6 +900,8 @@ namespace osu.Game.Tests.Database
string? temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp);
+ EnsureLoaded(realm.Realm);
+
// Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = realm.Realm.All().First();
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 6457a23a1b..76ec35d87d 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, IStorageResourceProvider resources)
- : base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
+ : base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName))
{
}
}
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
index 69e66942ab..7516e7500b 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -1,12 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using NUnit.Framework;
-using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using osu.Game.Database;
+using osu.Game.IO;
using osu.Game.Skinning;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
namespace osu.Game.Tests.NonVisual.Skinning
{
@@ -71,7 +80,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
var texture = legacySkin.GetTexture(requestedComponent);
Assert.IsNotNull(texture);
- Assert.AreEqual(textureStore.Textures[expectedTexture], texture);
+ Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width);
Assert.AreEqual(expectedScale, texture.ScaleAdjust);
}
@@ -88,23 +97,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
private class TestLegacySkin : LegacySkin
{
- public TestLegacySkin(TextureStore textureStore)
- : base(new SkinInfo(), null, null, string.Empty)
+ public TestLegacySkin(IResourceStore textureStore)
+ : base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
{
- Textures = textureStore;
+ }
+
+ private class TestResourceProvider : IStorageResourceProvider
+ {
+ private readonly IResourceStore textureStore;
+
+ public TestResourceProvider(IResourceStore textureStore)
+ {
+ this.textureStore = textureStore;
+ }
+
+ public AudioManager AudioManager => null;
+ public IResourceStore Files => null;
+ public IResourceStore Resources => null;
+ public RealmAccess RealmAccess => null;
+ public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => textureStore;
}
}
- private class TestTextureStore : TextureStore
+ private class TestTextureStore : IResourceStore
{
- public readonly Dictionary Textures;
+ public readonly Dictionary Textures;
public TestTextureStore(params string[] fileNames)
{
- Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1));
+ // use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures.
+ int width = 1;
+ Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image(width, width++)));
}
- public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => Textures.GetValueOrDefault(name);
+ public TextureUpload Get(string name) => Textures.GetValueOrDefault(name);
+
+ public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => Task.FromResult(Get(name));
+
+ public Stream GetStream(string name) => throw new NotImplementedException();
+
+ public IEnumerable GetAvailableResources() => throw new NotImplementedException();
+
+ public void Dispose()
+ {
+ }
}
}
}
diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs
index 71544e94f3..0c1981b35d 100644
--- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs
+++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
- : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
+ : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{
}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 870d6d8f57..d3cacaa88c 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
- : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
+ : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index eb1695b3df..53364b6d89 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
- CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
+ CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
index e58b9893ce..52854db235 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
@@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType().Single().IsPresent);
- AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
+ AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
ClickButtonWhenEnabled();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
@@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{
- AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
+ AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
ClickButtonWhenEnabled();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 292319171d..8da077cd44 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -163,6 +163,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1);
}
+ [Test]
+ public void TestHostGetsPinnedToTop()
+ {
+ AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
+ {
+ Id = 3,
+ Username = "Second",
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }));
+
+ AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
+ AddAssert("second user above first", () =>
+ {
+ var first = this.ChildrenOfType().ElementAt(0);
+ var second = this.ChildrenOfType().ElementAt(1);
+ return second.Y < first.Y;
+ });
+ }
+
[Test]
public void TestKickButtonOnlyPresentWhenHost()
{
@@ -202,9 +221,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestManyUsers()
{
+ const int users_count = 20;
+
AddStep("add many users", () =>
{
- for (int i = 0; i < 20; i++)
+ for (int i = 0; i < users_count; i++)
{
MultiplayerClient.AddUser(new APIUser
{
@@ -243,6 +264,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
});
+
+ AddRepeatStep("switch hosts", () => MultiplayerClient.TransferHost(RNG.Next(0, users_count)), 10);
+ AddStep("give host back", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));
}
[Test]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs
new file mode 100644
index 0000000000..ddc1c8c128
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneModSettingsArea : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ [Test]
+ public void TestModToggleArea()
+ {
+ ModSettingsArea modSettingsArea = null;
+
+ AddStep("create content", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Child = modSettingsArea = new ModSettingsArea()
+ });
+ AddStep("set DT", () => modSettingsArea.SelectedMods.Value = new[] { new OsuModDoubleTime() });
+ AddStep("set DA", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
+ AddStep("set FL+WU+DA+AD", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() });
+ AddStep("set empty", () => modSettingsArea.SelectedMods.Value = Array.Empty());
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs
index 93cfa9634e..f0aa857769 100644
--- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs
+++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
Action = () => game.GracefullyExit();
- folderButton.Action = storage.PresentExternally;
+ folderButton.Action = () => storage.PresentExternally();
ButtonText = "Close osu!";
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index e2a043490f..79d8bd3bb3 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapDecoder : LegacyDecoder
{
+ ///
+ /// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level.
+ ///
+ public const int EARLY_VERSION_TIMING_OFFSET = 24;
+
internal static RulesetStore RulesetStore;
private Beatmap beatmap;
@@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats
RulesetStore = new AssemblyRulesetStore();
}
- // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
- offset = FormatVersion < 5 ? 24 : 0;
+ offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
}
protected override Beatmap CreateTemplateObject()
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index d3f356bb24..7d28208157 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -225,7 +225,7 @@ namespace osu.Game.Beatmaps
{
try
{
- return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
+ return new LegacyBeatmapSkin(BeatmapInfo, resources);
}
catch (Exception e)
{
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index f0d4011ab8..8574002436 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -17,6 +19,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
@@ -28,8 +31,6 @@ using osu.Game.Stores;
using Realms;
using Realms.Exceptions;
-#nullable enable
-
namespace osu.Game.Database
{
///
@@ -46,6 +47,8 @@ namespace osu.Game.Database
private readonly IDatabaseContextFactory? efContextFactory;
+ private readonly SynchronizationContext? updateThreadSyncContext;
+
///
/// Version history:
/// 6 ~2021-10-18 First tracked version.
@@ -143,12 +146,15 @@ namespace osu.Game.Database
///
/// The game storage which will be used to create the realm backing file.
/// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.
+ /// The game update thread, used to post realm operations into a thread-safe context.
/// An EF factory used only for migration purposes.
- public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null)
+ public RealmAccess(Storage storage, string filename, GameThread? updateThread = null, IDatabaseContextFactory? efContextFactory = null)
{
this.storage = storage;
this.efContextFactory = efContextFactory;
+ updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current;
+
Filename = filename;
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
@@ -379,9 +385,6 @@ namespace osu.Game.Database
public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback)
where T : RealmObjectBase
{
- if (!ThreadSafety.IsUpdateThread)
- throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
-
lock (realmLock)
{
Func action = realm => query(realm).QueryAsyncWithNotifications(callback);
@@ -459,23 +462,24 @@ namespace osu.Game.Database
/// An which should be disposed to unsubscribe any inner subscription.
public IDisposable RegisterCustomSubscription(Func action)
{
- if (!ThreadSafety.IsUpdateThread)
- throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
-
- var syncContext = SynchronizationContext.Current;
+ if (updateThreadSyncContext == null)
+ throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration.");
total_subscriptions.Value++;
- registerSubscription(action);
+ if (ThreadSafety.IsUpdateThread)
+ updateThreadSyncContext.Send(_ => registerSubscription(action), null);
+ else
+ updateThreadSyncContext.Post(_ => registerSubscription(action), null);
// This token is returned to the consumer.
// When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
return new InvokeOnDisposal(() =>
{
if (ThreadSafety.IsUpdateThread)
- syncContext.Send(_ => unsubscribe(), null);
+ updateThreadSyncContext.Send(_ => unsubscribe(), null);
else
- syncContext.Post(_ => unsubscribe(), null);
+ updateThreadSyncContext.Post(_ => unsubscribe(), null);
void unsubscribe()
{
diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs
index 6f0f898de3..a6605de1d2 100644
--- a/osu.Game/IO/WrappedStorage.cs
+++ b/osu.Game/IO/WrappedStorage.cs
@@ -70,9 +70,9 @@ namespace osu.Game.IO
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
- public override void OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
+ public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
- public override void PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
+ public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
public override Storage GetStorageForDirectory(string path)
{
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 2f328a9339..d6099e5f72 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -171,6 +171,8 @@ namespace osu.Game.Online.Multiplayer
Room = joinedRoom;
APIRoom = room;
+ Debug.Assert(joinedRoom.Playlist.Count > 0);
+
APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
@@ -686,6 +688,8 @@ namespace osu.Game.Online.Multiplayer
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
+ Debug.Assert(Room.Playlist.Count > 0);
+
ItemRemoved?.Invoke(playlistItemId);
RoomUpdated?.Invoke();
});
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 5468db348e..7b9aca4086 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -200,7 +200,7 @@ namespace osu.Game
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
- dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory));
+ dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory));
dependencies.CacheAs(RulesetStore = new RealmRulesetStore(realm, Storage));
dependencies.CacheAs(RulesetStore);
diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs
new file mode 100644
index 0000000000..e0a30f60c2
--- /dev/null
+++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs
@@ -0,0 +1,176 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Overlays.Mods
+{
+ public class ModSettingsArea : CompositeDrawable
+ {
+ public Bindable> SelectedMods { get; } = new Bindable>();
+
+ private readonly Box background;
+ private readonly FillFlowContainer modSettingsFlow;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ public ModSettingsArea()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 250;
+
+ Anchor = Anchor.BottomRight;
+ Origin = Anchor.BottomRight;
+
+ InternalChild = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 2,
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new OsuScrollContainer(Direction.Horizontal)
+ {
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarOverlapsContent = false,
+ Child = modSettingsFlow = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Vertical = 7, Horizontal = 70 },
+ Spacing = new Vector2(7),
+ Direction = FillDirection.Horizontal
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ background.Colour = colourProvider.Dark3;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ SelectedMods.BindValueChanged(_ => updateMods());
+ }
+
+ private void updateMods()
+ {
+ modSettingsFlow.Clear();
+
+ foreach (var mod in SelectedMods.Value.OrderBy(mod => mod.Type).ThenBy(mod => mod.Acronym))
+ {
+ var settings = mod.CreateSettingsControls().ToList();
+
+ if (settings.Count > 0)
+ {
+ if (modSettingsFlow.Any())
+ {
+ modSettingsFlow.Add(new Box
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = 2,
+ Colour = colourProvider.Dark4,
+ });
+ }
+
+ modSettingsFlow.Add(new ModSettingsColumn(mod, settings));
+ }
+ }
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e) => true;
+ protected override bool OnHover(HoverEvent e) => true;
+
+ private class ModSettingsColumn : CompositeDrawable
+ {
+ public ModSettingsColumn(Mod mod, IEnumerable settingsControls)
+ {
+ Width = 250;
+ RelativeSizeAxes = Axes.Y;
+ Padding = new MarginPadding { Bottom = 7 };
+
+ InternalChild = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.Absolute, 10),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(7),
+ Children = new Drawable[]
+ {
+ new ModSwitchTiny(mod)
+ {
+ Active = { Value = true },
+ Scale = new Vector2(0.6f),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft
+ },
+ new OsuSpriteText
+ {
+ Text = mod.Name,
+ Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Margin = new MarginPadding { Bottom = 2 }
+ }
+ }
+ }
+ },
+ new[] { Empty() },
+ new Drawable[]
+ {
+ new OsuScrollContainer(Direction.Vertical)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Right = 7 },
+ ChildrenEnumerable = settingsControls,
+ Spacing = new Vector2(0, 7)
+ }
+ }
+ }
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
index 158d8811b5..0b4eca6379 100644
--- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(new SettingsButton
{
Text = GeneralSettingsStrings.OpenOsuFolder,
- Action = storage.PresentExternally,
+ Action = () => storage.PresentExternally(),
});
Add(new SettingsButton
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
index fefee370b9..754ace82c5 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Scoring.Legacy
private IBeatmap currentBeatmap;
private Ruleset currentRuleset;
+ private float beatmapOffset;
+
public Score Parse(Stream stream)
{
var score = new Score
@@ -72,6 +74,9 @@ namespace osu.Game.Scoring.Legacy
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
+ // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
+ beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
+
/* score.HpGraphString = */
sr.ReadString();
@@ -229,7 +234,7 @@ namespace osu.Game.Scoring.Legacy
private void readLegacyReplay(Replay replay, StreamReader reader)
{
- float lastTime = 0;
+ float lastTime = beatmapOffset;
ReplayFrame currentFrame = null;
string[] frames = reader.ReadToEnd().Split(',');
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
index f0ead05280..ae9afbf32e 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.IO;
using System.Linq;
using System.Text;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions;
using osu.Game.IO.Legacy;
using osu.Game.Replays.Legacy;
@@ -14,8 +17,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA;
-#nullable enable
-
namespace osu.Game.Scoring.Legacy
{
public class LegacyScoreEncoder
@@ -111,6 +112,9 @@ namespace osu.Game.Scoring.Legacy
{
StringBuilder replayData = new StringBuilder();
+ // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
+ double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
+
if (score.Replay != null)
{
int lastTime = 0;
@@ -120,7 +124,7 @@ namespace osu.Game.Scoring.Legacy
var legacyFrame = getLegacyFrame(f);
// Rounding because stable could only parse integral values
- int time = (int)Math.Round(legacyFrame.Time);
+ int time = (int)Math.Round(legacyFrame.Time + offset);
replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},"));
lastTime = time;
}
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index b03425fef4..a1f0d22efc 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu
private readonly ButtonArea buttonArea;
- private readonly Button backButton;
+ private readonly MainMenuButton backButton;
- private readonly List
- public class Button : BeatSyncedContainer, IStateful
+ public class MainMenuButton : BeatSyncedContainer, IStateful
{
public event Action StateChanged;
@@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
- public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
+ public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
{
this.sampleName = sampleName;
this.clickAction = clickAction;
@@ -209,7 +209,7 @@ namespace osu.Game.Screens.Menu
protected override bool OnKeyDown(KeyDownEvent e)
{
- if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed)
+ if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false;
if (TriggerKey == e.Key && TriggerKey != Key.Unknown)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs
index d1e28fb5e0..7f819d9e75 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs
@@ -42,8 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public MultiplayerCountdownButton()
{
- Icon = FontAwesome.Solid.CaretDown;
- IconScale = new Vector2(0.6f);
+ Icon = FontAwesome.Regular.Clock;
Add(background = new Box
{
@@ -52,6 +51,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
});
base.Action = this.ShowPopover;
+
+ TooltipText = "Countdown settings";
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index aef04c106d..a103d71120 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -165,6 +165,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
LengthLimit = 100,
},
},
+ // new Section("Room visibility")
+ // {
+ // Alpha = disabled_alpha,
+ // Child = AvailabilityPicker = new RoomAvailabilityPicker
+ // {
+ // Enabled = { Value = false }
+ // },
+ // },
new Section("Game type")
{
Child = new FillFlowContainer
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
index 71107095c2..0ec2c6560a 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
countdown = room?.Countdown;
if (room?.Countdown != null)
- countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true);
+ countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
else
{
countdownUpdateDelegate?.Cancel();
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index 96a665f33d..7ba0a63856 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@@ -187,9 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
var currentItem = Playlist.GetCurrentItem();
- Debug.Assert(currentItem != null);
-
- var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance();
+ var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
@@ -201,15 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else
userModsDisplay.FadeOut(fade_time);
- if (Client.IsHost && !User.Equals(Client.LocalUser))
- kickButton.FadeIn(fade_time);
- else
- kickButton.FadeOut(fade_time);
-
- if (Room.Host?.Equals(User) == true)
- crown.FadeIn(fade_time);
- else
- crown.FadeOut(fade_time);
+ kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
+ crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
index afb2111023..14b930f115 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
private FillFlowContainer panels;
+ [CanBeNull]
+ private ParticipantPanel currentHostPanel;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -55,6 +59,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user));
+
+ if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
+ {
+ // Reset position of previous host back to normal, if one existing.
+ if (currentHostPanel != null && panels.Contains(currentHostPanel))
+ panels.SetLayoutPosition(currentHostPanel, 0);
+
+ currentHostPanel = null;
+
+ // Change position of new host to display above all participants.
+ if (Room.Host != null)
+ {
+ currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
+
+ if (currentHostPanel != null)
+ panels.SetLayoutPosition(currentHostPanel, -1);
+ }
+ }
}
}
}
diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs
index c7033d37dc..f7b415e886 100644
--- a/osu.Game/Skinning/DefaultLegacySkin.cs
+++ b/osu.Game/Skinning/DefaultLegacySkin.cs
@@ -30,11 +30,9 @@ namespace osu.Game.Skinning
public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: base(
skin,
- new NamespacedResourceStore(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 LegacyDatabasedSkinResourceStore(skin, resources.Files).GetStream("skin.ini")
+ // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources.
+ skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null
)
{
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs
index 73f7cf6d39..414a316dec 100644
--- a/osu.Game/Skinning/ISkin.cs
+++ b/osu.Game/Skinning/ISkin.cs
@@ -1,7 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using JetBrains.Annotations;
+#nullable enable
+
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -21,16 +22,14 @@ namespace osu.Game.Skinning
///
/// The requested component.
/// A drawable representation for the requested component, or null if unavailable.
- [CanBeNull]
- Drawable GetDrawableComponent(ISkinComponent component);
+ Drawable? GetDrawableComponent(ISkinComponent component);
///
/// Retrieve a .
///
/// The requested texture.
/// A matching texture, or null if unavailable.
- [CanBeNull]
- Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
+ Texture? GetTexture(string componentName) => GetTexture(componentName, default, default);
///
/// Retrieve a .
@@ -39,23 +38,22 @@ namespace osu.Game.Skinning
/// The texture wrap mode in horizontal direction.
/// The texture wrap mode in vertical direction.
/// A matching texture, or null if unavailable.
- [CanBeNull]
- Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
+ Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
///
/// Retrieve a .
///
/// The requested sample.
/// A matching sample channel, or null if unavailable.
- [CanBeNull]
- ISample GetSample(ISampleInfo sampleInfo);
+ ISample? GetSample(ISampleInfo sampleInfo);
///
/// Retrieve a configuration value.
///
/// The requested configuration value.
/// A matching value boxed in an , or null if unavailable.
- [CanBeNull]
- IBindable GetConfig(TLookup lookup);
+ IBindable? GetConfig(TLookup lookup)
+ where TLookup : notnull
+ where TValue : notnull;
}
}
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index f80a980351..16a05f4197 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
@@ -20,14 +23,28 @@ namespace osu.Game.Skinning
protected override bool AllowManiaSkin => false;
protected override bool UseCustomSampleBanks => true;
- public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources)
- : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path)
+ ///
+ /// Construct a new legacy beatmap skin instance.
+ ///
+ /// The model for this beatmap.
+ /// Access to raw game resources.
+ public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
+ : base(createSkinInfo(beatmapInfo), resources, createRealmBackedStore(beatmapInfo, resources), beatmapInfo.Path.AsNonNull())
{
// Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer)
Configuration.AllowDefaultComboColoursFallback = false;
}
- public override Drawable GetDrawableComponent(ISkinComponent component)
+ private static IResourceStore createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
+ {
+ if (resources == null)
+ // should only ever be used in tests.
+ return new ResourceStore();
+
+ return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" });
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
{
if (component is SkinnableTargetComponent targetComponent)
{
@@ -46,7 +63,7 @@ namespace osu.Game.Skinning
return base.GetDrawableComponent(component);
}
- public override IBindable GetConfig(TLookup lookup)
+ public override IBindable? GetConfig(TLookup lookup)
{
switch (lookup)
{
@@ -62,10 +79,10 @@ namespace osu.Game.Skinning
return base.GetConfig(lookup);
}
- protected override IBindable GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo)
+ protected override IBindable? GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo)
=> base.GetComboColour(source, combo.ComboIndexWithOffsets, combo);
- public override ISample GetSample(ISampleInfo sampleInfo)
+ public override ISample? GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
{
@@ -77,6 +94,10 @@ namespace osu.Game.Skinning
}
private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) =>
- new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty };
+ new SkinInfo
+ {
+ Name = beatmapInfo.ToString(),
+ Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty
+ };
}
}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 359d9e5624..92713023f4 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -15,6 +17,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
+using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -27,12 +30,6 @@ namespace osu.Game.Skinning
{
public class LegacySkin : Skin
{
- [CanBeNull]
- protected TextureStore Textures;
-
- [CanBeNull]
- protected ISampleStore Samples;
-
///
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
@@ -51,7 +48,7 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
public LegacySkin(SkinInfo skin, IStorageResourceProvider resources)
- : this(skin, new LegacyDatabasedSkinResourceStore(skin, resources.Files), resources, "skin.ini")
+ : this(skin, resources, null)
{
}
@@ -59,36 +56,12 @@ namespace osu.Game.Skinning
/// Construct a new legacy skin instance.
///
/// The model for this skin.
- /// A storage for looking up files within this skin using user-facing filenames.
/// Access to raw game resources.
+ /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern.
/// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.
- protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
- : this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename))
+ protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage, string configurationFilename = @"skin.ini")
+ : base(skin, resources, storage, configurationFilename)
{
- }
-
- ///
- /// Construct a new legacy skin instance.
- ///
- /// The model for this skin.
- /// A storage for looking up files within this skin using user-facing filenames.
- /// Access to raw game resources.
- /// An optional stream containing the contents of a skin.ini file.
- protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream)
- : base(skin, resources, configurationStream)
- {
- if (storage != null)
- {
- var samples = resources?.AudioManager?.GetSampleStore(storage);
- if (samples != null)
- samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
-
- Samples = samples;
- Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage));
-
- (storage as ResourceStore)?.AddExtension("ogg");
- }
-
// todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution.
hasKeyTexture = new Lazy(() => this.GetAnimation(
lookupForMania(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true,
@@ -110,7 +83,7 @@ namespace osu.Game.Skinning
}
}
- public override IBindable GetConfig(TLookup lookup)
+ public override IBindable? GetConfig(TLookup lookup)
{
switch (lookup)
{
@@ -156,7 +129,7 @@ namespace osu.Game.Skinning
return null;
}
- private IBindable lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup)
+ private IBindable? lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup)
{
if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing))
maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys);
@@ -296,20 +269,20 @@ namespace osu.Game.Skinning
/// The source to retrieve the combo colours from.
/// The preferred index for retrieving the combo colour with.
/// Information on the combo whose using the returned colour.
- protected virtual IBindable GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo)
+ protected virtual IBindable? GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo)
{
var colour = source.ComboColours?[colourIndex % source.ComboColours.Count];
return colour.HasValue ? new Bindable(colour.Value) : null;
}
- private IBindable getCustomColour(IHasCustomColours source, string lookup)
+ private IBindable? getCustomColour(IHasCustomColours source, string lookup)
=> source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null;
- private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup)
+ private IBindable? getManiaImage(LegacyManiaSkinConfiguration source, string lookup)
=> source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable(image) : null;
- [CanBeNull]
- private IBindable legacySettingLookup(SkinConfiguration.LegacySetting legacySetting)
+ private IBindable? legacySettingLookup(SkinConfiguration.LegacySetting legacySetting)
+ where TValue : notnull
{
switch (legacySetting)
{
@@ -321,8 +294,9 @@ namespace osu.Game.Skinning
}
}
- [CanBeNull]
- private IBindable genericLookup(TLookup lookup)
+ private IBindable? genericLookup(TLookup lookup)
+ where TLookup : notnull
+ where TValue : notnull
{
try
{
@@ -345,7 +319,7 @@ namespace osu.Game.Skinning
return null;
}
- public override Drawable GetDrawableComponent(ISkinComponent component)
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
{
if (base.GetDrawableComponent(component) is Drawable c)
return c;
@@ -385,26 +359,15 @@ namespace osu.Game.Skinning
}
})
{
- Children = this.HasFont(LegacyFont.Score)
- ? new Drawable[]
- {
- new LegacyComboCounter(),
- new LegacyScoreCounter(),
- new LegacyAccuracyCounter(),
- new LegacyHealthDisplay(),
- new SongProgress(),
- new BarHitErrorMeter(),
- }
- : new Drawable[]
- {
- // TODO: these should fallback to using osu!classic skin textures, rather than doing this.
- new DefaultComboCounter(),
- new DefaultScoreCounter(),
- new DefaultAccuracyCounter(),
- new DefaultHealthDisplay(),
- new SongProgress(),
- new BarHitErrorMeter(),
- }
+ Children = new Drawable[]
+ {
+ new LegacyComboCounter(),
+ new LegacyScoreCounter(),
+ new LegacyAccuracyCounter(),
+ new LegacyHealthDisplay(),
+ new SongProgress(),
+ new BarHitErrorMeter(),
+ }
};
return skinnableTargetWrapper;
@@ -414,7 +377,7 @@ namespace osu.Game.Skinning
case GameplaySkinComponent resultComponent:
// TODO: this should be inside the judgement pieces.
- Func createDrawable = () => getJudgementAnimation(resultComponent.Component);
+ Func createDrawable = () => getJudgementAnimation(resultComponent.Component);
// kind of wasteful that we throw this away, but should do for now.
if (createDrawable() != null)
@@ -433,7 +396,7 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false);
}
- private Texture getParticleTexture(HitResult result)
+ private Texture? getParticleTexture(HitResult result)
{
switch (result)
{
@@ -450,7 +413,7 @@ namespace osu.Game.Skinning
return null;
}
- private Drawable getJudgementAnimation(HitResult result)
+ private Drawable? getJudgementAnimation(HitResult result)
{
switch (result)
{
@@ -470,7 +433,7 @@ namespace osu.Game.Skinning
return null;
}
- public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
+ public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
foreach (string name in getFallbackNames(componentName))
{
@@ -498,7 +461,7 @@ namespace osu.Game.Skinning
return null;
}
- public override ISample GetSample(ISampleInfo sampleInfo)
+ public override ISample? GetSample(ISampleInfo sampleInfo)
{
IEnumerable lookupNames;
@@ -551,12 +514,5 @@ namespace osu.Game.Skinning
// Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
yield return componentName.Split('/').Last();
}
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
- Textures?.Dispose();
- Samples?.Dispose();
- }
}
}
diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs
deleted file mode 100644
index 2487a469c8..0000000000
--- a/osu.Game/Skinning/LegacySkinResourceStore.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Extensions;
-using osu.Framework.IO.Stores;
-using osu.Game.Database;
-using osu.Game.Extensions;
-
-namespace osu.Game.Skinning
-{
- public class LegacySkinResourceStore : ResourceStore
- {
- private readonly IHasNamedFiles source;
-
- public LegacySkinResourceStore(IHasNamedFiles source, IResourceStore underlyingStore)
- : base(underlyingStore)
- {
- this.source = source;
- }
-
- protected override IEnumerable GetFilenames(string name)
- {
- foreach (string filename in base.GetFilenames(name))
- {
- string path = getPathForFile(filename.ToStandardisedPath());
- if (path != null)
- yield return path;
- }
- }
-
- private string getPathForFile(string filename) =>
- source.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
-
- public override IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename);
- }
-}
diff --git a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs
similarity index 72%
rename from osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs
rename to osu.Game/Skinning/RealmBackedResourceStore.cs
index cd90fea9bb..fc9036727f 100644
--- a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs
+++ b/osu.Game/Skinning/RealmBackedResourceStore.cs
@@ -4,21 +4,29 @@
using System.Collections.Generic;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
+using osu.Game.Database;
using osu.Game.Extensions;
namespace osu.Game.Skinning
{
- public class LegacyDatabasedSkinResourceStore : ResourceStore
+ public class RealmBackedResourceStore : ResourceStore
{
private readonly Dictionary fileToStoragePathMapping = new Dictionary();
- public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore underlyingStore)
+ public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore underlyingStore, string[] extensions = null)
: base(underlyingStore)
{
+ // Must be initialised before the file cache.
+ if (extensions != null)
+ {
+ foreach (string extension in extensions)
+ AddExtension(extension);
+ }
+
initialiseFileCache(source);
}
- private void initialiseFileCache(SkinInfo source)
+ private void initialiseFileCache(IHasRealmFiles source)
{
fileToStoragePathMapping.Clear();
foreach (var f in source.Files)
diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs
index 4787b5a4e9..48286bff59 100644
--- a/osu.Game/Skinning/ResourceStoreBackedSkin.cs
+++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs
@@ -46,7 +46,10 @@ namespace osu.Game.Skinning
return null;
}
- public IBindable? GetConfig(TLookup lookup) => null;
+ public IBindable? GetConfig(TLookup lookup)
+ where TLookup : notnull
+ where TValue : notnull
+ => null;
public void Dispose()
{
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index 931bdfed48..2f01bb7301 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -1,22 +1,24 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
-using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
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.Game.Audio;
using osu.Game.Database;
-using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
@@ -24,8 +26,17 @@ namespace osu.Game.Skinning
{
public abstract class Skin : IDisposable, ISkin
{
+ ///
+ /// A texture store which can be used to perform user file lookups for this skin.
+ ///
+ protected TextureStore? Textures { get; }
+
+ ///
+ /// A sample store which can be used to perform user file lookups for this skin.
+ ///
+ protected ISampleStore? Samples { get; }
+
public readonly Live SkinInfo;
- private readonly IStorageResourceProvider resources;
public SkinConfiguration Configuration { get; set; }
@@ -33,66 +44,80 @@ namespace osu.Game.Skinning
private readonly Dictionary drawableComponentInfo = new Dictionary();
- public abstract ISample GetSample(ISampleInfo sampleInfo);
+ public abstract ISample? GetSample(ISampleInfo sampleInfo);
- public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
+ public Texture? GetTexture(string componentName) => GetTexture(componentName, default, default);
- public abstract Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
+ public abstract Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
- public abstract IBindable GetConfig(TLookup lookup);
+ public abstract IBindable? GetConfig(TLookup lookup)
+ where TLookup : notnull
+ where TValue : notnull;
- protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null)
+ ///
+ /// Construct a new skin.
+ ///
+ /// The skin's metadata. Usually a live realm object.
+ /// Access to game-wide resources.
+ /// An optional store which will *replace* all file lookups that are usually sourced from .
+ /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini".
+ protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = @"skin.ini")
{
- SkinInfo = resources?.RealmAccess != null
- ? skin.ToLive(resources.RealmAccess)
- // This path should only be used in some tests.
- : skin.ToLiveUnmanaged();
+ if (resources != null)
+ {
+ SkinInfo = skin.ToLive(resources.RealmAccess);
- this.resources = resources;
+ storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" });
- configurationStream ??= getConfigurationStream();
+ var samples = resources.AudioManager?.GetSampleStore(storage);
+ if (samples != null)
+ samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
+
+ Samples = samples;
+ Textures = new TextureStore(resources.CreateTextureLoaderStore(storage));
+ }
+ else
+ {
+ // Generally only used for tests.
+ SkinInfo = skin.ToLiveUnmanaged();
+ }
+
+ var configurationStream = storage?.GetStream(configurationFilename);
if (configurationStream != null)
+ {
// stream will be closed after use by LineBufferedReader.
ParseConfigurationStream(configurationStream);
+ Debug.Assert(Configuration != null);
+ }
else
Configuration = new SkinConfiguration();
// skininfo files may be null for default skin.
- SkinInfo.PerformRead(s =>
+ foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
{
- // we may want to move this to some kind of async operation in the future.
- foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
+ string filename = $"{skinnableTarget}.json";
+
+ byte[]? bytes = storage?.Get(filename);
+
+ if (bytes == null)
+ continue;
+
+ try
{
- string filename = $"{skinnableTarget}.json";
+ string jsonContent = Encoding.UTF8.GetString(bytes);
+ var deserializedContent = JsonConvert.DeserializeObject>(jsonContent);
- // skininfo files may be null for default skin.
- var fileInfo = s.Files.FirstOrDefault(f => f.Filename == filename);
-
- if (fileInfo == null)
+ if (deserializedContent == null)
continue;
- byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath());
-
- if (bytes == null)
- continue;
-
- try
- {
- string jsonContent = Encoding.UTF8.GetString(bytes);
- var deserializedContent = JsonConvert.DeserializeObject>(jsonContent);
-
- if (deserializedContent == null)
- continue;
-
- DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
- }
- catch (Exception ex)
- {
- Logger.Error(ex, "Failed to load skin configuration.");
- }
+ DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
}
- });
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Failed to load skin configuration.");
+ }
+ }
}
protected virtual void ParseConfigurationStream(Stream stream)
@@ -101,16 +126,6 @@ namespace osu.Game.Skinning
Configuration = new LegacySkinDecoder().Decode(reader);
}
- private Stream getConfigurationStream()
- {
- string path = SkinInfo.PerformRead(s => s.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath());
-
- if (string.IsNullOrEmpty(path))
- return null;
-
- return resources?.Files.GetStream(path);
- }
-
///
/// Remove all stored customisations for the provided target.
///
@@ -129,7 +144,7 @@ namespace osu.Game.Skinning
DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray();
}
- public virtual Drawable GetDrawableComponent(ISkinComponent component)
+ public virtual Drawable? GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
@@ -137,9 +152,23 @@ namespace osu.Game.Skinning
if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo))
return null;
+ var components = new List();
+
+ foreach (var i in skinnableInfo)
+ {
+ try
+ {
+ components.Add(i.CreateInstance());
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Unable to create skin component {i.Type.Name}");
+ }
+ }
+
return new SkinnableTargetComponentsContainer
{
- ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance())
+ Children = components,
};
}
@@ -168,6 +197,9 @@ namespace osu.Game.Skinning
return;
isDisposed = true;
+
+ Textures?.Dispose();
+ Samples?.Dispose();
}
#endregion
diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
index 2a3e51b4f5..4667a385b3 100644
--- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
+++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
@@ -96,12 +96,14 @@ namespace osu.Game.Tests.Beatmaps
AddStep("setup skins", () =>
{
userSkinInfo.Files.Clear();
- userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile));
+ if (!string.IsNullOrEmpty(userFile))
+ userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile));
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Files.Clear();
- beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile));
+ if (!string.IsNullOrEmpty(beatmapFile))
+ beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile));
// Need to refresh the cached skin source to refresh the skin resource store.
dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this));
@@ -191,22 +193,32 @@ namespace osu.Game.Tests.Beatmaps
}
}
- private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap, IStorageResourceProvider
{
private readonly BeatmapInfo skinBeatmapInfo;
- private readonly IResourceStore resourceStore;
private readonly IStorageResourceProvider resources;
- public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources)
+ public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore accessMarkingResourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock,
+ IStorageResourceProvider resources)
: base(beatmap, storyboard, referenceClock, resources.AudioManager)
{
this.skinBeatmapInfo = skinBeatmapInfo;
- this.resourceStore = resourceStore;
+ Files = accessMarkingResourceStore;
this.resources = resources;
}
- protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, resources);
+ protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, this);
+
+ public AudioManager AudioManager => resources.AudioManager;
+
+ public IResourceStore Files { get; }
+
+ public IResourceStore Resources => resources.Resources;
+
+ public RealmAccess RealmAccess => resources.RealmAccess;
+
+ public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => resources.CreateTextureLoaderStore(underlyingStore);
}
}
}
diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs
index 5c522058d9..597c5e9a2b 100644
--- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs
+++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs
@@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
-using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Skinning;
@@ -112,7 +111,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod;
public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours)
- : base(beatmapInfo, new ResourceStore(), null)
+ : base(beatmapInfo, null)
{
if (hasColours)
{
@@ -141,7 +140,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan;
public TestSkin(bool hasCustomColours)
- : base(new SkinInfo(), new ResourceStore(), null, string.Empty)
+ : base(new SkinInfo(), null, null)
{
if (hasCustomColours)
{
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index f287a04d71..6c332c2408 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -115,11 +115,13 @@ namespace osu.Game.Tests.Visual
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- headlessHostStorage = (parent.Get() as HeadlessGameHost)?.Storage;
+ var host = parent.Get();
+
+ headlessHostStorage = (host as HeadlessGameHost)?.Storage;
Resources = parent.Get().Resources;
- realm = new Lazy(() => new RealmAccess(LocalStorage, "client"));
+ realm = new Lazy(() => new RealmAccess(LocalStorage, "client", host.UpdateThread));
RecycleLocalStorage(false);
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index 1107089a46..2e1ca09fe4 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual
},
new OsuSpriteText
{
- Text = skin?.SkinInfo?.Value.Name ?? "none",
+ Text = skin?.SkinInfo.Value.Name ?? "none",
Scale = new Vector2(1.5f),
Padding = new MarginPadding(5),
},
@@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual
private readonly bool extrapolateAnimations;
public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations)
- : base(skin, storage, resources, "skin.ini")
+ : base(skin, resources, storage)
{
this.extrapolateAnimations = extrapolateAnimations;
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1c1deaae8e..3c01f29671 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 23101c5af6..c8f170497d 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -84,7 +84,7 @@
-
+