1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 20:59:35 +08:00

Merge branch 'master' into editor-setup-no-state-change

This commit is contained in:
Bartłomiej Dach 2022-08-14 18:21:31 +02:00 committed by GitHub
commit 05ddfe7522
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
203 changed files with 2838 additions and 1031 deletions

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.X, PippidonAction.Button2), new KeyBinding(InputKey.X, PippidonAction.Button2),
}; };
public override Drawable CreateIcon() => new Sprite public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
{
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
} }
} }

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Pippidon
{
public class PippidonRulesetIcon : Sprite
{
private readonly Ruleset ruleset;
public PippidonRulesetIcon(Ruleset ruleset)
{
this.ruleset = ruleset;
}
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin");
}
}
}

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -47,10 +45,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.S, PippidonAction.MoveDown), new KeyBinding(InputKey.S, PippidonAction.MoveDown),
}; };
public override Drawable CreateIcon() => new Sprite public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
{
Margin = new MarginPadding { Top = 3 },
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
} }
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Pippidon
{
public class PippidonRulesetIcon : Sprite
{
private readonly Ruleset ruleset;
public PippidonRulesetIcon(Ruleset ruleset)
{
this.ruleset = ruleset;
Margin = new MarginPadding { Top = 3 };
}
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin");
}
}
}

