Merge branch 'master' into countdown-button-ux

This commit is contained in:
Dean Herbert 2022-03-26 10:34:51 +09:00 committed by GitHub
commit 03f24c8b58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 692 additions and 302 deletions

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.314.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage) public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). // 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)
{ {
} }
} }

View File

@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin private class TestLegacySkin : LegacySkin
{ {
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName) public TestLegacySkin(IResourceStore<byte[]> 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)
{ {
} }
} }

View File

@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; 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<ReplayFrame>
{
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] [Test]
public void TestCultureInvariance() public void TestCultureInvariance()
{ {
@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// rather than the classic ASCII U+002D HYPHEN-MINUS. // rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se"); CultureInfo.CurrentCulture = new CultureInfo("se");
var encodeStream = new MemoryStream(); var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder();
var decodedAfterEncode = decoder.Parse(decodeStream);
Assert.Multiple(() => 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] [TearDown]
public void TearDown() public void TearDown()
{ {
@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacyScoreDecoder : LegacyScoreDecoder private class TestLegacyScoreDecoder : LegacyScoreDecoder
{ {
private readonly int beatmapVersion;
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[] private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
{ {
new OsuRuleset(), new OsuRuleset(),
@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
new ManiaRuleset() new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); }.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 Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
MD5Hash = md5Hash, MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty() Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
} }
}); });
} }

View File

@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database
Live<BeatmapSetInfo>? imported; Live<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
{
imported = await importer.Import(reader); imported = await importer.Import(reader);
EnsureLoaded(realm.Realm);
}
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count()); Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database
new ImportTask(zipStream, string.Empty) new ImportTask(zipStream, string.Empty)
); );
realm.Run(r => r.Refresh());
checkBeatmapSetCount(realm.Realm, 0); checkBeatmapSetCount(realm.Realm, 0);
checkBeatmapCount(realm.Realm, 0); checkBeatmapCount(realm.Realm, 0);
@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database
{ {
} }
EnsureLoaded(realm.Realm);
checkBeatmapSetCount(realm.Realm, 1); checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realm.Realm, 12); checkBeatmapCount(realm.Realm, 12);
@ -726,6 +733,8 @@ namespace osu.Game.Tests.Database
var imported = importer.Import(toImport); var imported = importer.Import(toImport);
realm.Run(r => r.Refresh());
Assert.NotNull(imported); Assert.NotNull(imported);
Debug.Assert(imported != null); Debug.Assert(imported != null);
@ -891,6 +900,8 @@ namespace osu.Game.Tests.Database
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp); await importer.Import(temp);
EnsureLoaded(realm.Realm);
// Update via the beatmap, not the beatmap info, to ensure correct linking // Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First(); BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();

View File

@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin private class TestSkin : LegacySkin
{ {
public TestSkin(string resourceName, IStorageResourceProvider resources) public TestSkin(string resourceName, IStorageResourceProvider resources)
: base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini") : base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName))
{ {
} }
} }

View File

@ -1,12 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Audio;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning; using osu.Game.Skinning;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace osu.Game.Tests.NonVisual.Skinning namespace osu.Game.Tests.NonVisual.Skinning
{ {
@ -71,7 +80,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
var texture = legacySkin.GetTexture(requestedComponent); var texture = legacySkin.GetTexture(requestedComponent);
Assert.IsNotNull(texture); Assert.IsNotNull(texture);
Assert.AreEqual(textureStore.Textures[expectedTexture], texture); Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width);
Assert.AreEqual(expectedScale, texture.ScaleAdjust); Assert.AreEqual(expectedScale, texture.ScaleAdjust);
} }
@ -88,23 +97,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
private class TestLegacySkin : LegacySkin private class TestLegacySkin : LegacySkin
{ {
public TestLegacySkin(TextureStore textureStore) public TestLegacySkin(IResourceStore<TextureUpload> textureStore)
: base(new SkinInfo(), null, null, string.Empty) : base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
{ {
Textures = textureStore; }
private class TestResourceProvider : IStorageResourceProvider
{
private readonly IResourceStore<TextureUpload> textureStore;
public TestResourceProvider(IResourceStore<TextureUpload> textureStore)
{
this.textureStore = textureStore;
}
public AudioManager AudioManager => null;
public IResourceStore<byte[]> Files => null;
public IResourceStore<byte[]> Resources => null;
public RealmAccess RealmAccess => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => textureStore;
} }
} }
private class TestTextureStore : TextureStore private class TestTextureStore : IResourceStore<TextureUpload>
{ {
public readonly Dictionary<string, Texture> Textures; public readonly Dictionary<string, TextureUpload> Textures;
public TestTextureStore(params string[] fileNames) 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<Rgba32>(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<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => Task.FromResult(Get(name));
public Stream GetStream(string name) => throw new NotImplementedException();
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
public void Dispose()
{
}
} }
} }
} }

