1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 12:07:23 +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.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.X, PippidonAction.Button2),
};
public override Drawable CreateIcon() => new Sprite
{
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
}
}

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

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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.730.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.810.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.810.2" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Text;
using DiscordRPC;
@ -26,15 +24,15 @@ namespace osu.Desktop
{
private const string client_id = "367827983903490050";
private DiscordRpcClient client;
private DiscordRpcClient client = null!;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private IBindable<APIUser> user;
private IBindable<APIUser> user = null!;
[Resolved]
private IAPIProvider api { get; set; }
private IAPIProvider api { get; set; } = null!;
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
@ -130,7 +128,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty;
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);
else
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)
{
@ -183,10 +181,10 @@ namespace osu.Desktop
switch (activity)
{
case UserActivity.InGame game:
return game.BeatmapInfo.ToString();
return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit:
return edit.BeatmapInfo.ToString();
return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.InLobby lobby:
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Desktop.LegacyIpc
{
/// <summary>
@ -13,7 +11,7 @@ namespace osu.Desktop.LegacyIpc
/// </remarks>
public class LegacyIpcDifficultyCalculationRequest
{
public string BeatmapFile { get; set; }
public string BeatmapFile { get; set; } = string.Empty;
public int RulesetId { 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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Desktop.LegacyIpc
{
/// <summary>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Platform;
using Newtonsoft.Json.Linq;
@ -39,17 +37,20 @@ namespace osu.Desktop.LegacyIpc
public new object Value
{
get => base.Value;
set => base.Value = new Data
{
MessageType = value.GetType().Name,
MessageData = value
};
set => base.Value = new Data(value.GetType().Name, value);
}
public class Data
{
public string MessageType { get; set; }
public object MessageData { get; set; }
public string MessageType { get; }
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.IO;
using System.Runtime.Versioning;
@ -27,7 +25,7 @@ namespace osu.Desktop
private const string base_game_name = @"osu";
#endif
private static LegacyTcpIpcProvider legacyIpc;
private static LegacyTcpIpcProvider? legacyIpc;
[STAThread]
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Security.Principal;
using osu.Framework;
@ -21,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component
{
[Resolved]
private INotificationOverlay notifications { get; set; }
private INotificationOverlay notifications { get; set; } = null!;
private bool elevated;

View File

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

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -14,12 +12,12 @@ namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable<bool> disableWinKey;
private IBindable<bool> localUserPlaying;
private IBindable<bool> isActive;
private Bindable<bool> disableWinKey = null!;
private IBindable<bool> localUserPlaying = null!;
private IBindable<bool> isActive = null!;
[Resolved]
private GameHost host { get; set; }
private GameHost host { get; set; } = null!;
[BackgroundDependencyLoader]
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Runtime.InteropServices;
@ -21,7 +19,7 @@ namespace osu.Desktop.Windows
private const int wm_syskeyup = 261;
//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;
[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 double ScoreMultiplier => 1;
public override double ScoreMultiplier => 0.9;
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@ -16,21 +13,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
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)
: base(barLine)
{
RelativeSizeAxes = Axes.X;
Height = 2f;
Height = barLine.Major ? 1.7f : 1.2f;
AddInternal(new Box
{
@ -38,34 +25,33 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Colour = new Color4(255, 204, 33, 255),
Alpha = barLine.Major ? 0.5f : 0.2f
});
if (barLine.Major)
{
AddInternal(new EquilateralTriangle
Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(new Circle
{
Name = "Left triangle",
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopCentre,
Size = new Vector2(triangle_height),
X = -triangle_offset,
Rotation = 90
Name = "Left line",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Size = size,
X = -line_offset,
});
AddInternal(new EquilateralTriangle
AddInternal(new Circle
{
Name = "Right triangle",
Anchor = Anchor.BottomRight,
Origin = Anchor.TopCentre,
Size = new Vector2(triangle_height),
X = triangle_offset,
Rotation = -90
Name = "Right line",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
Size = size,
X = line_offset,
});
}
if (!barLine.Major)
Alpha = 0.2f;
}
protected override void UpdateInitialTransforms()

View File

@ -9,7 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
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.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;

View File

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

View File

@ -6,7 +6,8 @@ using System.Diagnostics;
using System.Linq;
using Moq;
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.Textures;
using osu.Framework.Testing;
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene
{
[Resolved]
private IRenderer renderer { get; set; } = null!;
private static readonly object?[][] texture_priority_cases =
{
// 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.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
{
@ -84,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
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);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
private const double acute_angle_multiplier = 2.0;
private const double slider_multiplier = 1.5;
private const double acute_angle_multiplier = 1.95;
private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
/// <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);
}
if (osuLastObj.TravelTime != 0)
if (osuLastObj.BaseObject is Slider)
{
// Reward sliders based on velocity.
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 hidden_bonus = 0.2;
private const double min_velocity = 0.5;
private const double slider_multiplier = 1.3;
/// <summary>
/// Evaluates the difficulty of memorising and hitting an object, based on:
/// <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>length and speed of the current object (for sliders),</description></item>
/// <item><description>and whether the hidden mod is enabled.</description></item>
/// </list>
/// </summary>
@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden)
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;
}
}

View File

@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
speedRating = 0.0;
flashlightRating *= 0.7;
}
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;
@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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 drainRate = beatmap.Difficulty.DrainRate;

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
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 int scoreMaxCombo;
private int countGreat;
@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
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))
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))
{
// 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 + countMeh, totalHits);
// https://www.desmos.com/calculator/bc9eybdthb
// 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);
@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
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.
@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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 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);
// 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;
}
@ -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 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
{
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 float maximum_slider_radius = normalised_radius * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f;
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; }
/// <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>
public double TravelTime { get; private set; }
@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider 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);
}
@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return;
// 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)
{
@ -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.
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++)
{
@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat)
{
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalised_radius;
requiredMovement = NORMALISED_RADIUS;
}
if (currMovementLength > requiredMovement)
@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1)
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)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain;
private double skillMultiplier => 23.25;
private double skillMultiplier => 23.55;
private double strainDecayBase => 0.15;
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 ModType Type => ModType.Automation;
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 bool PerformFail() => false;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
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) };
private IFrameStableClock gameplayClock = null!;

