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 @@ - +