View File

@ -77,7 +77,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin public class BeatmapSkinSource : LegacyBeatmapSkin
{ {
public BeatmapSkinSource() public BeatmapSkinSource()
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null) : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{ {
} }

View File

@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin public class BeatmapSkinSource : LegacyBeatmapSkin
{ {
public BeatmapSkinSource() public BeatmapSkinSource()
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null) : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{ {
} }
} }

View File

@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestEmptyLegacyBeatmapSkinFallsBack() 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<SkinnableTargetContainer>().All(c => c.ComponentsLoaded)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
} }

View File

@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().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<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart() 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<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);

View File

@ -163,6 +163,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1); AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().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<ParticipantPanel>().ElementAt(0);
var second = this.ChildrenOfType<ParticipantPanel>().ElementAt(1);
return second.Y < first.Y;
});
}
[Test] [Test]
public void TestKickButtonOnlyPresentWhenHost() public void TestKickButtonOnlyPresentWhenHost()
{ {
@ -202,9 +221,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestManyUsers() public void TestManyUsers()
{ {
const int users_count = 20;
AddStep("add many users", () => AddStep("add many users", () =>
{ {
for (int i = 0; i < 20; i++) for (int i = 0; i < users_count; i++)
{ {
MultiplayerClient.AddUser(new APIUser 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] [Test]

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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<Mod>());
}
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
Action = () => game.GracefullyExit(); Action = () => game.GracefullyExit();
folderButton.Action = storage.PresentExternally; folderButton.Action = () => storage.PresentExternally();
ButtonText = "Close osu!"; ButtonText = "Close osu!";
} }

View File

@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{ {
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap> public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
{ {
/// <summary>
/// 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.
/// </summary>
public const int EARLY_VERSION_TIMING_OFFSET = 24;
internal static RulesetStore RulesetStore; internal static RulesetStore RulesetStore;
private Beatmap beatmap; private Beatmap beatmap;
@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats
RulesetStore = new AssemblyRulesetStore(); RulesetStore = new AssemblyRulesetStore();
} }
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
offset = FormatVersion < 5 ? 24 : 0;
} }
protected override Beatmap CreateTemplateObject() protected override Beatmap CreateTemplateObject()

View File

@ -225,7 +225,7 @@ namespace osu.Game.Beatmaps
{ {
try try
{ {
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); return new LegacyBeatmapSkin(BeatmapInfo, resources);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@ -17,6 +19,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics; using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -28,8 +31,6 @@ using osu.Game.Stores;
using Realms; using Realms;
using Realms.Exceptions; using Realms.Exceptions;
#nullable enable
namespace osu.Game.Database namespace osu.Game.Database
{ {
/// <summary> /// <summary>
@ -46,6 +47,8 @@ namespace osu.Game.Database
private readonly IDatabaseContextFactory? efContextFactory; private readonly IDatabaseContextFactory? efContextFactory;
private readonly SynchronizationContext? updateThreadSyncContext;
/// <summary> /// <summary>
/// Version history: /// Version history:
/// 6 ~2021-10-18 First tracked version. /// 6 ~2021-10-18 First tracked version.
@ -143,12 +146,15 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="storage">The game storage which will be used to create the realm backing file.</param> /// <param name="storage">The game storage which will be used to create the realm backing file.</param>
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param> /// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
/// <param name="updateThread">The game update thread, used to post realm operations into a thread-safe context.</param>
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param> /// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
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.storage = storage;
this.efContextFactory = efContextFactory; this.efContextFactory = efContextFactory;
updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current;
Filename = filename; Filename = filename;
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
@ -379,9 +385,6 @@ namespace osu.Game.Database
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback) public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase where T : RealmObjectBase
{ {
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
lock (realmLock) lock (realmLock)
{ {
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback); Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
@ -459,23 +462,24 @@ namespace osu.Game.Database
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns> /// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action) public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action)
{ {
if (!ThreadSafety.IsUpdateThread) if (updateThreadSyncContext == null)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration.");
var syncContext = SynchronizationContext.Current;
total_subscriptions.Value++; 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. // 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). // When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
return new InvokeOnDisposal(() => return new InvokeOnDisposal(() =>
{ {
if (ThreadSafety.IsUpdateThread) if (ThreadSafety.IsUpdateThread)
syncContext.Send(_ => unsubscribe(), null); updateThreadSyncContext.Send(_ => unsubscribe(), null);
else else
syncContext.Post(_ => unsubscribe(), null); updateThreadSyncContext.Post(_ => unsubscribe(), null);
void unsubscribe() void unsubscribe()
{ {

View File

@ -70,9 +70,9 @@ namespace osu.Game.IO
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode); 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) public override Storage GetStorageForDirectory(string path)
{ {

View File

@ -171,6 +171,8 @@ namespace osu.Game.Online.Multiplayer
Room = joinedRoom; Room = joinedRoom;
APIRoom = room; APIRoom = room;
Debug.Assert(joinedRoom.Playlist.Count > 0);
APIRoom.Playlist.Clear(); APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); 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)); Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId); APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
Debug.Assert(Room.Playlist.Count > 0);
ItemRemoved?.Invoke(playlistItemId); ItemRemoved?.Invoke(playlistItemId);
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
}); });

View File

@ -200,7 +200,7 @@ namespace osu.Game
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); 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>(RulesetStore = new RealmRulesetStore(realm, Storage)); dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage));
dependencies.CacheAs<IRulesetStore>(RulesetStore); dependencies.CacheAs<IRulesetStore>(RulesetStore);

View File

@ -0,0 +1,176 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>();
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<Drawable> 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)
}
}
}
}
};
}
}
}
}