View File

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

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Text; using System.Text;
using DiscordRPC; using DiscordRPC;
@ -26,15 +24,15 @@ namespace osu.Desktop
{ {
private const string client_id = "367827983903490050"; private const string client_id = "367827983903490050";
private DiscordRpcClient client; private DiscordRpcClient client = null!;
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private IBindable<APIUser> user; private IBindable<APIUser> user = null!;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; } = null!;
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>(); private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
@ -130,7 +128,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
{ {
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics statistics)) if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics))
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
@ -164,7 +162,7 @@ namespace osu.Desktop
}); });
} }
private IBeatmapInfo getBeatmap(UserActivity activity) private IBeatmapInfo? getBeatmap(UserActivity activity)
{ {
switch (activity) switch (activity)
{ {
@ -183,10 +181,10 @@ namespace osu.Desktop
switch (activity) switch (activity)
{ {
case UserActivity.InGame game: case UserActivity.InGame game:
return game.BeatmapInfo.ToString(); return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit: case UserActivity.Editing edit:
return edit.BeatmapInfo.ToString(); return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.InLobby lobby: case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Desktop.LegacyIpc namespace osu.Desktop.LegacyIpc
{ {
/// <summary> /// <summary>
@ -13,7 +11,7 @@ namespace osu.Desktop.LegacyIpc
/// </remarks> /// </remarks>
public class LegacyIpcDifficultyCalculationRequest public class LegacyIpcDifficultyCalculationRequest
{ {
public string BeatmapFile { get; set; } public string BeatmapFile { get; set; } = string.Empty;
public int RulesetId { get; set; } public int RulesetId { get; set; }
public int Mods { get; set; } public int Mods { get; set; }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Desktop.LegacyIpc namespace osu.Desktop.LegacyIpc
{ {
/// <summary> /// <summary>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Platform; using osu.Framework.Platform;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -39,17 +37,20 @@ namespace osu.Desktop.LegacyIpc
public new object Value public new object Value
{ {
get => base.Value; get => base.Value;
set => base.Value = new Data set => base.Value = new Data(value.GetType().Name, value);
{
MessageType = value.GetType().Name,
MessageData = value
};
} }
public class Data public class Data
{ {
public string MessageType { get; set; } public string MessageType { get; }
public object MessageData { get; set; }
public object MessageData { get; }
public Data(string messageType, object messageData)
{
MessageType = messageType;
MessageData = messageData;
}
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
@ -27,7 +25,7 @@ namespace osu.Desktop
private const string base_game_name = @"osu"; private const string base_game_name = @"osu";
#endif #endif
private static LegacyTcpIpcProvider legacyIpc; private static LegacyTcpIpcProvider? legacyIpc;
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Security.Principal; using System.Security.Principal;
using osu.Framework; using osu.Framework;
@ -21,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component public class ElevatedPrivilegesChecker : Component
{ {
[Resolved] [Resolved]
private INotificationOverlay notifications { get; set; } private INotificationOverlay notifications { get; set; } = null!;
private bool elevated; private bool elevated;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -26,8 +24,8 @@ namespace osu.Desktop.Updater
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{ {
private UpdateManager updateManager; private UpdateManager? updateManager;
private INotificationOverlay notificationOverlay; private INotificationOverlay notificationOverlay = null!;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@ -50,12 +48,12 @@ namespace osu.Desktop.Updater
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null)
{ {
// should we schedule a retry on completion of this check? // should we schedule a retry on completion of this check?
bool scheduleRecheck = true; bool scheduleRecheck = true;
const string github_token = null; // TODO: populate. const string? github_token = null; // TODO: populate.
try try
{ {
@ -145,7 +143,7 @@ namespace osu.Desktop.Updater
private class UpdateCompleteNotification : ProgressCompletionNotification private class UpdateCompleteNotification : ProgressCompletionNotification
{ {
[Resolved] [Resolved]
private OsuGame game { get; set; } private OsuGame game { get; set; } = null!;
public UpdateCompleteNotification(SquirrelUpdateManager updateManager) public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
{ {
@ -154,7 +152,7 @@ namespace osu.Desktop.Updater
Activated = () => Activated = () =>
{ {
updateManager.PrepareUpdateAsync() updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game?.AttemptExit())); .ContinueWith(_ => updateManager.Schedule(() => game.AttemptExit()));
return true; return true;
}; };
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,12 +12,12 @@ namespace osu.Desktop.Windows
{ {
public class GameplayWinKeyBlocker : Component public class GameplayWinKeyBlocker : Component
{ {
private Bindable<bool> disableWinKey; private Bindable<bool> disableWinKey = null!;
private IBindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying = null!;
private IBindable<bool> isActive; private IBindable<bool> isActive = null!;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config) private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -21,7 +19,7 @@ namespace osu.Desktop.Windows
private const int wm_syskeyup = 261; private const int wm_syskeyup = 261;
//Resharper disable once NotAccessedField.Local //Resharper disable once NotAccessedField.Local
private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC private static LowLevelKeyboardProcDelegate? keyboardHookDelegate; // keeping a reference alive for the GC
private static IntPtr keyHook; private static IntPtr keyHook;
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]

View 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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneBarLine : ManiaSkinnableTestScene
{
[Test]
public void TestMinor()
{
AddStep("Create barlines", () => recreate());
}
private void recreate(Func<IEnumerable<BarLine>>? createBarLines = null)
{
var stageDefinitions = new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
};
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
{
if (createBarLines != null)
{
var barLines = createBarLines();
foreach (var b in barLines)
s.Add(b);
return;
}
for (int i = 0; i < 64; i++)
{
s.Add(new BarLine
{
StartTime = Time.Current + i * 500,
Major = i % 4 == 0,
});
}
}));
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "CS"; public override string Acronym => "CS";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.9;
public override string Description => "No more tricky speed changes!"; public override string Description => "No more tricky speed changes!";

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -16,21 +13,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableBarLine : DrawableManiaHitObject<BarLine> public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{ {
/// <summary>
/// Height of major bar line triangles.
/// </summary>
private const float triangle_height = 12;
/// <summary>
/// Offset of the major bar line triangles from the sides of the bar line.
/// </summary>
private const float triangle_offset = 9;
public DrawableBarLine(BarLine barLine) public DrawableBarLine(BarLine barLine)
: base(barLine) : base(barLine)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 2f; Height = barLine.Major ? 1.7f : 1.2f;
AddInternal(new Box AddInternal(new Box
{ {
@ -38,34 +25,33 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = new Color4(255, 204, 33, 255), Alpha = barLine.Major ? 0.5f : 0.2f
}); });
if (barLine.Major) if (barLine.Major)
{ {
AddInternal(new EquilateralTriangle Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(new Circle
{ {
Name = "Left triangle", Name = "Left line",
Anchor = Anchor.BottomLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre, Origin = Anchor.CentreRight,
Size = new Vector2(triangle_height),
X = -triangle_offset, Size = size,
Rotation = 90 X = -line_offset,
}); });
AddInternal(new EquilateralTriangle AddInternal(new Circle
{ {
Name = "Right triangle", Name = "Right line",
Anchor = Anchor.BottomRight, Anchor = Anchor.CentreRight,
Origin = Anchor.TopCentre, Origin = Anchor.CentreLeft,
Size = new Vector2(triangle_height), Size = size,
X = triangle_offset, X = line_offset,
Rotation = -90
}); });
} }
if (!barLine.Major)
Alpha = 0.2f;
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -9,7 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;

View File

@ -6,7 +6,8 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Allocation;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[HeadlessTest] [HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene public class LegacyMainCirclePieceTest : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; } = null!;
private static readonly object?[][] texture_priority_cases = private static readonly object?[][] texture_priority_cases =
{ {
// default priority lookup // default priority lookup
@ -76,7 +80,12 @@ namespace osu.Game.Rulesets.Osu.Tests
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase(); skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>())) skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName }); .Returns((string componentName, WrapMode _, WrapMode _) =>
{
var tex = renderer.CreateTexture(1, 1);
tex.AssetName = componentName;
return tex;
});
Child = new DependencyProvidingContainer Child = new DependencyProvidingContainer
{ {
@ -84,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Child = piece = new TestLegacyMainCirclePiece(priorityLookup), Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
}; };
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray(); var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2); Debug.Assert(sprites.Length <= 2);
}); });

View File

@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6369583000323935d, 206, "diffcalc-test")] [TestCase(6.7115569159190587d, 206, "diffcalc-test")]
[TestCase(1.4476531024675374d, 45, "zero-length-sliders")] [TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.8816128335486386d, 206, "diffcalc-test")] [TestCase(8.9757300665532966d, 206, "diffcalc-test")]
[TestCase(1.7540389962596916d, 45, "zero-length-sliders")] [TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6369583000323935d, 239, "diffcalc-test")] [TestCase(6.7115569159190587d, 239, "diffcalc-test")]
[TestCase(1.4476531024675374d, 54, "zero-length-sliders")] [TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Audio; using osu.Game.Audio;
@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneCursorTrail : OsuTestScene public class TestSceneCursorTrail : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; }
[Test] [Test]
public void TestSmoothCursorTrail() public void TestSmoothCursorTrail()
{ {
@ -44,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(false); var skinContainer = new LegacySkinContainer(renderer, false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -58,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(true); var skinContainer = new LegacySkinContainer(renderer, true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -82,10 +85,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached(typeof(ISkinSource))] [Cached(typeof(ISkinSource))]
private class LegacySkinContainer : Container, ISkinSource private class LegacySkinContainer : Container, ISkinSource
{ {
private readonly IRenderer renderer;
private readonly bool disjoint; private readonly bool disjoint;
public LegacySkinContainer(bool disjoint) public LegacySkinContainer(IRenderer renderer, bool disjoint)
{ {
this.renderer = renderer;
this.disjoint = disjoint; this.disjoint = disjoint;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -98,14 +103,14 @@ namespace osu.Game.Rulesets.Osu.Tests
switch (componentName) switch (componentName)
{ {
case "cursortrail": case "cursortrail":
var tex = new Texture(Texture.WhitePixel.TextureGL); var tex = new Texture(renderer.WhitePixel);
if (disjoint) if (disjoint)
tex.ScaleAdjust = 1 / 25f; tex.ScaleAdjust = 1 / 25f;
return tex; return tex;
case "cursormiddle": case "cursormiddle":
return disjoint ? null : Texture.WhitePixel; return disjoint ? null : renderer.WhitePixel;
} }
return null; return null;

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;

View File

@ -12,7 +12,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Timing; using osu.Framework.Timing;

View File

@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator public static class AimEvaluator
{ {
private const double wide_angle_multiplier = 1.5; private const double wide_angle_multiplier = 1.5;
private const double acute_angle_multiplier = 2.0; private const double acute_angle_multiplier = 1.95;
private const double slider_multiplier = 1.5; private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75; private const double velocity_change_multiplier = 0.75;
/// <summary> /// <summary>
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
} }
if (osuLastObj.TravelTime != 0) if (osuLastObj.BaseObject is Slider)
{ {
// Reward sliders based on velocity. // Reward sliders based on velocity.
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;

View File

@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double max_opacity_bonus = 0.4; private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2; private const double hidden_bonus = 0.2;
private const double min_velocity = 0.5;
private const double slider_multiplier = 1.3;
/// <summary> /// <summary>
/// Evaluates the difficulty of memorising and hitting an object, based on: /// Evaluates the difficulty of memorising and hitting an object, based on:
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>distance between the previous and current object,</description></item> /// <item><description>distance between a number of previous objects and the current object,</description></item>
/// <item><description>the visual opacity of the current object,</description></item> /// <item><description>the visual opacity of the current object,</description></item>
/// <item><description>length and speed of the current object (for sliders),</description></item>
/// <item><description>and whether the hidden mod is enabled.</description></item> /// <item><description>and whether the hidden mod is enabled.</description></item>
/// </list> /// </list>
/// </summary> /// </summary>
@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden) if (hidden)
result *= 1.0 + hidden_bonus; result *= 1.0 + hidden_bonus;
double sliderBonus = 0.0;
if (osuCurrent.BaseObject is Slider osuSlider)
{
// Invert the scaling factor to determine the true travel distance independent of circle size.
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
// Reward sliders based on velocity.
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
// Longer sliders require more memorisation.
sliderBonus *= pixelTravelDistance;
// Nerf sliders with repeats, as less memorisation is required.
if (osuSlider.RepeatCount > 0)
sliderBonus /= (osuSlider.RepeatCount + 1);
}
result += sliderBonus * slider_multiplier;
return result; return result;
} }
} }

View File

@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax)) if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
speedRating = 0.0; speedRating = 0.0;
flashlightRating *= 0.7;
}
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
); );
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuPerformanceCalculator : PerformanceCalculator public class OsuPerformanceCalculator : PerformanceCalculator
{ {
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
private double accuracy; private double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
private int countGreat; private int countGreat;
@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes); effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = PERFORMANCE_BASE_MULTIPLIER;
if (score.Mods.Any(m => m is OsuModNoFail)) if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
{ {
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. // https://www.desmos.com/calculator/bc9eybdthb
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
multiplier *= 0.6; // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
} }
double aimValue = computeAimValue(score, osuAttributes); double aimValue = computeAimValue(score, osuAttributes);
@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (attributes.ApproachRate < 8.0) else if (attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
if (score.Mods.Any(h => h is OsuModRelax))
approachRateFactor = 0.0;
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue; return speedValue;
} }
@ -266,6 +278,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
} }
} }

View File

@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. /// <summary>
/// A distance by which all distances should be scaled in order to assume a uniform circle size.
/// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25; private const int min_delta_time = 25;
private const float maximum_slider_radius = normalised_radius * 2.4f; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f; private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary> /// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance. /// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for <see cref="Slider"/> objects.
/// </summary> /// </summary>
public double TravelTime { get; private set; } public double TravelTime { get; private set; }
@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)
{ {
computeSliderCursorPosition(currentSlider); computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance; // Bonus for repeat sliders until a better per nested object strain system can be achieved.
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
} }
@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return; return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalised_radius / (float)BaseObject.Radius; float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
if (BaseObject.Radius < 30) if (BaseObject.Radius < 30)
{ {
@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition; var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++) for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{ {
@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat) else if (currMovementObj is SliderRepeat)
{ {
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalised_radius; requiredMovement = NORMALISED_RADIUS;
} }
if (currMovementLength > requiredMovement) if (currMovementLength > requiredMovement)
@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1) if (i == slider.NestedHitObjects.Count - 1)
slider.LazyEndPosition = currCursorPosition; slider.LazyEndPosition = currCursorPosition;
} }
slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
} }
private Vector2 getEndCursorPosition(OsuHitObject hitObject) private Vector2 getEndCursorPosition(OsuHitObject hitObject)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain; private double currentStrain;
private double skillMultiplier => 23.25; private double skillMultiplier => 23.55;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => OsuIcon.ModAutopilot; public override IconUsage? Icon => OsuIcon.ModAutopilot;
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles your cursor is a magnet!"; public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock = null!; private IFrameStableClock gameplayClock = null!;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionPool = new DrawablePool<FollowPointConnection>(1, 200), connectionPool = new DrawablePool<FollowPointConnection>(10, 200),
pointPool = new DrawablePool<FollowPoint>(50, 1000) pointPool = new DrawablePool<FollowPoint>(50, 1000)
}; };
} }

View File

@ -9,9 +9,9 @@ using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
@ -68,8 +68,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders) private void load(IRenderer renderer, ShaderManager shaders)
{ {
texture ??= renderer.WhitePixel;
shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
} }
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
resetTime(); resetTime();
} }
private Texture texture = Texture.WhitePixel; private Texture texture;
public Texture Texture public Texture Texture
{ {
@ -222,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Vector2 size; private Vector2 size;
private Vector2 originPosition; private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1); private IVertexBatch<TexturedTrailVertex> vertexBatch;
public TrailDrawNode(CursorTrail source) public TrailDrawNode(CursorTrail source)
: base(source) : base(source)
@ -254,15 +255,17 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Source.parts.CopyTo(parts, 0); Source.parts.CopyTo(parts, 0);
} }
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(IRenderer renderer)
{ {
base.Draw(vertexAction); base.Draw(renderer);
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time); shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent); shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind(); texture.Bind();
RectangleF textureRect = texture.GetTextureRect(); RectangleF textureRect = texture.GetTextureRect();
@ -319,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
vertexBatch.Dispose(); vertexBatch?.Dispose();
} }
} }

View File