View File

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

View File

@ -9,9 +9,9 @@ using System.Runtime.InteropServices;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
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.Textures;
using osu.Framework.Input;
@ -68,8 +68,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
private void load(IRenderer renderer, ShaderManager shaders)
{
texture ??= renderer.WhitePixel;
shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
}
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
resetTime();
}
private Texture texture = Texture.WhitePixel;
private Texture texture;
public Texture Texture
{
@ -222,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Vector2 size;
private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
private IVertexBatch<TexturedTrailVertex> vertexBatch;
public TrailDrawNode(CursorTrail source)
: base(source)
@ -254,15 +255,17 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
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.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind();
texture.Bind();
RectangleF textureRect = texture.GetTextureRect();
@ -319,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
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.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -112,21 +114,36 @@ namespace osu.Game.Rulesets.Osu.UI
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
private void load(OsuRulesetConfigManager config, IBeatmap beatmap)
{
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
RegisterPool<HitCircle, DrawableHitCircle>(10, 100);
var osuBeatmap = (OsuBeatmap)beatmap;
RegisterPool<Slider, DrawableSlider>(10, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100);
RegisterPool<SliderTick, DrawableSliderTick>(10, 100);
RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
RegisterPool<HitCircle, DrawableHitCircle>(20, 100);
// handle edge cases where a beatmap has a slider with many repeats.
int maxRepeatsOnOneSlider = 0;
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<SpinnerTick, DrawableSpinnerTick>(10, 100);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
@ -173,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(10)
: base(20)
{
this.result = result;
this.onLoaded = onLoaded;

View File

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

View File

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

View File

@ -206,7 +206,7 @@ namespace osu.Game.Tests.Editing
}
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)
=> 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;

View File

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

View File

@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Timing;
@ -27,6 +27,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
private const string animation_name = "animation";
private const int frame_count = 6;
[Resolved]
private IRenderer renderer { get; set; }
[Cached(typeof(IAnimationTimeReference))]
private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference();
@ -35,9 +38,12 @@ namespace osu.Game.Tests.NonVisual.Skinning
[Test]
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);
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 readonly IRenderer renderer;
public TestSkin(IRenderer renderer)
{
this.renderer = renderer;
}
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();

