1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 08:12:56 +08:00

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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.314.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- 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)
// 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
{
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 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<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]
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<int, Ruleset> 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,
}
});
}

View File

@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database
Live<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
{
imported = await importer.Import(reader);
EnsureLoaded(realm.Realm);
}
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().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<BeatmapSetInfo>().First();

View File

@ -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))
{
}
}

View File

@ -1,12 +1,21 @@
// 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.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<TextureUpload> textureStore)
: 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)
{
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 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 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]
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));
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("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>();
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<MultiplayerReadyButton>();
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);
}
[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]
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]

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);
Action = () => game.GracefullyExit();
folderButton.Action = storage.PresentExternally;
folderButton.Action = () => storage.PresentExternally();
ButtonText = "Close osu!";
}

View File

@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{
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;
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()

View File

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

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#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
{
/// <summary>
@ -46,6 +47,8 @@ namespace osu.Game.Database
private readonly IDatabaseContextFactory? efContextFactory;
private readonly SynchronizationContext? updateThreadSyncContext;
/// <summary>
/// Version history:
/// 6 ~2021-10-18 First tracked version.
@ -143,12 +146,15 @@ namespace osu.Game.Database
/// </summary>
/// <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="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>
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<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
lock (realmLock)
{
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>
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> 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()
{

View File

@ -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)
{

View File

@ -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();
});

View File

@ -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>(RulesetStore = new RealmRulesetStore(realm, Storage));
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
{
Text = GeneralSettingsStrings.OpenOsuFolder,
Action = storage.PresentExternally,
Action = () => storage.PresentExternally(),
});
Add(new SettingsButton

View File

@ -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(',');

View File

@ -1,12 +1,15 @@
// 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.
#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;
}

View File

@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu
private readonly ButtonArea buttonArea;
private readonly Button backButton;
private readonly MainMenuButton backButton;
private readonly List<Button> buttonsTopLevel = new List<Button>();
private readonly List<Button> buttonsPlay = new List<Button>();
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private Sample sampleBack;
@ -100,8 +100,8 @@ namespace osu.Game.Screens.Menu
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),
backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
{
VisibleState = ButtonSystemState.Play,
},
@ -126,24 +126,24 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)]
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 Button(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.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
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 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);
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 Button(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.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
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 MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
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(buttonsTopLevel);
buttonArea.ForEach(b =>
{
if (b is Button)
if (b is MainMenuButton)
{
b.Origin = Anchor.CentreLeft;
b.Anchor = Anchor.CentreLeft;
@ -305,7 +305,7 @@ namespace osu.Game.Screens.Menu
{
buttonArea.ButtonSystemState = state;
foreach (var b in buttonArea.Children.OfType<Button>())
foreach (var b in buttonArea.Children.OfType<MainMenuButton>())
b.ButtonSystemState = state;
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.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).
/// </summary>
public class Button : BeatSyncedContainer, IStateful<ButtonState>
public class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
{
public event Action<ButtonState> 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)

View File

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

View File

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

View File

@ -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();

View File

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

View File

@ -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<ParticipantPanel> 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);
}
}
}
}
}

View File

@ -30,11 +30,9 @@ namespace osu.Game.Skinning
public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: base(
skin,
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"),
resources,
// A default legacy skin may still have a skin.ini if it is modified by the user.
// We must specify the stream directly as we are redirecting storage to the osu-resources location for other files.
new 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<byte[]>(resources.Resources, "Skins/Legacy") : null
)
{
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.
// 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
/// </summary>
/// <param name="component">The requested component.</param>
/// <returns>A drawable representation for the requested component, or null if unavailable.</returns>
[CanBeNull]
Drawable GetDrawableComponent(ISkinComponent component);
Drawable? GetDrawableComponent(ISkinComponent component);
/// <summary>
/// Retrieve a <see cref="Texture"/>.
/// </summary>
/// <param name="componentName">The requested texture.</param>
/// <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>
/// 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="wrapModeT">The texture wrap mode in vertical direction.</param>
/// <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>
/// Retrieve a <see cref="SampleChannel"/>.
/// </summary>
/// <param name="sampleInfo">The requested sample.</param>
/// <returns>A matching sample channel, or null if unavailable.</returns>
[CanBeNull]
ISample GetSample(ISampleInfo sampleInfo);
ISample? GetSample(ISampleInfo sampleInfo);
/// <summary>
/// Retrieve a configuration value.
/// </summary>
/// <param name="lookup">The requested configuration value.</param>
/// <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.
// 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<byte[]> storage, IStorageResourceProvider resources)
: base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path)
/// <summary>
/// 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)
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)
{
@ -46,7 +63,7 @@ namespace osu.Game.Skinning
return base.GetDrawableComponent(component);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{
switch (lookup)
{
@ -62,10 +79,10 @@ namespace osu.Game.Skinning
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);
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
};
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#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;
/// <summary>
/// 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.
/// </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="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>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
: this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename))
protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini")
: 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.
hasKeyTexture = new Lazy<bool>(() => this.GetAnimation(
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)
{
@ -156,7 +129,7 @@ namespace osu.Game.Skinning
return null;
}
private IBindable<TValue> lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup)
private IBindable<TValue>? lookupForMania<TValue>(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
/// <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="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];
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;
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;
[CanBeNull]
private IBindable<TValue> legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
private IBindable<TValue>? legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
where TValue : notnull
{
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
{
@ -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<HitResult> resultComponent:
// 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.
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<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").
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 osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.Extensions;
namespace osu.Game.Skinning
{
public class LegacyDatabasedSkinResourceStore : ResourceStore<byte[]>
public class RealmBackedResourceStore : ResourceStore<byte[]>
{
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)
{
// 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)

View File

@ -46,7 +46,10 @@ namespace osu.Game.Skinning
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()
{

View File

@ -1,22 +1,24 @@
// 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.
#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
{
/// <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;
private readonly IStorageResourceProvider resources;
public SkinConfiguration Configuration { get; set; }
@ -33,66 +44,80 @@ namespace osu.Game.Skinning
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
? 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<IEnumerable<SkinnableInfo>>(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<IEnumerable<SkinnableInfo>>(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);
}
/// <summary>
/// Remove all stored customisations for the provided target.
/// </summary>
@ -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<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
{
ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance())
Children = components,
};
}
@ -168,6 +197,9 @@ namespace osu.Game.Skinning
return;
isDisposed = true;
Textures?.Dispose();
Samples?.Dispose();
}
#endregion

View File

@ -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<byte[]> resourceStore;
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)
{
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<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.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<byte[]>(), 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<byte[]>(), null, string.Empty)
: base(new SkinInfo(), null, null)
{
if (hasCustomColours)
{

View File

@ -115,11 +115,13 @@ namespace osu.Game.Tests.Visual
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;
realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, "client"));
realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, "client", host.UpdateThread));
RecycleLocalStorage(false);

View File

@ -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<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations)
: base(skin, storage, resources, "skin.ini")
: base(skin, resources, storage)
{
this.extrapolateAnimations = extrapolateAnimations;
}

View File

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

View File

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