diff --git a/osu.Android.props b/osu.Android.props
index 3682a44b9f..5b65670869 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index b2487568ce..0c21c75290 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -18,6 +19,7 @@ using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
using osu.Desktop.Windows;
+using osu.Framework.Threading;
using osu.Game.IO;
namespace osu.Desktop
@@ -144,13 +146,39 @@ namespace osu.Desktop
desktopWindow.DragDrop += f => fileDrop(new[] { f });
}
+ private readonly List importableFiles = new List();
+ private ScheduledDelegate importSchedule;
+
private void fileDrop(string[] filePaths)
{
- var firstExtension = Path.GetExtension(filePaths.First());
+ lock (importableFiles)
+ {
+ var firstExtension = Path.GetExtension(filePaths.First());
- if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
+ if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
- Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
+ importableFiles.AddRange(filePaths);
+
+ Logger.Log($"Adding {filePaths.Length} files for import");
+
+ // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
+ // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
+ importSchedule?.Cancel();
+ importSchedule = Scheduler.AddDelayed(handlePendingImports, 100);
+ }
+ }
+
+ private void handlePendingImports()
+ {
+ lock (importableFiles)
+ {
+ Logger.Log($"Handling batch import of {importableFiles.Count} files");
+
+ var paths = importableFiles.ToArray();
+ importableFiles.Clear();
+
+ Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index 4b008d2734..bba42dea97 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -3,13 +3,16 @@
using System.Linq;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHidden : ModHidden
+ public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Play with fading fruits.";
public override double ScoreMultiplier => 1.06;
@@ -17,6 +20,14 @@ namespace osu.Game.Rulesets.Catch.Mods
private const double fade_out_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44;
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset;
+ var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield;
+
+ catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
+ }
+
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
base.ApplyNormalVisibilityState(hitObject, state);
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index ed875e7002..5d57e84b75 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Catch.UI
///
public bool HyperDashing => hyperDashModifier != 1;
+ ///
+ /// Whether fruit should appear on the plate.
+ ///
+ public bool CatchFruitOnPlate { get; set; } = true;
+
///
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
///
@@ -237,7 +242,8 @@ namespace osu.Game.Rulesets.Catch.UI
{
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
- placeCaughtObject(palpableObject, positionInStack);
+ if (CatchFruitOnPlate)
+ placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 7ae69bf7d7..42ea12214f 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -288,17 +288,56 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
+ [Test]
+ public void TestHitTailBeforeLastTick()
+ {
+ const int tick_rate = 8;
+ const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
+ const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ }
+ },
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_last_tick - 5)
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertLastTickJudgement(HitResult.LargeTickMiss);
+ assertTailJudgement(HitResult.Ok);
+ }
+
private void assertHeadJudgement(HitResult result)
- => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
+ => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);
private void assertTailJudgement(HitResult result)
- => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result);
+ => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result);
private void assertNoteJudgement(HitResult result)
- => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result);
+ => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result);
private void assertTickJudgement(HitResult result)
- => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick
+ => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result));
+
+ private void assertLastTickJudgement(HitResult result)
+ => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result);
private ScoreAccessibleReplayPlayer currentPlayer;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 7a0e3b2b76..26393c8edb 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
if (IsForCurrentRuleset)
{
- TargetColumns = (int)Math.Max(1, roundedCircleSize);
+ TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo);
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
@@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
originalTargetColumns = TargetColumns;
}
+ public static int GetColumnCountForNonConvert(BeatmapInfo beatmap)
+ {
+ var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize);
+ return (int)Math.Max(1, roundedCircleSize);
+ }
+
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
new file mode 100644
index 0000000000..d9a278ef29
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Filter;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Filter;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class ManiaFilterCriteria : IRulesetFilterCriteria
+ {
+ private FilterCriteria.OptionalRange keys;
+
+ public bool Matches(BeatmapInfo beatmap)
+ {
+ return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)));
+ }
+
+ public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
+ {
+ switch (key)
+ {
+ case "key":
+ case "keys":
+ return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index d624e094ad..88b63606b9 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -22,6 +22,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
@@ -382,6 +383,11 @@ namespace osu.Game.Rulesets.Mania
}
}
};
+
+ public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
+ {
+ return new ManiaFilterCriteria();
+ }
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 4f062753a6..828ee7b03e 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -233,6 +233,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
if (Tail.AllJudged)
{
+ foreach (var tick in tickContainer)
+ {
+ if (!tick.Judged)
+ tick.MissForcefully();
+ }
+
ApplyResult(r => r.Type = r.Judgement.MaxResult);
endHold();
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 871339ae7b..82301bd37f 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -35,8 +35,18 @@ namespace osu.Game.Rulesets.Osu.Edit
referenceOrigin = null;
}
- public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
- moveSelection(moveEvent.InstantDelta);
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ // this will potentially move the selection out of bounds...
+ foreach (var h in hitObjects)
+ h.Position += moveEvent.InstantDelta;
+
+ // but this will be corrected.
+ moveSelectionInBounds();
+ return true;
+ }
///
/// During a transform, the initial origin is stored so it can be used throughout the operation.
@@ -140,36 +150,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
- {
- Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
- Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
-
- foreach (var point in slider.Path.ControlPoints)
- point.Position.Value *= pathRelativeDeltaScale;
- }
+ scaleSlider(slider, scale);
else
- {
- // move the selection before scaling if dragging from top or left anchors.
- if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
- if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
-
- Quad quad = getSurroundingQuad(hitObjects);
-
- foreach (var h in hitObjects)
- {
- var newPosition = h.Position;
-
- // guard against no-ops and NaN.
- if (scale.X != 0 && quad.Width > 0)
- newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X);
-
- if (scale.Y != 0 && quad.Height > 0)
- newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y);
-
- h.Position = newPosition;
- }
- }
+ scaleHitObjects(hitObjects, reference, scale);
+ moveSelectionInBounds();
return true;
}
@@ -207,28 +192,124 @@ namespace osu.Game.Rulesets.Osu.Edit
return true;
}
- private bool moveSelection(Vector2 delta)
+ private void scaleSlider(Slider slider, Vector2 scale)
+ {
+ Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
+
+ // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
+ scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
+
+ Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height);
+
+ Queue oldControlPoints = new Queue();
+
+ foreach (var point in slider.Path.ControlPoints)
+ {
+ oldControlPoints.Enqueue(point.Position.Value);
+ point.Position.Value *= pathRelativeDeltaScale;
+ }
+
+ //if sliderhead or sliderend end up outside playfield, revert scaling.
+ Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
+ (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
+
+ if (xInBounds && yInBounds)
+ return;
+
+ foreach (var point in slider.Path.ControlPoints)
+ point.Position.Value = oldControlPoints.Dequeue();
+ }
+
+ private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
+ {
+ scale = getClampedScale(hitObjects, reference, scale);
+
+ // move the selection before scaling if dragging from top or left anchors.
+ float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
+ float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
+
+ Quad selectionQuad = getSurroundingQuad(hitObjects);
+
+ foreach (var h in hitObjects)
+ {
+ var newPosition = h.Position;
+
+ // guard against no-ops and NaN.
+ if (scale.X != 0 && selectionQuad.Width > 0)
+ newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
+
+ if (scale.Y != 0 && selectionQuad.Height > 0)
+ newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
+
+ h.Position = newPosition;
+ }
+ }
+
+ private (bool X, bool Y) isQuadInBounds(Quad quad)
+ {
+ bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
+ bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
+
+ return (xInBounds, yInBounds);
+ }
+
+ private void moveSelectionInBounds()
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
- Vector2 newTopLeft = quad.TopLeft + delta;
- if (newTopLeft.X < 0)
- delta.X -= newTopLeft.X;
- if (newTopLeft.Y < 0)
- delta.Y -= newTopLeft.Y;
+ Vector2 delta = Vector2.Zero;
- Vector2 newBottomRight = quad.BottomRight + delta;
- if (newBottomRight.X > DrawWidth)
- delta.X -= newBottomRight.X - DrawWidth;
- if (newBottomRight.Y > DrawHeight)
- delta.Y -= newBottomRight.Y - DrawHeight;
+ if (quad.TopLeft.X < 0)
+ delta.X -= quad.TopLeft.X;
+ if (quad.TopLeft.Y < 0)
+ delta.Y -= quad.TopLeft.Y;
+
+ if (quad.BottomRight.X > DrawWidth)
+ delta.X -= quad.BottomRight.X - DrawWidth;
+ if (quad.BottomRight.Y > DrawHeight)
+ delta.Y -= quad.BottomRight.Y - DrawHeight;
foreach (var h in hitObjects)
h.Position += delta;
+ }
- return true;
+ ///
+ /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
+ ///
+ /// The hitobjects to be scaled
+ /// The anchor from which the scale operation is performed
+ /// The scale to be clamped
+ /// The clamped scale vector
+ private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
+ {
+ float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
+ float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
+
+ Quad selectionQuad = getSurroundingQuad(hitObjects);
+
+ //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
+ Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
+
+ //max Size -> playfield bounds
+ if (scaledQuad.TopLeft.X < 0)
+ scale.X += scaledQuad.TopLeft.X;
+ if (scaledQuad.TopLeft.Y < 0)
+ scale.Y += scaledQuad.TopLeft.Y;
+
+ if (scaledQuad.BottomRight.X > DrawWidth)
+ scale.X -= scaledQuad.BottomRight.X - DrawWidth;
+ if (scaledQuad.BottomRight.Y > DrawHeight)
+ scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
+
+ //min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
+ Vector2 scaledSize = selectionQuad.Size + scale;
+ Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
+
+ scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
+
+ return scale;
}
///
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index f2bb518b2e..3e25e22b5f 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -43,6 +43,29 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaEscapeAndConfirm();
}
+ [Test]
+ public void TestRetryCountIncrements()
+ {
+ Player player = null;
+
+ PushAndConfirm(() => new TestSongSelect());
+
+ AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
+
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
+
+ AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
+ AddAssert("retry count is 0", () => player.RestartCount == 0);
+
+ AddStep("attempt to retry", () => player.ChildrenOfType().First().Action());
+ AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player);
+
+ AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
+ AddAssert("retry count is 1", () => player.RestartCount == 1);
+ }
+
[Test]
public void TestRetryFromResults()
{
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index a7f6c8c0d3..a62980addf 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -45,6 +45,8 @@ namespace osu.Game.Tests.Visual.Settings
public Bindable AreaOffset { get; } = new Bindable();
public Bindable AreaSize { get; } = new Bindable();
+ public Bindable Rotation { get; } = new Bindable();
+
public IBindable Tablet => tablet;
private readonly Bindable tablet = new Bindable();
diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
index 4791da93c6..4c5f5a7a1a 100644
--- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
@@ -26,26 +26,26 @@ namespace osu.Game.Tournament.Tests.NonVisual
string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(CheckIPCLocation));
// Set up a fake IPC client for the IPC Storage to switch to.
- string testCeDir = Path.Combine(basePath, "stable-ce");
- Directory.CreateDirectory(testCeDir);
+ string testStableInstallDirectory = Path.Combine(basePath, "stable-ce");
+ Directory.CreateDirectory(testStableInstallDirectory);
- string ipcFile = Path.Combine(testCeDir, "ipc.txt");
+ string ipcFile = Path.Combine(testStableInstallDirectory, "ipc.txt");
File.WriteAllText(ipcFile, string.Empty);
try
{
var osu = loadOsu(host);
TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get();
- FileBasedIPC ipc = (FileBasedIPC)osu.Dependencies.Get();
+ FileBasedIPC ipc = null;
- waitForOrAssert(() => ipc != null, @"ipc could not be populated in a reasonable amount of time");
+ waitForOrAssert(() => (ipc = osu.Dependencies.Get() as FileBasedIPC) != null, @"ipc could not be populated in a reasonable amount of time");
- Assert.True(ipc.SetIPCLocation(testCeDir));
+ Assert.True(ipc.SetIPCLocation(testStableInstallDirectory));
Assert.True(storage.AllTournaments.Exists("stable.json"));
}
finally
{
- host.Storage.DeleteDirectory(testCeDir);
+ host.Storage.DeleteDirectory(testStableInstallDirectory);
host.Storage.DeleteDirectory("tournaments");
host.Exit();
}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index e1c7b67a8c..7eef0f7158 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -429,6 +429,9 @@ namespace osu.Game
public async Task Import(params string[] paths)
{
+ if (paths.Length == 0)
+ return;
+
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
foreach (var importer in fileImporters)
diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
new file mode 100644
index 0000000000..3e8da9f7d0
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
@@ -0,0 +1,109 @@
+// Copyright (c) ppy Pty Ltd . 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Overlays.Settings.Sections.Input
+{
+ internal class RotationPresetButtons : FillFlowContainer
+ {
+ private readonly ITabletHandler tabletHandler;
+
+ private Bindable rotation;
+
+ private const int height = 50;
+
+ public RotationPresetButtons(ITabletHandler tabletHandler)
+ {
+ this.tabletHandler = tabletHandler;
+
+ RelativeSizeAxes = Axes.X;
+ Height = height;
+
+ for (int i = 0; i < 360; i += 90)
+ {
+ var presetRotation = i;
+
+ Add(new RotationButton(i)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = height,
+ Width = 0.25f,
+ Text = $"{presetRotation}ยบ",
+ Action = () => tabletHandler.Rotation.Value = presetRotation,
+ });
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ rotation = tabletHandler.Rotation.GetBoundCopy();
+ rotation.BindValueChanged(val =>
+ {
+ foreach (var b in Children.OfType())
+ b.IsSelected = b.Preset == val.NewValue;
+ }, true);
+ }
+
+ public class RotationButton : TriangleButton
+ {
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public readonly int Preset;
+
+ public RotationButton(int preset)
+ {
+ Preset = preset;
+ }
+
+ private bool isSelected;
+
+ public bool IsSelected
+ {
+ get => isSelected;
+ set
+ {
+ if (value == isSelected)
+ return;
+
+ isSelected = value;
+
+ if (IsLoaded)
+ updateColour();
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateColour();
+ }
+
+ private void updateColour()
+ {
+ if (isSelected)
+ {
+ BackgroundColour = colours.BlueDark;
+ Triangles.ColourDark = colours.BlueDarker;
+ Triangles.ColourLight = colours.Blue;
+ }
+ else
+ {
+ BackgroundColour = colours.Gray4;
+ Triangles.ColourDark = colours.Gray5;
+ Triangles.ColourLight = colours.Gray6;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index ecb8acce54..f61742093c 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private readonly Bindable areaOffset = new Bindable();
private readonly Bindable areaSize = new Bindable();
+ private readonly BindableNumber rotation = new BindableNumber();
+
private readonly IBindable tablet = new Bindable();
private OsuSpriteText tabletName;
@@ -124,6 +126,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
}, true);
+ rotation.BindTo(handler.Rotation);
+ rotation.BindValueChanged(val =>
+ {
+ usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint)
+ .OnComplete(_ => checkBounds()); // required as we are using SSDQ.
+ });
+
tablet.BindTo(handler.Tablet);
tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails));
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index bd0f7ddc4c..d770c18878 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private readonly BindableNumber sizeX = new BindableNumber { MinValue = 10 };
private readonly BindableNumber sizeY = new BindableNumber { MinValue = 10 };
+ private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 };
+
[Resolved]
private GameHost host { get; set; }
@@ -110,12 +112,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
},
new SettingsSlider
- {
- TransferValueOnCommit = true,
- LabelText = "Aspect Ratio",
- Current = aspectRatio
- },
- new SettingsSlider
{
TransferValueOnCommit = true,
LabelText = "X Offset",
@@ -127,6 +123,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
LabelText = "Y Offset",
Current = offsetY
},
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Rotation",
+ Current = rotation
+ },
+ new RotationPresetButtons(tabletHandler),
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Aspect Ratio",
+ Current = aspectRatio
+ },
new SettingsCheckbox
{
LabelText = "Lock aspect ratio",
@@ -153,6 +162,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
base.LoadComplete();
+ rotation.BindTo(tabletHandler.Rotation);
+
areaOffset.BindTo(tabletHandler.AreaOffset);
areaOffset.BindValueChanged(val =>
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 79f457c050..5ab557804e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
+using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@@ -19,6 +20,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
@@ -70,6 +72,50 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.ControlPressed)
+ {
+ switch (e.Key)
+ {
+ case Key.Left:
+ moveSelection(new Vector2(-1, 0));
+ return true;
+
+ case Key.Right:
+ moveSelection(new Vector2(1, 0));
+ return true;
+
+ case Key.Up:
+ moveSelection(new Vector2(0, -1));
+ return true;
+
+ case Key.Down:
+ moveSelection(new Vector2(0, 1));
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
+ ///
+ ///
+ private void moveSelection(Vector2 delta)
+ {
+ var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault();
+
+ if (firstBlueprint == null)
+ return;
+
+ // convert to game space coordinates
+ delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
+
+ SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, firstBlueprint.ScreenSpaceSelectionPoint + delta));
+ }
+
private void updatePlacementNewCombo()
{
if (currentPlacement?.HitObject is IHasComboInformation c)
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 7d906cdc5b..679b3c7313 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -309,10 +309,8 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen())
return;
- var restartCount = player?.RestartCount + 1 ?? 0;
-
player = createPlayer();
- player.RestartCount = restartCount;
+ player.RestartCount = restartCount++;
player.RestartRequested = restartRequested;
LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
@@ -428,6 +426,8 @@ namespace osu.Game.Screens.Play
private Bindable muteWarningShownOnce;
+ private int restartCount;
+
private void showMuteWarningIfNeeded()
{
if (!muteWarningShownOnce.Value)
diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs
index 04a358436e..5ddcd86d28 100644
--- a/osu.Game/Users/UserStatistics.cs
+++ b/osu.Game/Users/UserStatistics.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Users
public double Accuracy;
[JsonIgnore]
- public string DisplayAccuracy => Accuracy.FormatAccuracy();
+ public string DisplayAccuracy => (Accuracy / 100).FormatAccuracy();
[JsonProperty(@"play_count")]
public int PlayCount;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 6d571218fc..6a7f7e7026 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index ceb46eae87..4aa3ad1c61 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+