@ -11,9 +11,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -112,21 +114,36 @@ namespace osu.Game.Rulesets.Osu.UI
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config) private void load(OsuRulesetConfigManager config, IBeatmap beatmap)
{ {
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
RegisterPool<HitCircle, DrawableHitCircle>(10, 100); var osuBeatmap = (OsuBeatmap)beatmap;
RegisterPool<Slider, DrawableSlider>(10, 100); RegisterPool<HitCircle, DrawableHitCircle>(20, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100); // handle edge cases where a beatmap has a slider with many repeats.
RegisterPool<SliderTick, DrawableSliderTick>(10, 100); int maxRepeatsOnOneSlider = 0;
RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50); int maxTicksOnOneSlider = 0;
if (osuBeatmap != null)
{
foreach (var slider in osuBeatmap.HitObjects.OfType<Slider>())
{
maxRepeatsOnOneSlider = Math.Max(maxRepeatsOnOneSlider, slider.RepeatCount);
maxTicksOnOneSlider = Math.Max(maxTicksOnOneSlider, slider.NestedHitObjects.OfType<SliderTick>().Count());
}
}
RegisterPool<Slider, DrawableSlider>(20, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(20, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(20, 100);
RegisterPool<SliderTick, DrawableSliderTick>(Math.Max(maxTicksOnOneSlider, 20), Math.Max(maxTicksOnOneSlider, 200));
RegisterPool<SliderRepeat, DrawableSliderRepeat>(Math.Max(maxRepeatsOnOneSlider, 20), Math.Max(maxRepeatsOnOneSlider, 200));
RegisterPool<Spinner, DrawableSpinner>(2, 20); RegisterPool<Spinner, DrawableSpinner>(2, 20);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 100); RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100); RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
} }
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
@ -173,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly Action<DrawableOsuJudgement> onLoaded; private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded) public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(10) : base(20)
{ {
this.result = result; this.result = result;
this.onLoaded = onLoaded; this.onLoaded = onLoaded;

View File

@ -6,8 +6,8 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;

View File

@ -9,6 +9,7 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -55,7 +56,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestAcceptable() public void TestAcceptable()
{ {
var context = getContext(new Texture(1920, 1080)); var context = getContext(new DummyRenderer().CreateTexture(1920, 1080));
Assert.That(check.Run(context), Is.Empty); Assert.That(check.Run(context), Is.Empty);
} }
@ -63,7 +64,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooHighResolution() public void TestTooHighResolution()
{ {
var context = getContext(new Texture(3840, 2160)); var context = getContext(new DummyRenderer().CreateTexture(3840, 2160));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -74,7 +75,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestLowResolution() public void TestLowResolution()
{ {
var context = getContext(new Texture(640, 480)); var context = getContext(new DummyRenderer().CreateTexture(640, 480));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -85,7 +86,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooLowResolution() public void TestTooLowResolution()
{ {
var context = getContext(new Texture(100, 100)); var context = getContext(new DummyRenderer().CreateTexture(100, 100));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -96,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooUncompressed() public void TestTooUncompressed()
{ {
var context = getContext(new Texture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3])); var context = getContext(new DummyRenderer().CreateTexture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3]));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestStreamClosed() public void TestStreamClosed()
{ {
var background = new Texture(1920, 1080); var background = new DummyRenderer().CreateTexture(1920, 1080);
var stream = new Mock<MemoryStream>(new byte[1024 * 1024]); var stream = new Mock<MemoryStream>(new byte[1024 * 1024]);
var context = getContext(background, stream.Object); var context = getContext(background, stream.Object);

View File

@ -206,7 +206,7 @@ namespace osu.Game.Tests.Editing
} }
private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance); => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance));
private void assertDurationToDistance(double duration, float expectedDistance) private void assertDurationToDistance(double duration, float expectedDistance)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance); => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -11,8 +11,10 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -36,6 +38,9 @@ namespace osu.Game.Tests.Gameplay
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[Resolved]
private GameHost host { get; set; }
[Test] [Test]
public void TestRetrieveTopLevelSample() public void TestRetrieveTopLevelSample()
{ {
@ -202,6 +207,7 @@ namespace osu.Game.Tests.Gameplay
#region IResourceStorageProvider #region IResourceStorageProvider
public IRenderer Renderer => host.Renderer;
public AudioManager AudioManager => Audio; public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null; public IResourceStore<byte[]> Files => null;
public new IResourceStore<byte[]> Resources => base.Resources; public new IResourceStore<byte[]> Resources => base.Resources;

View File

@ -36,6 +36,23 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single())); Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
} }
[Test]
public void TestAudioEqualityCaseSensitivity()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
// empty by default so let's set it..
beatmapSetA.Beatmaps.First().Metadata.AudioFile = "audio.mp3";
beatmapSetB.Beatmaps.First().Metadata.AudioFile = "audio.mp3";
addAudioFile(beatmapSetA, "abc", "AuDiO.mP3");
addAudioFile(beatmapSetB, "abc", "audio.mp3");
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test] [Test]
public void TestAudioEqualitySameHash() public void TestAudioEqualitySameHash()
{ {

View File

@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -27,6 +27,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
private const string animation_name = "animation"; private const string animation_name = "animation";
private const int frame_count = 6; private const int frame_count = 6;
[Resolved]
private IRenderer renderer { get; set; }
[Cached(typeof(IAnimationTimeReference))] [Cached(typeof(IAnimationTimeReference))]
private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference(); private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference();
@ -35,9 +38,12 @@ namespace osu.Game.Tests.NonVisual.Skinning
[Test] [Test]
public void TestAnimationTimeReferenceChange() public void TestAnimationTimeReferenceChange()
{ {
ISkin skin = new TestSkin(); AddStep("get animation", () =>
{
ISkin skin = new TestSkin(renderer);
Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false));
});
AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false)));
AddAssert("frame count correct", () => animation.FrameCount == frame_count); AddAssert("frame count correct", () => animation.FrameCount == frame_count);
assertPlaybackPosition(0); assertPlaybackPosition(0);
@ -55,9 +61,16 @@ namespace osu.Game.Tests.NonVisual.Skinning
{ {
private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray(); private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray();
private readonly IRenderer renderer;
public TestSkin(IRenderer renderer)
{
this.renderer = renderer;
}
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
return lookup_names.Contains(componentName) ? Texture.WhitePixel : null; return lookup_names.Contains(componentName) ? renderer.WhitePixel : null;
} }
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();

View File

