1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 13:22:55 +08:00

Merge branch 'master' into realtime-leaderboard

This commit is contained in:
Bartłomiej Dach 2020-12-23 09:56:47 +01:00 committed by GitHub
commit 64095307de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 675 additions and 226 deletions

View File

@ -157,10 +157,16 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects)
{
h.Position = new Vector2(
quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
);
var newPosition = h.Position;
// guard against no-ops and NaN.
if (scale.X != 0 && quad.Width > 0)
newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X);
if (scale.Y != 0 && quad.Height > 0)
newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y);
h.Position = newPosition;
}
}

View File

@ -10,9 +10,11 @@ using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -27,7 +29,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneStoryboardSamples : OsuTestScene
public class TestSceneStoryboardSamples : OsuTestScene, IStorageResourceProvider
{
[Test]
public void TestRetrieveTopLevelSample()
@ -35,7 +37,7 @@ namespace osu.Game.Tests.Gameplay
ISkin skin = null;
SampleChannel channel = null;
AddStep("create skin", () => skin = new TestSkin("test-sample", Audio));
AddStep("create skin", () => skin = new TestSkin("test-sample", this));
AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample")));
AddAssert("sample is non-null", () => channel != null);
@ -47,7 +49,7 @@ namespace osu.Game.Tests.Gameplay
ISkin skin = null;
SampleChannel channel = null;
AddStep("create skin", () => skin = new TestSkin("folder/test-sample", Audio));
AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this));
AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample")));
AddAssert("sample is non-null", () => channel != null);
@ -105,7 +107,7 @@ namespace osu.Game.Tests.Gameplay
AddStep("setup storyboard sample", () =>
{
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this);
SelectedMods.Value = new[] { testedMod };
var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
@ -128,8 +130,8 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, AudioManager audioManager)
: base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini")
public TestSkin(string resourceName, IStorageResourceProvider resources)
: base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini")
{
}
}
@ -158,15 +160,15 @@ namespace osu.Game.Tests.Gameplay
private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
{
private readonly AudioManager audio;
private readonly IStorageResourceProvider resources;
public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio)
: base(ruleset, null, audio)
public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, IStorageResourceProvider resources)
: base(ruleset, null, resources.AudioManager)
{
this.audio = audio;
this.resources = resources;
}
protected override ISkin GetSkin() => new TestSkin("test-sample", audio);
protected override ISkin GetSkin() => new TestSkin("test-sample", resources);
}
private class TestDrawableStoryboardSample : DrawableStoryboardSample
@ -176,5 +178,13 @@ namespace osu.Game.Tests.Gameplay
{
}
}
#region IResourceStorageProvider
public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
#endregion
}
}

View File

@ -8,7 +8,7 @@ using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneParticipantsList : MultiplayerTestScene
public class TestSceneTimeshiftParticipantsList : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
@ -20,6 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Room.RecentParticipants.Add(new User
{
Username = "peppy",
CurrentModeRank = 1234,
Id = 2
});
}

View File

@ -55,8 +55,14 @@ namespace osu.Game.Tests.Visual.Navigation
var secondimport = importBeatmap(3);
presentAndConfirm(secondimport);
// Test presenting same beatmap more than once
presentAndConfirm(secondimport);
presentSecondDifficultyAndConfirm(firstImport, 1);
presentSecondDifficultyAndConfirm(secondimport, 3);
// Test presenting same beatmap more than once
presentSecondDifficultyAndConfirm(secondimport, 3);
}
[Test]

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
using osu.Game.Users;
@ -13,7 +14,7 @@ using osuTK;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneParticipantsList : RealtimeMultiplayerTestScene
public class TestSceneRealtimeMultiplayerParticipantsList : RealtimeMultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
@ -65,13 +66,13 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
[Test]
public void TestToggleReadyState()
{
AddAssert("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddAssert("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready));
AddUntilStep("ready mark visible", () => this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddUntilStep("ready mark visible", () => this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
}
[Test]
@ -104,11 +105,11 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
Id = i,
Username = $"User {i}",
CurrentModeRank = RNG.Next(1, 100000),
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
if (i % 2 == 0)
Client.ChangeUserState(i, MultiplayerUserState.Ready);
Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1));
}
});
}