View File

@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(new SettingsButton Add(new SettingsButton
{ {
Text = GeneralSettingsStrings.OpenOsuFolder, Text = GeneralSettingsStrings.OpenOsuFolder,
Action = storage.PresentExternally, Action = () => storage.PresentExternally(),
}); });
Add(new SettingsButton Add(new SettingsButton

View File

@ -23,6 +23,8 @@ namespace osu.Game.Scoring.Legacy
private IBeatmap currentBeatmap; private IBeatmap currentBeatmap;
private Ruleset currentRuleset; private Ruleset currentRuleset;
private float beatmapOffset;
public Score Parse(Stream stream) public Score Parse(Stream stream)
{ {
var score = new Score var score = new Score
@ -72,6 +74,9 @@ namespace osu.Game.Scoring.Legacy
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; 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 = */ /* score.HpGraphString = */
sr.ReadString(); sr.ReadString();
@ -229,7 +234,7 @@ namespace osu.Game.Scoring.Legacy
private void readLegacyReplay(Replay replay, StreamReader reader) private void readLegacyReplay(Replay replay, StreamReader reader)
{ {
float lastTime = 0; float lastTime = beatmapOffset;
ReplayFrame currentFrame = null; ReplayFrame currentFrame = null;
string[] frames = reader.ReadToEnd().Split(','); string[] frames = reader.ReadToEnd().Split(',');

View File

@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
@ -14,8 +17,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.LZMA;
#nullable enable
namespace osu.Game.Scoring.Legacy namespace osu.Game.Scoring.Legacy
{ {
public class LegacyScoreEncoder public class LegacyScoreEncoder
@ -111,6 +112,9 @@ namespace osu.Game.Scoring.Legacy
{ {
StringBuilder replayData = new StringBuilder(); 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) if (score.Replay != null)
{ {
int lastTime = 0; int lastTime = 0;
@ -120,7 +124,7 @@ namespace osu.Game.Scoring.Legacy
var legacyFrame = getLegacyFrame(f); var legacyFrame = getLegacyFrame(f);
// Rounding because stable could only parse integral values // 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},")); replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},"));
lastTime = time; lastTime = time;
} }

View File

@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu
private readonly ButtonArea buttonArea; private readonly ButtonArea buttonArea;
private readonly Button backButton; private readonly MainMenuButton backButton;
private readonly List<Button> buttonsTopLevel = new List<Button>(); private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<Button> buttonsPlay = new List<Button>(); private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private Sample sampleBack; private Sample sampleBack;
@ -100,8 +100,8 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] buttonArea.AddRange(new Drawable[]
{ {
new Button(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
{ {
VisibleState = ButtonSystemState.Play, VisibleState = ButtonSystemState.Play,
}, },
@ -126,24 +126,24 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
{ {
buttonsPlay.Add(new Button(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new Button(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new Button(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit) if (host.CanExit)
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsTopLevel); buttonArea.AddRange(buttonsTopLevel);
buttonArea.ForEach(b => buttonArea.ForEach(b =>
{ {
if (b is Button) if (b is MainMenuButton)
{ {
b.Origin = Anchor.CentreLeft; b.Origin = Anchor.CentreLeft;
b.Anchor = Anchor.CentreLeft; b.Anchor = Anchor.CentreLeft;
@ -305,7 +305,7 @@ namespace osu.Game.Screens.Menu
{ {
buttonArea.ButtonSystemState = state; buttonArea.ButtonSystemState = state;
foreach (var b in buttonArea.Children.OfType<Button>()) foreach (var b in buttonArea.Children.OfType<MainMenuButton>())
b.ButtonSystemState = state; b.ButtonSystemState = state;
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Menu
/// Button designed specifically for the osu!next main menu. /// Button designed specifically for the osu!next main menu.
/// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape). /// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
/// </summary> /// </summary>
public class Button : BeatSyncedContainer, IStateful<ButtonState> public class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
{ {
public event Action<ButtonState> StateChanged; public event Action<ButtonState> StateChanged;
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); 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.sampleName = sampleName;
this.clickAction = clickAction; this.clickAction = clickAction;
@ -209,7 +209,7 @@ namespace osu.Game.Screens.Menu
protected override bool OnKeyDown(KeyDownEvent e) 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; return false;
if (TriggerKey == e.Key && TriggerKey != Key.Unknown) if (TriggerKey == e.Key && TriggerKey != Key.Unknown)

View File

@ -42,8 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public MultiplayerCountdownButton() public MultiplayerCountdownButton()
{ {
Icon = FontAwesome.Solid.CaretDown; Icon = FontAwesome.Regular.Clock;
IconScale = new Vector2(0.6f);
Add(background = new Box Add(background = new Box
{ {
@ -52,6 +51,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}); });
base.Action = this.ShowPopover; base.Action = this.ShowPopover;
TooltipText = "Countdown settings";
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -165,6 +165,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
LengthLimit = 100, LengthLimit = 100,
}, },
}, },
// new Section("Room visibility")
// {
// Alpha = disabled_alpha,
// Child = AvailabilityPicker = new RoomAvailabilityPicker
// {
// Enabled = { Value = false }
// },
// },
new Section("Game type") new Section("Game type")
{ {
Child = new FillFlowContainer Child = new FillFlowContainer

View File

@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
countdown = room?.Countdown; countdown = room?.Countdown;
if (room?.Countdown != null) if (room?.Countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
else else
{ {
countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate?.Cancel();

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -187,9 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50; const double fade_time = 50;
var currentItem = Playlist.GetCurrentItem(); var currentItem = Playlist.GetCurrentItem();
Debug.Assert(currentItem != null); var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance();
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
@ -201,15 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else else
userModsDisplay.FadeOut(fade_time); userModsDisplay.FadeOut(fade_time);
if (Client.IsHost && !User.Equals(Client.LocalUser)) kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
kickButton.FadeIn(fade_time); crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
else
kickButton.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
else
crown.FadeOut(fade_time);
// 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 // 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. // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{ {
private FillFlowContainer<ParticipantPanel> panels; private FillFlowContainer<ParticipantPanel> panels;
[CanBeNull]
private ParticipantPanel currentHostPanel;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -55,6 +59,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// Add panels for all users new to the room. // Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User))) foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(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);
}
}
} }
} }
} }

View File

@ -30,11 +30,9 @@ namespace osu.Game.Skinning
public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources) public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: base( : base(
skin, skin,
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"),
resources, resources,
// A default legacy skin may still have a skin.ini if it is modified by the user. // 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.
// We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. skin.Protected ? new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy") : null
new LegacyDatabasedSkinResourceStore(skin, resources.Files).GetStream("skin.ini")
) )
{ {
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);

View File

@ -1,7 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations; #nullable enable
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -21,16 +22,14 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
/// <param name="component">The requested component.</param> /// <param name="component">The requested component.</param>
/// <returns>A drawable representation for the requested component, or null if unavailable.</returns> /// <returns>A drawable representation for the requested component, or null if unavailable.</returns>
[CanBeNull] Drawable? GetDrawableComponent(ISkinComponent component);
Drawable GetDrawableComponent(ISkinComponent component);
/// <summary> /// <summary>
/// Retrieve a <see cref="Texture"/>. /// Retrieve a <see cref="Texture"/>.
/// </summary> /// </summary>
/// <param name="componentName">The requested texture.</param> /// <param name="componentName">The requested texture.</param>
/// <returns>A matching texture, or null if unavailable.</returns> /// <returns>A matching texture, or null if unavailable.</returns>
[CanBeNull] Texture? GetTexture(string componentName) => GetTexture(componentName, default, default);
Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
/// <summary> /// <summary>
/// Retrieve a <see cref="Texture"/>. /// Retrieve a <see cref="Texture"/>.
@ -39,23 +38,22 @@ namespace osu.Game.Skinning
/// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param> /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
/// <param name="wrapModeT">The texture wrap mode in vertical direction.</param> /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
/// <returns>A matching texture, or null if unavailable.</returns> /// <returns>A matching texture, or null if unavailable.</returns>
[CanBeNull] Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
/// <summary> /// <summary>
/// Retrieve a <see cref="SampleChannel"/>. /// Retrieve a <see cref="SampleChannel"/>.
/// </summary> /// </summary>
/// <param name="sampleInfo">The requested sample.</param> /// <param name="sampleInfo">The requested sample.</param>
/// <returns>A matching sample channel, or null if unavailable.</returns> /// <returns>A matching sample channel, or null if unavailable.</returns>
[CanBeNull] ISample? GetSample(ISampleInfo sampleInfo);
ISample GetSample(ISampleInfo sampleInfo);
/// <summary> /// <summary>
/// Retrieve a configuration value. /// Retrieve a configuration value.
/// </summary> /// </summary>
/// <param name="lookup">The requested configuration value.</param> /// <param name="lookup">The requested configuration value.</param>
/// <returns>A matching value boxed in an <see cref="IBindable{TValue}"/>, or null if unavailable.</returns> /// <returns>A matching value boxed in an <see cref="IBindable{TValue}"/>, or null if unavailable.</returns>
[CanBeNull] IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup); where TLookup : notnull
where TValue : notnull;
} }
} }

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
@ -20,14 +23,28 @@ namespace osu.Game.Skinning
protected override bool AllowManiaSkin => false; protected override bool AllowManiaSkin => false;
protected override bool UseCustomSampleBanks => true; protected override bool UseCustomSampleBanks => true;
public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore<byte[]> storage, IStorageResourceProvider resources) /// <summary>
: base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) /// Construct a new legacy beatmap skin instance.
/// </summary>
/// <param name="beatmapInfo">The model for this beatmap.</param>
/// <param name="resources">Access to raw game resources.</param>
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) // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer)
Configuration.AllowDefaultComboColoursFallback = false; Configuration.AllowDefaultComboColoursFallback = false;
} }
public override Drawable GetDrawableComponent(ISkinComponent component) private static IResourceStore<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
{
if (resources == null)
// should only ever be used in tests.
return new ResourceStore<byte[]>();
return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" });
}
public override Drawable? GetDrawableComponent(ISkinComponent component)
{ {
if (component is SkinnableTargetComponent targetComponent) if (component is SkinnableTargetComponent targetComponent)
{ {
@ -46,7 +63,7 @@ namespace osu.Game.Skinning
return base.GetDrawableComponent(component); return base.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)
{ {
@ -62,10 +79,10 @@ namespace osu.Game.Skinning
return base.GetConfig<TLookup, TValue>(lookup); return base.GetConfig<TLookup, TValue>(lookup);
} }
protected override IBindable<Color4> GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo) protected override IBindable<Color4>? GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo)
=> base.GetComboColour(source, combo.ComboIndexWithOffsets, 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) if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
{ {
@ -77,6 +94,10 @@ namespace osu.Game.Skinning
} }
private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => 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
};
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -15,6 +17,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -27,12 +30,6 @@ namespace osu.Game.Skinning
{ {
public class LegacySkin : Skin public class LegacySkin : Skin
{ {
[CanBeNull]
protected TextureStore Textures;
[CanBeNull]
protected ISampleStore Samples;
/// <summary> /// <summary>
/// Whether texture for the keys exists. /// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned. /// Used to determine if the mania ruleset is skinned.
@ -51,7 +48,7 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) 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. /// Construct a new legacy skin instance.
/// </summary> /// </summary>
/// <param name="skin">The model for this skin.</param> /// <param name="skin">The model for this skin.</param>
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
/// <param name="resources">Access to raw game resources.</param> /// <param name="resources">Access to raw game resources.</param>
/// <param name="storage">An optional store which will be used for looking up skin resources. If null, one will be created from realm <see cref="IHasRealmFiles"/> pattern.</param>
/// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param> /// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini")
: this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename)) : base(skin, resources, storage, configurationFilename)
{ {
}
/// <summary>
/// Construct a new legacy skin instance.
/// </summary>
/// <param name="skin">The model for this skin.</param>
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
/// <param name="resources">Access to raw game resources.</param>
/// <param name="configurationStream">An optional stream containing the contents of a skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream)
: base(skin, resources, configurationStream)
{
if (storage != null)
{
var samples = resources?.AudioManager?.GetSampleStore(storage);
if (samples != null)
samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
Samples = samples;
Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage));
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
}
// todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution.
hasKeyTexture = new Lazy<bool>(() => this.GetAnimation( hasKeyTexture = new Lazy<bool>(() => this.GetAnimation(
lookupForMania<string>(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, lookupForMania<string>(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true,
@ -110,7 +83,7 @@ namespace osu.Game.Skinning
} }
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)
{ {
@ -156,7 +129,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
private IBindable<TValue> lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup) private IBindable<TValue>? lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup)
{ {
if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing))
maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys);
@ -296,20 +269,20 @@ namespace osu.Game.Skinning
/// <param name="source">The source to retrieve the combo colours from.</param> /// <param name="source">The source to retrieve the combo colours from.</param>
/// <param name="colourIndex">The preferred index for retrieving the combo colour with.</param> /// <param name="colourIndex">The preferred index for retrieving the combo colour with.</param>
/// <param name="combo">Information on the combo whose using the returned colour.</param> /// <param name="combo">Information on the combo whose using the returned colour.</param>
protected virtual IBindable<Color4> GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo) protected virtual IBindable<Color4>? GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo)
{ {
var colour = source.ComboColours?[colourIndex % source.ComboColours.Count]; var colour = source.ComboColours?[colourIndex % source.ComboColours.Count];
return colour.HasValue ? new Bindable<Color4>(colour.Value) : null; return colour.HasValue ? new Bindable<Color4>(colour.Value) : null;
} }
private IBindable<Color4> getCustomColour(IHasCustomColours source, string lookup) private IBindable<Color4>? getCustomColour(IHasCustomColours source, string lookup)
=> source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable<Color4>(col) : null; => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable<Color4>(col) : null;
private IBindable<string> getManiaImage(LegacyManiaSkinConfiguration source, string lookup) private IBindable<string>? getManiaImage(LegacyManiaSkinConfiguration source, string lookup)
=> source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable<string>(image) : null; => source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable<string>(image) : null;
[CanBeNull] private IBindable<TValue>? legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
private IBindable<TValue> legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting) where TValue : notnull
{ {
switch (legacySetting) switch (legacySetting)
{ {
@ -321,8 +294,9 @@ namespace osu.Game.Skinning
} }
} }
[CanBeNull] private IBindable<TValue>? genericLookup<TLookup, TValue>(TLookup lookup)
private IBindable<TValue> genericLookup<TLookup, TValue>(TLookup lookup) where TLookup : notnull
where TValue : notnull
{ {
try try
{ {
@ -345,7 +319,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable? GetDrawableComponent(ISkinComponent component)
{ {
if (base.GetDrawableComponent(component) is Drawable c) if (base.GetDrawableComponent(component) is Drawable c)
return c; return c;
@ -385,26 +359,15 @@ namespace osu.Game.Skinning
} }
}) })
{ {
Children = this.HasFont(LegacyFont.Score) Children = new Drawable[]
? new Drawable[] {
{ new LegacyComboCounter(),
new LegacyComboCounter(), new LegacyScoreCounter(),
new LegacyScoreCounter(), new LegacyAccuracyCounter(),
new LegacyAccuracyCounter(), new LegacyHealthDisplay(),
new LegacyHealthDisplay(), new SongProgress(),
new SongProgress(), new BarHitErrorMeter(),
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(),
}
}; };
return skinnableTargetWrapper; return skinnableTargetWrapper;
@ -414,7 +377,7 @@ namespace osu.Game.Skinning
case GameplaySkinComponent<HitResult> resultComponent: case GameplaySkinComponent<HitResult> resultComponent:
// TODO: this should be inside the judgement pieces. // TODO: this should be inside the judgement pieces.
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component); Func<Drawable?> createDrawable = () => getJudgementAnimation(resultComponent.Component);
// kind of wasteful that we throw this away, but should do for now. // kind of wasteful that we throw this away, but should do for now.
if (createDrawable() != null) if (createDrawable() != null)
@ -433,7 +396,7 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false); return this.GetAnimation(component.LookupName, false, false);
} }
private Texture getParticleTexture(HitResult result) private Texture? getParticleTexture(HitResult result)
{ {
switch (result) switch (result)
{ {
@ -450,7 +413,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
private Drawable getJudgementAnimation(HitResult result) private Drawable? getJudgementAnimation(HitResult result)
{ {
switch (result) switch (result)
{ {
@ -470,7 +433,7 @@ namespace osu.Game.Skinning
return null; 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)) foreach (string name in getFallbackNames(componentName))
{ {
@ -498,7 +461,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
public override ISample GetSample(ISampleInfo sampleInfo) public override ISample? GetSample(ISampleInfo sampleInfo)
{ {
IEnumerable<string> lookupNames; IEnumerable<string> 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"). // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
yield return componentName.Split('/').Last(); yield return componentName.Split('/').Last();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Textures?.Dispose();
Samples?.Dispose();
}
} }
} }

View File

@ -1,39 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
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<byte[]>
{
private readonly IHasNamedFiles source;
public LegacySkinResourceStore(IHasNamedFiles source, IResourceStore<byte[]> underlyingStore)
: base(underlyingStore)
{
this.source = source;
}
protected override IEnumerable<string> 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<string> GetAvailableResources() => source.Files.Select(f => f.Filename);
}
}

View File

@ -4,21 +4,29 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacyDatabasedSkinResourceStore : ResourceStore<byte[]> public class RealmBackedResourceStore : ResourceStore<byte[]>
{ {
private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>(); private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>();
public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore<byte[]> underlyingStore) public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore<byte[]> underlyingStore, string[] extensions = null)
: base(underlyingStore) : base(underlyingStore)
{ {
// Must be initialised before the file cache.
if (extensions != null)
{
foreach (string extension in extensions)
AddExtension(extension);
}
initialiseFileCache(source); initialiseFileCache(source);
} }
private void initialiseFileCache(SkinInfo source) private void initialiseFileCache(IHasRealmFiles source)
{ {
fileToStoragePathMapping.Clear(); fileToStoragePathMapping.Clear();
foreach (var f in source.Files) foreach (var f in source.Files)

View File

@ -46,7 +46,10 @@ namespace osu.Game.Skinning
return null; return null;
} }
public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) => null; public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
where TLookup : notnull
where TValue : notnull
=> null;
public void Dispose() public void Dispose()
{ {

View File

@ -1,22 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -24,8 +26,17 @@ namespace osu.Game.Skinning
{ {
public abstract class Skin : IDisposable, ISkin public abstract class Skin : IDisposable, ISkin
{ {
/// <summary>
/// A texture store which can be used to perform user file lookups for this skin.
/// </summary>
protected TextureStore? Textures { get; }
/// <summary>
/// A sample store which can be used to perform user file lookups for this skin.
/// </summary>
protected ISampleStore? Samples { get; }
public readonly Live<SkinInfo> SkinInfo; public readonly Live<SkinInfo> SkinInfo;
private readonly IStorageResourceProvider resources;
public SkinConfiguration Configuration { get; set; } public SkinConfiguration Configuration { get; set; }
@ -33,66 +44,80 @@ namespace osu.Game.Skinning
private readonly Dictionary<SkinnableTarget, SkinnableInfo[]> drawableComponentInfo = new Dictionary<SkinnableTarget, SkinnableInfo[]>(); private readonly Dictionary<SkinnableTarget, SkinnableInfo[]> drawableComponentInfo = new Dictionary<SkinnableTarget, SkinnableInfo[]>();
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<TValue> GetConfig<TLookup, TValue>(TLookup lookup); public abstract IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
where TLookup : notnull
where TValue : notnull;
protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) /// <summary>
/// Construct a new skin.
/// </summary>
/// <param name="skin">The skin's metadata. Usually a live realm object.</param>
/// <param name="resources">Access to game-wide resources.</param>
/// <param name="storage">An optional store which will *replace* all file lookups that are usually sourced from <paramref name="skin"/>.</param>
/// <param name="configurationFilename">An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini".</param>
protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = @"skin.ini")
{ {
SkinInfo = resources?.RealmAccess != null if (resources != null)
? skin.ToLive(resources.RealmAccess) {
// This path should only be used in some tests. SkinInfo = skin.ToLive(resources.RealmAccess);
: skin.ToLiveUnmanaged();
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) if (configurationStream != null)
{
// stream will be closed after use by LineBufferedReader. // stream will be closed after use by LineBufferedReader.
ParseConfigurationStream(configurationStream); ParseConfigurationStream(configurationStream);
Debug.Assert(Configuration != null);
}
else else
Configuration = new SkinConfiguration(); Configuration = new SkinConfiguration();
// skininfo files may be null for default skin. // 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. string filename = $"{skinnableTarget}.json";
foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
byte[]? bytes = storage?.Get(filename);
if (bytes == null)
continue;
try
{ {
string filename = $"{skinnableTarget}.json"; string jsonContent = Encoding.UTF8.GetString(bytes);
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
// skininfo files may be null for default skin. if (deserializedContent == null)
var fileInfo = s.Files.FirstOrDefault(f => f.Filename == filename);
if (fileInfo == null)
continue; continue;
byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath()); DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
if (bytes == null)
continue;
try
{
string jsonContent = Encoding.UTF8.GetString(bytes);
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
if (deserializedContent == null)
continue;
DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to load skin configuration.");
}
} }
}); catch (Exception ex)
{
Logger.Error(ex, "Failed to load skin configuration.");
}
}
} }
protected virtual void ParseConfigurationStream(Stream stream) protected virtual void ParseConfigurationStream(Stream stream)
@ -101,16 +126,6 @@ namespace osu.Game.Skinning
Configuration = new LegacySkinDecoder().Decode(reader); 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);
}
/// <summary> /// <summary>
/// Remove all stored customisations for the provided target. /// Remove all stored customisations for the provided target.
/// </summary> /// </summary>
@ -129,7 +144,7 @@ namespace osu.Game.Skinning
DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray();
} }
public virtual Drawable GetDrawableComponent(ISkinComponent component) public virtual Drawable? GetDrawableComponent(ISkinComponent component)
{ {
switch (component) switch (component)
{ {
@ -137,9 +152,23 @@ namespace osu.Game.Skinning
if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo)) if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo))
return null; return null;
var components = new List<Drawable>();
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 return new SkinnableTargetComponentsContainer
{ {
ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) Children = components,
}; };
} }
@ -168,6 +197,9 @@ namespace osu.Game.Skinning
return; return;
isDisposed = true; isDisposed = true;
Textures?.Dispose();
Samples?.Dispose();
} }
#endregion #endregion

View File

@ -96,12 +96,14 @@ namespace osu.Game.Tests.Beatmaps
AddStep("setup skins", () => AddStep("setup skins", () =>
{ {
userSkinInfo.Files.Clear(); 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); Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Files.Clear(); 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. // Need to refresh the cached skin source to refresh the skin resource store.
dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); 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 BeatmapInfo skinBeatmapInfo;
private readonly IResourceStore<byte[]> resourceStore;
private readonly IStorageResourceProvider resources; private readonly IStorageResourceProvider resources;
public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources) public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> accessMarkingResourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock,
IStorageResourceProvider resources)
: base(beatmap, storyboard, referenceClock, resources.AudioManager) : base(beatmap, storyboard, referenceClock, resources.AudioManager)
{ {
this.skinBeatmapInfo = skinBeatmapInfo; this.skinBeatmapInfo = skinBeatmapInfo;
this.resourceStore = resourceStore; Files = accessMarkingResourceStore;
this.resources = resources; 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<byte[]> Files { get; }
public IResourceStore<byte[]> Resources => resources.Resources;
public RealmAccess RealmAccess => resources.RealmAccess;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => resources.CreateTextureLoaderStore(underlyingStore);
} }
} }
} }