@ -11,6 +11,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database; using osu.Game.Database;
@ -141,6 +143,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
this.textureStore = textureStore; this.textureStore = textureStore;
} }
public IRenderer Renderer => new DummyRenderer();
public AudioManager AudioManager => null; public AudioManager AudioManager => null;
public IResourceStore<byte[]> Files => null; public IResourceStore<byte[]> Files => null;
public IResourceStore<byte[]> Resources => null; public IResourceStore<byte[]> Resources => null;

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -148,6 +149,16 @@ namespace osu.Game.Tests.Online
Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d)); Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d));
} }
[Test]
public void TestSerialisedModSettingPresence()
{
var mod = new TestMod();
mod.TestSetting.Value = mod.TestSetting.Default;
JObject serialised = JObject.Parse(JsonConvert.SerializeObject(new APIMod(mod)));
Assert.False(serialised.ContainsKey("settings"));
}
private class TestRuleset : Ruleset private class TestRuleset : Ruleset
{ {
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]

View File

@ -128,6 +128,8 @@ namespace osu.Game.Tests.Resources
var rulesetInfo = getRuleset(); var rulesetInfo = getRuleset();
string hash = Guid.NewGuid().ToString().ComputeMD5Hash();
yield return new BeatmapInfo yield return new BeatmapInfo
{ {
OnlineID = beatmapId, OnlineID = beatmapId,
@ -136,7 +138,8 @@ namespace osu.Game.Tests.Resources
Length = length, Length = length,
BeatmapSet = beatmapSet, BeatmapSet = beatmapSet,
BPM = bpm, BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), Hash = hash,
MD5Hash = hash,
Ruleset = rulesetInfo, Ruleset = rulesetInfo,
Metadata = metadata.DeepClone(), Metadata = metadata.DeepClone(),
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty

View File

@ -15,11 +15,12 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -77,9 +78,9 @@ namespace osu.Game.Tests.Rulesets
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore()); dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore(parent.Get<GameHost>().Renderer));
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore()); dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager()); dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager(parent.Get<GameHost>().Renderer));
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies); return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
} }
@ -95,6 +96,11 @@ namespace osu.Game.Tests.Rulesets
private class TestTextureStore : TextureStore private class TestTextureStore : TextureStore
{ {
public TestTextureStore(IRenderer renderer)
: base(renderer)
{
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
@ -148,8 +154,8 @@ namespace osu.Game.Tests.Rulesets
private class TestShaderManager : ShaderManager private class TestShaderManager : ShaderManager
{ {
public TestShaderManager() public TestShaderManager(IRenderer renderer)
: base(new ResourceStore<byte[]>()) : base(renderer, new ResourceStore<byte[]>())
{ {
} }

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -6,10 +6,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
@ -21,6 +22,9 @@ namespace osu.Game.Tests.Skins
[HeadlessTest] [HeadlessTest]
public class TestSceneSkinProvidingContainer : OsuTestScene public class TestSceneSkinProvidingContainer : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; }
/// <summary> /// <summary>
/// Ensures that the first inserted skin after resetting (via source change) /// Ensures that the first inserted skin after resetting (via source change)
/// is always prioritised over others when providing the same resource. /// is always prioritised over others when providing the same resource.
@ -35,7 +39,7 @@ namespace osu.Game.Tests.Skins
{ {
var sources = new List<TestSkin>(); var sources = new List<TestSkin>();
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
sources.Add(new TestSkin()); sources.Add(new TestSkin(renderer));
mostPrioritisedSource = sources.First(); mostPrioritisedSource = sources.First();
@ -76,12 +80,19 @@ namespace osu.Game.Tests.Skins
{ {
public const string TEXTURE_NAME = "virtual-texture"; public const string TEXTURE_NAME = "virtual-texture";
private readonly IRenderer renderer;
public TestSkin(IRenderer renderer)
{
this.renderer = renderer;
}
public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
if (componentName == TEXTURE_NAME) if (componentName == TEXTURE_NAME)
return Texture.WhitePixel; return renderer.WhitePixel;
return null; return null;
} }

View File

@ -1,14 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -19,9 +29,9 @@ namespace osu.Game.Tests.Skins
public class TestSceneSkinResources : OsuTestScene public class TestSceneSkinResources : OsuTestScene
{ {
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; } = null!;
private ISkin skin; private ISkin skin = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -32,5 +42,56 @@ namespace osu.Game.Tests.Skins
[Test] [Test]
public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null); public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null);
[Test]
public void TestSampleRetrievalOrder()
{
Mock<IStorageResourceProvider> mockResourceProvider = null!;
Mock<IResourceStore<byte[]>> mockResourceStore = null!;
List<string> lookedUpFileNames = null!;
AddStep("setup mock providers provider", () =>
{
lookedUpFileNames = new List<string>();
mockResourceProvider = new Mock<IStorageResourceProvider>();
mockResourceProvider.Setup(m => m.AudioManager).Returns(Audio);
mockResourceProvider.Setup(m => m.Renderer).Returns(new DummyRenderer());
mockResourceStore = new Mock<IResourceStore<byte[]>>();
mockResourceStore.Setup(r => r.Get(It.IsAny<string>()))
.Callback<string>(n => lookedUpFileNames.Add(n))
.Returns<byte>(null);
});
AddStep("query sample", () =>
{
TestSkin testSkin = new TestSkin(new SkinInfo(), mockResourceProvider.Object, new ResourceStore<byte[]>(mockResourceStore.Object));
testSkin.GetSample(new SampleInfo());
});
AddAssert("sample lookups were in correct order", () =>
{
string[] lookups = lookedUpFileNames.Where(f => f.StartsWith(TestSkin.SAMPLE_NAME, StringComparison.Ordinal)).ToArray();
return Path.GetExtension(lookups[0]) == string.Empty
&& Path.GetExtension(lookups[1]) == ".wav"
&& Path.GetExtension(lookups[2]) == ".mp3"
&& Path.GetExtension(lookups[3]) == ".ogg";
});
}
private class TestSkin : Skin
{
public const string SAMPLE_NAME = "test-sample";
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = "skin.ini")
: base(skin, resources, storage, configurationFilename)
{
}
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public override ISample GetSample(ISampleInfo sampleInfo) => Samples.AsNonNull().Get(SAMPLE_NAME);
}
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -43,6 +44,9 @@ namespace osu.Game.Tests.Visual.Background
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[Resolved]
private IRenderer renderer { get; set; }
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.Background
Id = API.LocalUser.Value.Id + 1, Id = API.LocalUser.Value.Id + 1,
}); });
private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio); private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(renderer, Audio);
private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio); private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio);
private class TestBackgroundScreenDefault : BackgroundScreenDefault private class TestBackgroundScreenDefault : BackgroundScreenDefault
@ -274,12 +278,15 @@ namespace osu.Game.Tests.Visual.Background
private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap
{ {
public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager) private readonly IRenderer renderer;
public UniqueBackgroundTestWorkingBeatmap(IRenderer renderer, AudioManager audioManager)
: base(new Beatmap(), null, audioManager) : base(new Beatmap(), null, audioManager)
{ {
this.renderer = renderer;
} }
protected override Texture GetBackground() => new Texture(1, 1); protected override Texture GetBackground() => renderer.CreateTexture(1, 1);
} }
private class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap private class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap

View File

@ -10,7 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
@ -28,11 +28,9 @@ namespace osu.Game.Tests.Visual.Background
[Resolved] [Resolved]
private SessionStatics statics { get; set; } private SessionStatics statics { get; set; }
[Cached(typeof(LargeTextureStore))]
private LookupLoggingTextureStore textureStore = new LookupLoggingTextureStore();
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private LookupLoggingTextureStore textureStore;
private SeasonalBackgroundLoader backgroundLoader; private SeasonalBackgroundLoader backgroundLoader;
private Container backgroundContainer; private Container backgroundContainer;
@ -45,15 +43,32 @@ namespace osu.Game.Tests.Visual.Background
"Backgrounds/bg3" "Backgrounds/bg3"
}; };
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
textureStore = new LookupLoggingTextureStore(dependencies.Get<IRenderer>());
dependencies.CacheAs(typeof(LargeTextureStore), textureStore);
return dependencies;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore wrappedStore) private void load(LargeTextureStore wrappedStore)
{ {
textureStore.AddStore(wrappedStore); textureStore.AddStore(wrappedStore);
Add(backgroundContainer = new Container Child = new DependencyProvidingContainer
{ {
RelativeSizeAxes = Axes.Both CachedDependencies = new (Type, object)[]
}); {
(typeof(LargeTextureStore), textureStore)
},
Child = backgroundContainer = new Container
{
RelativeSizeAxes = Axes.Both
}
};
} }
[SetUp] [SetUp]
@ -193,6 +208,11 @@ namespace osu.Game.Tests.Visual.Background
{ {
public List<string> PerformedLookups { get; } = new List<string>(); public List<string> PerformedLookups { get; } = new List<string>();
public LookupLoggingTextureStore(IRenderer renderer)
: base(renderer)
{
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
PerformedLookups.Add(name); PerformedLookups.Add(name);

View File

@ -200,10 +200,12 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("click confirmation", () => AddStep("click confirmation", () =>
{ {
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First()); InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.Click(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
}); });
assertCollectionCount(0); assertCollectionCount(0);
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
} }
[Test] [Test]

View File

@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Components
AddAssert("track stopped", () => !track.IsRunning); AddAssert("track stopped", () => !track.IsRunning);
} }
private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(null); private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(CreateAPIBeatmapSet());
private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack() private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack()
{ {

View File

@ -56,8 +56,10 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestCreateNewBeatmap() public void TestCreateNewBeatmap()
{ {
AddAssert("status is none", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None);
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false); AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
} }
[Test] [Test]
@ -208,6 +210,8 @@ namespace osu.Game.Tests.Visual.Editing
}); });
AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName);
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => AddAssert("new beatmap persisted", () =>
@ -218,7 +222,7 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName && beatmap.DifficultyName == secondDifficultyName
&& set != null && set != null
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
}); });
} }
@ -294,7 +298,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);
AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1);
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("reset clock", () => Clock.Seek(0)); AddStep("reset clock", () => Clock.Seek(0));
AddStep("start clock", Clock.Start); AddStep("start clock", () => Clock.Start());
AddAssert("clock running", () => Clock.IsRunning); AddAssert("clock running", () => Clock.IsRunning);
AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250));
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
AddStep("start clock again", Clock.Start); AddStep("start clock again", () => Clock.Start());
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
@ -76,20 +76,20 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("reset clock", () => Clock.Seek(0)); AddStep("reset clock", () => Clock.Seek(0));
AddStep("stop clock", Clock.Stop); AddStep("stop clock", () => Clock.Stop());
AddAssert("clock stopped", () => !Clock.IsRunning); AddAssert("clock stopped", () => !Clock.IsRunning);
AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength));
AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
AddStep("start clock again", Clock.Start); AddStep("start clock again", () => Clock.Start());
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
[Test] [Test]
public void TestClampWhenSeekOutsideBeatmapBounds() public void TestClampWhenSeekOutsideBeatmapBounds()
{ {
AddStep("stop clock", Clock.Stop); AddStep("stop clock", () => Clock.Stop());
AddStep("seek before start time", () => Clock.Seek(-1000)); AddStep("seek before start time", () => Clock.Seek(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);

View File

@ -60,17 +60,17 @@ namespace osu.Game.Tests.Visual.Editing
// Forwards // Forwards
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(33)", () => Clock.Seek(33)); AddStep("Seek(33)", () => Clock.Seek(33));
AddAssert("Time = 33", () => Clock.CurrentTime == 33); checkTime(33);
AddStep("Seek(89)", () => Clock.Seek(89)); AddStep("Seek(89)", () => Clock.Seek(89));
AddAssert("Time = 89", () => Clock.CurrentTime == 89); checkTime(89);
// Backwards // Backwards
AddStep("Seek(25)", () => Clock.Seek(25)); AddStep("Seek(25)", () => Clock.Seek(25));
AddAssert("Time = 25", () => Clock.CurrentTime == 25); checkTime(25);
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -83,19 +83,19 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -108,17 +108,17 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
} }
/// <summary> /// <summary>
@ -130,15 +130,15 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 200", () => Clock.CurrentTime == 200); checkTime(200);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -150,17 +150,17 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -174,28 +174,28 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(49)", () => Clock.Seek(49)); AddStep("Seek(49)", () => Clock.Seek(49));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999)); AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(99)", () => Clock.Seek(99)); AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999)); AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 150); checkTime(150);
AddStep("Seek(174)", () => Clock.Seek(174)); AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(349)", () => Clock.Seek(349)); AddStep("Seek(349)", () => Clock.Seek(349));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("Seek(399)", () => Clock.Seek(399)); AddStep("Seek(399)", () => Clock.Seek(399));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(449)", () => Clock.Seek(449)); AddStep("Seek(449)", () => Clock.Seek(449));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -208,15 +208,15 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 150", () => Clock.CurrentTime == 150); checkTime(150);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -229,17 +229,17 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -253,16 +253,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(451)", () => Clock.Seek(451)); AddStep("Seek(451)", () => Clock.Seek(451));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
AddStep("Seek(450.999)", () => Clock.Seek(450.999)); AddStep("Seek(450.999)", () => Clock.Seek(450.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
AddStep("Seek(401)", () => Clock.Seek(401)); AddStep("Seek(401)", () => Clock.Seek(401));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(401.999)", () => Clock.Seek(401.999)); AddStep("Seek(401.999)", () => Clock.Seek(401.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
} }
/// <summary> /// <summary>
@ -297,9 +297,11 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime);
} }
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
private void checkTime(double expectedTime) => AddAssert($"Current time is {expectedTime}", () => Clock.CurrentTime, () => Is.EqualTo(expectedTime));
private void reset() private void reset()
{ {
AddStep("Reset", () => Clock.Seek(0)); AddStep("Reset", () => Clock.Seek(0));

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -120,7 +119,7 @@ namespace osu.Game.Tests.Visual.Editing
private void pressAndCheckTime(Key key, double expectedTime) private void pressAndCheckTime(Key key, double expectedTime)
{ {
AddStep($"press {key}", () => InputManager.Key(key)); AddStep($"press {key}", () => InputManager.Key(key));
AddUntilStep($"time is {expectedTime}", () => Precision.AlmostEquals(expectedTime, EditorClock.CurrentTime, 1)); AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
} }
} }
} }