View File

@ -0,0 +1,211 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Visual.Navigation;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneBeatmapRecommendations : OsuGameTestScene
{
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("register request handling", () =>
{
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(getUser(userRequest.Ruleset.ID));
break;
}
};
});
base.SetUpSteps();
User getUser(int? rulesetID)
{
return new User
{
Username = @"Dummy",
Id = 1001,
Statistics = new UserStatistics
{
PP = getNecessaryPP(rulesetID)
}
};
}
decimal getNecessaryPP(int? rulesetID)
{
switch (rulesetID)
{
case 0:
return 336; // recommended star rating of 2
case 1:
return 928; // SR 3
case 2:
return 1905; // SR 4
case 3:
return 3329; // SR 5
default:
return 0;
}
}
}
[Test]
public void TestPresentedBeatmapIsRecommended()
{
List<BeatmapSetInfo> beatmapSets = null;
const int import_count = 5;
AddStep("import 5 maps", () =>
{
beatmapSets = new List<BeatmapSetInfo>();
for (int i = 0; i < import_count; ++i)
{
beatmapSets.Add(importBeatmapSet(i, Enumerable.Repeat(new OsuRuleset().RulesetInfo, 5)));
}
});
AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(beatmapSets));
presentAndConfirm(() => beatmapSets[3], 2);
}
[Test]
public void TestCurrentRulesetIsRecommended()
{
BeatmapSetInfo catchSet = null, mixedSet = null;
AddStep("create catch beatmapset", () => catchSet = importBeatmapSet(0, new[] { new CatchRuleset().RulesetInfo }));
AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1,
new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new ManiaRuleset().RulesetInfo }));
AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { catchSet, mixedSet }));
// Switch to catch
presentAndConfirm(() => catchSet, 1);
// Present mixed difficulty set, expect current ruleset to be selected
presentAndConfirm(() => mixedSet, 2);
}
[Test]
public void TestBestRulesetIsRecommended()
{
BeatmapSetInfo osuSet = null, mixedSet = null;
AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo }));
AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1,
new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new ManiaRuleset().RulesetInfo }));
AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, mixedSet }));
// Make sure we are on standard ruleset
presentAndConfirm(() => osuSet, 1);
// Present mixed difficulty set, expect ruleset with highest star difficulty
presentAndConfirm(() => mixedSet, 3);
}
[Test]
public void TestSecondBestRulesetIsRecommended()
{
BeatmapSetInfo osuSet = null, mixedSet = null;
AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo }));
AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1,
new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new TaikoRuleset().RulesetInfo }));
AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, mixedSet }));
// Make sure we are on standard ruleset
presentAndConfirm(() => osuSet, 1);
// Present mixed difficulty set, expect ruleset with second highest star difficulty
presentAndConfirm(() => mixedSet, 2);
}
[Test]
public void TestCorrectStarRatingIsUsed()
{
BeatmapSetInfo osuSet = null, maniaSet = null;
AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo }));
AddStep("create mania beatmapset", () => maniaSet = importBeatmapSet(1, Enumerable.Repeat(new ManiaRuleset().RulesetInfo, 10)));
AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, maniaSet }));
// Make sure we are on standard ruleset
presentAndConfirm(() => osuSet, 1);
// Present mania set, expect the difficulty that matches recommended mania star rating
presentAndConfirm(() => maniaSet, 5);
}
private BeatmapSetInfo importBeatmapSet(int importID, IEnumerable<RulesetInfo> difficultyRulesets)
{
var metadata = new BeatmapMetadata
{
Artist = "SomeArtist",
AuthorString = "SomeAuthor",
Title = $"import {importID}"
};
var beatmapSet = new BeatmapSetInfo
{
Hash = Guid.NewGuid().ToString(),
OnlineBeatmapSetID = importID,
Metadata = metadata,
Beatmaps = difficultyRulesets.Select((ruleset, difficultyIndex) => new BeatmapInfo
{
OnlineBeatmapID = importID * 1024 + difficultyIndex,
Metadata = metadata,
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
StarDifficulty = difficultyIndex + 1,
Version = $"SR{difficultyIndex + 1}"
}).ToList()
};
return Game.BeatmapManager.Import(beatmapSet).Result;
}
private bool ensureAllBeatmapSetsImported(IEnumerable<BeatmapSetInfo> beatmapSets) => beatmapSets.All(set => set != null);
private void presentAndConfirm(Func<BeatmapSetInfo> getImport, int expectedDiff)
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
AddUntilStep("recommended beatmap displayed", () =>
{
int? expectedID = getImport().Beatmaps[expectedDiff - 1].OnlineBeatmapID;
return Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == expectedID;
});
}
}
}