View File

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

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@ -148,6 +149,16 @@ namespace osu.Game.Tests.Online
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
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
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.Game.Audio;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
@ -19,9 +29,9 @@ namespace osu.Game.Tests.Skins
public class TestSceneSkinResources : OsuTestScene
{
[Resolved]
private SkinManager skins { get; set; }
private SkinManager skins { get; set; } = null!;
private ISkin skin;
private ISkin skin = null!;
[BackgroundDependencyLoader]
private void load()
@ -32,5 +42,56 @@ namespace osu.Game.Tests.Skins
[Test]
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.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osu.Framework.Screens;
using osu.Framework.Testing;
@ -43,6 +44,9 @@ namespace osu.Game.Tests.Visual.Background
[Resolved]
private OsuConfigManager config { get; set; }
[Resolved]
private IRenderer renderer { get; set; }
[SetUpSteps]
public void SetUpSteps()
{
@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.Background
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 class TestBackgroundScreenDefault : BackgroundScreenDefault
@ -274,12 +278,15 @@ namespace osu.Game.Tests.Visual.Background
private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap
{
public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager)
private readonly IRenderer renderer;
public UniqueBackgroundTestWorkingBeatmap(IRenderer renderer, AudioManager 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

View File

@ -10,7 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
@ -28,11 +28,9 @@ namespace osu.Game.Tests.Visual.Background
[Resolved]
private SessionStatics statics { get; set; }
[Cached(typeof(LargeTextureStore))]
private LookupLoggingTextureStore textureStore = new LookupLoggingTextureStore();
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private LookupLoggingTextureStore textureStore;
private SeasonalBackgroundLoader backgroundLoader;
private Container backgroundContainer;
@ -45,15 +43,32 @@ namespace osu.Game.Tests.Visual.Background
"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]
private void load(LargeTextureStore 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]
@ -193,6 +208,11 @@ namespace osu.Game.Tests.Visual.Background
{
public List<string> PerformedLookups { get; } = new List<string>();
public LookupLoggingTextureStore(IRenderer renderer)
: base(renderer)
{
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
{
PerformedLookups.Add(name);

View File

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

View File

@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Components
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()
{

View File

@ -56,8 +56,10 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestCreateNewBeatmap()
{
AddAssert("status is none", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
}
[Test]
@ -208,6 +210,8 @@ namespace osu.Game.Tests.Visual.Editing
});
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("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
@ -218,7 +222,7 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
&& 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("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);
AddStep("save beatmap", () => Editor.Save());

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
{
AddStep("reset clock", () => Clock.Seek(0));
AddStep("start clock", Clock.Start);
AddStep("start clock", () => Clock.Start());
AddAssert("clock running", () => Clock.IsRunning);
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);
AddStep("start clock again", Clock.Start);
AddStep("start clock again", () => Clock.Start());
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("stop clock", Clock.Stop);
AddStep("stop clock", () => Clock.Stop());
AddAssert("clock stopped", () => !Clock.IsRunning);
AddStep("seek exactly to end", () => Clock.Seek(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);
}
[Test]
public void TestClampWhenSeekOutsideBeatmapBounds()
{
AddStep("stop clock", Clock.Stop);
AddStep("stop clock", () => Clock.Stop());
AddStep("seek before start time", () => Clock.Seek(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);

View File

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

View File

@ -4,7 +4,6 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
@ -120,7 +119,7 @@ namespace osu.Game.Tests.Visual.Editing
private void pressAndCheckTime(Key key, double expectedTime)
{
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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;

View File

@ -13,7 +13,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
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);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
SpectatorClient.SendEndPlay(userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
playingUsers.Remove(user);
});

View File

@ -83,6 +83,20 @@ namespace osu.Game.Tests.Visual.Online
Beatmap = dummyBeatmap,
},
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,
Type = RecentActivityType.BeatmapsetApprove,
@ -90,6 +104,13 @@ namespace osu.Game.Tests.Visual.Online
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Ranked,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{
User = dummyUser,
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"))));
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)));
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()));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
@ -168,15 +169,15 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
addClickAddOrRemoveButtonStep(1);
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);
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]
@ -226,6 +227,8 @@ namespace osu.Game.Tests.Visual.SongSelect
=> AddUntilStep($"collection dropdown header displays '{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) =>
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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -9,29 +10,55 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
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.Dialog;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModPresetColumn : OsuTestScene
public class TestSceneModPresetColumn : OsuManualInputManagerTestScene
{
protected override bool UseFreshStoragePerRun => true;
private Container<Drawable> content = null!;
protected override Container<Drawable> Content => content;
private RulesetStore rulesets = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay = new DialogOverlay();
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(rulesets = new RealmRulesetStore(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]
@ -57,15 +84,10 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestBasicOperation()
{
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
AddStep("create content", () => Child = new Container
AddStep("create content", () => Child = new ModPresetColumn
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30),
Child = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
@ -112,15 +134,11 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSoftDeleteSupport()
{
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,
Padding = new MarginPadding(30),
Child = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
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>())
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>())
preset.DeletePending = false;
@ -146,6 +166,101 @@ namespace osu.Game.Tests.Visual.UserInterface
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[]
{
new ModPreset

View File

@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -23,6 +26,12 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset selected mods", () => SelectedMods.SetDefault());
}
[Test]
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[]
{
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));
ModColumn lastColumn = null;
ModSelectColumn lastColumn = null;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Music;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
@ -21,13 +23,25 @@ namespace osu.Game.Tests.Visual.UserInterface
{
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]
public void Setup() => Schedule(() =>
@ -46,16 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface
}
};
beatmapSets.Clear();
for (int i = 0; i < item_count; i++)
{
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged());
beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo());
}
first = beatmapSets.First();
playlistOverlay.BeatmapSets.BindTo(beatmapSets);
beatmapSets.First().ToLive(Realm);
});
[Test]
@ -70,9 +80,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any());
PlaylistItem firstItem = null!;
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.PressButton(MouseButton.Left);
});
@ -83,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface
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));
}
@ -101,6 +115,68 @@ namespace osu.Game.Tests.Visual.UserInterface
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter)
.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.
/// </summary>
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;
LastOnlineUpdate = null;
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?)
@ -198,8 +199,8 @@ namespace osu.Game.Beatmaps
Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash;
string? fileHashX = x.BeatmapSet.GetFile(getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.GetFile(getFilename(y.Metadata))?.File.Hash;
return fileHashX == fileHashY;
}