View File

@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -13,7 +13,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API;
using osu.Game.Overlays.Toolbar;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
{
public TestSceneToolbarUserButton()
{
Container mainContainer;
Children = new Drawable[]
{
mainContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
new ToolbarUserButton(),
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
}
},
}
},
};
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
}
[Test]
public void TestLoginLogout()
{
AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
}
[Test]
public void TestStates()
{
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
foreach (var state in Enum.GetValues<APIState>())
{
AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
}
}
}
}

View File

@ -432,8 +432,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
var user = playingUsers.Single(u => u.UserID == userId); var user = playingUsers.Single(u => u.UserID == userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
SpectatorClient.SendEndPlay(userId); SpectatorClient.SendEndPlay(userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
playingUsers.Remove(user); playingUsers.Remove(user);
}); });

View File

@ -83,6 +83,20 @@ namespace osu.Game.Tests.Visual.Online
Beatmap = dummyBeatmap, Beatmap = dummyBeatmap,
}, },
new APIRecentActivity new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Approved,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Loved,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{ {
User = dummyUser, User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove, Type = RecentActivityType.BeatmapsetApprove,
@ -90,6 +104,13 @@ namespace osu.Game.Tests.Visual.Online
Beatmapset = dummyBeatmap, Beatmapset = dummyBeatmap,
}, },
new APIRecentActivity new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Ranked,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{ {
User = dummyUser, User = dummyUser,
Type = RecentActivityType.BeatmapsetDelete, Type = RecentActivityType.BeatmapsetDelete,

View File

@ -150,13 +150,14 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1"); assertCollectionDropdownContains("1");
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
} }
[Test] [Test]
@ -168,15 +169,15 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1"); assertCollectionDropdownContains("1");
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
addClickAddOrRemoveButtonStep(1); addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
addClickAddOrRemoveButtonStep(1); addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
} }
[Test] [Test]
@ -226,6 +227,8 @@ namespace osu.Game.Tests.Visual.SongSelect
=> AddUntilStep($"collection dropdown header displays '{collectionName}'", => AddUntilStep($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == (control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName)); () => shouldDisplay == (control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -9,29 +10,55 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneModPresetColumn : OsuTestScene public class TestSceneModPresetColumn : OsuManualInputManagerTestScene
{ {
protected override bool UseFreshStoragePerRun => true; protected override bool UseFreshStoragePerRun => true;
private Container<Drawable> content = null!;
protected override Container<Drawable> Content => content;
private RulesetStore rulesets = null!; private RulesetStore rulesets = null!;
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay = new DialogOverlay();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
base.Content.AddRange(new Drawable[]
{
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = content = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30),
}
},
dialogOverlay
});
} }
[SetUpSteps] [SetUpSteps]
@ -57,15 +84,10 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestBasicOperation() public void TestBasicOperation()
{ {
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
AddStep("create content", () => Child = new Container AddStep("create content", () => Child = new ModPresetColumn
{ {
RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre,
Padding = new MarginPadding(30), Origin = Anchor.Centre,
Child = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}); });
AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3); AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
@ -112,15 +134,11 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSoftDeleteSupport() public void TestSoftDeleteSupport()
{ {
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
AddStep("create content", () => Child = new Container AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = new ModPresetColumn
{ {
RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre,
Padding = new MarginPadding(30), Origin = Anchor.Centre,
Child = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}); });
AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3); AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
@ -136,9 +154,11 @@ namespace osu.Game.Tests.Visual.UserInterface
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
preset.DeletePending = true; preset.DeletePending = true;
})); }));
AddUntilStep("no panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 0); AddUntilStep("no panels visible", () => !this.ChildrenOfType<ModPresetPanel>().Any());
AddStep("undelete preset", () => Realm.Write(r => AddStep("select mods from first preset", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() });
AddStep("undelete presets", () => Realm.Write(r =>
{ {
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
preset.DeletePending = false; preset.DeletePending = false;
@ -146,6 +166,101 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3); AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
} }
[Test]
public void TestAddingFlow()
{
ModPresetColumn modPresetColumn = null!;
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddAssert("add preset button disabled", () => !this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDaycore(), new OsuModClassic() });
AddAssert("add preset button enabled", () => this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("click add preset button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<AddPresetButton>().Single());
InputManager.Click(MouseButton.Left);
});
OsuPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
AddStep("attempt preset creation", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddWaitStep("wait some", 3);
AddAssert("preset creation did not occur", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
AddUntilStep("popover is unchanged", () => this.ChildrenOfType<OsuPopover>().FirstOrDefault() == popover);
AddStep("fill preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "new preset");
AddStep("attempt preset creation", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddUntilStep("preset creation occurred", () => this.ChildrenOfType<ModPresetPanel>().Count() == 4);
AddStep("click add preset button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<AddPresetButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
}
[Test]
public void TestDeleteFlow()
{
ModPresetColumn modPresetColumn = null!;
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddStep("right click first panel", () =>
{
var panel = this.ChildrenOfType<ModPresetPanel>().First();
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Right);
});
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click delete", () =>
{
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().Single();
InputManager.MoveMouseTo(deleteItem);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for dialog", () => dialogOverlay.CurrentDialog is DeleteModPresetDialog);
AddStep("hold confirm", () =>
{
var confirmButton = this.ChildrenOfType<PopupDialogDangerousButton>().Single();
InputManager.MoveMouseTo(confirmButton);
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for dialog to close", () => dialogOverlay.CurrentDialog == null);
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddUntilStep("preset deletion occurred", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
AddAssert("preset soft-deleted", () => Realm.Run(r => r.All<ModPreset>().Count(preset => preset.DeletePending) == 1));
}
private ICollection<ModPreset> createTestPresets() => new[] private ICollection<ModPreset> createTestPresets() => new[]
{ {
new ModPreset new ModPreset

View File

@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -23,6 +26,12 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset selected mods", () => SelectedMods.SetDefault());
}
[Test] [Test]
public void TestVariousModPresets() public void TestVariousModPresets()
{ {
@ -37,6 +46,78 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
[Test]
public void TestPresetSelectionStateAfterExternalModChanges()
{
ModPresetPanel? panel = null;
AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f
});
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HR", () => SelectedMods.Value = new[] { new OsuModHardRock() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is active", () => panel.AsNonNull().Active.Value);
AddStep("set mods to HR+customised DT", () => SelectedMods.Value = new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime
{
SpeedChange = { Value = 1.25 }
}
});
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is active", () => panel.AsNonNull().Active.Value);
AddStep("customise mod in place", () => SelectedMods.Value.OfType<OsuModDoubleTime>().Single().SpeedChange.Value = 1.33);
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
}
[Test]
public void TestActivatingPresetTogglesIncludedMods()
{
ModPresetPanel? panel = null;
AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f
});
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(Array.Empty<Mod>());
AddStep("set different mod", () => SelectedMods.Value = new[] { new OsuModHidden() });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
}
private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods)
=> AddAssert("selected mods changed correctly", () => new HashSet<Mod>(SelectedMods.Value).SetEquals(mods));
private static IEnumerable<ModPreset> createTestPresets() => new[] private static IEnumerable<ModPreset> createTestPresets() => new[]
{ {
new ModPreset new ModPreset

View File

@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value)); AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value));
ModColumn lastColumn = null; ModSelectColumn lastColumn = null;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value); AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
AddStep("request scroll to last column", () => AddStep("request scroll to last column", () =>

View File

@ -1,18 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Music; using osu.Game.Overlays.Music;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -21,13 +23,25 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene
{ {
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>(); protected override bool UseFreshStoragePerRun => true;
private PlaylistOverlay playlistOverlay; private PlaylistOverlay playlistOverlay = null!;
private Live<BeatmapSetInfo> first; private BeatmapManager beatmapManager = null!;
private const int item_count = 100; private const int item_count = 20;
private List<BeatmapSetInfo> beatmapSets => beatmapManager.GetAllUsableBeatmapSets();
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
@ -46,16 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}; };
beatmapSets.Clear();
for (int i = 0; i < item_count; i++) for (int i = 0; i < item_count; i++)
{ {
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged()); beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo());
} }
first = beatmapSets.First(); beatmapSets.First().ToLive(Realm);
playlistOverlay.BeatmapSets.BindTo(beatmapSets);
}); });
[Test] [Test]
@ -70,9 +80,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any());
PlaylistItem firstItem = null!;
AddStep("hold 1st item handle", () => AddStep("hold 1st item handle", () =>
{ {
var handle = this.ChildrenOfType<OsuRearrangeableListItem<Live<BeatmapSetInfo>>.PlaylistItemHandle>().First(); firstItem = this.ChildrenOfType<PlaylistItem>().First();
var handle = firstItem.ChildrenOfType<PlaylistItem.PlaylistItemHandle>().First();
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
}); });
@ -83,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft); InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft);
}); });
AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first)); AddAssert("first is moved", () => playlistOverlay.ChildrenOfType<Playlist>().Single().Items.ElementAt(4).Value.Equals(firstItem.Model.Value));
AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left));
} }
@ -101,6 +115,68 @@ namespace osu.Game.Tests.Visual.UserInterface
() => playlistOverlay.ChildrenOfType<PlaylistItem>() () => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter) .Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
AddStep("Import new non-matching beatmap", () =>
{
var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(1);
testBeatmapSetInfo.Beatmaps.Single().Metadata.Title = "no guid";
beatmapManager.Import(testBeatmapSetInfo);
});
AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
AddAssert("results filtered correctly",
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
}
[Test]
public void TestCollectionFiltering()
{
NowPlayingCollectionDropdown collectionDropdown() => playlistOverlay.ChildrenOfType<NowPlayingCollectionDropdown>().Single();
AddStep("Add collection", () =>
{
Dependencies.Get<RealmAccess>().Write(r =>
{
r.RemoveAll<BeatmapCollection>();
r.Add(new BeatmapCollection("wang"));
});
});
AddUntilStep("wait for dropdown to have new collection", () => collectionDropdown().Items.Count() == 2);
AddStep("Filter to collection", () =>
{
collectionDropdown().Current.Value = collectionDropdown().Items.Last();
});
AddUntilStep("No items present", () => !playlistOverlay.ChildrenOfType<PlaylistItem>().Any(i => i.MatchingFilter));
AddStep("Import new non-matching beatmap", () =>
{
beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(1));
});
AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
AddUntilStep("No items matching", () => !playlistOverlay.ChildrenOfType<PlaylistItem>().Any(i => i.MatchingFilter));
BeatmapSetInfo collectionAddedBeatmapSet = null!;
AddStep("Import new matching beatmap", () =>
{
collectionAddedBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1);
beatmapManager.Import(collectionAddedBeatmapSet);
Realm.Write(r => r.All<BeatmapCollection>().First().BeatmapMD5Hashes.Add(collectionAddedBeatmapSet.Beatmaps.First().MD5Hash));
});
AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
AddUntilStep("Only matching item",
() => playlistOverlay.ChildrenOfType<PlaylistItem>().Where(i => i.MatchingFilter).Select(i => i.Model.ID), () => Is.EquivalentTo(new[] { collectionAddedBeatmapSet.ID }));
} }
} }
} }