View File

@ -16,6 +16,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -28,8 +29,8 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Users;
using osu.Game.Skinning;
using osu.Game.Users;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Beatmaps
@ -38,7 +39,7 @@ namespace osu.Game.Beatmaps
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
[ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable, IBeatmapResourceProvider
{
/// <summary>
/// Fired when a single difficulty has been hidden.
@ -68,9 +69,12 @@ namespace osu.Game.Beatmaps
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager;
private readonly TextureStore textureStore;
private readonly LargeTextureStore largeTextureStore;
private readonly ITrackStore trackStore;
[CanBeNull]
private readonly GameHost host;
[CanBeNull]
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
@ -80,6 +84,7 @@ namespace osu.Game.Beatmaps
{
this.rulesets = rulesets;
this.audioManager = audioManager;
this.host = host;
DefaultBeatmap = defaultBeatmap;
@ -92,7 +97,7 @@ namespace osu.Game.Beatmaps
if (performOnlineLookups)
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
trackStore = audioManager.GetTrackStore(Files.Store);
}
@ -302,7 +307,7 @@ namespace osu.Game.Beatmaps
beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager));
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
return working;
}
@ -492,6 +497,16 @@ namespace osu.Game.Beatmaps
onlineLookupQueue?.Dispose();
}
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
AudioManager IStorageResourceProvider.AudioManager => audioManager;
IResourceStore<byte[]> IStorageResourceProvider.Files => Files.Store;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
#endregion
/// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary>

View File