View File

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

View File

@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps
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.
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);
// 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.Hash = stream.ComputeSHA2Hash();
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
Realm.Write(r =>
{

View File

@ -3,13 +3,23 @@
#nullable disable
using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps
{
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,
[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)
{
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.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using osu.Framework.Development;
@ -51,6 +52,9 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Queue an update for a beatmap set.
/// </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="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)
@ -89,21 +93,26 @@ namespace osu.Game.Beatmaps
if (res != null)
{
beatmapInfo.Status = res.Status;
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.OnlineID = res.OnlineID;
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
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}.");
}
@ -202,19 +211,26 @@ namespace osu.Game.Beatmaps
{
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.
beatmapInfo.OnlineID = reader.GetInt32(1);
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
beatmapInfo.OnlineMD5Hash = reader.GetString(4);
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}.");
return true;
}
@ -233,6 +249,12 @@ namespace osu.Game.Beatmaps
private void logForModel(BeatmapSetInfo set, string 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()
{
cacheDownloadRequest?.Dispose();

View File

@ -6,15 +6,18 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
public class BeatmapSetOnlineStatusPill : CircularContainer
public class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
{
private BeatmapOnlineStatus status;
@ -96,5 +99,19 @@ namespace osu.Game.Beatmaps.Drawables
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.
// if so, do not make any changes.
if (loadedPreview != previewTrack)
{
loadedPreview.Dispose();
return;
}
AddInternal(loadedPreview);
toggleLoading(false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Models;
namespace osu.Game.Database
@ -11,8 +12,16 @@ namespace osu.Game.Database
/// </summary>
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; }
/// <summary>
/// A combined hash representing the model, based on the files it contains.
/// Implementation specific.
/// </summary>
string Hash { get; set; }
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Threading;
@ -27,27 +25,30 @@ namespace osu.Game.Database
public class LegacyImportManager : Component
{
[Resolved]
private SkinManager skins { get; set; }
private SkinManager skins { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; }
private BeatmapManager beatmaps { get; set; } = null!;
[Resolved]
private ScoreManager scores { get; set; }
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
private ScoreManager scores { get; set; } = null!;
[Resolved]
private IDialogOverlay dialogOverlay { get; set; }
private OsuGame? game { get; set; }
[Resolved]
private RealmAccess realmAccess { get; set; }
private IDialogOverlay dialogOverlay { get; set; } = null!;
[Resolved(canBeNull: true)]
private DesktopGameHost desktopGameHost { get; set; }
[Resolved]
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;
@ -98,6 +99,9 @@ namespace osu.Game.Database
stableStorage = GetCurrentStableStorage();
}
if (stableStorage == null)
return;
var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask;
@ -108,7 +112,14 @@ namespace osu.Game.Database
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
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))
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);
}
public StableStorage GetCurrentStableStorage()
public StableStorage? GetCurrentStableStorage()
{
if (cachedStorage != null)
return cachedStorage;

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Logging;
@ -107,7 +108,15 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Cancelled;
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.Linq;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
@ -79,7 +80,7 @@ namespace osu.Game.Database
/// </summary>
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)
{

View File

@ -25,6 +25,7 @@ using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using Realms;
@ -172,6 +173,11 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension;
#if DEBUG
if (!DebugUtils.IsNUnitRunning)
applyFilenameSchemaSuffix(ref Filename);
#endif
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// 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)
{
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
@ -292,6 +343,11 @@ namespace osu.Game.Database
foreach (var s in pendingDeleteSkins)
realm.Remove(s);
var pendingDeletePresets = realm.All<ModPreset>().Where(s => s.DeletePending);
foreach (var s in pendingDeletePresets)
realm.Remove(s);
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.Allocation;
using System.Collections.Generic;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Buffers;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Lists;
namespace osu.Game.Graphics.Backgrounds
@ -88,7 +87,7 @@ namespace osu.Game.Graphics.Backgrounds
private Random stableRandom;
private IShader shader;
private readonly Texture texture;
private Texture texture;
/// <summary>
/// Construct a new triangle visualisation.
@ -98,13 +97,12 @@ namespace osu.Game.Graphics.Backgrounds
{
if (seed != null)
stableRandom = new Random(seed.Value);
texture = Texture.WhitePixel;
}
[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);
}
@ -184,8 +182,8 @@ namespace osu.Game.Graphics.Backgrounds
private void addTriangles(bool randomY)
{
// limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = QuadVertexBuffer<TexturedVertex2D>.MAX_QUADS;
// Limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2);
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 Vector2 size;
private QuadBatch<TexturedVertex2D> vertexBatch;
private IVertexBatch<TexturedVertex2D> vertexBatch;
public TrianglesDrawNode(Triangles source)
: base(source)
@ -270,14 +268,14 @@ namespace osu.Game.Graphics.Backgrounds
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))
{
vertexBatch?.Dispose();
vertexBatch = new QuadBatch<TexturedVertex2D>(Source.AimCount, 1);
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
}
shader.Bind();
@ -297,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds
ColourInfo colourInfo = DrawColourInfo.Colour;
colourInfo.ApplyChild(particle.Colour);
DrawTriangle(
renderer.DrawTriangle(
texture,
triangle,
colourInfo,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
@ -10,13 +8,12 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// 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>
/// <remarks>
/// 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
{
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>
/// 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; }
/// <summary>
/// Whether this container is currently tracking a beatmap's timing data.
/// Whether this container is currently tracking a beat sync provider.
/// </summary>
protected bool IsBeatSyncedWithTrack { get; private set; }
[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)
{
@ -87,19 +86,18 @@ namespace osu.Game.Graphics.Containers
TimingControlPoint timingPoint;
EffectControlPoint effectPoint;
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null;
IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true;
double currentTrackTime;
if (IsBeatSyncedWithTrack)
{
Debug.Assert(BeatSyncSource.ControlPoints != null);
Debug.Assert(BeatSyncSource.Clock != null);
currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds;
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(currentTrackTime);
effectPoint = BeatSyncSource.ControlPoints.EffectPointAt(currentTrackTime);
timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT;
effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT;
}
else
{
@ -128,7 +126,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
if (ReferenceEquals(timingPoint, LastTimingPoint) && beatIndex == lastBeat)
if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat)
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.
@ -136,12 +134,13 @@ namespace osu.Game.Graphics.Containers
if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE)
{
using (BeginDelayedSequence(-TimeSinceLastBeat))
OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty);
OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.CurrentAmplitudes);
}
lastBeat = beatIndex;
LastTimingPoint = timingPoint;
LastEffectPoint = effectPoint;
lastTimingPoint = timingPoint;
IsKiaiTime = effectPoint.KiaiMode;
}
}
}

View File

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

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