View File

@ -105,5 +105,11 @@ namespace osu.Game.Audio
/// Retrieves the audio track. /// Retrieves the audio track.
/// </summary> /// </summary>
protected abstract Track? GetTrack(); protected abstract Track? GetTrack();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Track?.Dispose();
}
} }
} }

View File

@ -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.Game.Beatmaps
{
public static class BeatSyncProviderExtensions
{
/// <summary>
/// Check whether beat sync is currently available.
/// </summary>
public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null;
/// <summary>
/// Whether the beat sync provider is currently in a kiai section. Should make everything more epic.
/// </summary>
public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true;
}
}

View File

@ -121,7 +121,8 @@ namespace osu.Game.Beatmaps
OnlineID = -1; OnlineID = -1;
LastOnlineUpdate = null; LastOnlineUpdate = null;
OnlineMD5Hash = string.Empty; OnlineMD5Hash = string.Empty;
Status = BeatmapOnlineStatus.None; if (Status != BeatmapOnlineStatus.LocallyModified)
Status = BeatmapOnlineStatus.None;
} }
#region Properties we may not want persisted (but also maybe no harm?) #region Properties we may not want persisted (but also maybe no harm?)
@ -198,8 +199,8 @@ namespace osu.Game.Beatmaps
Debug.Assert(x.BeatmapSet != null); Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null); Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash; string? fileHashX = x.BeatmapSet.GetFile(getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash; string? fileHashY = y.BeatmapSet.GetFile(getFilename(y.Metadata))?.File.Hash;
return fileHashX == fileHashY; return fileHashX == fileHashY;
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;

View File

@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file. // ensure that two difficulties from the set don't point at the same beatmap file.
@ -313,9 +313,12 @@ namespace osu.Game.Beatmaps
beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
setInfo.Hash = beatmapImporter.ComputeHash(setInfo); setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
Realm.Write(r => Realm.Write(r =>
{ {

View File

@ -3,13 +3,23 @@
#nullable disable #nullable disable
using System.ComponentModel;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
public enum BeatmapOnlineStatus public enum BeatmapOnlineStatus
{ {
/// <summary>
/// This is a special status given when local changes are made via the editor.
/// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted.
/// </summary>
[Description("Local")]
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))]
LocallyModified = -4,
None = -3, None = -3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]

View File

@ -84,13 +84,6 @@ namespace osu.Game.Beatmaps
{ {
} }
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// </summary>
/// <param name="filename">The name of the file to get the storage path of.</param>
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
public bool Equals(BeatmapSetInfo? other) public bool Equals(BeatmapSetInfo? other)
{ {
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;

View File

@ -0,0 +1,33 @@
// 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 osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Models;
namespace osu.Game.Beatmaps
{
public static class BeatmapSetInfoExtensions
{
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// The lookup is case insensitive.
/// </summary>
/// <param name="model">The model to operate on.</param>
/// <param name="filename">The name of the file to get the storage path of.</param>
public static string? GetPathForFile(this IHasRealmFiles model, string filename) => model.GetFile(filename)?.File.GetStoragePath();
/// <summary>
/// Returns the file usage for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// The lookup is case insensitive.
/// </summary>
/// <param name="model">The model to operate on.</param>
/// <param name="filename">The name of the file to get the storage path of.</param>
public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) =>
model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
}
}

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using osu.Framework.Development; using osu.Framework.Development;
@ -51,6 +52,9 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Queue an update for a beatmap set. /// Queue an update for a beatmap set.
/// </summary> /// </summary>
/// <remarks>
/// This may happen during initial import, or at a later stage in response to a user action or server event.
/// </remarks>
/// <param name="beatmapSet">The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed).</param> /// <param name="beatmapSet">The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed).</param>
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param> /// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param>
public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch) public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch)
@ -89,21 +93,26 @@ namespace osu.Game.Beatmaps
if (res != null) if (res != null)
{ {
beatmapInfo.Status = res.Status; beatmapInfo.OnlineID = res.OnlineID;
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
beatmapInfo.OnlineMD5Hash = res.MD5Hash; beatmapInfo.OnlineMD5Hash = res.MD5Hash;
beatmapInfo.LastOnlineUpdate = res.LastUpdated; beatmapInfo.LastOnlineUpdate = res.LastUpdated;
beatmapInfo.OnlineID = res.OnlineID; Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; // Some metadata should only be applied if there's no local changes.
if (shouldSaveOnlineMetadata(beatmapInfo))
{
beatmapInfo.Status = res.Status;
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
}
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
{
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
}
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}."); logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
} }
@ -202,19 +211,26 @@ namespace osu.Game.Beatmaps
{ {
var status = (BeatmapOnlineStatus)reader.GetByte(2); var status = (BeatmapOnlineStatus)reader.GetByte(2);
beatmapInfo.Status = status; // Some metadata should only be applied if there's no local changes.
if (shouldSaveOnlineMetadata(beatmapInfo))
{
beatmapInfo.Status = status;
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
}
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Status = status;
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
// TODO: DateSubmitted and DateRanked are not provided by local cache. // TODO: DateSubmitted and DateRanked are not provided by local cache.
beatmapInfo.OnlineID = reader.GetInt32(1); beatmapInfo.OnlineID = reader.GetInt32(1);
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
beatmapInfo.OnlineMD5Hash = reader.GetString(4); beatmapInfo.OnlineMD5Hash = reader.GetString(4);
beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5); beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
{
beatmapInfo.BeatmapSet.Status = status;
}
logForModel(set, $"Cached local retrieval for {beatmapInfo}."); logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
return true; return true;
} }
@ -233,6 +249,12 @@ namespace osu.Game.Beatmaps
private void logForModel(BeatmapSetInfo set, string message) => private void logForModel(BeatmapSetInfo set, string message) =>
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}"); RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}");
/// <summary>
/// Check whether the provided beatmap is in a state where online "ranked" status metadata should be saved against it.
/// Handles the case where a user may have locally modified a beatmap in the editor and expects the local status to stick.
/// </summary>
private static bool shouldSaveOnlineMetadata(BeatmapInfo beatmapInfo) => beatmapInfo.MatchesOnlineVersion || beatmapInfo.Status != BeatmapOnlineStatus.LocallyModified;
public void Dispose() public void Dispose()
{ {
cacheDownloadRequest?.Dispose(); cacheDownloadRequest?.Dispose();

View File

@ -6,15 +6,18 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
public class BeatmapSetOnlineStatusPill : CircularContainer public class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
{ {
private BeatmapOnlineStatus status; private BeatmapOnlineStatus status;
@ -96,5 +99,19 @@ namespace osu.Game.Beatmaps.Drawables
background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter; background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter;
} }
public LocalisableString TooltipText
{
get
{
switch (Status)
{
case BeatmapOnlineStatus.LocallyModified:
return SongSelectStrings.LocallyModifiedTooltip;
}
return string.Empty;
}
}
} }
} }

View File

@ -118,7 +118,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
// another async load might have completed before this one. // another async load might have completed before this one.
// if so, do not make any changes. // if so, do not make any changes.
if (loadedPreview != previewTrack) if (loadedPreview != previewTrack)
{
loadedPreview.Dispose();
return; return;
}
AddInternal(loadedPreview); AddInternal(loadedPreview);
toggleLoading(false); toggleLoading(false);

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -14,12 +14,16 @@ namespace osu.Game.Beatmaps
/// Primarily intended for use with <see cref="BeatSyncedContainer"/>. /// Primarily intended for use with <see cref="BeatSyncedContainer"/>.
/// </summary> /// </summary>
[Cached] [Cached]
public interface IBeatSyncProvider public interface IBeatSyncProvider : IHasAmplitudes
{ {
/// <summary>
/// Access any available control points from a beatmap providing beat sync. If <c>null</c>, no current provider is available.
/// </summary>
ControlPointInfo? ControlPoints { get; } ControlPointInfo? ControlPoints { get; }
/// <summary>
/// Access a clock currently responsible for providing beat sync. If <c>null</c>, no current provider is available.
/// </summary>
IClock? Clock { get; } IClock? Clock { get; }
ChannelAmplitudes? Amplitudes { get; }
} }
} }

View File

@ -9,6 +9,8 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Lists; using osu.Framework.Lists;
@ -56,7 +58,7 @@ namespace osu.Game.Beatmaps
this.resources = resources; this.resources = resources;
this.host = host; this.host = host;
this.files = files; this.files = files;
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files)); largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
this.trackStore = trackStore; this.trackStore = trackStore;
} }
@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager; AudioManager IStorageResourceProvider.AudioManager => audioManager;
RealmAccess IStorageResourceProvider.RealmAccess => null; RealmAccess IStorageResourceProvider.RealmAccess => null;
IResourceStore<byte[]> IStorageResourceProvider.Files => files; IResourceStore<byte[]> IStorageResourceProvider.Files => files;

View File

@ -3,33 +3,17 @@
using System; using System;
using Humanizer; using Humanizer;
using osu.Framework.Graphics.Sprites;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
namespace osu.Game.Collections namespace osu.Game.Collections
{ {
public class DeleteCollectionDialog : PopupDialog public class DeleteCollectionDialog : DeleteConfirmationDialog
{ {
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction) public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
{ {
HeaderText = "Confirm deletion of";
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})"); BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
DeleteAction = deleteAction;
Icon = FontAwesome.Regular.TrashAlt;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Yes. Go for it.",
Action = deleteAction
},
new PopupDialogCancelButton
{
Text = @"No! Abort mission!",
},
};
} }
} }
} }

View File

@ -3,16 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum BackgroundSource public enum BackgroundSource
{ {
[LocalisableDescription(typeof(SkinSettingsStrings), nameof(SkinSettingsStrings.SkinSectionHeader))]
Skin, Skin,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.BeatmapHeader))]
Beatmap, Beatmap,
[Description("Beatmap (with storyboard / video)")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
BeatmapWithStoryboard, BeatmapWithStoryboard,
} }
} }

View File

@ -3,17 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum DiscordRichPresenceMode public enum DiscordRichPresenceMode
{ {
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceOff))]
Off, Off,
[Description("Hide identifiable information")] [LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.HideIdentifiableInformation))]
Limited, Limited,
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceFull))]
Full Full
} }
} }

View File

@ -3,17 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum HUDVisibilityMode public enum HUDVisibilityMode
{ {
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.NeverShowHUD))]
Never, Never,
[Description("Hide during gameplay")] [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.HideDuringGameplay))]
HideDuringGameplay, HideDuringGameplay,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.AlwaysShowHUD))]
Always Always
} }
} }

View File

@ -3,16 +3,17 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum RandomSelectAlgorithm public enum RandomSelectAlgorithm
{ {
[Description("Never repeat")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverRepeat))]
RandomPermutation, RandomPermutation,
[Description("True Random")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.TrueRandom))]
Random Random
} }
} }

View File

@ -3,17 +3,23 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ScalingMode public enum ScalingMode
{ {
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScalingOff))]
Off, Off,
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverything))]
Everything, Everything,
[Description("Excluding overlays")] [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverythingExcludingOverlays))]
ExcludeOverlays, ExcludeOverlays,
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleGameplay))]
Gameplay, Gameplay,
} }
} }

View File

@ -3,16 +3,17 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ScreenshotFormat public enum ScreenshotFormat
{ {
[Description("JPG (web-friendly)")] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Jpg))]
Jpg = 1, Jpg = 1,
[Description("PNG (lossless)")] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Png))]
Png = 2 Png = 2
} }
} }

View File

@ -3,6 +3,9 @@
#nullable disable #nullable disable
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum SeasonalBackgroundMode public enum SeasonalBackgroundMode
@ -10,16 +13,19 @@ namespace osu.Game.Configuration
/// <summary> /// <summary>
/// Seasonal backgrounds are shown regardless of season, if at all available. /// Seasonal backgrounds are shown regardless of season, if at all available.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.AlwaysSeasonalBackground))]
Always, Always,
/// <summary> /// <summary>
/// Seasonal backgrounds are shown only during their corresponding season. /// Seasonal backgrounds are shown only during their corresponding season.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SometimesSeasonalBackground))]
Sometimes, Sometimes,
/// <summary> /// <summary>
/// Seasonal backgrounds are never shown. /// Seasonal backgrounds are never shown.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverSeasonalBackground))]
Never Never
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Models; using osu.Game.Models;
namespace osu.Game.Database namespace osu.Game.Database
@ -11,8 +12,16 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public interface IHasRealmFiles public interface IHasRealmFiles
{ {
/// <summary>
/// Available files in this model, with locally filenames.
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
/// </summary>
IList<RealmNamedFileUsage> Files { get; } IList<RealmNamedFileUsage> Files { get; }
/// <summary>
/// A combined hash representing the model, based on the files it contains.
/// Implementation specific.
/// </summary>
string Hash { get; set; } string Hash { get; set; }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@ -27,27 +25,30 @@ namespace osu.Game.Database
public class LegacyImportManager : Component public class LegacyImportManager : Component
{ {
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; } = null!;
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; } = null!;
[Resolved] [Resolved]
private ScoreManager scores { get; set; } private ScoreManager scores { get; set; } = null!;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved] [Resolved]
private IDialogOverlay dialogOverlay { get; set; } private OsuGame? game { get; set; }
[Resolved] [Resolved]
private RealmAccess realmAccess { get; set; } private IDialogOverlay dialogOverlay { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DesktopGameHost desktopGameHost { get; set; } private RealmAccess realmAccess { get; set; } = null!;
private StableStorage cachedStorage; [Resolved(canBeNull: true)] // canBeNull required while we remain on mono for mobile platforms.
private DesktopGameHost? desktopGameHost { get; set; }
[Resolved]
private INotificationOverlay? notifications { get; set; }
private StableStorage? cachedStorage;
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
@ -98,6 +99,9 @@ namespace osu.Game.Database
stableStorage = GetCurrentStableStorage(); stableStorage = GetCurrentStableStorage();
} }
if (stableStorage == null)
return;
var importTasks = new List<Task>(); var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask; Task beatmapImportTask = Task.CompletedTask;
@ -108,7 +112,14 @@ namespace osu.Game.Database
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Collections)) if (content.HasFlagFast(StableContent.Collections))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); {
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess)
{
// Other legacy importers import via model managers which handle the posting of notifications.
// Collections are an exception.
PostNotification = n => notifications?.Post(n)
}.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
}
if (content.HasFlagFast(StableContent.Scores)) if (content.HasFlagFast(StableContent.Scores))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
@ -116,7 +127,7 @@ namespace osu.Game.Database
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
} }
public StableStorage GetCurrentStableStorage() public StableStorage? GetCurrentStableStorage()
{ {
if (cachedStorage != null) if (cachedStorage != null)
return cachedStorage; return cachedStorage;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer; using Humanizer;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -107,7 +108,15 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Cancelled; notification.State = ProgressNotificationState.Cancelled;
if (!(error is OperationCanceledException)) if (!(error is OperationCanceledException))
Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!"); {
if (error is WebException webException && webException.Message == @"TooManyRequests")
{
notification.Close();
PostNotification?.Invoke(new TooManyDownloadsNotification());
}
else
Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!");
}
} }
} }

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -79,7 +80,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public void AddFile(TModel item, Stream contents, string filename, Realm realm) public void AddFile(TModel item, Stream contents, string filename, Realm realm)
{ {
var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); var existing = item.GetFile(filename);
if (existing != null) if (existing != null)
{ {

View File

@ -25,6 +25,7 @@ using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using Realms; using Realms;
@ -172,6 +173,11 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension; Filename += realm_extension;
#if DEBUG
if (!DebugUtils.IsNUnitRunning)
applyFilenameSchemaSuffix(ref Filename);
#endif
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// Attempt to recover a newer database version if available. // Attempt to recover a newer database version if available.
@ -211,6 +217,51 @@ namespace osu.Game.Database
} }
} }
/// <summary>
/// Some developers may be annoyed if a newer version migration (ie. caused by testing a pull request)
/// cause their test database to be unusable with previous versions.
/// To get around this, store development databases against their realm version.
/// Note that this means changes made on newer realm versions will disappear.
/// </summary>
private void applyFilenameSchemaSuffix(ref string filename)
{
string originalFilename = filename;
filename = getVersionedFilename(schema_version);
// First check if the current realm version already exists...
if (storage.Exists(filename))
return;
// Check for a previous version we can use as a base database to migrate from...
for (int i = schema_version - 1; i >= 0; i--)
{
string previousFilename = getVersionedFilename(i);
if (storage.Exists(previousFilename))
{
copyPreviousVersion(previousFilename, filename);
return;
}
}
// Finally, check for a non-versioned file exists (aka before this method was added)...
if (storage.Exists(originalFilename))
copyPreviousVersion(originalFilename, filename);
void copyPreviousVersion(string previousFilename, string newFilename)
{
using (var previous = storage.GetStream(previousFilename))
using (var current = storage.CreateFileSafely(newFilename))
{
Logger.Log(@$"Copying previous realm database {previousFilename} to {newFilename} for migration to schema version {schema_version}");
previous.CopyTo(current);
}
}
string getVersionedFilename(int version) => originalFilename.Replace(realm_extension, $"_{version}{realm_extension}");
}
private void attemptRecoverFromFile(string recoveryFilename) private void attemptRecoverFromFile(string recoveryFilename)
{ {
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database); Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
@ -292,6 +343,11 @@ namespace osu.Game.Database
foreach (var s in pendingDeleteSkins) foreach (var s in pendingDeleteSkins)
realm.Remove(s); realm.Remove(s);
var pendingDeletePresets = realm.All<ModPreset>().Where(s => s.DeletePending);
foreach (var s in pendingDeletePresets)
realm.Remove(s);
transaction.Commit(); transaction.Commit();
} }

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Overlays.Notifications;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Database
{
public class TooManyDownloadsNotification : SimpleNotification
{
public TooManyDownloadsNotification()
{
Text = BeatmapsetsStrings.DownloadLimitExceeded;
Icon = FontAwesome.Solid.ExclamationCircle;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IconBackground.Colour = colours.RedDark;
}
}
}