@ -2,11 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
@ -21,16 +20,13 @@ namespace osu.Game.Beatmaps
[ExcludeFromDynamicCompile]
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
private readonly IResourceStore<byte[]> store;
private readonly TextureStore textureStore;
private readonly ITrackStore trackStore;
[NotNull]
private readonly IBeatmapResourceProvider resources;
public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager)
: base(beatmapInfo, audioManager)
public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources)
: base(beatmapInfo, resources.AudioManager)
{
this.store = store;
this.textureStore = textureStore;
this.trackStore = trackStore;
this.resources = resources;
}
protected override IBeatmap GetBeatmap()
@ -40,7 +36,7 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
catch (Exception e)
@ -61,7 +57,7 @@ namespace osu.Game.Beatmaps
try
{
return textureStore.Get(getPathForFile(Metadata.BackgroundFile));
return resources.LargeTextureStore.Get(getPathForFile(Metadata.BackgroundFile));
}
catch (Exception e)
{
@ -77,7 +73,7 @@ namespace osu.Game.Beatmaps
try
{
return trackStore.Get(getPathForFile(Metadata.AudioFile));
return resources.Tracks.Get(getPathForFile(Metadata.AudioFile));
}
catch (Exception e)
{
@ -93,7 +89,7 @@ namespace osu.Game.Beatmaps
try
{
var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
var trackData = resources.Files.GetStream(getPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData);
}
catch (Exception e)
@ -109,7 +105,7 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path))))
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
@ -118,7 +114,7 @@ namespace osu.Game.Beatmaps
storyboard = decoder.Decode(stream);
else
{
using (var secondaryStream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
using (var secondaryStream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
storyboard = decoder.Decode(stream, secondaryStream);
}
}
@ -138,7 +134,7 @@ namespace osu.Game.Beatmaps
{
try
{
return new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager);
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
}
catch (Exception e)
{

View File

@ -4,17 +4,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
namespace osu.Game.Screens.Select
namespace osu.Game.Beatmaps
{
/// <summary>
/// A class which will recommend the most suitable difficulty for the local user from a beatmap set.
/// This requires the user to be logged in, as it sources from the user's online profile.
/// </summary>
public class DifficultyRecommender : Component
{
[Resolved]
@ -26,7 +30,12 @@ namespace osu.Game.Screens.Select
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
private readonly Dictionary<RulesetInfo, double> recommendedStarDifficulty = new Dictionary<RulesetInfo, double>();
/// <summary>
/// The user for which the last requests were run.
/// </summary>
private int? requestedUserId;
private readonly Dictionary<RulesetInfo, double> recommendedDifficultyMapping = new Dictionary<RulesetInfo, double>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
@ -45,42 +54,64 @@ namespace osu.Game.Screens.Select
/// </remarks>
/// <param name="beatmaps">A collection of beatmaps to select a difficulty from.</param>
/// <returns>The recommended difficulty, or null if a recommendation could not be provided.</returns>
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
{
if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars))
foreach (var r in orderedRulesets)
{
return beatmaps.OrderBy(b =>
if (!recommendedDifficultyMapping.TryGetValue(r, out var recommendation))
continue;
BeatmapInfo beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
{
var difference = b.StarDifficulty - stars;
var difference = b.StarDifficulty - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
}).FirstOrDefault();
if (beatmap != null)
return beatmap;
}
return null;
}
private void calculateRecommendedDifficulties()
private void fetchRecommendedValues()
{
rulesets.AvailableRulesets.ForEach(rulesetInfo =>
if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId)
return;
requestedUserId = api.LocalUser.Value.Id;
// only query API for built-in rulesets
rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo =>
{
var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
req.Success += result =>
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
};
api.Queue(req);
});
}
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<RulesetInfo> orderedRulesets =>
recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
switch (state.NewValue)
{
case APIState.Online:
calculateRecommendedDifficulties();
fetchRecommendedValues();
break;
}
});

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.IO;
namespace osu.Game.Beatmaps
{
public interface IBeatmapResourceProvider : IStorageResourceProvider
{
/// <summary>
/// Retrieve a global large texture store, used for loading beatmap backgrounds.
/// </summary>
TextureStore LargeTextureStore { get; }
/// <summary>
/// Access a global track store for retrieving beatmap tracks from.
/// </summary>
ITrackStore Tracks { get; }
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
namespace osu.Game.IO
{
public interface IStorageResourceProvider
{
/// <summary>
/// Retrieve the game-wide audio manager.
/// </summary>
AudioManager AudioManager { get; }
/// <summary>
/// Access game-wide user files.
/// </summary>
IResourceStore<byte[]> Files { get; }
/// <summary>
/// Create a texture loader store based on an underlying data store.
/// </summary>
/// <param name="underlyingStore">The underlying provider of texture data (in arbitrary image formats).</param>
/// <returns>A texture loader store.</returns>
IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore);
}
}

View File

@ -9,14 +9,14 @@ namespace osu.Game.Online.API.Requests
public class GetUserRequest : APIRequest<User>
{
private readonly long? userId;
private readonly RulesetInfo ruleset;
public readonly RulesetInfo Ruleset;
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{
this.userId = userId;
this.ruleset = ruleset;
Ruleset = ruleset;
}
protected override string Target => userId.HasValue ? $@"users/{userId}/{ruleset?.ShortName}" : $@"me/{ruleset?.ShortName}";
protected override string Target => userId.HasValue ? $@"users/{userId}/{Ruleset?.ShortName}" : $@"me/{Ruleset?.ShortName}";
}
}

View File

@ -80,6 +80,9 @@ namespace osu.Game
private BeatmapSetOverlay beatmapSetOverlay;
[Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
[Cached]
private readonly ScreenshotManager screenshotManager = new ScreenshotManager();
@ -335,15 +338,17 @@ namespace osu.Game
/// The user should have already requested this interactively.
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
/// <param name="difficultyCriteria">
/// Optional predicate used to try and find a difficulty to select.
/// If omitted, this will try to present the first beatmap from the current ruleset.
/// In case of failure the first difficulty of the set will be presented, ignoring the predicate.
/// </param>
/// <param name="difficultyCriteria">Optional predicate used to narrow the set of difficulties to select from when presenting.</param>
/// <remarks>
/// Among items satisfying the predicate, the order of preference is:
/// <list type="bullet">
/// <item>beatmap with recommended difficulty, as provided by <see cref="DifficultyRecommender"/>,</item>
/// <item>first beatmap from the current ruleset,</item>
/// <item>first beatmap from any ruleset.</item>
/// </list>
/// </remarks>
public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate<BeatmapInfo> difficultyCriteria = null)
{
difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value);
var databasedSet = beatmap.OnlineBeatmapSetID != null
? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID)
: BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash);
@ -361,16 +366,23 @@ namespace osu.Game
menuScreen.LoadToSolo();
// we might even already be at the song
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo))
{
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true))
return;
}
// Find first beatmap that matches our predicate.
var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First();
// Find beatmaps that match our predicate.
var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList();
Ruleset.Value = first.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first);
// Use all beatmaps if predicate matched nothing
if (beatmaps.Count == 0)
beatmaps = databasedSet.Beatmaps;
// Prefer recommended beatmap if recommendations are available, else fallback to a sane selection.
var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps)
?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value))
?? beatmaps.First();
Ruleset.Value = selection.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
}, validScreens: new[] { typeof(PlaySongSelect) });
}
@ -630,6 +642,8 @@ namespace osu.Game
GetStableStorage = GetStorageForStableInstall
}, Add, true);
loadComponentSingleFile(difficultyRecommender, Add);
loadComponentSingleFile(screenshotManager, Add);
// dependency on notification overlay, dependent by settings overlay

View File

@ -1,7 +1,7 @@
// 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;
using System.Drawing;
using System.Linq;
using osu.Framework.Allocation;
@ -25,9 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private FillFlowContainer<SettingsSlider<float>> scalingSettings;
private readonly IBindable<Display> currentDisplay = new Bindable<Display>();
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private Bindable<ScalingMode> scalingMode;
private Bindable<Size> sizeFullscreen;
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
[Resolved]
private OsuGameBase game { get; set; }
@ -53,9 +57,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
if (host.Window != null)
{
currentDisplay.BindTo(host.Window.CurrentDisplayBindable);
windowModes.BindTo(host.Window.SupportedWindowModes);
Container resolutionSettingsContainer;
}
Children = new Drawable[]
{
@ -65,10 +70,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
ItemSource = windowModes,
Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
},
resolutionSettingsContainer = new Container
resolutionDropdown = new ResolutionSettingsDropdown
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
LabelText = "Resolution",
ShowsDefaultIndicator = false,
ItemSource = resolutions,
Current = sizeFullscreen
},
new SettingsSlider<float, UIScaleSlider>
{
@ -126,32 +133,34 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
},
};
scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
var resolutions = getResolutions();
if (resolutions.Count > 1)
windowModes.BindCollectionChanged((sender, args) =>
{
resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown
{
LabelText = "Resolution",
ShowsDefaultIndicator = false,
Items = resolutions,
Current = sizeFullscreen
};
windowModeDropdown.Current.BindValueChanged(mode =>
{
if (mode.NewValue == WindowMode.Fullscreen)
{
resolutionDropdown.Show();
sizeFullscreen.TriggerChange();
}
if (windowModes.Count > 1)
windowModeDropdown.Show();
else
resolutionDropdown.Hide();
windowModeDropdown.Hide();
}, true);
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
currentDisplay.BindValueChanged(display => Schedule(() =>
{
resolutions.RemoveRange(1, resolutions.Count - 1);
if (display.NewValue != null)
{
resolutions.AddRange(display.NewValue.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
.Select(m => m.Size)
.Distinct());
}
updateResolutionDropdown();
}), true);
scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
scalingMode.BindValueChanged(mode =>
{
scalingSettings.ClearTransforms();
@ -163,17 +172,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything);
}, true);
windowModes.CollectionChanged += (sender, args) => windowModesChanged();
windowModesChanged();
}
private void windowModesChanged()
void updateResolutionDropdown()
{
if (windowModes.Count > 1)
windowModeDropdown.Show();
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
resolutionDropdown.Show();
else
windowModeDropdown.Hide();
resolutionDropdown.Hide();
}
}
/// <summary>
@ -205,24 +210,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
preview.Expire();
}
private IReadOnlyList<Size> getResolutions()
{
var resolutions = new List<Size> { new Size(9999, 9999) };
var currentDisplay = game.Window?.CurrentDisplayBindable.Value;
if (currentDisplay != null)
{
resolutions.AddRange(currentDisplay.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => m.Size.Width)
.ThenByDescending(m => m.Size.Height)
.Select(m => m.Size)
.Distinct());
}
return resolutions;
}
private class ScalingPreview : ScalingContainer
{
public ScalingPreview()

View File

@ -5,6 +5,8 @@ namespace osu.Game.Rulesets
{
public interface ILegacyRuleset
{
const int MAX_LEGACY_RULESET_ID = 3;
/// <summary>
/// Identifies the server-side ID of a legacy ruleset.
/// </summary>

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
[Resolved]
private IAPIProvider api { get; set; }
private ReadyMark readyMark;
private StateDisplay userStateDisplay;
private SpriteIcon crown;
public ParticipantPanel(MultiplayerRoomUser user)
@ -122,12 +122,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
}
}
},
readyMark = new ReadyMark
userStateDisplay = new StateDisplay
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 10 },
Alpha = 0
}
}
}
@ -144,10 +143,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
const double fade_time = 50;
if (User.State == MultiplayerUserState.Ready)
readyMark.FadeIn(fade_time);
else
readyMark.FadeOut(fade_time);
userStateDisplay.Status = User.State;
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);

View File

@ -1,51 +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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ReadyMark : CompositeDrawable
{
public ReadyMark()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
Text = "ready",
Colour = Color4Extensions.FromHex("#DDFFFF")
},
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
Colour = Color4Extensions.FromHex("#AADD00")
}
}
};
}
}
}

View File

@ -0,0 +1,129 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.RealtimeMultiplayer;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class StateDisplay : CompositeDrawable
{
public StateDisplay()
{
AutoSizeAxes = Axes.Both;
Alpha = 0;
}
private MultiplayerUserState status;
private OsuSpriteText text;
private SpriteIcon icon;
private const double fade_time = 50;
public MultiplayerUserState Status
{
set
{
if (value == status)
return;
status = value;
if (IsLoaded)
updateStatus();
}
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Children = new Drawable[]
{
text = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
Colour = Color4Extensions.FromHex("#DDFFFF")
},
icon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateStatus();
}
[Resolved]
private OsuColour colours { get; set; }
private void updateStatus()
{
switch (status)
{
default:
this.FadeOut(fade_time);
return;
case MultiplayerUserState.Ready:
text.Text = "ready";
icon.Icon = FontAwesome.Solid.CheckCircle;
icon.Colour = Color4Extensions.FromHex("#AADD00");
break;
case MultiplayerUserState.WaitingForLoad:
text.Text = "loading";
icon.Icon = FontAwesome.Solid.PauseCircle;
icon.Colour = colours.Yellow;
break;
case MultiplayerUserState.Loaded:
text.Text = "loaded";
icon.Icon = FontAwesome.Solid.DotCircle;
icon.Colour = colours.YellowLight;
break;
case MultiplayerUserState.Playing:
text.Text = "playing";
icon.Icon = FontAwesome.Solid.PlayCircle;
icon.Colour = colours.BlueLight;
break;
case MultiplayerUserState.FinishedPlay:
text.Text = "results pending";
icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
icon.Colour = colours.BlueLighter;
break;
case MultiplayerUserState.Results:
text.Text = "results";
icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
icon.Colour = colours.BlueLighter;
break;
}
this.FadeIn(fade_time);
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
@ -41,7 +42,17 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
{
// this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join.
// should probably be done at a higher level, but due to the current structure of things this is the easiest place for now.
if (room.Status.Value is RoomStatusEnded)
{
onError?.Invoke("Cannot join an ended room.");
return;
}
base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
}
public override void PartRoom()
{

View File

@ -80,8 +80,6 @@ namespace osu.Game.Screens.Select
protected BeatmapCarousel Carousel { get; private set; }
private readonly DifficultyRecommender recommender = new DifficultyRecommender();
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@ -105,7 +103,7 @@ namespace osu.Game.Screens.Select
private MusicController music { get; set; }
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog)
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
{
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
@ -120,12 +118,11 @@ namespace osu.Game.Screens.Select
BleedBottom = Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
}, c => carouselContainer.Child = c);
AddRangeInternal(new Drawable[]
{
recommender,
new ResetScrollContainer(() => Carousel.ScrollToSelected())
{
RelativeSizeAxes = Axes.Y,

View File

@ -1,16 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.IO.Stores;
using osu.Game.IO;
using osuTK.Graphics;
namespace osu.Game.Skinning
{
public class DefaultLegacySkin : LegacySkin
{
public DefaultLegacySkin(IResourceStore<byte[]> storage, AudioManager audioManager)
: base(Info, storage, audioManager, string.Empty)
public DefaultLegacySkin(IResourceStore<byte[]> storage, IStorageResourceProvider resources)
: base(Info, storage, resources, string.Empty)
{
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
Configuration.AddComboColours(

View File

@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Skinning
@ -16,8 +16,8 @@ namespace osu.Game.Skinning
protected override bool AllowManiaSkin => false;
protected override bool UseCustomSampleBanks => true;
public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore<byte[]> storage, AudioManager audioManager)
: base(createSkinInfo(beatmap), new LegacySkinResourceStore<BeatmapSetFileInfo>(beatmap.BeatmapSet, storage), audioManager, beatmap.Path)
public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore<byte[]> storage, IStorageResourceProvider resources)
: base(createSkinInfo(beatmap), new LegacySkinResourceStore<BeatmapSetFileInfo>(beatmap.BeatmapSet, storage), resources, beatmap.Path)
{
// Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer)
Configuration.AllowDefaultComboColoursFallback = false;

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -54,12 +53,12 @@ namespace osu.Game.Skinning
private readonly Dictionary<int, LegacyManiaSkinConfiguration> maniaConfigurations = new Dictionary<int, LegacyManiaSkinConfiguration>();
public LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager)
: this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, storage), audioManager, "skin.ini")
public LegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, resources.Files), resources, "skin.ini")
{
}
protected LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager, string filename)
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string filename)
: base(skin)
{
using (var stream = storage?.GetStream(filename))
@ -85,12 +84,12 @@ namespace osu.Game.Skinning
if (storage != null)
{
var samples = audioManager?.GetSampleStore(storage);
var samples = resources?.AudioManager?.GetSampleStore(storage);
if (samples != null)
samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
Samples = samples;
Textures = new TextureStore(new TextureLoaderStore(storage));
Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage));
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
}

View File

@ -22,15 +22,18 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
namespace osu.Game.Skinning
{
[ExcludeFromDynamicCompile]
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>, ISkinSource
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>, ISkinSource, IStorageResourceProvider
{
private readonly AudioManager audio;
private readonly GameHost host;
private readonly IResourceStore<byte[]> legacyDefaultResources;
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>(new DefaultSkin());
@ -42,10 +45,12 @@ namespace osu.Game.Skinning
protected override string ImportFromStablePath => "Skins";
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio, IResourceStore<byte[]> legacyDefaultResources)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost)
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, AudioManager audio, IResourceStore<byte[]> legacyDefaultResources)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), host)
{
this.audio = audio;
this.host = host;
this.legacyDefaultResources = legacyDefaultResources;
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
@ -148,9 +153,9 @@ namespace osu.Game.Skinning
return new DefaultSkin();
if (skinInfo == DefaultLegacySkin.Info)
return new DefaultLegacySkin(legacyDefaultResources, audio);
return new DefaultLegacySkin(legacyDefaultResources, this);
return new LegacySkin(skinInfo, Files.Store, audio);
return new LegacySkin(skinInfo, this);
}
/// <summary>
@ -169,5 +174,13 @@ namespace osu.Game.Skinning
public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => CurrentSkin.Value.GetConfig<TLookup, TValue>(lookup);
#region IResourceStorageProvider
AudioManager IStorageResourceProvider.AudioManager => audio;
IResourceStore<byte[]> IStorageResourceProvider.Files => Files.Store;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
#endregion
}
}

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Timing;
@ -25,7 +26,7 @@ using osu.Game.Users;
namespace osu.Game.Tests.Beatmaps
{
[HeadlessTest]
public abstract class HitObjectSampleTest : PlayerTestScene
public abstract class HitObjectSampleTest : PlayerTestScene, IStorageResourceProvider
{
protected abstract IResourceStore<byte[]> Resources { get; }
protected LegacySkin Skin { get; private set; }
@ -58,7 +59,7 @@ namespace osu.Game.Tests.Beatmaps
protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap;
protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
=> new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, this);
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
@ -109,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps
};
// Need to refresh the cached skin source to refresh the skin resource store.
dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this));
});
}
@ -122,6 +123,14 @@ namespace osu.Game.Tests.Beatmaps
protected void AssertNoLookup(string name) => AddAssert($"\"{name}\" not looked up",
() => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && !userSkinResourceStore.PerformedLookups.Contains(name));
#region IResourceStorageProvider
public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => userSkinResourceStore;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
#endregion
private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
{
public ISkinSource SkinSource;
@ -191,14 +200,17 @@ namespace osu.Game.Tests.Beatmaps
private readonly BeatmapInfo skinBeatmapInfo;
private readonly IResourceStore<byte[]> resourceStore;
public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
: base(beatmap, storyboard, referenceClock, audio)
private readonly IStorageResourceProvider resources;
public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources)
: base(beatmap, storyboard, referenceClock, resources.AudioManager)
{
this.skinBeatmapInfo = skinBeatmapInfo;
this.resourceStore = resourceStore;
this.resources = resources;
}
protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager);
protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, resources);
}
}
}

View File

@ -3,7 +3,6 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.IO.Stores;
using osu.Game.Rulesets;
using osu.Game.Skinning;
@ -18,9 +17,9 @@ namespace osu.Game.Tests.Visual
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource);
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuGameBase game)
private void load(OsuGameBase game, SkinManager skins)
{
var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore<byte[]>(game.Resources, "Skins/Legacy"), audio);
var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore<byte[]>(game.Resources, "Skins/Legacy"), skins);
legacySkinSource = new SkinProvidingContainer(legacySkin);
}

View File

@ -13,8 +13,10 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using osuTK;
@ -22,13 +24,16 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual
{
public abstract class SkinnableTestScene : OsuGridTestScene
public abstract class SkinnableTestScene : OsuGridTestScene, IStorageResourceProvider
{
private Skin metricsSkin;
private Skin defaultSkin;
private Skin specialSkin;
private Skin oldSkin;
[Resolved]
private GameHost host { get; set; }
protected SkinnableTestScene()
: base(2, 3)
{
@ -39,10 +44,10 @@ namespace osu.Game.Tests.Visual
{
var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly);
metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/metrics_skin"), audio, true);
defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore<byte[]>(game.Resources, "Skins/Legacy"), audio);
specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/special_skin"), audio, true);
oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/old_skin"), audio, true);
metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/metrics_skin"), this, true);
defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore<byte[]>(game.Resources, "Skins/Legacy"), this);
specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/special_skin"), this, true);
oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/old_skin"), this, true);
}
private readonly List<Drawable> createdDrawables = new List<Drawable>();
@ -147,6 +152,14 @@ namespace osu.Game.Tests.Visual
protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value);
#region IResourceStorageProvider
public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
#endregion
private class OutlineBox : CompositeDrawable
{
public OutlineBox()
@ -170,8 +183,8 @@ namespace osu.Game.Tests.Visual
{
private readonly bool extrapolateAnimations;
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager, bool extrapolateAnimations)
: base(skin, storage, audioManager, "skin.ini")
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations)
: base(skin, storage, resources, "skin.ini")
{
this.extrapolateAnimations = extrapolateAnimations;
}