1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 08:02:38 +08:00

Compare commits

...

444 Commits

255 changed files with 5667 additions and 2243 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.809.2" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.907.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+1 -1
View File
@@ -80,7 +80,7 @@ namespace osu.Android
host.Window.CursorState |= CursorState.Hidden;
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
+4 -26
View File
@@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Performance;
using osu.Desktop.Security;
@@ -102,35 +102,13 @@ namespace osu.Desktop
if (!string.IsNullOrEmpty(packageManaged))
return new NoActionUpdateManager();
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
Debug.Assert(OperatingSystem.IsWindows());
return new SquirrelUpdateManager();
default:
return new SimpleUpdateManager();
}
return new VelopackUpdateManager();
}
public override bool RestartAppWhenExited()
{
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
Debug.Assert(OperatingSystem.IsWindows());
// Of note, this is an async method in squirrel that adds an arbitrary delay before returning
// likely to ensure the external process is in a good state.
//
// We're not waiting on that here, but the outro playing before the actual exit should be enough
// to cover this.
Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget();
return true;
}
return base.RestartAppWhenExited();
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
return true;
}
protected override void LoadComplete()
+21 -41
View File
@@ -14,7 +14,7 @@ using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
using SDL;
using Squirrel;
using Velopack;
namespace osu.Desktop
{
@@ -31,19 +31,11 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
/*
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
*
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
* namely by checking loaded assemblies:
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
*
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
* the app will then do completely broken things like:
* - not creating system shortcuts (as the logic is if'd out if "running tests")
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
*/
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
// last time has been fixed, let's not tempt fate.
setupVelopack();
if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
@@ -66,8 +58,6 @@ namespace osu.Desktop
return;
}
}
setupSquirrel();
}
// NVIDIA profiles are based on the executable name of a process.
@@ -177,32 +167,22 @@ namespace osu.Desktop
return false;
}
[SupportedOSPlatform("windows")]
private static void setupSquirrel()
private static void setupVelopack()
{
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
{
tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.InstallAssociations();
}, onAppUpdate: (_, tools) =>
{
tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.UpdateAssociations();
}, onAppUninstall: (_, tools) =>
{
tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry();
WindowsAssociationManager.UninstallAssociations();
}, onEveryRun: (_, _, _) =>
{
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly.
//
// This may turn out to be non-required after an alternative solution is implemented.
// see https://github.com/clowd/Clowd.Squirrel/issues/24
// tools.SetProcessAppUserModelId();
});
var app = VelopackApp.Build();
if (OperatingSystem.IsWindows())
configureWindows(app);
app.Run();
}
[SupportedOSPlatform("windows")]
private static void configureWindows(VelopackApp app)
{
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
}
}
}
@@ -1,180 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Squirrel.SimpleSplat;
using Squirrel.Sources;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
using UpdateManager = osu.Game.Updater.UpdateManager;
namespace osu.Desktop.Updater
{
[SupportedOSPlatform("windows")]
public partial class SquirrelUpdateManager : UpdateManager
{
private Squirrel.UpdateManager? updateManager;
private INotificationOverlay notificationOverlay = null!;
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
private static readonly Logger logger = Logger.GetLogger("updater");
/// <summary>
/// Whether an update has been downloaded but not yet applied.
/// </summary>
private bool updatePending;
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
[BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
{
notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
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.
try
{
// Avoid any kind of update checking while gameplay is running.
if (localUserInfo?.IsPlaying.Value == true)
return false;
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
if (info.ReleasesToApply.Count == 0)
{
if (updatePending)
{
// the user may have dismissed the completion notice, so show it again.
notificationOverlay.Post(new UpdateApplicationCompleteNotification
{
Activated = () =>
{
restartToApplyUpdate();
return true;
},
});
return true;
}
// no updates available. bail and retry later.
return false;
}
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification
{
CompletionClickAction = restartToApplyUpdate,
};
Schedule(() => notificationOverlay.Post(notification));
}
notification.StartDownload();
try
{
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.StartInstall();
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
}
catch (Exception e)
{
if (useDeltaPatching)
{
logger.Add(@"delta patching failed; will attempt full download!");
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
}
else
{
// In the case of an error, a separate notification will be displayed.
notification.FailDownload();
Logger.Error(e, @"update failed!");
}
}
}
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
return true;
}
private bool restartToApplyUpdate()
{
PrepareUpdateAsync()
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
return true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
updateManager?.Dispose();
}
private class SquirrelLogger : ILogger, IDisposable
{
public LogLevel Level { get; set; } = LogLevel.Info;
public void Write(string message, LogLevel logLevel)
{
if (logLevel < Level)
return;
logger.Add(message);
}
public void Dispose()
{
}
}
}
}
@@ -0,0 +1,133 @@
// 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.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Velopack;
using Velopack.Sources;
namespace osu.Desktop.Updater
{
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
{
private readonly UpdateManager updateManager;
private INotificationOverlay notificationOverlay = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
private UpdateInfo? pendingUpdate;
public VelopackUpdateManager()
{
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
{
AllowVersionDowngrade = true,
});
}
[BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
{
notificationOverlay = notifications;
}
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
try
{
// Avoid any kind of update checking while gameplay is running.
if (localUserInfo?.IsPlaying.Value == true)
return false;
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
if (pendingUpdate != null)
{
// If there is an update pending restart, show the notification to restart again.
notificationOverlay.Post(new UpdateApplicationCompleteNotification
{
Activated = () =>
{
restartToApplyUpdate();
return true;
}
});
return true;
}
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
// Handle no updates available.
if (pendingUpdate == null)
return false;
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification
{
CompletionClickAction = restartToApplyUpdate,
};
Schedule(() => notificationOverlay.Post(notification));
}
notification.StartDownload();
try
{
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
}
catch (Exception e)
{
// In the case of an error, a separate notification will be displayed.
notification.FailDownload();
Logger.Error(e, @"update failed!");
}
}
catch (Exception e)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
Logger.Log($@"update check failed ({e.Message})");
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
return true;
}
private bool restartToApplyUpdate()
{
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
Schedule(() => game.AttemptExit());
return true;
}
}
}
+1 -1
View File
@@ -23,9 +23,9 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
if (withModifiedSkin)
{
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("update target", () => Player.ChildrenOfType<SkinnableContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit());
CreateTest();
}
+2
View File
@@ -254,5 +254,7 @@ namespace osu.Game.Rulesets.Catch
return adjustedDifficulty;
}
public override bool EditorShowScrollSpeed => false;
}
}
@@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
public class CatchSkinComponentLookup : GameplaySkinComponentLookup<CatchSkinComponents>
public class CatchSkinComponentLookup : SkinComponentLookup<CatchSkinComponents>
{
public CatchSkinComponentLookup(CatchSkinComponents component)
: base(component)
@@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
return actualDistance / expectedDistance;
}
@@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Catch.Replays
{
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
private readonly float halfCatcherWidth;
public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
}
protected override void GenerateFrames()
@@ -47,10 +50,7 @@ namespace osu.Game.Rulesets.Catch.Replays
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
// todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
if (lastPosition - halfCatcherWidth < h.EffectiveX && lastPosition + halfCatcherWidth > h.EffectiveX)
{
// we are already in the correct range.
lastTime = h.StartTime;
@@ -30,23 +30,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
switch (lookup)
{
case SkinComponentsContainerLookup containerLookup:
case GlobalSkinnableContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
// Our own ruleset components default.
switch (containerLookup.Target)
switch (containerLookup.Lookup)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
case GlobalSkinnableContainers.MainHUDComponents:
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
{
@@ -56,10 +52,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.CentreRight;
keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
})
{
@@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
protected void SetTexture(Texture? texture, Texture? overlayTexture)
{
colouredSprite.Texture = texture;
overlaySprite.Texture = overlayTexture;
hyperSprite.Texture = texture;
// Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture.
if (colouredSprite.Texture != texture)
{
colouredSprite.Size = Vector2.Zero;
colouredSprite.Texture = texture;
}
if (overlaySprite.Texture != overlayTexture)
{
overlaySprite.Size = Vector2.Zero;
overlaySprite.Texture = overlayTexture;
}
if (hyperSprite.Texture != texture)
{
hyperSprite.Size = Vector2.Zero;
hyperSprite.Texture = texture;
}
}
}
}
+2 -2
View File
@@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI
if (Catcher.Dashing || Catcher.HyperDashing)
{
double generationInterval = Catcher.HyperDashing ? 25 : 50;
const double trail_generation_interval = 16;
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
}
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
{
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo();
[Cached]
private readonly StageDefinition stage = new StageDefinition(4);
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
protected ManiaSkinnableTestScene()
{
scrollingInfo.Direction.Value = ScrollingDirection.Down;
ScrollingInfo.Direction.Value = ScrollingDirection.Down;
Add(new Box
{
@@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Test]
public void TestScrollingDown()
{
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
}
[Test]
public void TestScrollingUp()
{
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
}
private class TestScrollingInfo : IScrollingInfo
protected class TestScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
@@ -1,13 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Skinning.Argon;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
@@ -17,22 +21,75 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
[SetUpSteps]
public void SetUpSteps()
[Test]
public void TestDisplay()
{
AddStep("setup", () => SetContents(s =>
{
if (s is ArgonSkin)
return new ArgonManiaComboCounter();
if (s is LegacySkin)
return new LegacyManiaComboCounter();
return new LegacyManiaComboCounter();
}));
setup(Anchor.Centre);
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
}
[Test]
public void TestAnchorOrigin()
{
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
setup(Anchor.TopCentre, 20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
check(Anchor.BottomCentre, -20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
setup(Anchor.BottomCentre, -20);
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
check(Anchor.TopCentre, 20);
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
setup(Anchor.Centre, 20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
check(Anchor.Centre, 20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
setup(Anchor.Centre, -20);
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
check(Anchor.Centre, -20);
}
private void setup(Anchor anchor, float y = 0)
{
AddStep($"setup {anchor} {y}", () => SetContents(s =>
{
var container = new Container
{
RelativeSizeAxes = Axes.Both,
};
if (s is ArgonSkin)
container.Add(new ArgonManiaComboCounter());
else if (s is LegacySkin)
container.Add(new LegacyManiaComboCounter());
else
container.Add(new LegacyManiaComboCounter());
container.Child.Anchor = anchor;
container.Child.Origin = Anchor.Centre;
container.Child.Y = y;
return container;
}));
}
private void check(Anchor anchor, float y)
{
AddAssert($"check {anchor} {y}", () =>
{
foreach (var combo in this.ChildrenOfType<ISerialisableDrawable>())
{
var drawableCombo = (Drawable)combo;
if (drawableCombo.Anchor != anchor || drawableCombo.Y != y)
return false;
}
return true;
});
}
}
}
@@ -6,7 +6,6 @@ using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -271,7 +270,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
});
}
else if (HitObject is IHasXPosition)
@@ -286,16 +285,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
/// <remarks>
/// osu!mania-specific beatmaps in stable only play samples at the start of the hold note.
/// </remarks>
private List<IList<HitSampleInfo>> defaultNodeSamples
=> new List<IList<HitSampleInfo>>
{
HitObject.Samples,
new List<HitSampleInfo>()
};
}
}
}
@@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponentLookup : GameplaySkinComponentLookup<ManiaSkinComponents>
public class ManiaSkinComponentLookup : SkinComponentLookup<ManiaSkinComponents>
{
/// <summary>
/// Creates a new <see cref="ManiaSkinComponentLookup"/>.
+21 -3
View File
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -91,6 +92,10 @@ namespace osu.Game.Rulesets.Mania.Objects
{
base.CreateNestedHitObjects(cancellationToken);
// Generally node samples will be populated by ManiaBeatmapConverter, but in a case like the editor they may not be.
// Ensure they are set to a sane default here.
NodeSamples ??= CreateDefaultNodeSamples(this);
AddNested(Head = new HeadNote
{
StartTime = StartTime,
@@ -102,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Objects
{
StartTime = EndTime,
Column = Column,
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
Samples = GetNodeSamples(NodeSamples.Count - 1),
});
AddNested(Body = new HoldNoteBody
@@ -116,7 +121,20 @@ namespace osu.Game.Rulesets.Mania.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) => nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
/// <summary>
/// Create the default note samples for a hold note, based off their main sample.
/// </summary>
/// <remarks>
/// By default, osu!mania beatmaps in only play samples at the start of the hold note.
/// </remarks>
/// <param name="obj">The object to use as a basis for the head sample.</param>
/// <returns>Defaults for assigning to <see cref="HoldNote.NodeSamples"/>.</returns>
public static List<IList<HitSampleInfo>> CreateDefaultNodeSamples(HitObject obj) => new List<IList<HitSampleInfo>>
{
obj.Samples,
new List<HitSampleInfo>(),
};
}
}
@@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (!Anchor.HasFlag(Anchor.y1))
{
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
}
if (Anchor.HasFlag(Anchor.y1))
return;
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
// change the sign of the Y coordinate in line with the scrolling direction.
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
@@ -28,18 +28,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
switch (lookup)
{
case SkinComponentsContainerLookup containerLookup:
case GlobalSkinnableContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
switch (containerLookup.Target)
switch (containerLookup.Lookup)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
@@ -59,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return null;
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
@@ -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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -44,16 +45,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (!Anchor.HasFlag(Anchor.y1))
{
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
}
if (Anchor.HasFlag(Anchor.y1))
return;
// since we flip the vertical anchor when changing scroll direction,
// we can use the sign of the Y value as an indicator to make the combo counter displayed correctly.
if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up))
Y = -Y;
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
// change the sign of the Y coordinate in line with the scrolling direction.
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
}
protected override void OnCountIncrement()
@@ -80,22 +80,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
switch (lookup)
{
case SkinComponentsContainerLookup containerLookup:
case GlobalSkinnableContainerLookup containerLookup:
// Modifications for global components.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
switch (containerLookup.Target)
switch (containerLookup.Lookup)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
@@ -114,7 +110,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return null;
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
return getResult(resultComponent.Component);
case ManiaSkinComponentLookup maniaComponent:
@@ -99,12 +99,6 @@ namespace osu.Game.Rulesets.Mania.UI
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Show();
return true;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
@@ -172,17 +166,6 @@ namespace osu.Game.Rulesets.Mania.UI
updateButton(false);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)
@@ -24,24 +24,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestGridToggles()
{
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(true);
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
AddStep("disable distance snap grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
gridActive<RectangularPositionSnapGrid>(true);
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("disable rectangular grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(false);
}
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
double distanceSnap = double.PositiveInfinity;
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestGridSizeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
gridSizeIs(4);
@@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderChangeStates : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[TestCase(SplineType.Catmull)]
[TestCase(SplineType.BSpline)]
[TestCase(SplineType.Linear)]
[TestCase(SplineType.PerfectCurve)]
public void TestSliderRetainsCurveTypes(SplineType splineType)
{
Slider? slider = null;
PathType pathType = new PathType(splineType);
AddStep("add slider", () => EditorBeatmap.Add(slider = new Slider
{
StartTime = 500,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, pathType),
new PathControlPoint(new Vector2(200, 0), pathType),
})
}));
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
AddStep("remove object", () => EditorBeatmap.Remove(slider));
AddAssert("slider removed", () => EditorBeatmap.HitObjects.Count == 0);
addUndoSteps();
AddAssert("slider not removed", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
}
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
}
}
@@ -163,6 +163,44 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
checkControlPointSelected(1, false);
}
[Test]
public void TestAdjustLength()
{
AddStep("move mouse to drag marker", () =>
{
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to control point 1", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[1].Position + new Vector2(60, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("expected distance halved",
() => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1));
AddStep("move mouse to drag marker", () =>
{
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse beyond last control point", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(100, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("expected distance is calculated distance",
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
moveMouseToControlPoint(1);
AddAssert("expected distance is unchanged",
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
}
private void moveHitObject()
{
AddStep("move hitobject", () =>
@@ -88,6 +88,21 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
}
[Test]
public void TestClickExpand()
{
createTest(() => new Container
{
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(10),
Child = new CursorTrail(),
});
AddStep("expand", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = new Vector2(3));
AddWaitStep("let the cursor trail draw a bit", 5);
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
}
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
{
Clear();
@@ -0,0 +1,133 @@
// 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.Graphics;
using osu.Framework.Testing;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneOsuAnalysisContainer : OsuTestScene
{
private TestReplayAnalysisOverlay analysisContainer = null!;
private ReplayAnalysisSettings settings = null!;
[Cached]
private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create analysis container", () =>
{
Children = new Drawable[]
{
new OsuPlayfieldAdjustmentContainer
{
Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()),
},
settings = new ReplayAnalysisSettings(config),
};
settings.ShowClickMarkers.Value = false;
settings.ShowAimMarkers.Value = false;
settings.ShowCursorPath.Value = false;
});
}
[Test]
public void TestEverythingOn()
{
AddStep("enable everything", () =>
{
settings.ShowClickMarkers.Value = true;
settings.ShowAimMarkers.Value = true;
settings.ShowCursorPath.Value = true;
});
}
[Test]
public void TestHitMarkers()
{
AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true);
AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible);
AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false);
AddUntilStep("hit markers not visible", () => !analysisContainer.HitMarkersVisible);
}
[Test]
public void TestAimMarker()
{
AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true);
AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible);
AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false);
AddUntilStep("aim markers not visible", () => !analysisContainer.AimMarkersVisible);
}
[Test]
public void TestAimLines()
{
AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true);
AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible);
AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false);
AddUntilStep("aim lines not visible", () => !analysisContainer.AimLinesVisible);
}
private Replay fabricateReplay()
{
var frames = new List<ReplayFrame>();
var random = new Random();
int posX = 250;
int posY = 250;
var actions = new HashSet<OsuAction>();
for (int i = 0; i < 1000; i++)
{
posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600);
posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600);
if (random.NextDouble() > (actions.Count == 0 ? 0.9 : 0.95))
{
actions.Add(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
}
else if (random.NextDouble() > 0.7)
{
actions.Remove(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
}
frames.Add(new OsuReplayFrame
{
Time = Time.Current + i * 15,
Position = new Vector2(posX, posY),
Actions = actions.ToList(),
});
}
return new Replay { Frames = frames };
}
private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay
{
public TestReplayAnalysisOverlay(Replay replay)
: base(replay)
{
}
public bool HitMarkersVisible => ClickMarkers?.Alpha > 0 && ClickMarkers.Entries.Any();
public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any();
public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1;
}
}
}
@@ -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.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.UI;
@@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
{
public class OsuRulesetConfigManager : RulesetConfigManager<OsuRulesetSetting>
{
public OsuRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
public OsuRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{
}
@@ -24,6 +22,12 @@ namespace osu.Game.Rulesets.Osu.Configuration
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
SetDefault(OsuRulesetSetting.ReplayClickMarkersEnabled, false);
SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false);
SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false);
SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false);
SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 800);
}
}
@@ -34,5 +38,12 @@ namespace osu.Game.Rulesets.Osu.Configuration
ShowCursorTrail,
ShowCursorRipples,
PlayfieldBorderStyle,
// Replay
ReplayClickMarkersEnabled,
ReplayFrameMarkersEnabled,
ReplayCursorPathEnabled,
ReplayCursorHideEnabled,
ReplayAnalysisDisplayLength,
}
}
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint;
private readonly T hitObject;
private readonly Circle circle;
private readonly FastCircle circle;
private readonly Drawable markerRing;
[Resolved]
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new[]
{
circle = new Circle
circle = new FastCircle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -1,7 +1,10 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
@@ -9,11 +12,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public partial class SliderCircleOverlay : CompositeDrawable
{
public SliderEndDragMarker? EndDragMarker { get; }
public RectangleF VisibleQuad
{
get
{
var result = CirclePiece.ScreenSpaceDrawQuad.AABBFloat;
if (endDragMarkerContainer == null) return result;
var size = result.Size * 1.4f;
var location = result.TopLeft - result.Size * 0.2f;
return new RectangleF(location, size);
}
}
protected readonly HitCirclePiece CirclePiece;
private readonly Slider slider;
private readonly SliderPosition position;
private readonly HitCircleOverlapMarker? marker;
private readonly Container? endDragMarkerContainer;
public SliderCircleOverlay(Slider slider, SliderPosition position)
{
@@ -24,26 +44,49 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
AddInternal(marker = new HitCircleOverlapMarker());
AddInternal(CirclePiece = new HitCirclePiece());
if (position == SliderPosition.End)
{
AddInternal(endDragMarkerContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding(-2.5f),
Child = EndDragMarker = new SliderEndDragMarker()
});
}
}
protected override void Update()
{
base.Update();
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle :
slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.LastRepeat!;
CirclePiece.UpdateFrom(circle);
marker?.UpdateFrom(circle);
if (endDragMarkerContainer != null)
{
endDragMarkerContainer.Position = circle.Position;
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
}
}
public override void Hide()
{
CirclePiece.Hide();
endDragMarkerContainer?.Hide();
}
public override void Show()
{
CirclePiece.Show();
endDragMarkerContainer?.Show();
}
}
}
@@ -0,0 +1,84 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Lines;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public partial class SliderEndDragMarker : SmoothPath
{
public Action<DragStartEvent>? StartDrag { get; set; }
public Action<DragEvent>? Drag { get; set; }
public Action? EndDrag { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
var path = PathApproximator.CircularArcToPiecewiseLinear([
new Vector2(0, OsuHitObject.OBJECT_RADIUS),
new Vector2(OsuHitObject.OBJECT_RADIUS, 0),
new Vector2(0, -OsuHitObject.OBJECT_RADIUS)
]);
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
PathRadius = 5;
Vertices = path;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
protected override bool OnDragStart(DragStartEvent e)
{
updateState();
StartDrag?.Invoke(e);
return true;
}
protected override void OnDrag(DragEvent e)
{
updateState();
base.OnDrag(e);
Drag?.Invoke(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
updateState();
EndDrag?.Invoke();
base.OnDragEnd(e);
}
private void updateState()
{
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
}
}
}
@@ -1,13 +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 System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -29,30 +25,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public new Slider HitObject => (Slider)base.HitObject;
private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece;
private PathControlPointVisualiser<Slider> controlPointVisualiser;
private SliderBodyPiece bodyPiece = null!;
private HitCirclePiece headCirclePiece = null!;
private HitCirclePiece tailCirclePiece = null!;
private PathControlPointVisualiser<Slider> controlPointVisualiser = null!;
private InputManager inputManager;
private InputManager inputManager = null!;
private PathControlPoint? cursor;
private SliderPlacementState state;
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
[CanBeNull]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved]
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
[CanBeNull]
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
[Resolved]
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
@@ -84,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
inputManager = GetContainingInputManager()!;
if (freehandToolboxGroup != null)
{
@@ -108,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
private EditorBeatmap editorBeatmap { get; set; } = null!;
public override void UpdateTimeAndPosition(SnapResult result)
{
@@ -151,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint))
placeNewControlPoint();
else
else if (lastPoint != null)
beginNewSegment(lastPoint);
break;
@@ -162,9 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginNewSegment(PathControlPoint lastPoint)
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
@@ -359,8 +352,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
cursor.Position = getCursorPosition();
}
else if (cursor != null)
{
@@ -374,19 +366,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
private Vector2 getCursorPosition()
{
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
/// <summary>
/// Whether a new control point can be placed at the current mouse position.
/// </summary>
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
/// <returns>Whether a new control point can be placed at the current position.</returns>
private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
private bool canPlaceNewControlPoint(out PathControlPoint? lastPoint)
{
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
lastPoint = last;
return lastPiece.IsHovered != true;
// We may only place a new control point if the cursor is not overlapping with the last control point.
// If snapping is enabled, the cursor may not hover the last piece while still placing the control point at the same position.
return !lastPiece.IsHovered && (last is null || Vector2.DistanceSquared(last.Position, getCursorPosition()) > 1f);
}
private void placeNewControlPoint()
@@ -429,7 +429,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Replace this segment with a circular arc if it is a reasonable substitute.
var circleArcSegment = tryCircleArc(segment);
if (circleArcSegment is not null)
if (circleArcSegment != null)
{
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
@@ -446,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
private Vector2[] tryCircleArc(List<Vector2> segment)
private Vector2[]? tryCircleArc(List<Vector2> segment)
{
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
@@ -1,13 +1,13 @@
// 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.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
@@ -33,27 +33,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleOverlay HeadOverlay { get; private set; }
protected SliderCircleOverlay TailOverlay { get; private set; }
protected SliderBodyPiece BodyPiece { get; private set; } = null!;
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
protected SliderCircleOverlay TailOverlay { get; private set; } = null!;
[CanBeNull]
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
protected PathControlPointVisualiser<Slider>? ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved]
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
[Resolved]
private IPlacementHandler? placementHandler { get; set; }
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get; set; }
[Resolved]
private EditorBeatmap? editorBeatmap { get; set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; }
[Resolved]
private BindableBeatDivisor? beatDivisor { get; set; }
private PathControlPoint? placementControlPoint;
public override Quad SelectionQuad
{
@@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
result = RectangleF.Union(result, HeadOverlay.VisibleQuad);
result = RectangleF.Union(result, TailOverlay.VisibleQuad);
if (ControlPointVisualiser != null)
{
foreach (var piece in ControlPointVisualiser.Pieces)
@@ -76,6 +80,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
// Cached slider path which ignored the expected distance value.
private readonly Cached<SliderPath> fullPathCache = new Cached<SliderPath>();
private Vector2 lastRightClickPosition;
public SliderSelectionBlueprint(Slider slider)
: base(slider)
{
@@ -91,6 +100,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
};
// tail will always have a non-null end drag marker.
Debug.Assert(TailOverlay.EndDragMarker != null);
TailOverlay.EndDragMarker.StartDrag += startAdjustingLength;
TailOverlay.EndDragMarker.Drag += adjustLength;
TailOverlay.EndDragMarker.EndDrag += endAdjustLength;
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
}
@@ -99,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.LoadComplete();
controlPoints.BindTo(HitObject.Path.ControlPoints);
controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate();
pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
@@ -123,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false;
hoveredControlPoint.IsSelected.Value = true;
ControlPointVisualiser.DeleteSelected();
ControlPointVisualiser?.DeleteSelected();
return true;
}
@@ -141,7 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override bool OnHover(HoverEvent e)
{
updateVisualDefinition();
return base.OnHover(e);
}
@@ -186,17 +202,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
private Vector2 rightClickPosition;
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (e.Button)
{
case MouseButton.Right:
rightClickPosition = e.MouseDownPosition;
lastRightClickPosition = e.MouseDownPosition;
return false; // Allow right click to be handled by context menu
case MouseButton.Left:
// If there's more than two objects selected, ctrl+click should deselect
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
{
@@ -212,8 +227,134 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false;
}
[CanBeNull]
private PathControlPoint placementControlPoint;
#region Length Adjustment (independent of path nodes)
private Vector2 lengthAdjustMouseOffset;
private double oldDuration;
private double oldVelocityMultiplier;
private double desiredDistance;
private bool isAdjustingLength;
private bool adjustVelocityMomentary;
private void startAdjustingLength(DragStartEvent e)
{
isAdjustingLength = true;
adjustVelocityMomentary = e.ShiftPressed;
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier;
oldVelocityMultiplier = HitObject.SliderVelocityMultiplier;
changeHandler?.BeginChange();
}
private void endAdjustLength()
{
trimExcessControlPoints(HitObject.Path);
changeHandler?.EndChange();
isAdjustingLength = false;
}
private void adjustLength(MouseEvent e) => adjustLength(findClosestPathDistance(e), e.ShiftPressed);
private void adjustLength(double proposedDistance, bool adjustVelocity)
{
desiredDistance = proposedDistance;
double proposedVelocity = oldVelocityMultiplier;
if (adjustVelocity)
{
proposedVelocity = proposedDistance / oldDuration;
proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
}
else
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
return;
HitObject.SliderVelocityMultiplier = proposedVelocity;
HitObject.Path.ExpectedDistance.Value = proposedDistance;
editorBeatmap?.Update(HitObject);
}
/// <summary>
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
/// </summary>
/// <param name="sliderPath">The slider path to trim control points of.</param>
private void trimExcessControlPoints(SliderPath sliderPath)
{
if (!sliderPath.ExpectedDistance.Value.HasValue)
return;
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
int segmentIndex = 0;
for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++)
{
if (!sliderPath.ControlPoints[i].Type.HasValue) continue;
if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3))
{
sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1);
sliderPath.ControlPoints[^1].Type = null;
break;
}
segmentIndex++;
}
}
/// <summary>
/// Finds the expected distance value for which the slider end is closest to the mouse position.
/// </summary>
private double findClosestPathDistance(MouseEvent e)
{
const double step1 = 10;
const double step2 = 0.1;
const double longer_distance_bias = 0.01;
var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset;
if (!fullPathCache.IsValid)
fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray());
// Do a linear search to find the closest point on the path to the mouse position.
double bestValue = 0;
double minDistance = double.MaxValue;
for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1)
{
double t = d / fullPathCache.Value.CalculatedDistance;
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
if (dist >= minDistance) continue;
minDistance = dist;
bestValue = d;
}
// Do another linear search to fine-tune the result.
double maxValue = Math.Min(bestValue + step1, fullPathCache.Value.CalculatedDistance);
for (double d = bestValue - step1; d <= maxValue; d += step2)
{
double t = d / fullPathCache.Value.CalculatedDistance;
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
if (dist >= minDistance) continue;
minDistance = dist;
bestValue = d;
}
return bestValue;
}
#endregion
protected override bool OnDragStart(DragStartEvent e)
{
@@ -255,9 +396,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
if (isAdjustingLength && e.ShiftPressed != adjustVelocityMomentary)
{
adjustVelocityMomentary = e.ShiftPressed;
adjustLength(desiredDistance, adjustVelocityMomentary);
return true;
}
return false;
}
protected override void OnKeyUp(KeyUpEvent e)
{
if (!IsSelected || !isAdjustingLength || e.ShiftPressed == adjustVelocityMomentary) return;
adjustVelocityMomentary = e.ShiftPressed;
adjustLength(desiredDistance, adjustVelocityMomentary);
}
private PathControlPoint addControlPoint(Vector2 position)
{
position -= HitObject.Position;
@@ -326,6 +482,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
{
if (editorBeatmap == null)
return;
// Arbitrary gap in milliseconds to put between split slider pieces
const double split_gap = 100;
@@ -432,7 +591,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
{
changeHandler?.BeginChange();
addControlPoint(rightClickPosition);
addControlPoint(lastRightClickPosition);
changeHandler?.EndChange();
}),
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
@@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class GenerateToolboxGroup : EditorToolboxGroup
{
private readonly EditorToolButton polygonButton;
public GenerateToolboxGroup()
: base("Generate")
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
polygonButton = new EditorToolButton("Polygon",
() => new SpriteIcon { Icon = FontAwesome.Solid.Spinner },
() => new PolygonGenerationPopover()),
}
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat) return false;
switch (e.Key)
{
case Key.D:
if (!e.ControlPressed || !e.ShiftPressed)
return false;
polygonButton.TriggerClick();
return true;
default:
return false;
}
}
}
}
@@ -54,24 +54,21 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
.Concat(new[]
{
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
});
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
.Concat(DistanceSnapProvider.CreateTernaryButtons());
private BindableList<HitObject> selectedHitObjects;
private Bindable<HitObject> placementObject;
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached]
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
[Cached]
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
protected readonly FreehandSliderToolboxGroup FreehandSliderToolboxGroup = new FreehandSliderToolboxGroup();
[BackgroundDependencyLoader]
private void load()
@@ -110,7 +107,8 @@ namespace osu.Game.Rulesets.Osu.Edit
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
},
FreehandlSliderToolboxGroup
new GenerateToolboxGroup(),
FreehandSliderToolboxGroup
}
);
}
@@ -0,0 +1,193 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PolygonGenerationPopover : OsuPopover
{
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
private SliderWithTextBoxInput<int> repeatCountInput = null!;
private SliderWithTextBoxInput<int> pointInput = null!;
private RoundedButton commitButton = null!;
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
private bool began;
private bool committed;
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private HitObjectComposer composer { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
{
Current = new BindableNumber<double>(1)
{
MinValue = 0.1,
MaxValue = 6,
Precision = 0.1,
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
},
Instantaneous = true
},
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
{
Current = new BindableNumber<int>
{
MinValue = 0,
MaxValue = 180,
Precision = 1
},
Instantaneous = true
},
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
{
Current = new BindableNumber<int>(1)
{
MinValue = 1,
MaxValue = 10,
Precision = 1
},
Instantaneous = true
},
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
{
Current = new BindableNumber<int>(3)
{
MinValue = 3,
MaxValue = 10,
Precision = 1,
},
Instantaneous = true
},
commitButton = new RoundedButton
{
RelativeSizeAxes = Axes.X,
Text = "Create",
Action = commit
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
changeHandler?.BeginChange();
began = true;
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
tryCreatePolygon();
}
private void tryCreatePolygon()
{
double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime);
TimingControlPoint timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(startTime);
double timeSpacing = timingPoint.BeatLength / editorBeatmap.BeatDivisor;
IHasSliderVelocity lastWithSliderVelocity = editorBeatmap.HitObjects.Where(ho => ho.GetEndTime() <= startTime).OfType<IHasSliderVelocity>().LastOrDefault() ?? new Slider();
double velocity = OsuHitObject.BASE_SCORING_DISTANCE * editorBeatmap.Difficulty.SliderMultiplier
/ LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(lastWithSliderVelocity, timingPoint, OsuRuleset.SHORT_NAME);
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
editorBeatmap.RemoveRange(insertedCircles);
insertedCircles.Clear();
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
bool first = true;
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
{
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
var circle = new HitCircle
{
Position = position,
StartTime = startTime,
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
};
// TODO: probably ensure samples also follow current ternary status (not trivial)
circle.Samples.Add(circle.CreateHitSampleInfo());
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
{
commitButton.Enabled.Value = false;
return;
}
insertedCircles.Add(circle);
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
first = false;
}
editorBeatmap.AddRange(insertedCircles);
commitButton.Enabled.Value = true;
}
private void commit()
{
changeHandler?.EndChange();
committed = true;
Hide();
}
protected override void PopOut()
{
base.PopOut();
if (began && !committed)
{
editorBeatmap.RemoveRange(insertedCircles);
changeHandler?.EndChange();
}
}
}
}
+9 -1
View File
@@ -9,6 +9,7 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@@ -162,6 +163,10 @@ namespace osu.Game.Rulesets.Osu.Objects
[JsonIgnore]
public SliderTailCircle TailCircle { get; protected set; }
[JsonIgnore]
[CanBeNull]
public SliderRepeat LastRepeat { get; protected set; }
public Slider()
{
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
@@ -225,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Repeat:
AddNested(new SliderRepeat(this)
AddNested(LastRepeat = new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
@@ -248,6 +253,9 @@ namespace osu.Game.Rulesets.Osu.Objects
if (TailCircle != null)
TailCircle.Position = EndPosition;
if (LastRepeat != null)
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
}
protected void UpdateNestedSamples()
+2
View File
@@ -359,5 +359,7 @@ namespace osu.Game.Rulesets.Osu
return adjustedDifficulty;
}
public override bool EditorShowScrollSpeed => false;
}
}
@@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu
{
public class OsuSkinComponentLookup : GameplaySkinComponentLookup<OsuSkinComponents>
public class OsuSkinComponentLookup : SkinComponentLookup<OsuSkinComponents>
{
public OsuSkinComponentLookup(OsuSkinComponents component)
: base(component)
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
HitResult result = resultComponent.Component;
// This should eventually be moved to a skin setting, when supported.
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
HitResult result = resultComponent.Component;
switch (result)
@@ -44,23 +44,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
switch (lookup)
{
case SkinComponentsContainerLookup containerLookup:
case GlobalSkinnableContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
// Our own ruleset components default.
switch (containerLookup.Target)
switch (containerLookup.Lookup)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
@@ -69,10 +65,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.CentreRight;
keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
+11 -4
View File
@@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private double timeOffset;
private float time;
/// <summary>
/// The scale used on creation of a new trail part.
/// </summary>
public Vector2 NewPartScale = Vector2.One;
private Anchor trailOrigin = Anchor.Centre;
protected Anchor TrailOrigin
@@ -188,6 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
parts[currentIndex].Position = localSpacePosition;
parts[currentIndex].Time = time + 1;
parts[currentIndex].Scale = NewPartScale;
++parts[currentIndex].InvalidationID;
currentIndex = (currentIndex + 1) % max_sprites;
@@ -199,6 +205,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public Vector2 Position;
public float Time;
public Vector2 Scale;
public long InvalidationID;
}
@@ -280,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@@ -289,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@@ -298,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y),
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@@ -307,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y),
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
@@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
/// <summary>
/// The current expanded scale of the cursor.
/// </summary>
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
public IBindable<float> CursorScale => cursorScale;
private readonly Bindable<float> cursorScale = new BindableFloat(1);
@@ -23,14 +23,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
protected override Drawable CreateCursor() => new OsuCursor();
protected override Container<Drawable> Content => fadeContainer;
private readonly Container<Drawable> fadeContainer;
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
private readonly Drawable cursorTrail;
private readonly SkinnableDrawable cursorTrail;
private readonly CursorRippleVisualiser rippleVisualiser;
@@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
InternalChild = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new[]
Children = new CompositeDrawable[]
{
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
rippleVisualiser = new CursorRippleVisualiser(),
@@ -79,6 +78,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
ActiveCursor.Contract();
}
protected override void Update()
{
base.Update();
if (cursorTrail.Drawable is CursorTrail trail)
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
switch (e.Action)
+24 -5
View File
@@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
@@ -25,18 +26,36 @@ namespace osu.Game.Rulesets.Osu.UI
{
public partial class DrawableOsuRuleset : DrawableRuleset<OsuHitObject>
{
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
private Bindable<bool>? cursorHideEnabled;
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods)
{
}
public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h) => null;
[BackgroundDependencyLoader]
private void load(ReplayPlayer? replayPlayer)
{
if (replayPlayer != null)
{
PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay));
replayPlayer.AddSettings(new ReplayAnalysisSettings(Config));
cursorHideEnabled = Config.GetBindable<bool>(OsuRulesetSetting.ReplayCursorHideEnabled);
// I have little faith in this working (other things touch cursor visibility) but haven't broken it yet.
// Let's wait for someone to report an issue before spending too much time on it.
cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true);
}
}
public override DrawableHitObject<OsuHitObject>? CreateDrawableRepresentation(OsuHitObject h) => null;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor
+10 -6
View File
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -35,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI
{
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
var drawableOsuRuleset = (DrawableOsuRuleset?)drawableRuleset;
if (drawableOsuRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
var osuPlayfield = drawableOsuRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
@@ -45,13 +48,14 @@ namespace osu.Game.Rulesets.Osu.UI
{
Child = clickToResumeCursor = new OsuClickToResumeCursor
{
ResumeRequested = () =>
ResumeRequested = action =>
{
// since the user had to press a button to tap the resume cursor,
// block that press event from potentially reaching a hit circle that's behind the cursor.
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
if (inputBlocker != null)
// note this only matters when the user didn't pause while they were holding the same key that they are resuming with.
if (inputBlocker != null && !drawableOsuRuleset.AsNonNull().KeyBindingInputManager.PressedActions.Contains(action))
inputBlocker.BlockNextPress = true;
Resume();
@@ -94,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
public override bool HandlePositionalInput => true;
public Action? ResumeRequested;
public Action<OsuAction>? ResumeRequested;
private Container scaleTransitionContainer = null!;
public OsuClickToResumeCursor()
@@ -136,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.UI
return false;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
ResumeRequested?.Invoke(e.Action);
return true;
}
@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Performance;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class AnalysisFrameEntry : LifetimeEntry
{
public OsuAction[] Action { get; }
public Vector2 Position { get; }
public AnalysisFrameEntry(double time, double displayLength, Vector2 position, params OsuAction[] action)
{
LifetimeStart = time;
LifetimeEnd = time + displayLength;
Position = position;
Action = action;
}
}
}
@@ -0,0 +1,28 @@
// 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.Game.Graphics;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public abstract partial class AnalysisMarker : PoolableDrawableWithLifetime<AnalysisFrameEntry>
{
[Resolved]
protected OsuColour Colours { get; private set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
}
protected override void OnApply(AnalysisFrameEntry entry)
{
Position = entry.Position;
Depth = -(float)entry.LifetimeEnd;
}
}
}
@@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
/// <summary>
/// A marker which shows one click, with visuals focusing on the button which was clicked and the precise location of the click.
/// </summary>
public partial class ClickMarker : AnalysisMarker
{
private CircularProgress leftClickDisplay = null!;
private CircularProgress rightClickDisplay = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.125f),
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = Colours.Gray5,
},
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Colours.Gray5,
Masking = true,
BorderThickness = 2.2f,
BorderColour = Color4.White,
Child = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0,
},
},
leftClickDisplay = new CircularProgress
{
Colour = Colours.Yellow,
Size = new Vector2(0.95f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Rotation = 180,
Progress = 0.5f,
InnerRadius = 0.18f,
RelativeSizeAxes = Axes.Both,
},
rightClickDisplay = new CircularProgress
{
Colour = Colours.Yellow,
Size = new Vector2(0.95f),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Progress = 0.5f,
InnerRadius = 0.18f,
RelativeSizeAxes = Axes.Both,
},
};
Size = new Vector2(16);
}
protected override void OnApply(AnalysisFrameEntry entry)
{
base.OnApply(entry);
leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
}
}
}
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
{
private readonly DrawablePool<ClickMarker> clickMarkerPool;
public ClickMarkerContainer()
{
AddInternal(clickMarkerPool = new DrawablePool<ClickMarker>(30));
}
protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry));
}
}
@@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Performance;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class CursorPathContainer : Path
{
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
private readonly SortedSet<AnalysisFrameEntry> aliveEntries = new SortedSet<AnalysisFrameEntry>(new AimLinePointComparator());
public CursorPathContainer()
{
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
PathRadius = 0.5f;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Pink2;
BackgroundColour = colours.Pink2.Opacity(0);
}
protected override void Update()
{
base.Update();
lifetimeManager.Update(Time.Current);
}
public void Add(AnalysisFrameEntry entry) => lifetimeManager.AddEntry(entry);
private void entryBecameAlive(LifetimeEntry entry)
{
aliveEntries.Add((AnalysisFrameEntry)entry);
updateVertices();
}
private void entryBecameDead(LifetimeEntry entry)
{
aliveEntries.Remove((AnalysisFrameEntry)entry);
updateVertices();
}
private void updateVertices()
{
ClearVertices();
Vector2 min = Vector2.Zero;
foreach (var entry in aliveEntries)
{
AddVertex(entry.Position);
if (entry.Position.X < min.X)
min.X = entry.Position.X;
if (entry.Position.Y < min.Y)
min.Y = entry.Position.Y;
}
Position = min;
}
private sealed class AimLinePointComparator : IComparer<AnalysisFrameEntry>
{
public int Compare(AnalysisFrameEntry? x, AnalysisFrameEntry? y)
{
ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y);
return x.LifetimeStart.CompareTo(y.LifetimeStart);
}
}
}
}
@@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
/// <summary>
/// A marker which shows one movement frame, include any buttons which are pressed.
/// </summary>
public partial class FrameMarker : AnalysisMarker
{
private CircularProgress leftClickDisplay = null!;
private CircularProgress rightClickDisplay = null!;
private Circle mainCircle = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
mainCircle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Colours.Pink2,
},
leftClickDisplay = new CircularProgress
{
Colour = Colours.Yellow,
Size = new Vector2(0.8f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Rotation = 180,
Progress = 0.5f,
InnerRadius = 0.5f,
RelativeSizeAxes = Axes.Both,
},
rightClickDisplay = new CircularProgress
{
Colour = Colours.Yellow,
Size = new Vector2(0.8f),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Progress = 0.5f,
InnerRadius = 0.5f,
RelativeSizeAxes = Axes.Both,
},
};
}
protected override void OnApply(AnalysisFrameEntry entry)
{
base.OnApply(entry);
Size = new Vector2(entry.Action.Any() ? 4 : 2.5f);
mainCircle.Colour = entry.Action.Any() ? Colours.Gray4 : Colours.Pink2;
leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
}
}
}
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class FrameMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
{
private readonly DrawablePool<FrameMarker> pool;
public FrameMarkerContainer()
{
AddInternal(pool = new DrawablePool<FrameMarker>(80));
}
protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry));
}
}
@@ -0,0 +1,128 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI.ReplayAnalysis;
namespace osu.Game.Rulesets.Osu.UI
{
public partial class ReplayAnalysisOverlay : CompositeDrawable
{
private BindableBool showClickMarkers { get; } = new BindableBool();
private BindableBool showFrameMarkers { get; } = new BindableBool();
private BindableBool showCursorPath { get; } = new BindableBool();
private BindableInt displayLength { get; } = new BindableInt();
protected ClickMarkerContainer? ClickMarkers;
protected FrameMarkerContainer? FrameMarkers;
protected CursorPathContainer? CursorPath;
private readonly Replay replay;
public ReplayAnalysisOverlay(Replay replay)
{
RelativeSizeAxes = Axes.Both;
this.replay = replay;
}
private bool requireDisplay => showClickMarkers.Value || showFrameMarkers.Value || showCursorPath.Value;
[BackgroundDependencyLoader]
private void load(OsuRulesetConfigManager config)
{
config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers);
config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers);
config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath);
config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, displayLength);
}
protected override void LoadComplete()
{
base.LoadComplete();
displayLength.BindValueChanged(_ =>
{
// Need to fully reload to make this work.
loaded.Invalidate();
}, true);
}
private readonly Cached loaded = new Cached();
private CancellationTokenSource? generationCancellationSource;
protected override void Update()
{
base.Update();
if (requireDisplay)
initialise();
if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0;
if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0;
if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0;
}
private void initialise()
{
if (loaded.IsValid)
return;
loaded.Validate();
generationCancellationSource?.Cancel();
generationCancellationSource = new CancellationTokenSource();
// It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer`
var newDrawables = new Drawable[]
{
CursorPath = new CursorPathContainer(),
ClickMarkers = new ClickMarkerContainer(),
FrameMarkers = new FrameMarkerContainer(),
};
bool leftHeld = false;
bool rightHeld = false;
// This should probably be async as well, but it's a bit of a pain to debounce and everything.
// Let's address concerns when they are raised.
foreach (var frame in replay.Frames)
{
var osuFrame = (OsuReplayFrame)frame;
bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton);
bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton);
if (leftHeld && !leftButton)
leftHeld = false;
else if (!leftHeld && leftButton)
{
leftHeld = true;
ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton));
}
if (rightHeld && !rightButton)
rightHeld = false;
else if (!rightHeld && rightButton)
{
rightHeld = true;
ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton));
}
FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray()));
CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position));
}
LoadComponentsAsync(newDrawables, drawables => InternalChildrenEnumerable = drawables, generationCancellationSource.Token);
}
}
}
@@ -0,0 +1,55 @@
// 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.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Screens.Play.PlayerSettings;
namespace osu.Game.Rulesets.Osu.UI
{
public partial class ReplayAnalysisSettings : PlayerSettingsGroup
{
private readonly OsuRulesetConfigManager config;
[SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowClickMarkers { get; } = new BindableBool();
[SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowAimMarkers { get; } = new BindableBool();
[SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowCursorPath { get; } = new BindableBool();
[SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))]
public BindableBool HideSkinCursor { get; } = new BindableBool();
[SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar<int>))]
public BindableInt DisplayLength { get; } = new BindableInt
{
MinValue = 200,
MaxValue = 2000,
Default = 800,
Precision = 200,
};
public ReplayAnalysisSettings(OsuRulesetConfigManager config)
: base("Analysis Settings")
{
this.config = config;
}
[BackgroundDependencyLoader]
private void load()
{
AddRange(this.CreateSettingsControls());
config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers);
config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers);
config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath);
config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor);
config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength);
}
}
}
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is GameplaySkinComponentLookup<HitResult>)
if (lookup is SkinComponentLookup<HitResult>)
{
// if a taiko skin is providing explosion sprites, hide the judgements completely
if (hasExplosion.Value)
@@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
public class TaikoSkinComponentLookup : GameplaySkinComponentLookup<TaikoSkinComponents>
public class TaikoSkinComponentLookup : SkinComponentLookup<TaikoSkinComponents>
{
public TaikoSkinComponentLookup(TaikoSkinComponents component)
: base(component)
@@ -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.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
@@ -286,5 +287,62 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
}
[Test]
public void TestBinarySearchEmptyList()
{
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.FirstFound), Is.EqualTo(-1));
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Leftmost), Is.EqualTo(-1));
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Rightmost), Is.EqualTo(-1));
}
[TestCase(new[] { 1 }, 0, -1)]
[TestCase(new[] { 1 }, 1, 0)]
[TestCase(new[] { 1 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 0, -1)]
[TestCase(new[] { 1, 3 }, 1, 0)]
[TestCase(new[] { 1, 3 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 3, 1)]
[TestCase(new[] { 1, 3 }, 4, -3)]
public void TestBinarySearchUniqueScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestBinarySearchFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestBinarySearchLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 1)]
[TestCase(new[] { 1, 2, 2 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 2)]
public void TestBinarySearchRightMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
}
}
@@ -537,7 +537,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
[TestCaseSource(nameof(correct_date_query_examples))]
public void TestValidDateQueries(string dateQuery)
{
string query = $"played<{dateQuery} time";
string query = $"lastplayed<{dateQuery} time";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
@@ -571,7 +571,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
[Test]
public void TestGreaterDateQuery()
{
const string query = "played>50";
const string query = "lastplayed>50";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
@@ -584,7 +584,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
[Test]
public void TestLowerDateQuery()
{
const string query = "played<50";
const string query = "lastplayed<50";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.LastPlayed.Max, Is.Null);
@@ -597,7 +597,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
[Test]
public void TestBothSidesDateQuery()
{
const string query = "played>3M played<1y6M";
const string query = "lastplayed>3M lastplayed<1y6M";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null);
@@ -611,7 +611,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
[Test]
public void TestEqualDateQuery()
{
const string query = "played=50";
const string query = "lastplayed=50";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
@@ -620,11 +620,38 @@ namespace osu.Game.Tests.NonVisual.Filtering
[Test]
public void TestOutOfRangeDateQuery()
{
const string query = "played<10000y";
const string query = "lastplayed<10000y";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
}
private static readonly object[] played_query_tests =
{
new object[] { "0", DateTimeOffset.MinValue, true },
new object[] { "0", DateTimeOffset.Now, false },
new object[] { "false", DateTimeOffset.MinValue, true },
new object[] { "false", DateTimeOffset.Now, false },
new object[] { "no", DateTimeOffset.MinValue, true },
new object[] { "no", DateTimeOffset.Now, false },
new object[] { "1", DateTimeOffset.MinValue, false },
new object[] { "1", DateTimeOffset.Now, true },
new object[] { "true", DateTimeOffset.MinValue, false },
new object[] { "true", DateTimeOffset.Now, true },
new object[] { "yes", DateTimeOffset.MinValue, false },
new object[] { "yes", DateTimeOffset.Now, true },
};
[Test]
[TestCaseSource(nameof(played_query_tests))]
public void TestPlayedQuery(string query, DateTimeOffset reference, bool matched)
{
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, $"played={query}");
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
Assert.AreEqual(matched, filterCriteria.LastPlayed.IsInRange(reference));
}
}
}
@@ -257,7 +257,7 @@ namespace osu.Game.Tests.Online
{
}
protected override string Target => null;
protected override string Target => string.Empty;
}
}
}
+23 -10
View File
@@ -12,6 +12,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
@@ -107,7 +108,7 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
}
}
@@ -120,8 +121,20 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
}
}
[Test]
public void TestDeserialiseInvalidDrawables()
{
using (var stream = TestResources.OpenResource("Archives/argon-invalid-drawable.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos.Any(kvp => kvp.Value.AllDrawables.Any(d => d.Type == typeof(StarFountain))), Is.False);
}
}
@@ -134,10 +147,10 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First();
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
@@ -148,10 +161,10 @@ namespace osu.Game.Tests.Skins
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
}
}
@@ -18,6 +18,7 @@ using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -48,13 +49,18 @@ namespace osu.Game.Tests.Visual.Background
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Add(detachedBeatmapStore);
Beatmap.SetDefault();
}
@@ -2,19 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Resources;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest;
namespace osu.Game.Tests.Visual.DailyChallenge
@@ -27,63 +29,61 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay();
private Room room = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.Add(notificationOverlay);
base.Content.Add(metadataClient);
Add(notificationOverlay);
Add(metadataClient);
// add button to observe for daily challenge changes and perform its logic.
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
}
[Test]
[Solo]
public void TestDailyChallenge()
{
var room = new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First())
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallengeIntro(room)));
startChallenge(1234);
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
}
[Test]
public void TestNotifications()
public void TestPlayIntroOnceFlag()
{
var room = new Room
startChallenge(1234);
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
startChallenge(1235);
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
}
private void startChallenge(int roomId)
{
AddStep("add room", () =>
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
API.Perform(new CreateRoomRequest(room = new Room
{
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
RoomID = { Value = roomId },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo))
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
StartDate = { Value = DateTimeOffset.Now },
EndDate = { Value = DateTimeOffset.Now.AddHours(24) },
Category = { Value = RoomCategory.DailyChallenge }
}));
});
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
}
}
}
@@ -24,7 +24,10 @@ namespace osu.Game.Tests.Visual.Editing
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true });
beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false });
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000));
editorBeatmap = new EditorBeatmap(beatmap);
}
@@ -7,6 +7,7 @@ using Humanizer;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
@@ -307,6 +308,46 @@ namespace osu.Game.Tests.Visual.Editing
hitObjectNodeHasSampleVolume(0, 1, 10);
}
[Test]
public void TestSamplePointSeek()
{
AddStep("add slider", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 0,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples =
{
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
},
RepeatCount = 1
});
});
seekSamplePiece(-1);
editorTimeIs(0);
samplePopoverIsOpen();
seekSamplePiece(-1);
editorTimeIs(0);
samplePopoverIsOpen();
seekSamplePiece(1);
editorTimeIs(406);
seekSamplePiece(1);
editorTimeIs(813);
seekSamplePiece(1);
editorTimeIs(1627);
seekSamplePiece(1);
editorTimeIs(1627);
}
[Test]
public void TestHotkeysMultipleSelectionWithSameSampleBank()
{
@@ -548,6 +589,63 @@ namespace osu.Game.Tests.Visual.Editing
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
}
[Test]
public void TestHotkeysUnifySliderSamplesAndNodeSamples()
{
AddStep("add slider", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 1000,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM),
},
NodeSamples = new List<IList<HitSampleInfo>>
{
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
},
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
},
}
});
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("set soft bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.E);
InputManager.ReleaseKey(Key.LShift);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
AddStep("unify whistle addition", () => InputManager.Key(Key.W));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
}
[Test]
public void TestSelectingObjectDoesNotMutateSamples()
{
@@ -569,7 +667,7 @@ namespace osu.Game.Tests.Visual.Editing
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(samplePiece);
InputManager.Click(MouseButton.Left);
@@ -583,6 +681,21 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left);
});
private void seekSamplePiece(int direction) => AddStep($"seek sample piece {direction}", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(direction < 1 ? Key.Left : Key.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
InputManager.ReleaseKey(Key.ControlLeft);
});
private void samplePopoverIsOpen() => AddUntilStep("sample popover is open", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault(o => o.IsPresent);
return popover != null;
});
private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
@@ -727,5 +840,7 @@ namespace osu.Game.Tests.Visual.Editing
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
}
}
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded
&& songSelect.BeatmapSetsLoaded
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
@@ -96,32 +96,6 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
[Test]
public void TestCommitPlacementViaGlobalAction()
{
Playfield playfield = null!;
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
AddStep("move mouse to top left of playfield", () =>
{
playfield = this.ChildrenOfType<Playfield>().Single();
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right of playfield", () =>
{
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("confirm via global action", () =>
{
globalActionContainer.TriggerPressed(GlobalAction.Select);
globalActionContainer.TriggerReleased(GlobalAction.Select);
});
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
[Test]
public void TestAbortPlacementViaGlobalAction()
{
@@ -272,11 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("confirm via global action", () =>
{
globalActionContainer.TriggerPressed(GlobalAction.Select);
globalActionContainer.TriggerReleased(GlobalAction.Select);
});
AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
AddAssert("slider samples have drum bank", () => EditorBeatmap.HitObjects[0].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
@@ -114,40 +114,6 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestTrackingCurrentTimeWhileRunning()
{
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670);
AddStep("Seek to just before next point", () => EditorClock.Seek(69000));
AddStep("Start clock", () => EditorClock.Start());
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
}
[Test]
public void TestTrackingCurrentTimeWhilePaused()
{
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670);
AddStep("Seek to later", () => EditorClock.Seek(80000));
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
}
[Test]
public void TestScrollControlGroupIntoView()
{
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@@ -136,6 +137,59 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
[Test]
public void TestCalibrationFromNonZeroWithImmediateReferenceScore()
{
const double average_error = -4.5;
const double initial_offset = -2;
AddStep("Set beatmap offset non-neutral", () => Realm.Write(r =>
{
r.Add(new BeatmapInfo
{
ID = Beatmap.Value.BeatmapInfo.ID,
Ruleset = Beatmap.Value.BeatmapInfo.Ruleset,
UserSettings =
{
Offset = initial_offset,
}
});
}));
AddStep("Create control with preloaded reference score", () =>
{
Child = new PlayerSettingsGroup("Some settings")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
offsetControl = new BeatmapOffsetControl
{
ReferenceScore =
{
Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
}
}
}
}
};
});
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error));
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll<BeatmapInfo>()));
}
[Test]
public void TestCalibrationNoChange()
{
@@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinnableContainers.MainHUDComponents, skinManager.CurrentSkin.Value));
}
protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin)
@@ -53,9 +53,9 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource)
protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainers target, ISkin expectedSource)
{
var targetContainer = Player.ChildrenOfType<SkinComponentsContainer>().First(s => s.Lookup.Target == target);
var targetContainer = Player.ChildrenOfType<SkinnableContainer>().First(s => s.Lookup.Lookup == target);
var actualComponentsContainer = targetContainer.ChildrenOfType<Container>().SingleOrDefault(c => c.Parent == targetContainer);
if (actualComponentsContainer == null)
@@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var actualInfo = actualComponentsContainer.CreateSerialisedInfo();
var expectedComponentsContainer = expectedSource.GetDrawableComponent(new SkinComponentsContainerLookup(target)) as Container;
var expectedComponentsContainer = expectedSource.GetDrawableComponent(new GlobalSkinnableContainerLookup(target)) as Container;
if (expectedComponentsContainer == null)
return false;
@@ -7,9 +7,13 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -28,14 +32,20 @@ namespace osu.Game.Tests.Visual.Gameplay
public TestSceneBreakTracker()
{
AddRange(new Drawable[]
Children = new Drawable[]
{
new Box
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
},
breakTracker = new TestBreakTracker(),
breakOverlay = new BreakOverlay(true, null)
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
{
ProcessCustomClock = false,
BreakTracker = breakTracker,
}
});
};
}
protected override void Update()
@@ -48,9 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestShowBreaks()
{
setClock(false);
addShowBreakStep(2);
addShowBreakStep(5);
addShowBreakStep(15);
}
@@ -115,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep($"show '{seconds}s' break", () =>
{
breakOverlay.Breaks = breakTracker.Breaks = new List<BreakPeriod>
breakTracker.Breaks = new List<BreakPeriod>
{
new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000)
};
@@ -129,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks)
{
AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks);
AddStep($"load {breakDescription}", () => breakTracker.Breaks = breaks);
seekAndAssertBreak("seek back to 0", 0, false);
}
@@ -175,6 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
public TestBreakTracker()
: base(0, new ScoreProcessor(new OsuRuleset()))
{
FramedManualClock = new FramedClock(manualClock = new ManualClock());
ProcessCustomClock = false;
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinnableContainer>().First();
private Drawable keyCounterContent => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<Drawable>().Skip(1).First();
public TestSceneHUDOverlay()
@@ -174,6 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay
holdForMenu.Action += () => activated = true;
});
AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
@@ -214,6 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
});
AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
@@ -240,8 +242,8 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew();
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Alpha == 0);
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
AddStep("bind on update", () =>
{
@@ -258,10 +260,10 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew();
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Alpha == 0);
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Reload());
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().ComponentsLoaded);
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Reload());
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().ComponentsLoaded);
}
private void createNew(Action<HUDOverlay>? action = null)
@@ -1,13 +1,13 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Input;
@@ -21,11 +21,19 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms
private HoldForMenuButton holdForMenuButton;
private HoldForMenuButton holdForMenuButton = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("set button always on", () =>
{
config.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true);
});
AddStep("create button", () =>
{
exitAction = false;
@@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(2000, 0)]
[TestCase(3000, first_hit_object - 3000)]
[TestCase(10000, first_hit_object - 10000)]
[FlakyTest]
public void TestLeadInProducesCorrectStartTime(double leadIn, double expectedStartTime)
{
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
@@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(0, 0)]
[TestCase(-1000, -1000)]
[TestCase(-10000, -10000)]
[FlakyTest]
public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime)
{
var storyboard = new Storyboard();
@@ -64,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(0, 0, true)]
[TestCase(-1000, -1000, true)]
[TestCase(-10000, -10000, true)]
[FlakyTest]
public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop)
{
const double loop_start_time = -20000;
@@ -320,6 +320,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExitViaHoldToExit()
{
AddStep("set hold button always visible", () => LocalConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
AddStep("exit", () =>
{
InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.First(c => c is HoldToConfirmContainer));
@@ -47,6 +47,16 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 10000,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 15000,
}
}
};
@@ -256,7 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked()
public void TestOsuHitCircleNotReceivingInputOnResume()
{
KeyCounter counter = null!;
@@ -281,19 +291,82 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Any());
}
[Test]
public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingSameKey()
{
KeyCounter counter = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
AddStep("press Z", () => InputManager.PressKey(Key.Z));
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1));
AddStep("pause", () => Player.Pause());
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
seekTo(5000);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 2, true);
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2));
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 2, false);
}
[Test]
public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingOtherKey()
{
loadPlayer(() => new OsuRuleset());
AddStep("press X", () => InputManager.PressKey(Key.X));
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1));
seekTo(5000);
AddStep("pause", () => Player.Pause());
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1));
AddStep("press X", () => InputManager.PressKey(Key.X));
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2));
}
private void loadPlayer(Func<Ruleset> createRuleset)
{
AddStep("set ruleset", () => currentRuleset = createRuleset());
AddStep("load player", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinnableContainer>().All(s => s.ComponentsLoaded));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
seekTo(0);
AddAssert("not in break", () => !Player.IsBreakTime.Value);
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
}
private void checkKey(Func<KeyCounter> counter, int count, bool active)
{
AddAssert($"key count = {count}", () => counter().CountPresses.Value, () => Is.EqualTo(count));
@@ -8,7 +8,9 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -26,6 +28,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -177,6 +180,30 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestEmptyFailStillImports()
{
prepareTestAPI(true);
createPlayerTest(true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("attempt import", () =>
{
InputManager.MoveMouseTo(Player.ChildrenOfType<SaveFailedScoreButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for import to start", () => Player.ScoreImportStarted);
AddStep("allow import", () => Player.AllowImportCompletion.Release());
AddUntilStep("import completed", () => Player.ImportedScore, () => Is.Not.Null);
AddAssert("ensure no submission", () => Player.SubmittedScore, () => Is.Null);
}
[Test]
public void TestSubmissionOnFail()
{
@@ -378,6 +405,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public SemaphoreSlim AllowImportCompletion { get; }
public Score ImportedScore { get; private set; }
public new FailOverlay FailOverlay => base.FailOverlay;
public FakeImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base(allowPause, showResults, pauseOnFocusLost)
{
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private SkinManager skins { get; set; } = null!;
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
private SkinnableContainer targetContainer => Player.ChildrenOfType<SkinnableContainer>().First();
[SetUpSteps]
public override void SetUpSteps()
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Add big black boxes", () =>
{
var target = Player.ChildrenOfType<SkinComponentsContainer>().First();
var target = Player.ChildrenOfType<SkinnableContainer>().First();
target.Add(box1 = new BigBlackBox
{
Position = new Vector2(-90),
@@ -200,14 +200,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestUndoEditHistory()
{
SkinComponentsContainer firstTarget = null!;
SkinnableContainer firstTarget = null!;
TestSkinEditorChangeHandler changeHandler = null!;
byte[] defaultState = null!;
IEnumerable<ISerialisableDrawable> testComponents = null!;
AddStep("Load necessary things", () =>
{
firstTarget = Player.ChildrenOfType<SkinComponentsContainer>().First();
firstTarget = Player.ChildrenOfType<SkinnableContainer>().First();
changeHandler = new TestSkinEditorChangeHandler(firstTarget);
changeHandler.SaveState();
@@ -377,11 +377,11 @@ namespace osu.Game.Tests.Visual.Gameplay
() => Is.EqualTo(3));
}
private SkinComponentsContainer globalHUDTarget => Player.ChildrenOfType<SkinComponentsContainer>()
.Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null);
private SkinnableContainer globalHUDTarget => Player.ChildrenOfType<SkinnableContainer>()
.Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null);
private SkinComponentsContainer rulesetHUDTarget => Player.ChildrenOfType<SkinComponentsContainer>()
.Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null);
private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType<SkinnableContainer>()
.Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null);
[Test]
public void TestMigrationArgon()
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleEditor()
{
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
var skinComponentsContainer = new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.SongSelect));
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
{
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinnableContainer>().First();
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
public TestSceneSkinnableHUDOverlay()
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddUntilStep("HUD overlay loaded", () => hudOverlay.IsAlive);
AddUntilStep("components container loaded",
() => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Any(scc => scc.ComponentsLoaded));
() => hudOverlay.ChildrenOfType<SkinnableContainer>().Any(scc => scc.ComponentsLoaded));
}
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
@@ -4,17 +4,17 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public partial class TestSceneStarFountain : OsuTestScene
{
[SetUpSteps]
public void SetUpSteps()
[Test]
public void TestMenu()
{
AddStep("make fountains", () =>
{
@@ -34,11 +34,7 @@ namespace osu.Game.Tests.Visual.Menus
},
};
});
}
[Test]
public void TestPew()
{
AddRepeatStep("activate fountains sometimes", () =>
{
foreach (var fountain in Children.OfType<StarFountain>())
@@ -48,5 +44,34 @@ namespace osu.Game.Tests.Visual.Menus
}
}, 150);
}
[Test]
public void TestGameplay()
{
AddStep("make fountains", () =>
{
Children = new[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
};
});
AddStep("activate fountains", () =>
{
((StarFountain)Children[0]).Shoot(1);
((StarFountain)Children[1]).Shoot(-1);
});
}
}
}
@@ -97,6 +97,7 @@ namespace osu.Game.Tests.Visual.Menus
public void TestTransientUserStatisticsDisplay()
{
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
AddStep("Gain", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
@@ -113,6 +114,7 @@ namespace osu.Game.Tests.Visual.Menus
PP = 1357
});
});
AddStep("Loss", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
@@ -129,7 +131,9 @@ namespace osu.Game.Tests.Visual.Menus
PP = 1234
});
});
AddStep("No change", () =>
// Tests flooring logic works as expected.
AddStep("Tiny increase in PP", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
@@ -137,14 +141,32 @@ namespace osu.Game.Tests.Visual.Menus
new UserStatistics
{
GlobalRank = 111_111,
PP = 1357
PP = 1357.6m
},
new UserStatistics
{
GlobalRank = 111_111,
PP = 1357
PP = 1358.1m
});
});
AddStep("No change 1", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
GlobalRank = 111_111,
PP = 1357m
},
new UserStatistics
{
GlobalRank = 111_111,
PP = 1357.1m
});
});
AddStep("Was null", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
@@ -161,6 +183,7 @@ namespace osu.Game.Tests.Visual.Menus
PP = 1357
});
});
AddStep("Became null", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
@@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -45,9 +46,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
Add(detachedBeatmapStore);
}
public override void SetUpSteps()
@@ -19,6 +19,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@@ -65,9 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
Add(detachedBeatmapStore);
}
public override void SetUpSteps()
@@ -45,11 +45,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));
Add(detachedBeatmapStore);
}
public override void SetUpSteps()
@@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -33,13 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
var beatmapSet = TestResources.CreateTestBeatmapSetInfo();
manager.Import(beatmapSet);
Add(detachedBeatmapStore);
}
public override void SetUpSteps()
@@ -165,16 +165,19 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
[Solo]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{
prepareBeatmap();
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddStep("test gameplay", () => getEditor().TestGameplay());
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
AddStep("test gameplay", () => getEditor().TestGameplay());
AddUntilStep("wait for player", () =>
{
// notifications may fire at almost any inopportune time and cause annoying test failures.
@@ -183,8 +186,7 @@ namespace osu.Game.Tests.Visual.Navigation
Game.CloseAllOverlays();
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
});
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
@@ -352,7 +354,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded);
&& songSelect.BeatmapSetsLoaded);
}
private void openEditor()
@@ -176,6 +176,12 @@ namespace osu.Game.Tests.Visual.Navigation
private void confirmBeatmapInSongSelect(Func<BeatmapSetInfo> getImport)
{
AddUntilStep("wait for carousel loaded", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().SingleOrDefault()?.IsLoaded == true;
});
AddUntilStep("beatmap in song select", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
@@ -187,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
@@ -197,7 +203,7 @@ namespace osu.Game.Tests.Visual.Navigation
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 1024 + 2;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID));
}
@@ -276,8 +276,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("delete beatmap files", () =>
{
foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu"))
Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath()));
FileUtils.AttemptOperation(() =>
{
foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu"))
Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath()));
});
});
AddStep("invalidate cache", () =>
@@ -1032,9 +1035,11 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestTouchScreenDetectionInGame()
{
BeatmapSetInfo beatmapSet = null;
PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet));
AddStep("select", () => InputManager.Key(Key.Enter));
Player player = null;
@@ -336,13 +336,13 @@ namespace osu.Game.Tests.Visual.Navigation
});
AddStep("change to triangles skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(SkinInfo.TRIANGLES_SKIN.ToString()));
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
// sort of implicitly relies on song select not being skinnable.
// TODO: revisit if the above ever changes
AddUntilStep("skin changed", () => !skinEditor.ChildrenOfType<SkinBlueprint>().Any());
AddStep("change back to modified skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(editedSkinId.ToString()));
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
AddUntilStep("changes saved", () => skinEditor.ChildrenOfType<SkinBlueprint>().Any());
}
@@ -446,7 +446,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Show overlay with channel 1", () =>
{
channelManager.JoinChannel(testChannel1);
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
chatOverlay.Show();
});
waitForChannel1Visible();
@@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Show overlay with channel 1", () =>
{
channelManager.JoinChannel(testChannel1);
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
chatOverlay.Show();
});
waitForChannel1Visible();
@@ -17,6 +17,7 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Comments;
using osu.Game.Overlays.Comments.Buttons;
namespace osu.Game.Tests.Visual.Online
{
@@ -58,6 +59,11 @@ namespace osu.Game.Tests.Visual.Online
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
AddUntilStep("show more button hidden",
() => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().Alpha == 0);
if (withPinned)
AddAssert("pinned comment replies collapsed", () => commentsContainer.ChildrenOfType<ShowRepliesButton>().First().Expanded.Value, () => Is.False);
else
AddAssert("first comment replies expanded", () => commentsContainer.ChildrenOfType<ShowRepliesButton>().First().Expanded.Value, () => Is.True);
}
[TestCase(false)]
@@ -302,7 +308,7 @@ namespace osu.Game.Tests.Visual.Online
bundle.Comments.Add(new Comment
{
Id = 20,
Message = "Reply to pinned comment",
Message = "Reply to pinned comment initially hidden",
LegacyName = "AbandonedUser",
CreatedAt = DateTimeOffset.Now,
VotesCount = 0,
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -11,6 +12,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Tests.Visual.Online
@@ -60,5 +62,12 @@ namespace osu.Game.Tests.Visual.Online
change.Invoke(User.Value!.User.DailyChallengeStatistics);
User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset);
}
[Test]
public void TestPlayCountRankingTier()
{
AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze);
AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver);
}
}
}
@@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Ranking;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneCollectionButton : OsuManualInputManagerTestScene
{
private CollectionButton? collectionButton;
private readonly BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 };
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create button", () => Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = collectionButton = new CollectionButton(beatmapInfo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
});
}
[Test]
public void TestCollectionButton()
{
AddStep("click collection button", () =>
{
InputManager.MoveMouseTo(collectionButton!);
InputManager.Click(MouseButton.Left);
});
AddAssert("collection popover is visible", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Visible);
AddStep("click outside popover", () =>
{
InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft);
InputManager.Click(MouseButton.Left);
});
AddAssert("collection popover is hidden", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Hidden);
AddStep("click collection button", () =>
{
InputManager.MoveMouseTo(collectionButton!);
InputManager.Click(MouseButton.Left);
});
AddStep("press escape", () => InputManager.Key(Key.Escape));
AddAssert("collection popover is hidden", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Hidden);
}
}
}

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