View File

@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -112,7 +111,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod;
public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours)
: base(beatmapInfo, new ResourceStore<byte[]>(), null) : base(beatmapInfo, null)
{ {
if (hasColours) if (hasColours)
{ {
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan;
public TestSkin(bool hasCustomColours) public TestSkin(bool hasCustomColours)
: base(new SkinInfo(), new ResourceStore<byte[]>(), null, string.Empty) : base(new SkinInfo(), null, null)
{ {
if (hasCustomColours) if (hasCustomColours)
{ {

View File

@ -115,11 +115,13 @@ namespace osu.Game.Tests.Visual
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
headlessHostStorage = (parent.Get<GameHost>() as HeadlessGameHost)?.Storage; var host = parent.Get<GameHost>();
headlessHostStorage = (host as HeadlessGameHost)?.Storage;
Resources = parent.Get<OsuGameBase>().Resources; Resources = parent.Get<OsuGameBase>().Resources;
realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, "client")); realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, "client", host.UpdateThread));
RecycleLocalStorage(false); RecycleLocalStorage(false);

View File

@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual
}, },
new OsuSpriteText new OsuSpriteText
{ {
Text = skin?.SkinInfo?.Value.Name ?? "none", Text = skin?.SkinInfo.Value.Name ?? "none",
Scale = new Vector2(1.5f), Scale = new Vector2(1.5f),
Padding = new MarginPadding(5), Padding = new MarginPadding(5),
}, },
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual
private readonly bool extrapolateAnimations; private readonly bool extrapolateAnimations;
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations) public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations)
: base(skin, storage, resources, "skin.ini") : base(skin, resources, storage)
{ {
this.extrapolateAnimations = extrapolateAnimations; this.extrapolateAnimations = extrapolateAnimations;
} }

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="Realm" Version="10.10.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.314.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.325.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
<PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="Sentry" Version="3.14.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -61,8 +61,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.314.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.325.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.314.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.325.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />