mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 14:53:21 +08:00
Merge branch 'master' into multiplayer-delayed-playlist-load-broken
This commit is contained in:
commit
52230a6f00
@ -10,3 +10,6 @@ T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal
|
||||
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
||||
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1127.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1203.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1207.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. -->
|
||||
|
@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// A difficulty calculation request from the legacy client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synchronise any changes with osu!stable.
|
||||
/// </remarks>
|
||||
public class LegacyIpcDifficultyCalculationRequest
|
||||
{
|
||||
public string BeatmapFile { get; set; }
|
||||
public int RulesetId { get; set; }
|
||||
public int Mods { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +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.
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// A difficulty calculation response returned to the legacy client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synchronise any changes with osu!stable.
|
||||
/// </remarks>
|
||||
public class LegacyIpcDifficultyCalculationResponse
|
||||
{
|
||||
public double StarRating { get; set; }
|
||||
}
|
||||
}
|
53
osu.Desktop/LegacyIpc/LegacyIpcMessage.cs
Normal file
53
osu.Desktop/LegacyIpc/LegacyIpcMessage.cs
Normal file
@ -0,0 +1,53 @@
|
||||
// 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.Platform;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IpcMessage"/> that can be used to communicate to and from legacy clients.
|
||||
/// <para>
|
||||
/// In order to deserialise types at either end, types must be serialised as their <see cref="System.Type.AssemblyQualifiedName"/>,
|
||||
/// however this cannot be done since osu!stable and osu!lazer live in two different assemblies.
|
||||
/// <br />
|
||||
/// To get around this, this class exists which serialises a payload (<see cref="LegacyIpcMessage.Data"/>) as an <see cref="System.Object"/> type,
|
||||
/// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib).
|
||||
/// The payload contains the data to be sent over the IPC channel.
|
||||
/// <br />
|
||||
/// At either end, Json.NET deserialises the payload into a <see cref="JObject"/> which is manually converted back into the expected <see cref="LegacyIpcMessage.Data"/> type,
|
||||
/// which then further contains another <see cref="JObject"/> representing the data sent over the IPC channel whose type can likewise be lazily matched through
|
||||
/// <see cref="LegacyIpcMessage.Data.MessageType"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synchronise any changes with osu-stable.
|
||||
/// </remarks>
|
||||
public class LegacyIpcMessage : IpcMessage
|
||||
{
|
||||
public LegacyIpcMessage()
|
||||
{
|
||||
// Types/assemblies are not inter-compatible, so always serialise/deserialise into objects.
|
||||
base.Type = typeof(object).FullName;
|
||||
}
|
||||
|
||||
public new string Type => base.Type; // Hide setter.
|
||||
|
||||
public new object Value
|
||||
{
|
||||
get => base.Value;
|
||||
set => base.Value = new Data
|
||||
{
|
||||
MessageType = value.GetType().Name,
|
||||
MessageData = value
|
||||
};
|
||||
}
|
||||
|
||||
public class Data
|
||||
{
|
||||
public string MessageType { get; set; }
|
||||
public object MessageData { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
121
osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
Normal file
121
osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
Normal file
@ -0,0 +1,121 @@
|
||||
// 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.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides IPC to legacy osu! clients.
|
||||
/// </summary>
|
||||
public class LegacyTcpIpcProvider : TcpIpcProvider
|
||||
{
|
||||
private static readonly Logger logger = Logger.GetLogger("legacy-ipc");
|
||||
|
||||
public LegacyTcpIpcProvider()
|
||||
: base(45357)
|
||||
{
|
||||
MessageReceived += msg =>
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Add("Processing legacy IPC message...");
|
||||
logger.Add($" {msg.Value}", LogLevel.Debug);
|
||||
|
||||
// See explanation in LegacyIpcMessage for why this is done this way.
|
||||
var legacyData = ((JObject)msg.Value).ToObject<LegacyIpcMessage.Data>();
|
||||
object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType);
|
||||
|
||||
return new LegacyIpcMessage
|
||||
{
|
||||
Value = onLegacyIpcMessageReceived(value)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private object parseObject(JObject value, string type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case nameof(LegacyIpcDifficultyCalculationRequest):
|
||||
return value.ToObject<LegacyIpcDifficultyCalculationRequest>()
|
||||
?? throw new InvalidOperationException($"Failed to parse request {value}");
|
||||
|
||||
case nameof(LegacyIpcDifficultyCalculationResponse):
|
||||
return value.ToObject<LegacyIpcDifficultyCalculationResponse>()
|
||||
?? throw new InvalidOperationException($"Failed to parse request {value}");
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported object type {type}");
|
||||
}
|
||||
}
|
||||
|
||||
private object onLegacyIpcMessageReceived(object message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case LegacyIpcDifficultyCalculationRequest req:
|
||||
try
|
||||
{
|
||||
var ruleset = getLegacyRulesetFromID(req.RulesetId);
|
||||
|
||||
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
|
||||
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset);
|
||||
|
||||
return new LegacyIpcDifficultyCalculationResponse
|
||||
{
|
||||
StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new LegacyIpcDifficultyCalculationResponse();
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported message type {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Ruleset getLegacyRulesetFromID(int rulesetId)
|
||||
{
|
||||
switch (rulesetId)
|
||||
{
|
||||
case 0:
|
||||
return new OsuRuleset();
|
||||
|
||||
case 1:
|
||||
return new TaikoRuleset();
|
||||
|
||||
case 2:
|
||||
return new CatchRuleset();
|
||||
|
||||
case 3:
|
||||
return new ManiaRuleset();
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Invalid ruleset id");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
@ -18,8 +19,10 @@ namespace osu.Desktop
|
||||
{
|
||||
private const string base_game_name = @"osu";
|
||||
|
||||
private static LegacyTcpIpcProvider legacyIpc;
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
@ -69,14 +72,29 @@ namespace osu.Desktop
|
||||
throw new TimeoutException(@"IPC took too long to send");
|
||||
}
|
||||
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// we want to allow multiple instances to be started when in debug.
|
||||
if (!DebugUtils.IsDebugBuild)
|
||||
{
|
||||
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (host.IsPrimaryInstance)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Log("Starting legacy IPC provider...");
|
||||
legacyIpc = new LegacyTcpIpcProvider();
|
||||
legacyIpc.Bind();
|
||||
legacyIpc.StartAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Failed to start legacy IPC provider");
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,8 +102,6 @@ namespace osu.Desktop
|
||||
host.Run(new TournamentGame());
|
||||
else
|
||||
host.Run(new OsuGameDesktop(args));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
|
||||
{
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.CreateInfo()));
|
||||
var testSkinProvider = new SkinProvidingContainer(skin);
|
||||
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
|
||||
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestDefaultSkin()
|
||||
{
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLive());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacySkin()
|
||||
{
|
||||
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
|
||||
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLive());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<WorkingBeatmap> working { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
|
@ -7,16 +7,12 @@ using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class ManiaSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
|
@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public JudgementResult Result { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; }
|
||||
|
||||
private SkinnableDrawable skinnableExplosion;
|
||||
|
||||
public PoolableHitExplosion()
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
AddStep("create slider", () =>
|
||||
{
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
|
||||
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
|
||||
|
||||
Child = new SkinProvidingContainer(tintingSkin)
|
||||
|
@ -12,7 +12,6 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -149,9 +148,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
centreHit.Colour = colours.Pink;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayClock gameplayClock { get; set; }
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
{
|
||||
Drawable target = null;
|
||||
|
@ -2,14 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Scoring;
|
||||
@ -21,6 +27,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
[TestFixture]
|
||||
public class LegacyScoreDecoderTest
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
originalCulture = CultureInfo.CurrentCulture;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeManiaReplay()
|
||||
{
|
||||
@ -44,6 +58,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCultureInvariance()
|
||||
{
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
var scoreInfo = new TestScoreInfo(ruleset);
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = scoreInfo,
|
||||
Replay = new Replay
|
||||
{
|
||||
Frames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN,
|
||||
// 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);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode, Is.Not.Null);
|
||||
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Date, Is.EqualTo(scoreInfo.Date));
|
||||
|
||||
Assert.That(decodedAfterEncode.Replay.Frames.Count, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
|
||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
|
||||
|
@ -25,9 +25,6 @@ namespace osu.Game.Tests.Beatmaps
|
||||
|
||||
private BeatmapSetInfo importedSet;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
private TestBeatmapDifficultyCache difficultyCache;
|
||||
|
||||
private IBindable<StarDifficulty?> starDifficultyBindable;
|
||||
|
@ -5,8 +5,8 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -48,7 +48,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
{
|
||||
var subscription = context.All<RealmBeatmap>().SubscribeForNotifications((sender, changes, error) =>
|
||||
var subscription = context.All<RealmBeatmap>().QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
{
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
@ -61,7 +61,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
}
|
||||
|
||||
subscription.Dispose();
|
||||
subscription?.Dispose();
|
||||
}
|
||||
|
||||
Assert.IsTrue(callbackRan);
|
||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
using (var updateThreadContext = realmFactory.CreateContext())
|
||||
{
|
||||
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
|
||||
updateThreadContext.All<RealmBeatmap>().QueryAsyncWithNotifications(gotChange);
|
||||
ILive<RealmBeatmap>? liveBeatmap = null;
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
|
@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
var rulesets = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
|
||||
Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
|
||||
Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
|
||||
Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
|
||||
Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
|
||||
Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
private class TestSkin : LegacySkin
|
||||
{
|
||||
public TestSkin(string resourceName, IStorageResourceProvider resources)
|
||||
: base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini")
|
||||
: base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,6 @@ namespace osu.Game.Tests.Input
|
||||
[Resolved]
|
||||
private FrameworkConfigManager frameworkConfigManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager osuConfigManager { get; set; }
|
||||
|
||||
[TestCase(WindowMode.Windowed)]
|
||||
[TestCase(WindowMode.Borderless)]
|
||||
public void TestDisableConfining(WindowMode windowMode)
|
||||
|
@ -16,6 +16,27 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
[HeadlessTest]
|
||||
public class StatefulMultiplayerClientTest : MultiplayerTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestUserAddedOnJoin()
|
||||
{
|
||||
var user = new APIUser { Id = 33 };
|
||||
|
||||
AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserRemovedOnLeave()
|
||||
{
|
||||
var user = new APIUser { Id = 44 };
|
||||
|
||||
AddStep("add user", () => Client.AddUser(user));
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
|
||||
AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
|
||||
AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayingUserTracking()
|
||||
{
|
||||
|
@ -1,35 +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 NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class StatefulMultiplayerClientTest : MultiplayerTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestUserAddedOnJoin()
|
||||
{
|
||||
var user = new APIUser { Id = 33 };
|
||||
|
||||
AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserRemovedOnLeave()
|
||||
{
|
||||
var user = new APIUser { Id = 44 };
|
||||
|
||||
AddStep("add user", () => Client.AddUser(user));
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
|
||||
AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
|
||||
AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Skinning;
|
||||
@ -163,32 +164,109 @@ namespace osu.Game.Tests.Skins.IO
|
||||
assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestExportThenImportDefaultSkin() => runSkinTest(osu =>
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
skinManager.EnsureMutableSkin();
|
||||
|
||||
MemoryStream exportStream = new MemoryStream();
|
||||
|
||||
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
|
||||
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
|
||||
Assert.Greater(exportStream.Length, 0);
|
||||
});
|
||||
|
||||
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
|
||||
imported.Result.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestExportThenImportClassicSkin() => runSkinTest(osu =>
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
|
||||
|
||||
skinManager.EnsureMutableSkin();
|
||||
|
||||
MemoryStream exportStream = new MemoryStream();
|
||||
|
||||
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
|
||||
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
|
||||
Assert.Greater(exportStream.Length, 0);
|
||||
});
|
||||
|
||||
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
|
||||
imported.Result.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
private void assertCorrectMetadata(SkinInfo import1, string name, string creator, OsuGameBase osu)
|
||||
private void assertCorrectMetadata(ILive<SkinInfo> import1, string name, string creator, OsuGameBase osu)
|
||||
{
|
||||
Assert.That(import1.Name, Is.EqualTo(name));
|
||||
Assert.That(import1.Creator, Is.EqualTo(creator));
|
||||
import1.PerformRead(i =>
|
||||
{
|
||||
Assert.That(i.Name, Is.EqualTo(name));
|
||||
Assert.That(i.Creator, Is.EqualTo(creator));
|
||||
|
||||
// for extra safety let's reconstruct the skin, reading from the skin.ini.
|
||||
var instance = import1.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
|
||||
// for extra safety let's reconstruct the skin, reading from the skin.ini.
|
||||
var instance = i.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
|
||||
|
||||
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
|
||||
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
|
||||
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
|
||||
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertImportedBoth(SkinInfo import1, SkinInfo import2)
|
||||
private void assertImportedBoth(ILive<SkinInfo> import1, ILive<SkinInfo> import2)
|
||||
{
|
||||
Assert.That(import2.ID, Is.Not.EqualTo(import1.ID));
|
||||
Assert.That(import2.Hash, Is.Not.EqualTo(import1.Hash));
|
||||
Assert.That(import2.Files.Select(f => f.FileInfoID), Is.Not.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
|
||||
import1.PerformRead(i1 => import2.PerformRead(i2 =>
|
||||
{
|
||||
Assert.That(i2.ID, Is.Not.EqualTo(i1.ID));
|
||||
Assert.That(i2.Hash, Is.Not.EqualTo(i1.Hash));
|
||||
Assert.That(i2.Files.First(), Is.Not.EqualTo(i1.Files.First()));
|
||||
}));
|
||||
}
|
||||
|
||||
private void assertImportedOnce(SkinInfo import1, SkinInfo import2)
|
||||
private void assertImportedOnce(ILive<SkinInfo> import1, ILive<SkinInfo> import2)
|
||||
{
|
||||
Assert.That(import2.ID, Is.EqualTo(import1.ID));
|
||||
Assert.That(import2.Hash, Is.EqualTo(import1.Hash));
|
||||
Assert.That(import2.Files.Select(f => f.FileInfoID), Is.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
|
||||
import1.PerformRead(i1 => import2.PerformRead(i2 =>
|
||||
{
|
||||
Assert.That(i2.ID, Is.EqualTo(i1.ID));
|
||||
Assert.That(i2.Hash, Is.EqualTo(i1.Hash));
|
||||
Assert.That(i2.Files.First(), Is.EqualTo(i1.Files.First()));
|
||||
}));
|
||||
}
|
||||
|
||||
private MemoryStream createEmptyOsk()
|
||||
@ -255,10 +333,10 @@ namespace osu.Game.Tests.Skins.IO
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
|
||||
private async Task<ILive<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
return (await skinManager.Import(archive)).Value;
|
||||
return await skinManager.Import(archive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins
|
||||
private void load()
|
||||
{
|
||||
var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result;
|
||||
skin = skins.GetSkin(imported.Value);
|
||||
skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -122,7 +123,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private void setCustomSkin()
|
||||
{
|
||||
// feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin.
|
||||
AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 });
|
||||
AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLive());
|
||||
}
|
||||
|
||||
private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault());
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -41,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||
{
|
||||
CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
|
||||
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
}
|
||||
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("setup skins", () =>
|
||||
{
|
||||
skinManager.CurrentSkinInfo.Value = gameCurrentSkin;
|
||||
skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLive();
|
||||
currentBeatmapSkin = getBeatmapSkin();
|
||||
});
|
||||
});
|
||||
|
@ -43,83 +43,88 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
replay = new Replay();
|
||||
AddStep("Reset recorder state", cleanUpState);
|
||||
|
||||
Add(new GridContainer
|
||||
AddStep("Setup containers", () =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
replay = new Replay();
|
||||
|
||||
Add(new GridContainer
|
||||
{
|
||||
new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
new Drawable[]
|
||||
{
|
||||
Recorder = recorder = new TestReplayRecorder(new Score
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
|
||||
})
|
||||
{
|
||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
Recorder = recorder = new TestReplayRecorder(new Score
|
||||
{
|
||||
new Box
|
||||
Replay = replay,
|
||||
ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
|
||||
})
|
||||
{
|
||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Colour = Color4.Brown,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Recording",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Brown,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Recording",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
|
||||
{
|
||||
new Box
|
||||
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Colour = Color4.DarkBlue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Playback",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Playback",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
@ -184,7 +189,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TearDownSteps]
|
||||
public void TearDown()
|
||||
{
|
||||
AddStep("stop recorder", () => recorder.Expire());
|
||||
AddStep("stop recorder", cleanUpState);
|
||||
}
|
||||
|
||||
private void cleanUpState()
|
||||
{
|
||||
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
|
||||
recorder?.RemoveAndDisposeImmediately();
|
||||
recorder = null;
|
||||
}
|
||||
|
||||
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
|
||||
|
@ -1,228 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneReplayRecording : OsuTestScene
|
||||
{
|
||||
private readonly TestRulesetInputManager playbackManager;
|
||||
|
||||
private readonly TestRulesetInputManager recordingManager;
|
||||
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||
|
||||
public TestSceneReplayRecording()
|
||||
{
|
||||
Replay replay = new Replay();
|
||||
|
||||
Add(new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Recorder = new TestReplayRecorder(new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
|
||||
})
|
||||
{
|
||||
ScreenSpaceToGamefield = pos => recordingManager?.ToLocalSpace(pos) ?? Vector2.Zero,
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Brown,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Recording",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
|
||||
{
|
||||
GamefieldToScreenSpace = pos => playbackManager?.ToScreenSpace(pos) ?? Vector2.Zero,
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Playback",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
|
||||
{
|
||||
public TestFramedReplayInputHandler(Replay replay)
|
||||
: base(replay)
|
||||
{
|
||||
}
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
|
||||
inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
|
||||
}
|
||||
}
|
||||
|
||||
public class TestConsumer : CompositeDrawable, IKeyBindingHandler<TestAction>
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
private readonly Box box;
|
||||
|
||||
public TestConsumer()
|
||||
{
|
||||
Size = new Vector2(30);
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
Position = e.MousePosition;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<TestAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
box.Colour = Color4.White;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<TestAction> e)
|
||||
{
|
||||
box.Colour = Color4.Black;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRulesetInputManager : RulesetInputManager<TestAction>
|
||||
{
|
||||
public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||
: base(ruleset, variant, unique)
|
||||
{
|
||||
}
|
||||
|
||||
protected override KeyBindingContainer<TestAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||
=> new TestKeyBindingContainer();
|
||||
|
||||
internal class TestKeyBindingContainer : KeyBindingContainer<TestAction>
|
||||
{
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.MouseLeft, TestAction.Down),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class TestReplayFrame : ReplayFrame
|
||||
{
|
||||
public Vector2 Position;
|
||||
|
||||
public List<TestAction> Actions = new List<TestAction>();
|
||||
|
||||
public TestReplayFrame(double time, Vector2 position, params TestAction[] actions)
|
||||
: base(time)
|
||||
{
|
||||
Position = position;
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TestAction
|
||||
{
|
||||
Down,
|
||||
}
|
||||
|
||||
internal class TestReplayRecorder : ReplayRecorder<TestAction>
|
||||
{
|
||||
public TestReplayRecorder(Score target)
|
||||
: base(target)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<TestAction> actions, ReplayFrame previousFrame) =>
|
||||
new TestReplayFrame(Time.Current, mousePosition, actions.ToArray());
|
||||
}
|
||||
}
|
@ -2,12 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Skinning.Editor;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -16,9 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
private SkinEditor skinEditor;
|
||||
|
||||
[Resolved]
|
||||
private SkinManager skinManager { get; set; }
|
||||
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
[SetUpSteps]
|
||||
|
@ -10,7 +10,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private Drawable hideTarget => hudOverlay.KeyCounter;
|
||||
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestComboCounterIncrementing()
|
||||
{
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private TestReplayRecorder recorder;
|
||||
|
||||
private readonly ManualClock manualClock = new ManualClock();
|
||||
private ManualClock manualClock;
|
||||
|
||||
private OsuSpriteText latencyDisplay;
|
||||
|
||||
@ -66,113 +66,121 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
replay = new Replay();
|
||||
AddStep("Reset recorder state", cleanUpState);
|
||||
|
||||
users.BindTo(spectatorClient.PlayingUsers);
|
||||
users.BindCollectionChanged((obj, args) =>
|
||||
AddStep("Setup containers", () =>
|
||||
{
|
||||
switch (args.Action)
|
||||
replay = new Replay();
|
||||
manualClock = new ManualClock();
|
||||
|
||||
spectatorClient.OnNewFrames += onNewFrames;
|
||||
|
||||
users.BindTo(spectatorClient.PlayingUsers);
|
||||
users.BindCollectionChanged((obj, args) =>
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
Debug.Assert(args.NewItems != null);
|
||||
|
||||
foreach (int user in args.NewItems)
|
||||
{
|
||||
if (user == api.LocalUser.Value.Id)
|
||||
spectatorClient.WatchUser(user);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
Debug.Assert(args.OldItems != null);
|
||||
|
||||
foreach (int user in args.OldItems)
|
||||
{
|
||||
if (user == api.LocalUser.Value.Id)
|
||||
spectatorClient.StopWatchingUser(user);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
spectatorClient.OnNewFrames += onNewFrames;
|
||||
|
||||
Add(new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
switch (args.Action)
|
||||
{
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
Debug.Assert(args.NewItems != null);
|
||||
|
||||
foreach (int user in args.NewItems)
|
||||
{
|
||||
if (user == api.LocalUser.Value.Id)
|
||||
spectatorClient.WatchUser(user);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
Debug.Assert(args.OldItems != null);
|
||||
|
||||
foreach (int user in args.OldItems)
|
||||
{
|
||||
if (user == api.LocalUser.Value.Id)
|
||||
spectatorClient.StopWatchingUser(user);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
Recorder = recorder = new TestReplayRecorder
|
||||
new Drawable[]
|
||||
{
|
||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
new Box
|
||||
Recorder = recorder = new TestReplayRecorder
|
||||
{
|
||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
Colour = Color4.Brown,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Brown,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Sending",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Sending",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Clock = new FramedClock(manualClock),
|
||||
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
|
||||
{
|
||||
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Receiving",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Clock = new FramedClock(manualClock),
|
||||
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
|
||||
{
|
||||
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Receiving",
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new TestInputConsumer()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
latencyDisplay = new OsuSpriteText()
|
||||
};
|
||||
});
|
||||
|
||||
Add(latencyDisplay = new OsuSpriteText());
|
||||
});
|
||||
}
|
||||
|
||||
private void onNewFrames(int userId, FrameDataBundle frames)
|
||||
{
|
||||
@ -189,6 +197,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("Wait for user input", () => { });
|
||||
}
|
||||
|
||||
private double latency = SpectatorClient.TIME_BETWEEN_SENDS;
|
||||
@ -232,11 +241,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TearDownSteps]
|
||||
public void TearDown()
|
||||
{
|
||||
AddStep("stop recorder", () =>
|
||||
{
|
||||
recorder.Expire();
|
||||
spectatorClient.OnNewFrames -= onNewFrames;
|
||||
});
|
||||
AddStep("stop recorder", cleanUpState);
|
||||
}
|
||||
|
||||
private void cleanUpState()
|
||||
{
|
||||
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
|
||||
recorder?.RemoveAndDisposeImmediately();
|
||||
recorder = null;
|
||||
spectatorClient.OnNewFrames -= onNewFrames;
|
||||
}
|
||||
|
||||
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
private TestToolbar toolbar;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
|
@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
|
||||
@ -172,6 +174,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiplayerRooms()
|
||||
{
|
||||
AddStep("create rooms", () => Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new[]
|
||||
{
|
||||
new DrawableMatchRoom(new Room
|
||||
{
|
||||
Name = { Value = "A host-only room" },
|
||||
QueueMode = { Value = QueueMode.HostOnly },
|
||||
Type = { Value = MatchType.HeadToHead }
|
||||
}),
|
||||
new DrawableMatchRoom(new Room
|
||||
{
|
||||
Name = { Value = "An all-players, team-versus room" },
|
||||
QueueMode = { Value = QueueMode.AllPlayers },
|
||||
Type = { Value = MatchType.TeamVersus }
|
||||
}),
|
||||
new DrawableMatchRoom(new Room
|
||||
{
|
||||
Name = { Value = "A round-robin room" },
|
||||
QueueMode = { Value = QueueMode.AllPlayersRoundRobin },
|
||||
Type = { Value = MatchType.HeadToHead }
|
||||
}),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DrawableRoom createLoungeRoom(Room room)
|
||||
{
|
||||
room.Host.Value ??= new APIUser { Username = "peppy", Id = 2 };
|
||||
|
@ -2,11 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
@ -18,12 +15,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
{
|
||||
|
@ -397,6 +397,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect()
|
||||
{
|
||||
createRoom(() => new Room
|
||||
{
|
||||
Name = { Value = "Test Room" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
|
||||
Ruleset = { Value = new OsuRuleset().RulesetInfo },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("Enter song select", () =>
|
||||
{
|
||||
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen;
|
||||
|
||||
((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
|
||||
|
||||
AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
|
||||
|
||||
AddStep("Select next beatmap", () => InputManager.Key(Key.Down));
|
||||
|
||||
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID);
|
||||
|
||||
AddStep("start match externally", () => client.StartMatch());
|
||||
|
||||
AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is Player);
|
||||
|
||||
AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
|
||||
{
|
||||
|
@ -0,0 +1,234 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMultiplayerPlaylist : MultiplayerTestScene
|
||||
{
|
||||
private MultiplayerPlaylist list;
|
||||
private BeatmapManager beatmaps;
|
||||
private RulesetStore rulesets;
|
||||
private BeatmapSetInfo importedSet;
|
||||
private BeatmapInfo importedBeatmap;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = list = new MultiplayerPlaylist
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.4f, 0.8f)
|
||||
};
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public new void SetUpSteps()
|
||||
{
|
||||
AddStep("import beatmap", () =>
|
||||
{
|
||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
|
||||
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
|
||||
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
|
||||
});
|
||||
|
||||
AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonExpiredItemsAddedToQueueList()
|
||||
{
|
||||
assertItemInQueueListStep(1, 0);
|
||||
|
||||
addItemStep();
|
||||
assertItemInQueueListStep(2, 1);
|
||||
|
||||
addItemStep();
|
||||
assertItemInQueueListStep(3, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpiredItemsAddedToHistoryList()
|
||||
{
|
||||
assertItemInQueueListStep(1, 0);
|
||||
|
||||
addItemStep(true);
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
|
||||
addItemStep(true);
|
||||
assertItemInHistoryListStep(3, 0);
|
||||
assertItemInHistoryListStep(2, 1);
|
||||
|
||||
// Initial item is still in the queue.
|
||||
assertItemInQueueListStep(1, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpiredItemsMoveToQueueList()
|
||||
{
|
||||
addItemStep();
|
||||
addItemStep();
|
||||
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
assertItemInHistoryListStep(1, 0);
|
||||
assertItemInQueueListStep(2, 0);
|
||||
assertItemInQueueListStep(3, 1);
|
||||
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
assertItemInHistoryListStep(1, 1);
|
||||
assertItemInQueueListStep(3, 0);
|
||||
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
assertItemInHistoryListStep(3, 0);
|
||||
assertItemInHistoryListStep(2, 1);
|
||||
assertItemInHistoryListStep(1, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestListsClearedWhenRoomLeft()
|
||||
{
|
||||
addItemStep();
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddUntilStep("wait for room part", () => Client.Room == null);
|
||||
|
||||
AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
||||
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
||||
}
|
||||
|
||||
[Ignore("Expired items are initially removed from the room.")]
|
||||
[Test]
|
||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
||||
{
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddUntilStep("wait for room part", () => Client.Room == null);
|
||||
|
||||
AddStep("join room with items", () =>
|
||||
{
|
||||
RoomManager.CreateRoom(new Room
|
||||
{
|
||||
Name = { Value = "test name" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value }
|
||||
},
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value },
|
||||
Expired = true
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
|
||||
assertItemInQueueListStep(1, 0);
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a step to create a new playlist item.
|
||||
/// </summary>
|
||||
private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = importedBeatmap },
|
||||
BeatmapID = importedBeatmap.OnlineID ?? -1,
|
||||
Expired = expired,
|
||||
PlayedAt = DateTimeOffset.Now
|
||||
})));
|
||||
|
||||
/// <summary>
|
||||
/// Asserts the position of a given playlist item in the queue list.
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The item id.</param>
|
||||
/// <param name="visualIndex">The index at which the item should appear visually. The item with index 0 is at the top of the list.</param>
|
||||
private void assertItemInQueueListStep(int playlistItemId, int visualIndex)
|
||||
{
|
||||
changeDisplayModeStep(MultiplayerPlaylistDisplayMode.Queue);
|
||||
|
||||
AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () =>
|
||||
{
|
||||
return !inHistoryList(playlistItemId)
|
||||
&& this.ChildrenOfType<MultiplayerQueueList>()
|
||||
.Single()
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem>()
|
||||
.OrderBy(drawable => drawable.Position.Y)
|
||||
.TakeWhile(drawable => drawable.Item.ID != playlistItemId)
|
||||
.Count() == visualIndex;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts the position of a given playlist item in the history list.
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The item id.</param>
|
||||
/// <param name="visualIndex">The index at which the item should appear visually. The item with index 0 is at the top of the list.</param>
|
||||
private void assertItemInHistoryListStep(int playlistItemId, int visualIndex)
|
||||
{
|
||||
changeDisplayModeStep(MultiplayerPlaylistDisplayMode.History);
|
||||
|
||||
AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () =>
|
||||
{
|
||||
return !inQueueList(playlistItemId)
|
||||
&& this.ChildrenOfType<MultiplayerHistoryList>()
|
||||
.Single()
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem>()
|
||||
.OrderBy(drawable => drawable.Position.Y)
|
||||
.TakeWhile(drawable => drawable.Item.ID != playlistItemId)
|
||||
.Count() == visualIndex;
|
||||
});
|
||||
}
|
||||
|
||||
private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
|
||||
|
||||
private bool inQueueList(int playlistItemId)
|
||||
{
|
||||
return this.ChildrenOfType<MultiplayerQueueList>()
|
||||
.Single()
|
||||
.Items.Any(i => i.ID == playlistItemId);
|
||||
}
|
||||
|
||||
private bool inHistoryList(int playlistItemId)
|
||||
{
|
||||
return this.ChildrenOfType<MultiplayerHistoryList>()
|
||||
.Single()
|
||||
.Items.Any(i => i.ID == playlistItemId);
|
||||
}
|
||||
}
|
||||
}
|
@ -24,9 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestScenePlaylistsSongSelect : OnlinePlayTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
private BeatmapManager manager;
|
||||
|
||||
private RulesetStore rulesets;
|
||||
|
39
osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
Normal file
39
osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Settings.Sections;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Skinning.Editor;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public class TestSceneEditDefaultSkin : OsuGameTestScene
|
||||
{
|
||||
private SkinManager skinManager => Game.Dependencies.Get<SkinManager>();
|
||||
private SkinEditorOverlay skinEditor => Game.Dependencies.Get<SkinEditorOverlay>();
|
||||
|
||||
[Test]
|
||||
public void TestEditDefaultSkin()
|
||||
{
|
||||
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN);
|
||||
|
||||
AddStep("open settings", () => { Game.Settings.Show(); });
|
||||
|
||||
// Until step requires as settings has a delayed load.
|
||||
AddUntilStep("export button disabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == false);
|
||||
|
||||
// Will create a mutable skin.
|
||||
AddStep("open skin editor", () => skinEditor.Show());
|
||||
|
||||
// Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
|
||||
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN);
|
||||
AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
|
||||
|
||||
AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public class TestSceneMouseWheelVolumeAdjust : OsuGameTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
// Headless tests are always at minimum volume. This covers interactive tests, matching that initial value.
|
||||
AddStep("Set volume to min", () => Game.Audio.Volume.Value = 0);
|
||||
AddAssert("Volume is min", () => Game.Audio.AggregateVolume.Value == 0);
|
||||
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustVolumeFromMainMenu()
|
||||
{
|
||||
// First scroll makes volume controls appear, second adjusts volume.
|
||||
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
|
||||
AddUntilStep("Volume is above zero", () => Game.Audio.AggregateVolume.Value > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustVolumeFromPlayerWheelEnabled()
|
||||
{
|
||||
loadToPlayerNonBreakTime();
|
||||
|
||||
// First scroll makes volume controls appear, second adjusts volume.
|
||||
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
|
||||
AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustVolumeFromPlayerWheelDisabled()
|
||||
{
|
||||
AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true));
|
||||
|
||||
loadToPlayerNonBreakTime();
|
||||
|
||||
// First scroll makes volume controls appear, second adjusts volume.
|
||||
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
|
||||
AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustVolumeFromPlayerWheelDisabledHoldingAlt()
|
||||
{
|
||||
AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true));
|
||||
|
||||
loadToPlayerNonBreakTime();
|
||||
|
||||
// First scroll makes volume controls appear, second adjusts volume.
|
||||
AddRepeatStep("Adjust volume using mouse wheel holding alt", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.AltLeft);
|
||||
InputManager.ScrollVerticalBy(5);
|
||||
InputManager.ReleaseKey(Key.AltLeft);
|
||||
}, 2);
|
||||
|
||||
AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
|
||||
}
|
||||
|
||||
private void loadToPlayerNonBreakTime()
|
||||
{
|
||||
Player player = null;
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("wait for player", () =>
|
||||
{
|
||||
// dismiss any notifications that may appear (ie. muted notification).
|
||||
clickMouseInCentre();
|
||||
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
|
||||
}
|
||||
|
||||
private void clickMouseInCentre()
|
||||
{
|
||||
InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
}
|
||||
}
|
@ -83,9 +83,6 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Resolved]
|
||||
private OsuGameBase gameBase { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestNullRulesetHandled()
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestMultipleRulesetsBeatmapSet()
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestLoading()
|
||||
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@ -47,9 +46,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[CanBeNull]
|
||||
private Func<Channel, List<Message>> onGetMessages;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
public TestSceneChatOverlay()
|
||||
{
|
||||
channels = Enumerable.Range(1, 10)
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
private TestUserListPanel evast;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; }
|
||||
private IRulesetStore rulesetStore { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
|
@ -4,8 +4,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile;
|
||||
@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
private readonly TestUserProfileOverlay profile;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
public static readonly APIUser TEST_USER = new APIUser
|
||||
{
|
||||
Username = @"Somebody",
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
@ -20,9 +19,6 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public class TestSceneContractedPanelMiddleContent : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestShowPanel()
|
||||
{
|
||||
|
@ -135,6 +135,35 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60));
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestEarlyActivationEffectPoint(bool earlyActivating)
|
||||
{
|
||||
double earlyActivationMilliseconds = earlyActivating ? 100 : 0;
|
||||
ControlPoint actualEffectPoint = null;
|
||||
|
||||
AddStep($"set early activation to {earlyActivationMilliseconds}", () => beatContainer.EarlyActivationMilliseconds = earlyActivationMilliseconds);
|
||||
|
||||
AddStep("seek before kiai effect point", () =>
|
||||
{
|
||||
ControlPoint expectedEffectPoint = Beatmap.Value.Beatmap.ControlPointInfo.EffectPoints.First(ep => ep.KiaiMode);
|
||||
actualEffectPoint = null;
|
||||
beatContainer.AllowMistimedEventFiring = false;
|
||||
|
||||
beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) =>
|
||||
{
|
||||
if (Precision.AlmostEquals(gameplayClockContainer.CurrentTime + earlyActivationMilliseconds, expectedEffectPoint.Time, BeatSyncedContainer.MISTIMED_ALLOWANCE))
|
||||
actualEffectPoint = effectControlPoint;
|
||||
};
|
||||
|
||||
gameplayClockContainer.Seek(expectedEffectPoint.Time - earlyActivationMilliseconds);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for effect point", () => actualEffectPoint != null);
|
||||
|
||||
AddAssert("effect has kiai", () => actualEffectPoint != null && ((EffectControlPoint)actualEffectPoint).KiaiMode);
|
||||
}
|
||||
|
||||
private class TestBeatSyncedContainer : BeatSyncedContainer
|
||||
{
|
||||
private const int flash_layer_height = 150;
|
||||
@ -145,6 +174,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
set => base.AllowMistimedEventFiring = value;
|
||||
}
|
||||
|
||||
public new double EarlyActivationMilliseconds
|
||||
{
|
||||
get => base.EarlyActivationMilliseconds;
|
||||
set => base.EarlyActivationMilliseconds = value;
|
||||
}
|
||||
|
||||
private readonly InfoString timingPointCount;
|
||||
private readonly InfoString currentTimingPoint;
|
||||
private readonly InfoString beatCount;
|
||||
|
@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private BeatmapSetInfo testBeatmap;
|
||||
private IAPIProvider api;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase osu, IAPIProvider api, RulesetStore rulesets)
|
||||
{
|
||||
|
@ -6,19 +6,20 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Tournament.Components;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Components
|
||||
{
|
||||
public class TestSceneTournamentBeatmapPanel : TournamentTestScene
|
||||
{
|
||||
/// <remarks>
|
||||
/// Warning: the below API instance is actually the online API, rather than the dummy API provided by the test.
|
||||
/// It cannot be trivially replaced because setting <see cref="OsuTestScene.UseOnlineAPI"/> to <see langword="true"/> causes <see cref="OsuTestScene.API"/> to no longer be usable.
|
||||
/// </remarks>
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Tournament.Tests.Components
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
private FillFlowContainer<TournamentBeatmapPanel> fillFlow;
|
||||
|
||||
|
@ -96,7 +96,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
||||
Directory.CreateDirectory(flagsPath);
|
||||
|
||||
// Define testing files corresponding to the specific file migrations that are needed
|
||||
string bracketFile = Path.Combine(osuRoot, "bracket.json");
|
||||
string bracketFile = Path.Combine(osuRoot, TournamentGameBase.BRACKET_FILENAME);
|
||||
|
||||
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
|
||||
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
|
||||
@ -133,7 +133,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
|
||||
|
||||
Assert.True(storage.Exists("bracket.json"));
|
||||
Assert.True(storage.Exists(TournamentGameBase.BRACKET_FILENAME));
|
||||
Assert.True(storage.Exists("drawings.txt"));
|
||||
Assert.True(storage.Exists("drawings_results.txt"));
|
||||
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components
|
||||
private readonly string modAcronym;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
public TournamentModIcon(string modAcronym)
|
||||
{
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Tournament.IO
|
||||
DeleteRecursive(source);
|
||||
}
|
||||
|
||||
moveFileIfExists("bracket.json", destination);
|
||||
moveFileIfExists(TournamentGameBase.BRACKET_FILENAME, destination);
|
||||
moveFileIfExists("drawings.txt", destination);
|
||||
moveFileIfExists("drawings_results.txt", destination);
|
||||
moveFileIfExists("drawings.ini", destination);
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Tournament.IPC
|
||||
protected IAPIProvider API { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected RulesetStore Rulesets { get; private set; }
|
||||
protected IRulesetStore Rulesets { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
@ -25,9 +25,6 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
|
||||
protected override BindableList<SeedingResult> Storage => team.SeedingResults;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private TournamentSceneManager sceneManager { get; set; }
|
||||
|
||||
public SeedingEditorScreen(TournamentTeam team, TournamentScreen parentScreen)
|
||||
: base(parentScreen)
|
||||
{
|
||||
@ -38,9 +35,6 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
{
|
||||
public SeedingResult Model { get; }
|
||||
|
||||
[Resolved]
|
||||
private LadderInfo ladderInfo { get; set; }
|
||||
|
||||
public SeedingResultRow(TournamentTeam team, SeedingResult round)
|
||||
{
|
||||
Model = round;
|
||||
|
@ -21,9 +21,6 @@ namespace osu.Game.Tournament.Screens.Setup
|
||||
{
|
||||
public class StablePathSelectScreen : TournamentScreen
|
||||
{
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private TournamentSceneManager sceneManager { get; set; }
|
||||
|
||||
|
@ -71,7 +71,7 @@ namespace osu.Game.Tournament
|
||||
loadingSpinner.Expire();
|
||||
|
||||
Logger.Error(t.Exception, "Couldn't load bracket with error");
|
||||
Add(new WarningBox("Your bracket.json file could not be parsed. Please check runtime.log for more details."));
|
||||
Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details."));
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.API.Requests;
|
||||
@ -26,15 +27,15 @@ namespace osu.Game.Tournament
|
||||
[Cached(typeof(TournamentGameBase))]
|
||||
public class TournamentGameBase : OsuGameBase
|
||||
{
|
||||
private const string bracket_filename = "bracket.json";
|
||||
public const string BRACKET_FILENAME = @"bracket.json";
|
||||
private LadderInfo ladder;
|
||||
private TournamentStorage storage;
|
||||
private DependencyContainer dependencies;
|
||||
private FileBasedIPC ipc;
|
||||
|
||||
protected Task BracketLoadTask => taskCompletionSource.Task;
|
||||
protected Task BracketLoadTask => bracketLoadTaskCompletionSource.Task;
|
||||
|
||||
private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
private readonly TaskCompletionSource<bool> bracketLoadTaskCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
@ -71,9 +72,9 @@ namespace osu.Game.Tournament
|
||||
{
|
||||
try
|
||||
{
|
||||
if (storage.Exists(bracket_filename))
|
||||
if (storage.Exists(BRACKET_FILENAME))
|
||||
{
|
||||
using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open))
|
||||
using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open))
|
||||
using (var sr = new StreamReader(stream))
|
||||
ladder = JsonConvert.DeserializeObject<LadderInfo>(sr.ReadToEnd(), new JsonPointConverter());
|
||||
}
|
||||
@ -144,7 +145,7 @@ namespace osu.Game.Tournament
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
taskCompletionSource.SetException(e);
|
||||
bracketLoadTaskCompletionSource.SetException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -156,7 +157,7 @@ namespace osu.Game.Tournament
|
||||
dependencies.CacheAs<MatchIPCInfo>(ipc = new FileBasedIPC());
|
||||
Add(ipc);
|
||||
|
||||
taskCompletionSource.SetResult(true);
|
||||
bracketLoadTaskCompletionSource.SetResult(true);
|
||||
|
||||
initialisationText.Expire();
|
||||
});
|
||||
@ -292,6 +293,12 @@ namespace osu.Game.Tournament
|
||||
|
||||
protected virtual void SaveChanges()
|
||||
{
|
||||
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
|
||||
{
|
||||
Logger.Log("Inhibiting bracket save as bracket parsing failed");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var r in ladder.Rounds)
|
||||
r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList();
|
||||
|
||||
@ -309,7 +316,7 @@ namespace osu.Game.Tournament
|
||||
Converters = new JsonConverter[] { new JsonPointConverter() }
|
||||
});
|
||||
|
||||
using (var stream = storage.GetStream(bracket_filename, FileAccess.Write, FileMode.Create))
|
||||
using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create))
|
||||
using (var sw = new StreamWriter(stream))
|
||||
sw.Write(serialisedLadder);
|
||||
}
|
||||
|
@ -18,9 +18,6 @@ namespace osu.Game.Audio
|
||||
|
||||
private readonly BindableDouble muteBindable = new BindableDouble();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; }
|
||||
|
||||
private ITrackStore trackStore;
|
||||
|
||||
protected TrackManagerPreviewTrack CurrentTrack;
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> ruleset { get; set; }
|
||||
@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
private int? requestedUserId;
|
||||
|
||||
private readonly Dictionary<RulesetInfo, double> recommendedDifficultyMapping = new Dictionary<RulesetInfo, double>();
|
||||
private readonly Dictionary<IRulesetInfo, double> recommendedDifficultyMapping = new Dictionary<IRulesetInfo, double>();
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
|
||||
@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps
|
||||
/// Rulesets ordered descending by their respective recommended difficulties.
|
||||
/// The currently selected ruleset will always be first.
|
||||
/// </returns>
|
||||
private IEnumerable<RulesetInfo> orderedRulesets
|
||||
private IEnumerable<IRulesetInfo> orderedRulesets
|
||||
{
|
||||
get
|
||||
{
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
|
52
osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
Normal file
52
osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// 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.IO;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="WorkingBeatmap"/> which can be constructed directly from a .osu file, providing an implementation for
|
||||
/// <see cref="WorkingBeatmap.GetPlayableBeatmap(osu.Game.Rulesets.IRulesetInfo,System.Collections.Generic.IReadOnlyList{osu.Game.Rulesets.Mods.Mod})"/>.
|
||||
/// </summary>
|
||||
public class FlatFileWorkingBeatmap : WorkingBeatmap
|
||||
{
|
||||
private readonly Beatmap beatmap;
|
||||
|
||||
public FlatFileWorkingBeatmap(string file, Func<int, Ruleset> rulesetProvider, int? beatmapId = null)
|
||||
: this(readFromFile(file), rulesetProvider, beatmapId)
|
||||
{
|
||||
}
|
||||
|
||||
private FlatFileWorkingBeatmap(Beatmap beatmap, Func<int, Ruleset> rulesetProvider, int? beatmapId = null)
|
||||
: base(beatmap.BeatmapInfo, null)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
|
||||
beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo;
|
||||
|
||||
if (beatmapId.HasValue)
|
||||
beatmap.BeatmapInfo.OnlineID = beatmapId;
|
||||
}
|
||||
|
||||
private static Beatmap readFromFile(string filename)
|
||||
{
|
||||
using (var stream = File.OpenRead(filename))
|
||||
using (var reader = new LineBufferedReader(stream))
|
||||
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
protected internal override ISkin GetSkin() => throw new NotImplementedException();
|
||||
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
@ -193,9 +192,6 @@ namespace osu.Game.Collections
|
||||
[NotNull]
|
||||
protected new CollectionFilterMenuItem Item => ((DropdownMenuItem<CollectionFilterMenuItem>)base.Item).Value;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
|
@ -40,9 +40,6 @@ namespace osu.Game.Collections
|
||||
|
||||
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
@ -27,7 +28,7 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
// UI/selection defaults
|
||||
SetDefault(OsuSetting.Ruleset, string.Empty);
|
||||
SetDefault(OsuSetting.Skin, 0, -1, int.MaxValue);
|
||||
SetDefault(OsuSetting.Skin, SkinInfo.DEFAULT_SKIN.ToString());
|
||||
|
||||
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
|
||||
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
|
||||
@ -210,9 +211,12 @@ namespace osu.Game.Configuration
|
||||
value: scalingMode.GetLocalisableDescription()
|
||||
)
|
||||
),
|
||||
new TrackedSetting<int>(OsuSetting.Skin, skin =>
|
||||
new TrackedSetting<string>(OsuSetting.Skin, skin =>
|
||||
{
|
||||
string skinName = LookupSkinName(skin) ?? string.Empty;
|
||||
string skinName = string.Empty;
|
||||
|
||||
if (Guid.TryParse(skin, out var id))
|
||||
skinName = LookupSkinName(id) ?? string.Empty;
|
||||
|
||||
return new SettingDescription(
|
||||
rawValue: skinName,
|
||||
@ -233,7 +237,7 @@ namespace osu.Game.Configuration
|
||||
};
|
||||
}
|
||||
|
||||
public Func<int, string> LookupSkinName { private get; set; }
|
||||
public Func<Guid, string> LookupSkinName { private get; set; }
|
||||
|
||||
public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; }
|
||||
}
|
||||
|
148
osu.Game/Database/EFToRealmMigrator.cs
Normal file
148
osu.Game/Database/EFToRealmMigrator.cs
Normal file
@ -0,0 +1,148 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
internal class EFToRealmMigrator
|
||||
{
|
||||
private readonly DatabaseContextFactory efContextFactory;
|
||||
private readonly RealmContextFactory realmContextFactory;
|
||||
private readonly OsuConfigManager config;
|
||||
|
||||
public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config)
|
||||
{
|
||||
this.efContextFactory = efContextFactory;
|
||||
this.realmContextFactory = realmContextFactory;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
using (var db = efContextFactory.GetForWrite())
|
||||
{
|
||||
migrateSettings(db);
|
||||
migrateSkins(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateSkins(DatabaseWriteUsage db)
|
||||
{
|
||||
// can be removed 20220530.
|
||||
var existingSkins = db.Context.SkinInfo
|
||||
.Include(s => s.Files)
|
||||
.ThenInclude(f => f.FileInfo)
|
||||
.ToList();
|
||||
|
||||
// previous entries in EF are removed post migration.
|
||||
if (!existingSkins.Any())
|
||||
return;
|
||||
|
||||
var userSkinChoice = config.GetBindable<string>(OsuSetting.Skin);
|
||||
int.TryParse(userSkinChoice.Value, out int userSkinInt);
|
||||
|
||||
switch (userSkinInt)
|
||||
{
|
||||
case EFSkinInfo.DEFAULT_SKIN:
|
||||
userSkinChoice.Value = SkinInfo.DEFAULT_SKIN.ToString();
|
||||
break;
|
||||
|
||||
case EFSkinInfo.CLASSIC_SKIN:
|
||||
userSkinChoice.Value = SkinInfo.CLASSIC_SKIN.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
// only migrate data if the realm database is empty.
|
||||
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
|
||||
{
|
||||
foreach (var skin in existingSkins)
|
||||
{
|
||||
var realmSkin = new SkinInfo
|
||||
{
|
||||
Name = skin.Name,
|
||||
Creator = skin.Creator,
|
||||
Hash = skin.Hash,
|
||||
Protected = false,
|
||||
InstantiationInfo = skin.InstantiationInfo,
|
||||
};
|
||||
|
||||
foreach (var file in skin.Files)
|
||||
{
|
||||
var realmFile = realm.Find<RealmFile>(file.FileInfo.Hash);
|
||||
|
||||
if (realmFile == null)
|
||||
realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash });
|
||||
|
||||
realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename));
|
||||
}
|
||||
|
||||
realm.Add(realmSkin);
|
||||
|
||||
if (skin.ID == userSkinInt)
|
||||
userSkinChoice.Value = realmSkin.ID.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
db.Context.RemoveRange(existingSkins);
|
||||
// Intentionally don't clean up the files, so they don't get purged by EF.
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateSettings(DatabaseWriteUsage db)
|
||||
{
|
||||
// migrate ruleset settings. can be removed 20220315.
|
||||
var existingSettings = db.Context.DatabasedSetting;
|
||||
|
||||
// previous entries in EF are removed post migration.
|
||||
if (!existingSettings.Any())
|
||||
return;
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!realm.All<RealmRulesetSetting>().Any())
|
||||
{
|
||||
foreach (var dkb in existingSettings)
|
||||
{
|
||||
if (dkb.RulesetID == null)
|
||||
continue;
|
||||
|
||||
string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
|
||||
|
||||
if (string.IsNullOrEmpty(shortName))
|
||||
continue;
|
||||
|
||||
realm.Add(new RealmRulesetSetting
|
||||
{
|
||||
Key = dkb.Key,
|
||||
Value = dkb.StringValue,
|
||||
RulesetName = shortName,
|
||||
Variant = dkb.Variant ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
db.Context.RemoveRange(existingSettings);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
||||
efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ namespace osu.Game.Database
|
||||
void DeleteFile(TModel model, TFileModel file);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new file.
|
||||
/// Add a new file. If the file already exists, it is overwritten.
|
||||
/// </summary>
|
||||
/// <param name="model">The item to operate on.</param>
|
||||
/// <param name="contents">The new file contents.</param>
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Database
|
||||
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
|
||||
public DbSet<FileInfo> FileInfo { get; set; }
|
||||
public DbSet<RulesetInfo> RulesetInfo { get; set; }
|
||||
public DbSet<SkinInfo> SkinInfo { get; set; }
|
||||
public DbSet<EFSkinInfo> SkinInfo { get; set; }
|
||||
public DbSet<ScoreInfo> ScoreInfo { get; set; }
|
||||
|
||||
// migrated to realm
|
||||
@ -133,8 +133,9 @@ namespace osu.Game.Database
|
||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
|
||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
|
||||
|
||||
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.Hash).IsUnique();
|
||||
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.DeletePending);
|
||||
modelBuilder.Entity<EFSkinInfo>().HasIndex(b => b.Hash).IsUnique();
|
||||
modelBuilder.Entity<EFSkinInfo>().HasIndex(b => b.DeletePending);
|
||||
modelBuilder.Entity<EFSkinInfo>().HasMany(s => s.Files).WithOne(f => f.SkinInfo);
|
||||
|
||||
modelBuilder.Entity<DatabasedSetting>().HasIndex(b => new { b.RulesetID, b.Variant });
|
||||
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Statistics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using Realms;
|
||||
|
||||
@ -101,10 +102,6 @@ namespace osu.Game.Database
|
||||
|
||||
// This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date.
|
||||
cleanupPendingDeletions();
|
||||
|
||||
// Data migration is handled separately from schema migrations.
|
||||
// This is required as the user may be initialising realm for the first time ever, which would result in no schema migrations running.
|
||||
migrateDataFromEF();
|
||||
}
|
||||
|
||||
private void cleanupPendingDeletions()
|
||||
@ -122,6 +119,11 @@ namespace osu.Game.Database
|
||||
realm.Remove(s);
|
||||
}
|
||||
|
||||
var pendingDeleteSkins = realm.All<SkinInfo>().Where(s => s.DeletePending);
|
||||
|
||||
foreach (var s in pendingDeleteSkins)
|
||||
realm.Remove(s);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
@ -193,53 +195,6 @@ namespace osu.Game.Database
|
||||
};
|
||||
}
|
||||
|
||||
private void migrateDataFromEF()
|
||||
{
|
||||
if (efContextFactory == null)
|
||||
return;
|
||||
|
||||
using (var db = efContextFactory.GetForWrite())
|
||||
{
|
||||
// migrate ruleset settings. can be removed 20220315.
|
||||
var existingSettings = db.Context.DatabasedSetting;
|
||||
|
||||
// previous entries in EF are removed post migration.
|
||||
if (!existingSettings.Any())
|
||||
return;
|
||||
|
||||
using (var realm = CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!realm.All<RealmRulesetSetting>().Any())
|
||||
{
|
||||
foreach (var dkb in existingSettings)
|
||||
{
|
||||
if (dkb.RulesetID == null)
|
||||
continue;
|
||||
|
||||
string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
|
||||
|
||||
if (string.IsNullOrEmpty(shortName))
|
||||
continue;
|
||||
|
||||
realm.Add(new RealmRulesetSetting
|
||||
{
|
||||
Key = dkb.Key,
|
||||
Value = dkb.StringValue,
|
||||
RulesetName = shortName,
|
||||
Variant = dkb.Variant ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
db.Context.RemoveRange(existingSettings);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
||||
{
|
||||
for (ulong i = lastSchemaVersion + 1; i <= schema_version; i++)
|
||||
|
@ -103,5 +103,7 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
public bool Equals(ILive<T>? other) => ID == other?.ID;
|
||||
|
||||
public override string ToString() => PerformRead(i => i.ToString());
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AutoMapper;
|
||||
using osu.Framework.Development;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class RealmObjectExtensions
|
||||
@ -60,5 +64,109 @@ namespace osu.Game.Database
|
||||
{
|
||||
return new RealmLive<T>(realmObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a callback to be invoked each time this <see cref="T:Realms.IRealmCollection`1" /> changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first callback will be invoked with the initial <see cref="T:Realms.IRealmCollection`1" /> after the asynchronous query completes,
|
||||
/// and then called again after each write transaction which changes either any of the objects in the collection, or
|
||||
/// which objects are in the collection. The <c>changes</c> parameter will
|
||||
/// be <c>null</c> the first time the callback is invoked with the initial results. For each call after that,
|
||||
/// it will contain information about which rows in the results were added, removed or modified.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If a write transaction did not modify any objects in this <see cref="T:Realms.IRealmCollection`1" />, the callback is not invoked at all.
|
||||
/// If an error occurs the callback will be invoked with <c>null</c> for the <c>sender</c> parameter and a non-<c>null</c> <c>error</c>.
|
||||
/// Currently the only errors that can occur are when opening the <see cref="T:Realms.Realm" /> on the background worker thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// At the time when the block is called, the <see cref="T:Realms.IRealmCollection`1" /> object will be fully evaluated
|
||||
/// and up-to-date, and as long as you do not perform a write transaction on the same thread
|
||||
/// or explicitly call <see cref="M:Realms.Realm.Refresh" />, accessing it will never perform blocking work.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Notifications are delivered via the standard event loop, and so can't be delivered while the event loop is blocked by other activity.
|
||||
/// When notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification.
|
||||
/// This can include the notification with the initial collection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="collection">The <see cref="IRealmCollection{T}"/> to observe for changes.</param>
|
||||
/// <param name="callback">The callback to be invoked with the updated <see cref="T:Realms.IRealmCollection`1" />.</param>
|
||||
/// <returns>
|
||||
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||
/// To stop receiving notifications, call <see cref="M:System.IDisposable.Dispose" />.
|
||||
///
|
||||
/// May be null in the case the provided collection is not managed.
|
||||
/// </returns>
|
||||
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0})" />
|
||||
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0})" />
|
||||
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
|
||||
where T : RealmObjectBase
|
||||
{
|
||||
// Subscriptions can only work on the main thread.
|
||||
if (!ThreadSafety.IsUpdateThread)
|
||||
throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread.");
|
||||
|
||||
return collection.SubscribeForNotifications(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A convenience method that casts <see cref="IQueryable{T}"/> to <see cref="IRealmCollection{T}"/> and subscribes for change notifications.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
|
||||
/// </remarks>
|
||||
/// <param name="list">The <see cref="IQueryable{T}"/> to observe for changes.</param>
|
||||
/// <typeparam name="T">Type of the elements in the list.</typeparam>
|
||||
/// <seealso cref="IRealmCollection{T}.SubscribeForNotifications"/>
|
||||
/// <param name="callback">The callback to be invoked with the updated <see cref="IRealmCollection{T}"/>.</param>
|
||||
/// <returns>
|
||||
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
|
||||
///
|
||||
/// May be null in the case the provided collection is not managed.
|
||||
/// </returns>
|
||||
public static IDisposable? QueryAsyncWithNotifications<T>(this IQueryable<T> list, NotificationCallbackDelegate<T> callback)
|
||||
where T : RealmObjectBase
|
||||
{
|
||||
// Subscribing to non-managed instances doesn't work.
|
||||
// In this usage, the instance may be non-managed in tests.
|
||||
if (!(list is IRealmCollection<T> realmCollection))
|
||||
return null;
|
||||
|
||||
return QueryAsyncWithNotifications(realmCollection, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A convenience method that casts <see cref="IList{T}"/> to <see cref="IRealmCollection{T}"/> and subscribes for change notifications.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
|
||||
/// </remarks>
|
||||
/// <param name="list">The <see cref="IList{T}"/> to observe for changes.</param>
|
||||
/// <typeparam name="T">Type of the elements in the list.</typeparam>
|
||||
/// <seealso cref="IRealmCollection{T}.SubscribeForNotifications"/>
|
||||
/// <param name="callback">The callback to be invoked with the updated <see cref="IRealmCollection{T}"/>.</param>
|
||||
/// <returns>
|
||||
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
|
||||
///
|
||||
/// May be null in the case the provided collection is not managed.
|
||||
/// </returns>
|
||||
public static IDisposable? QueryAsyncWithNotifications<T>(this IList<T> list, NotificationCallbackDelegate<T> callback)
|
||||
where T : RealmObjectBase
|
||||
{
|
||||
// Subscribing to non-managed instances doesn't work.
|
||||
// In this usage, the instance may be non-managed in tests.
|
||||
if (!(list is IRealmCollection<T> realmCollection))
|
||||
return null;
|
||||
|
||||
return QueryAsyncWithNotifications(realmCollection, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ namespace osu.Game.Graphics.Containers
|
||||
if (clock == null)
|
||||
return;
|
||||
|
||||
double currentTrackTime = clock.CurrentTime;
|
||||
double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds;
|
||||
|
||||
if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded)
|
||||
{
|
||||
@ -132,13 +132,11 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
// this may be the case where the beat syncing clock has been paused.
|
||||
// we still want to show an idle animation, so use this container's time instead.
|
||||
currentTrackTime = Clock.CurrentTime;
|
||||
currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds;
|
||||
timingPoint = TimingControlPoint.DEFAULT;
|
||||
effectPoint = EffectControlPoint.DEFAULT;
|
||||
}
|
||||
|
||||
currentTrackTime += EarlyActivationMilliseconds;
|
||||
|
||||
double beatLength = timingPoint.BeatLength / Divisor;
|
||||
|
||||
while (beatLength < MinimumBeatLength)
|
||||
|
@ -18,6 +18,7 @@ namespace osu.Game.IPC
|
||||
: base(host)
|
||||
{
|
||||
this.importer = importer;
|
||||
|
||||
MessageReceived += msg =>
|
||||
{
|
||||
Debug.Assert(importer != null);
|
||||
@ -25,6 +26,8 @@ namespace osu.Game.IPC
|
||||
{
|
||||
if (t.Exception != null) throw t.Exception;
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
@ -56,7 +55,7 @@ namespace osu.Game.Input.Bindings
|
||||
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
|
||||
|
||||
realmSubscription = realmKeyBindings
|
||||
.SubscribeForNotifications((sender, changes, error) =>
|
||||
.QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
{
|
||||
// first subscription ignored as we are handling this in LoadComplete.
|
||||
if (changes == null)
|
||||
|
@ -35,9 +35,14 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString ConfineMouseMode => new TranslatableString(getKey(@"confine_mouse_mode"), @"Confine mouse cursor to window");
|
||||
|
||||
/// <summary>
|
||||
/// "Disable mouse wheel during gameplay"
|
||||
/// "Disable mouse wheel adjusting volume during gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString DisableMouseWheel => new TranslatableString(getKey(@"disable_mouse_wheel"), @"Disable mouse wheel during gameplay");
|
||||
public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Volume can still be adjusted using the mouse wheel by holding "Alt""
|
||||
/// </summary>
|
||||
public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt""");
|
||||
|
||||
/// <summary>
|
||||
/// "Disable mouse buttons during gameplay"
|
||||
|
@ -50,6 +50,8 @@ namespace osu.Game.Models
|
||||
|
||||
public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
|
||||
|
||||
public bool Equals(IRulesetInfo? other) => other is RealmRuleset b && Equals(b);
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public RealmRuleset Clone() => new RealmRuleset
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Online.API
|
||||
if (WebRequest != null)
|
||||
{
|
||||
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
|
||||
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes");
|
||||
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,12 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
var request = base.CreateWebRequest();
|
||||
|
||||
request.AddParameter(@"id", beatmapInfo.OnlineID.ToString());
|
||||
request.AddParameter(@"checksum", beatmapInfo.MD5Hash);
|
||||
request.AddParameter(@"filename", filename);
|
||||
if (beatmapInfo.OnlineID > 0)
|
||||
request.AddParameter(@"id", beatmapInfo.OnlineID.ToString());
|
||||
if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash))
|
||||
request.AddParameter(@"checksum", beatmapInfo.MD5Hash);
|
||||
if (!string.IsNullOrEmpty(filename))
|
||||
request.AddParameter(@"filename", filename);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests
|
||||
public class GetUserRequest : APIRequest<APIUser>
|
||||
{
|
||||
public readonly string Lookup;
|
||||
public readonly RulesetInfo Ruleset;
|
||||
public readonly IRulesetInfo Ruleset;
|
||||
private readonly LookupType lookupType;
|
||||
|
||||
/// <summary>
|
||||
@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to get.</param>
|
||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
|
||||
public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null)
|
||||
{
|
||||
Lookup = userId.ToString();
|
||||
lookupType = LookupType.Id;
|
||||
@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests
|
||||
/// </summary>
|
||||
/// <param name="username">The user to get.</param>
|
||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||
public GetUserRequest(string username = null, RulesetInfo ruleset = null)
|
||||
public GetUserRequest(string username = null, IRulesetInfo ruleset = null)
|
||||
{
|
||||
Lookup = username;
|
||||
lookupType = LookupType.Username;
|
||||
|
@ -65,9 +65,6 @@ namespace osu.Game.Online.Leaderboards
|
||||
[Resolved(CanBeNull = true)]
|
||||
private SongSelect songSelect { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
|
@ -3,13 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Online.Leaderboards
|
||||
@ -25,9 +23,6 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
public UserTopScoreContainer(Func<TScoreInfo, LeaderboardScore> createScoreDelegate)
|
||||
{
|
||||
this.createScoreDelegate = createScoreDelegate;
|
||||
|
@ -32,12 +32,36 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
public event Action? RoomUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new user joins the room.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserJoined;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user leaves the room of their own accord.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user was kicked from the room forcefully.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserKicked;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new item is added to the playlist.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerPlaylistItem>? ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item is removed from the playlist. The provided <c>long</c> is the playlist's item ID.
|
||||
/// </summary>
|
||||
public event Action<long>? ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item's details change.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerPlaylistItem>? ItemChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
|
||||
/// </summary>
|
||||
@ -94,7 +118,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
protected IAPIProvider API { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
protected RulesetStore Rulesets { get; private set; } = null!;
|
||||
protected IRulesetStore Rulesets { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
@ -642,6 +666,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room.Playlist.Add(item);
|
||||
APIRoom.Playlist.Add(playlistItem);
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
}
|
||||
@ -661,6 +686,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
|
||||
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
|
||||
|
||||
ItemRemoved?.Invoke(playlistItemId);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
@ -691,6 +717,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID)
|
||||
CurrentMatchPlayingItem.Value = playlistItem;
|
||||
|
||||
ItemChanged?.Invoke(item);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
}
|
||||
@ -729,6 +756,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
private async Task<PlaylistItem> createPlaylistItem(MultiplayerPlaylistItem item, bool populateBeatmapImmediately)
|
||||
{
|
||||
var ruleset = Rulesets.GetRuleset(item.RulesetID);
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
var rulesetInstance = ruleset.CreateInstance();
|
||||
|
||||
var playlistItem = new PlaylistItem
|
||||
@ -737,7 +767,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
BeatmapID = item.BeatmapID,
|
||||
OwnerID = item.OwnerID,
|
||||
Ruleset = { Value = ruleset },
|
||||
Expired = item.Expired
|
||||
Expired = item.Expired,
|
||||
PlaylistOrder = item.PlaylistOrder,
|
||||
PlayedAt = item.PlayedAt
|
||||
};
|
||||
|
||||
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
|
||||
|
@ -39,6 +39,22 @@ namespace osu.Game.Online.Rooms
|
||||
[Key(7)]
|
||||
public bool Expired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The order in which this <see cref="MultiplayerPlaylistItem"/> will be played relative to others.
|
||||
/// Playlist items should be played in increasing order (lower values are played first).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only valid for items which are not <see cref="Expired"/>. The value for expired items is undefined and should not be used.
|
||||
/// </remarks>
|
||||
[Key(8)]
|
||||
public ushort PlaylistOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date when this <see cref="MultiplayerPlaylistItem"/> was played.
|
||||
/// </summary>
|
||||
[Key(9)]
|
||||
public DateTimeOffset? PlayedAt { get; set; }
|
||||
|
||||
public MultiplayerPlaylistItem()
|
||||
{
|
||||
}
|
||||
@ -52,6 +68,8 @@ namespace osu.Game.Online.Rooms
|
||||
RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray();
|
||||
AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray();
|
||||
Expired = item.Expired;
|
||||
PlaylistOrder = item.PlaylistOrder ?? 0;
|
||||
PlayedAt = item.PlayedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
@ -33,6 +34,12 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("expired")]
|
||||
public bool Expired { get; set; }
|
||||
|
||||
[JsonProperty("playlist_order")]
|
||||
public ushort? PlaylistOrder { get; set; }
|
||||
|
||||
[JsonProperty("played_at")]
|
||||
public DateTimeOffset? PlayedAt { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IBindable<bool> Valid => valid;
|
||||
|
||||
@ -79,11 +86,13 @@ namespace osu.Game.Online.Rooms
|
||||
|
||||
public void MarkInvalid() => valid.Value = false;
|
||||
|
||||
public void MapObjects(RulesetStore rulesets)
|
||||
public void MapObjects(IRulesetStore rulesets)
|
||||
{
|
||||
Beatmap.Value ??= apiBeatmap;
|
||||
Ruleset.Value ??= rulesets.GetRuleset(RulesetID);
|
||||
|
||||
Debug.Assert(Ruleset.Value != null);
|
||||
|
||||
Ruleset rulesetInstance = Ruleset.Value.CreateInstance();
|
||||
|
||||
if (allowedModsBacking != null)
|
||||
|
@ -136,30 +136,32 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
public void BeginPlaying(GameplayState state, Score score)
|
||||
{
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
// This schedule is only here to match the one below in `EndPlaying`.
|
||||
Schedule(() =>
|
||||
{
|
||||
if (IsPlaying)
|
||||
throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
|
||||
|
||||
if (IsPlaying)
|
||||
throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
|
||||
IsPlaying = true;
|
||||
|
||||
IsPlaying = true;
|
||||
// transfer state at point of beginning play
|
||||
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
|
||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||
|
||||
// transfer state at point of beginning play
|
||||
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
|
||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||
currentBeatmap = state.Beatmap;
|
||||
currentScore = score;
|
||||
|
||||
currentBeatmap = state.Beatmap;
|
||||
currentScore = score;
|
||||
|
||||
BeginPlayingInternal(currentState);
|
||||
BeginPlayingInternal(currentState);
|
||||
});
|
||||
}
|
||||
|
||||
public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data);
|
||||
|
||||
public void EndPlaying()
|
||||
{
|
||||
// This method is most commonly called via Dispose(), which is asynchronous.
|
||||
// Todo: This should not be a thing, but requires framework changes.
|
||||
// This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue).
|
||||
// We probably need to find a better way to handle this...
|
||||
Schedule(() =>
|
||||
{
|
||||
if (!IsPlaying)
|
||||
|
@ -161,7 +161,7 @@ namespace osu.Game
|
||||
|
||||
private Bindable<float> uiScale;
|
||||
|
||||
private Bindable<int> configSkin;
|
||||
private Bindable<string> configSkin;
|
||||
|
||||
private readonly string[] args;
|
||||
|
||||
@ -243,27 +243,22 @@ namespace osu.Game
|
||||
Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName;
|
||||
|
||||
// bind config int to database SkinInfo
|
||||
configSkin = LocalConfig.GetBindable<int>(OsuSetting.Skin);
|
||||
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID;
|
||||
configSkin = LocalConfig.GetBindable<string>(OsuSetting.Skin);
|
||||
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
|
||||
configSkin.ValueChanged += skinId =>
|
||||
{
|
||||
var skinInfo = SkinManager.Query(s => s.ID == skinId.NewValue);
|
||||
ILive<SkinInfo> skinInfo = null;
|
||||
|
||||
if (Guid.TryParse(skinId.NewValue, out var guid))
|
||||
skinInfo = SkinManager.Query(s => s.ID == guid);
|
||||
|
||||
if (skinInfo == null)
|
||||
{
|
||||
switch (skinId.NewValue)
|
||||
{
|
||||
case -1:
|
||||
skinInfo = DefaultLegacySkin.Info;
|
||||
break;
|
||||
|
||||
default:
|
||||
skinInfo = SkinInfo.Default;
|
||||
break;
|
||||
}
|
||||
if (guid == SkinInfo.CLASSIC_SKIN)
|
||||
skinInfo = DefaultLegacySkin.CreateInfo().ToLive();
|
||||
}
|
||||
|
||||
SkinManager.CurrentSkinInfo.Value = skinInfo;
|
||||
SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLive();
|
||||
};
|
||||
configSkin.TriggerChange();
|
||||
|
||||
@ -664,7 +659,7 @@ namespace osu.Game
|
||||
|
||||
// make config aware of how to lookup skins for on-screen display purposes.
|
||||
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
|
||||
LocalConfig.LookupSkinName = id => SkinManager.GetAllUsableSkins().FirstOrDefault(s => s.ID == id)?.ToString() ?? "Unknown";
|
||||
LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown";
|
||||
|
||||
LocalConfig.LookupKeyBindings = l =>
|
||||
{
|
||||
|
@ -196,9 +196,12 @@ namespace osu.Game
|
||||
runMigrations();
|
||||
|
||||
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage));
|
||||
dependencies.CacheAs<IRulesetStore>(RulesetStore);
|
||||
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory));
|
||||
|
||||
new EFToRealmMigrator(contextFactory, realmFactory, LocalConfig).Run();
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
|
||||
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
||||
@ -212,17 +215,9 @@ namespace osu.Game
|
||||
|
||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||
|
||||
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio));
|
||||
dependencies.Cache(SkinManager = new SkinManager(Storage, realmFactory, Host, Resources, Audio, Scheduler));
|
||||
dependencies.CacheAs<ISkinSource>(SkinManager);
|
||||
|
||||
// needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
|
||||
SkinManager.ItemRemoved += item => Schedule(() =>
|
||||
{
|
||||
// check the removed skin is not the current user choice. if it is, switch back to default.
|
||||
if (item.Equals(SkinManager.CurrentSkinInfo.Value))
|
||||
SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
|
||||
});
|
||||
|
||||
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
|
||||
|
||||
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
|
||||
|
@ -15,7 +15,6 @@ using osu.Framework.Threading;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -61,9 +60,6 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
public BeatmapListingFilterControl()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
private void onRulesetChanged(ValueChangedEvent<IRulesetInfo> ruleset)
|
||||
{
|
||||
@ -57,8 +57,13 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
if (ruleset.NewValue == null)
|
||||
return;
|
||||
|
||||
var rulesetInstance = rulesets.GetRuleset(ruleset.NewValue.OnlineID)?.CreateInstance();
|
||||
|
||||
if (rulesetInstance == null)
|
||||
return;
|
||||
|
||||
modsContainer.Add(new ModButton(new ModNoMod()));
|
||||
modsContainer.AddRange(rulesets.GetRuleset(ruleset.NewValue.OnlineID).CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m)));
|
||||
modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m)));
|
||||
|
||||
modsContainer.ForEach(button =>
|
||||
{
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -12,7 +11,6 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet;
|
||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -24,9 +22,6 @@ namespace osu.Game.Overlays
|
||||
public const float Y_PADDING = 25;
|
||||
public const float RIGHT_WIDTH = 275;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
private readonly Bindable<APIBeatmapSet> beatmapSet = new Bindable<APIBeatmapSet>();
|
||||
|
||||
// receive input outside our bounds so we can trigger a close event on ourselves.
|
||||
|
@ -43,9 +43,6 @@ namespace osu.Game.Overlays.Dashboard
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
|
@ -1,21 +1,20 @@
|
||||
// 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 osuTK;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
@ -24,9 +23,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected RulesetStore Rulesets { get; private set; }
|
||||
|
||||
protected int VisiblePages;
|
||||
protected int ItemsPerPage;
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
private readonly APIRecentActivity activity;
|
||||
|
||||
|
@ -1,21 +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 osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Rankings.Tables;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Rankings.Tables;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Rankings
|
||||
{
|
||||
@ -29,9 +29,6 @@ namespace osu.Game.Overlays.Rankings
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
private CancellationTokenSource cancellationToken;
|
||||
private GetSpotlightRankingsRequest getRankingsRequest;
|
||||
private GetSpotlightsRequest spotlightsRequest;
|
||||
|
@ -67,7 +67,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.DisableMouseWheel,
|
||||
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
|
||||
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel)
|
||||
},
|
||||
new SettingsCheckbox
|
||||
|
@ -106,7 +106,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
|
||||
{
|
||||
deleteSkinsButton.Enabled.Value = false;
|
||||
Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
|
||||
Task.Run(() =>
|
||||
{
|
||||
skins.Delete();
|
||||
}).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
@ -32,32 +32,26 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
Icon = FontAwesome.Solid.PaintBrush
|
||||
};
|
||||
|
||||
private readonly Bindable<SkinInfo> dropdownBindable = new Bindable<SkinInfo> { Default = SkinInfo.Default };
|
||||
private readonly Bindable<int> configBindable = new Bindable<int>();
|
||||
private readonly Bindable<ILive<SkinInfo>> dropdownBindable = new Bindable<ILive<SkinInfo>> { Default = DefaultSkin.CreateInfo().ToLive() };
|
||||
private readonly Bindable<string> configBindable = new Bindable<string>();
|
||||
|
||||
private static readonly SkinInfo random_skin_info = new SkinInfo
|
||||
private static readonly ILive<SkinInfo> random_skin_info = new SkinInfo
|
||||
{
|
||||
ID = SkinInfo.RANDOM_SKIN,
|
||||
Name = "<Random Skin>",
|
||||
};
|
||||
}.ToLive();
|
||||
|
||||
private List<SkinInfo> skinItems;
|
||||
|
||||
private int firstNonDefaultSkinIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
int index = skinItems.FindIndex(s => s.ID > 0);
|
||||
if (index < 0)
|
||||
index = skinItems.Count;
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
private List<ILive<SkinInfo>> skinItems;
|
||||
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmContextFactory realmFactory { get; set; }
|
||||
|
||||
private IDisposable realmSubscription;
|
||||
private IQueryable<SkinInfo> realmSkins;
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
|
||||
{
|
||||
@ -75,96 +69,95 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
new ExportSkinButton(),
|
||||
};
|
||||
|
||||
skins.ItemUpdated += itemUpdated;
|
||||
skins.ItemRemoved += itemRemoved;
|
||||
|
||||
config.BindWith(OsuSetting.Skin, configBindable);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
skinDropdown.Current = dropdownBindable;
|
||||
|
||||
realmSkins = realmFactory.Context.All<SkinInfo>()
|
||||
.Where(s => !s.DeletePending)
|
||||
.OrderByDescending(s => s.Protected) // protected skins should be at the top.
|
||||
.ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
realmSubscription = realmSkins
|
||||
.QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
{
|
||||
if (changes == null)
|
||||
return;
|
||||
|
||||
// Eventually this should be handling the individual changes rather than refreshing the whole dropdown.
|
||||
updateItems();
|
||||
});
|
||||
|
||||
updateItems();
|
||||
|
||||
// Todo: This should not be necessary when OsuConfigManager is databased
|
||||
if (skinDropdown.Items.All(s => s.ID != configBindable.Value))
|
||||
configBindable.Value = 0;
|
||||
configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig));
|
||||
updateSelectedSkinFromConfig();
|
||||
|
||||
configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true);
|
||||
dropdownBindable.BindValueChanged(skin =>
|
||||
{
|
||||
if (skin.NewValue == random_skin_info)
|
||||
if (skin.NewValue.Equals(random_skin_info))
|
||||
{
|
||||
var skinBefore = skins.CurrentSkinInfo.Value;
|
||||
|
||||
skins.SelectRandomSkin();
|
||||
|
||||
if (skinBefore == skins.CurrentSkinInfo.Value)
|
||||
{
|
||||
// the random selection didn't change the skin, so we should manually update the dropdown to match.
|
||||
dropdownBindable.Value = skins.CurrentSkinInfo.Value;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
configBindable.Value = skin.NewValue.ID;
|
||||
configBindable.Value = skin.NewValue.ID.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSelectedSkinFromConfig()
|
||||
{
|
||||
int id = configBindable.Value;
|
||||
ILive<SkinInfo> skin = null;
|
||||
|
||||
var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == id);
|
||||
if (Guid.TryParse(configBindable.Value, out var configId))
|
||||
skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId);
|
||||
|
||||
if (skin == null)
|
||||
{
|
||||
// there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown.
|
||||
// to avoid adding complexity, let's just ensure the item is added so we can perform the selection.
|
||||
skin = skins.Query(s => s.ID == id);
|
||||
addItem(skin);
|
||||
}
|
||||
|
||||
dropdownBindable.Value = skin;
|
||||
dropdownBindable.Value = skin ?? skinDropdown.Items.First();
|
||||
}
|
||||
|
||||
private void updateItems()
|
||||
{
|
||||
skinItems = skins.GetAllUsableSkins();
|
||||
skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info);
|
||||
sortUserSkins(skinItems);
|
||||
int protectedCount = realmSkins.Count(s => s.Protected);
|
||||
|
||||
skinItems = realmSkins.ToLive();
|
||||
|
||||
skinItems.Insert(protectedCount, random_skin_info);
|
||||
|
||||
skinDropdown.Items = skinItems;
|
||||
}
|
||||
|
||||
private void itemUpdated(SkinInfo item) => Schedule(() => addItem(item));
|
||||
|
||||
private void addItem(SkinInfo item)
|
||||
{
|
||||
List<SkinInfo> newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList();
|
||||
sortUserSkins(newDropdownItems);
|
||||
skinDropdown.Items = newDropdownItems;
|
||||
}
|
||||
|
||||
private void itemRemoved(SkinInfo item) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).ToArray());
|
||||
|
||||
private void sortUserSkins(List<SkinInfo> skinsList)
|
||||
{
|
||||
// Sort user skins separately from built-in skins
|
||||
skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex,
|
||||
Comparer<SkinInfo>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skins != null)
|
||||
{
|
||||
skins.ItemUpdated -= itemUpdated;
|
||||
skins.ItemRemoved -= itemRemoved;
|
||||
}
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
private class SkinSettingsDropdown : SettingsDropdown<SkinInfo>
|
||||
private class SkinSettingsDropdown : SettingsDropdown<ILive<SkinInfo>>
|
||||
{
|
||||
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();
|
||||
protected override OsuDropdown<ILive<SkinInfo>> CreateDropdown() => new SkinDropdownControl();
|
||||
|
||||
private class SkinDropdownControl : DropdownControl
|
||||
{
|
||||
protected override LocalisableString GenerateItemText(SkinInfo item) => item.ToString();
|
||||
protected override LocalisableString GenerateItemText(ILive<SkinInfo> item) => item.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private class ExportSkinButton : SettingsButton
|
||||
public class ExportSkinButton : SettingsButton
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
@ -179,16 +172,21 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
{
|
||||
Text = SkinSettingsStrings.ExportSkinButton;
|
||||
Action = export;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
||||
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true);
|
||||
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
|
||||
}
|
||||
|
||||
private void export()
|
||||
{
|
||||
try
|
||||
{
|
||||
new LegacySkinExporter(storage).Export(currentSkin.Value.SkinInfo);
|
||||
currentSkin.Value.SkinInfo.PerformRead(s => new LegacySkinExporter(storage).Export(s));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -5,19 +5,14 @@ using System.Linq;
|
||||
using Markdig.Extensions.Yaml;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Game.Graphics.Containers.Markdown;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
public class WikiMarkdownContainer : OsuMarkdownContainer
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
public string CurrentPath
|
||||
{
|
||||
set => DocumentUrl = value;
|
||||
|
@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
||||
/// </summary>
|
||||
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
||||
public IEnumerable<DifficultyAttributes> CalculateAll()
|
||||
public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
||||
{
|
||||
if (combination is MultiMod multi)
|
||||
yield return Calculate(multi.Mods);
|
||||
yield return Calculate(multi.Mods, cancellationToken);
|
||||
else
|
||||
yield return Calculate(combination.Yield());
|
||||
yield return Calculate(combination.Yield(), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +145,11 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
playableMods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
|
||||
Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
|
||||
// Only pass through the cancellation token if it's non-default.
|
||||
// This allows for the default timeout to be applied for playable beatmap construction.
|
||||
Beatmap = cancellationToken == default
|
||||
? beatmap.GetPlayableBeatmap(ruleset, playableMods)
|
||||
: beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user