View File

@ -14,9 +14,8 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Lists; using osu.Framework.Lists;
namespace osu.Game.Graphics.Backgrounds namespace osu.Game.Graphics.Backgrounds
@ -88,7 +87,7 @@ namespace osu.Game.Graphics.Backgrounds
private Random stableRandom; private Random stableRandom;
private IShader shader; private IShader shader;
private readonly Texture texture; private Texture texture;
/// <summary> /// <summary>
/// Construct a new triangle visualisation. /// Construct a new triangle visualisation.
@ -98,13 +97,12 @@ namespace osu.Game.Graphics.Backgrounds
{ {
if (seed != null) if (seed != null)
stableRandom = new Random(seed.Value); stableRandom = new Random(seed.Value);
texture = Texture.WhitePixel;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders) private void load(IRenderer renderer, ShaderManager shaders)
{ {
texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
} }
@ -184,8 +182,8 @@ namespace osu.Game.Graphics.Backgrounds
private void addTriangles(bool randomY) private void addTriangles(bool randomY)
{ {
// limited by the maximum size of QuadVertexBuffer for safety. // Limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = QuadVertexBuffer<TexturedVertex2D>.MAX_QUADS; const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2);
AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio)); AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio));
@ -251,7 +249,7 @@ namespace osu.Game.Graphics.Backgrounds
private readonly List<TriangleParticle> parts = new List<TriangleParticle>(); private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
private Vector2 size; private Vector2 size;
private QuadBatch<TexturedVertex2D> vertexBatch; private IVertexBatch<TexturedVertex2D> vertexBatch;
public TrianglesDrawNode(Triangles source) public TrianglesDrawNode(Triangles source)
: base(source) : base(source)
@ -270,14 +268,14 @@ namespace osu.Game.Graphics.Backgrounds
parts.AddRange(Source.parts); parts.AddRange(Source.parts);
} }
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(IRenderer renderer)
{ {
base.Draw(vertexAction); base.Draw(renderer);
if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount))
{ {
vertexBatch?.Dispose(); vertexBatch?.Dispose();
vertexBatch = new QuadBatch<TexturedVertex2D>(Source.AimCount, 1); vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
} }
shader.Bind(); shader.Bind();
@ -297,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds
ColourInfo colourInfo = DrawColourInfo.Colour; ColourInfo colourInfo = DrawColourInfo.Colour;
colourInfo.ApplyChild(particle.Colour); colourInfo.ApplyChild(particle.Colour);
DrawTriangle( renderer.DrawTriangle(
texture, texture,
triangle, triangle,
colourInfo, colourInfo,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -10,13 +8,12 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
/// <summary> /// <summary>
/// A container which fires a callback when a new beat is reached. /// A container which fires a callback when a new beat is reached.
/// Consumes a parent <see cref="GameplayClock"/> or <see cref="Beatmap"/> (whichever is first available). /// Consumes a parent <see cref="IBeatSyncProvider"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This container does not set its own clock to the source used for beat matching. /// This container does not set its own clock to the source used for beat matching.
@ -28,8 +25,10 @@ namespace osu.Game.Graphics.Containers
public class BeatSyncedContainer : Container public class BeatSyncedContainer : Container
{ {
private int lastBeat; private int lastBeat;
protected TimingControlPoint LastTimingPoint { get; private set; }
protected EffectControlPoint LastEffectPoint { get; private set; } private TimingControlPoint? lastTimingPoint { get; set; }
protected bool IsKiaiTime { get; private set; }
/// <summary> /// <summary>
/// The amount of time before a beat we should fire <see cref="OnNewBeat(int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes)"/>. /// The amount of time before a beat we should fire <see cref="OnNewBeat(int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes)"/>.
@ -71,12 +70,12 @@ namespace osu.Game.Graphics.Containers
public double MinimumBeatLength { get; set; } public double MinimumBeatLength { get; set; }
/// <summary> /// <summary>
/// Whether this container is currently tracking a beatmap's timing data. /// Whether this container is currently tracking a beat sync provider.
/// </summary> /// </summary>
protected bool IsBeatSyncedWithTrack { get; private set; } protected bool IsBeatSyncedWithTrack { get; private set; }
[Resolved] [Resolved]
protected IBeatSyncProvider BeatSyncSource { get; private set; } protected IBeatSyncProvider BeatSyncSource { get; private set; } = null!;
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{ {
@ -87,19 +86,18 @@ namespace osu.Game.Graphics.Containers
TimingControlPoint timingPoint; TimingControlPoint timingPoint;
EffectControlPoint effectPoint; EffectControlPoint effectPoint;
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null; IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true;
double currentTrackTime; double currentTrackTime;
if (IsBeatSyncedWithTrack) if (IsBeatSyncedWithTrack)
{ {
Debug.Assert(BeatSyncSource.ControlPoints != null);
Debug.Assert(BeatSyncSource.Clock != null); Debug.Assert(BeatSyncSource.Clock != null);
currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds;
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(currentTrackTime); timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT;
effectPoint = BeatSyncSource.ControlPoints.EffectPointAt(currentTrackTime); effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT;
} }
else else
{ {
@ -128,7 +126,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat; TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
if (ReferenceEquals(timingPoint, LastTimingPoint) && beatIndex == lastBeat) if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat)
return; return;
// as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat.
@ -136,12 +134,13 @@ namespace osu.Game.Graphics.Containers
if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE)
{ {
using (BeginDelayedSequence(-TimeSinceLastBeat)) using (BeginDelayedSequence(-TimeSinceLastBeat))
OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty); OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.CurrentAmplitudes);
} }
lastBeat = beatIndex; lastBeat = beatIndex;
LastTimingPoint = timingPoint; lastTimingPoint = timingPoint;
LastEffectPoint = effectPoint;
IsKiaiTime = effectPoint.KiaiMode;
} }
} }
} }

View File

@ -5,7 +5,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Rendering.Vertices;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Graphics.ES30; using osuTK.Graphics.ES30;

Some files were not shown because too many files have changed in this diff Show More