diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 213c5082ab..5c11f91994 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -121,21 +121,12 @@ jobs:
build-only-ios:
name: Build only (iOS)
- # change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
- runs-on: macos-12
+ runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
- # see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
- # remove once all workflow VMs use Xcode 14.1
- - name: Set Xcode Version
- shell: bash
- run: |
- sudo xcode-select -s "/Applications/Xcode_14.1.app"
- echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
-
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index e0f7820262..8a0b8250d5 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
new CatchModHidden(),
new CatchModFlashlight(),
+ new ModAccuracyChallenge(),
};
case ModType.Conversion:
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 0cec0ee075..d324682989 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
new ManiaModFlashlight(),
+ new ModAccuracyChallenge(),
};
case ModType.Conversion:
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index ecd840dda6..68a44eb2f8 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
public Vector2 PathStartLocation => body.PathOffset;
+ ///
+ /// Offset in absolute (local) coordinates from the end of the curve.
+ ///
+ public Vector2 PathEndLocation => body.PathEndOffset;
+
public SliderBodyPiece()
{
InternalChild = body = new ManualSliderBody
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index a51c223785..b502839e22 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
+ protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
+ {
+ DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
+ };
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs
new file mode 100644
index 0000000000..5b79753632
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModAccuracyChallenge : ModAccuracyChallenge
+ {
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 79a566e33c..48056a49de 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
- new OsuModStrictTracking()
+ new OsuModStrictTracking(),
+ new OsuModAccuracyChallenge(),
};
case ModType.Conversion:
diff --git a/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs
index 283687adfd..e7885e65de 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs
@@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
///
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
+ ///
+ /// Offset in absolute coordinates from the end of the curve.
+ ///
+ public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
+
///
/// Used to colour the path.
///
diff --git a/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs
index f8ee465cd6..0b7acc1f47 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset;
+ public override Vector2 PathEndOffset => snakedPathEndOffset;
+
///
/// The top-left position of the path when fully snaked.
///
@@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
///
private Vector2 snakedPathOffset;
+ ///
+ /// The offset of the end of path from when fully snaked.
+ ///
+ private Vector2 snakedPathEndOffset;
+
private DrawableSlider drawableSlider = null!;
[BackgroundDependencyLoader]
@@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
+ snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
index c75e179443..ffb8c802c7 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
@@ -10,6 +10,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Game.Configuration;
+using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
@@ -38,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.UI
mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons);
}
+ // Required to handle touches outside of the playfield when screen scaling is enabled.
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
protected override void OnTouchMove(TouchMoveEvent e)
{
base.OnTouchMove(e);
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index 3b3b3e606c..2ccdfd40e5 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Diagnostics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
- public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield
+ public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset
{
- private DrawableTaikoRuleset? drawableTaikoRuleset;
-
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
+ var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
}
-
- public void Update(Playfield playfield)
- {
- Debug.Assert(drawableTaikoRuleset != null);
-
- // Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
- const float scroll_rate = 10;
-
- // Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
- float ratio = drawableTaikoRuleset.DrawHeight / 480;
-
- drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index fe12cf9765..1d0f772db0 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -144,6 +144,7 @@ namespace osu.Game.Rulesets.Taiko
new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
new TaikoModHidden(),
new TaikoModFlashlight(),
+ new ModAccuracyChallenge(),
};
case ModType.Conversion:
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 146daa8c27..40203440c5 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Left;
- TimeRange.Value = 7000;
}
[BackgroundDependencyLoader]
@@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
KeyBindingInputManager.Add(new DrumTouchInputArea());
}
+ protected override void Update()
+ {
+ base.Update();
+
+ // Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
+ const float scroll_rate = 10;
+
+ // Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
+ float ratio = DrawHeight / 480;
+
+ TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 3f89bf9e9c..5aa2dd2ebf 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
@@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
+ [Resolved]
+ private RealmAccess realm { get; set; } = null!;
+
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps()
@@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
&& set != null
- && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
+ && set.PerformRead(s =>
+ s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
});
}
@@ -327,6 +332,56 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2));
}
+ [Test]
+ public void TestCopyDifficultyDoesNotChangeCollections()
+ {
+ string originalDifficultyName = Guid.NewGuid().ToString();
+
+ AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName);
+ AddStep("save beatmap", () => Editor.Save());
+
+ string originalMd5 = string.Empty;
+ BeatmapCollection collection = null!;
+
+ AddStep("setup a collection with original beatmap", () =>
+ {
+ collection = new BeatmapCollection("test copy");
+ collection.BeatmapMD5Hashes.Add(originalMd5 = EditorBeatmap.BeatmapInfo.MD5Hash);
+
+ realm.Write(r =>
+ {
+ r.Add(collection);
+ });
+ });
+
+ AddAssert("collection contains original beatmap", () =>
+ !string.IsNullOrEmpty(originalMd5) && collection.BeatmapMD5Hashes.Contains(originalMd5));
+
+ AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
+
+ AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
+ AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
+
+ AddUntilStep("wait for created", () =>
+ {
+ string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ return difficultyName != null && difficultyName != originalDifficultyName;
+ });
+
+ AddStep("save without changes", () => Editor.Save());
+
+ AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
+ && collection.BeatmapMD5Hashes.Contains(originalMd5));
+
+ AddStep("clean up collection", () =>
+ {
+ realm.Write(r =>
+ {
+ r.Remove(collection);
+ });
+ });
+ }
+
[Test]
public void TestCreateMultipleNewDifficultiesSucceeds()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index b353123649..0aa0295f7d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -123,7 +123,7 @@ needs_cleanup: true
AddStep("Add absolute image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
- markdownContainer.Text = "";
+ markdownContainer.Text = "";
});
}
@@ -133,7 +133,7 @@ needs_cleanup: true
AddStep("Add relative image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
- markdownContainer.Text = "";
+ markdownContainer.Text = "";
});
}
@@ -145,7 +145,7 @@ needs_cleanup: true
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image
-
+
Line after image";
});
@@ -170,12 +170,12 @@ Line after image";
markdownContainer.Text = @"
| Image | Name | Effect |
| :-: | :-: | :-- |
-|  | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
-|  | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
-|  | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
-|   | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
-|  | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
-|  | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
+|  | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
+|  | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
+|  | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
+|   | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
+|  | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
+|  | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
";
});
}
@@ -186,7 +186,7 @@ Line after image";
AddStep("Add image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
- markdownContainer.Text = "";
+ markdownContainer.Text = "";
});
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType().First().DelayedLoadCompleted);
@@ -270,6 +270,30 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed
});
}
+ [Test]
+ public void TestCodeSyntax()
+ {
+ AddStep("set content", () =>
+ {
+ markdownContainer.Text = @"
+This is a paragraph containing `inline code` synatax.
+Oh wow I do love the `WikiMarkdownContainer`, it is very cool!
+
+This is a line before the fenced code block:
+```csharp
+public class WikiMarkdownContainer : MarkdownContainer
+{
+ public WikiMarkdownContainer()
+ {
+ this.foo = bar;
+ }
+}
+```
+This is a line after the fenced code block!
+";
+ });
+ }
+
private partial class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 34637501fa..ad56bbbc3a 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
- Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
+ save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
@@ -280,77 +280,16 @@ namespace osu.Game.Beatmaps
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
///
- /// Saves an file against a given .
+ /// Saves an existing file against a given .
///
+ ///
+ /// This method will also update any user beatmap collection hash references to the new post-saved hash.
+ ///
/// The to save the content against. The file referenced by will be replaced.
/// The content to write.
/// The beatmap content to write, null if to be omitted.
- public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
- {
- var setInfo = beatmapInfo.BeatmapSet;
- Debug.Assert(setInfo != null);
-
- // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
- // This should hopefully be temporary, assuming said clone is eventually removed.
-
- // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
- // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
- // CopyTo() will undo such adjustments, while CopyFrom() will not.
- beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
-
- // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
- beatmapContent.BeatmapInfo = beatmapInfo;
-
- using (var stream = new MemoryStream())
- {
- using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
-
- stream.Seek(0, SeekOrigin.Begin);
-
- // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
- var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
- string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
-
- // ensure that two difficulties from the set don't point at the same beatmap file.
- if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
- throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
-
- if (existingFileInfo != null)
- DeleteFile(setInfo, existingFileInfo);
-
- string oldMd5Hash = beatmapInfo.MD5Hash;
-
- beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
- beatmapInfo.Hash = stream.ComputeSHA2Hash();
-
- beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
- beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
-
- AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
-
- updateHashAndMarkDirty(setInfo);
-
- Realm.Write(r =>
- {
- var liveBeatmapSet = r.Find(setInfo.ID);
-
- setInfo.CopyChangesToRealm(liveBeatmapSet);
-
- beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
-
- ProcessBeatmap?.Invoke((liveBeatmapSet, false));
- });
- }
-
- Debug.Assert(beatmapInfo.BeatmapSet != null);
-
- static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
- {
- var metadata = beatmapInfo.Metadata;
- return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
- }
- }
+ public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
+ save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
public void DeleteAllVideos()
{
@@ -460,6 +399,74 @@ namespace osu.Game.Beatmaps
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
}
+ private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections)
+ {
+ var setInfo = beatmapInfo.BeatmapSet;
+ Debug.Assert(setInfo != null);
+
+ // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
+ // This should hopefully be temporary, assuming said clone is eventually removed.
+
+ // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
+ // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
+ // CopyTo() will undo such adjustments, while CopyFrom() will not.
+ beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
+
+ // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
+ beatmapContent.BeatmapInfo = beatmapInfo;
+
+ using (var stream = new MemoryStream())
+ {
+ using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
+ new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
+ var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
+ string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
+
+ // ensure that two difficulties from the set don't point at the same beatmap file.
+ if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
+ throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
+
+ if (existingFileInfo != null)
+ DeleteFile(setInfo, existingFileInfo);
+
+ string oldMd5Hash = beatmapInfo.MD5Hash;
+
+ beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
+ beatmapInfo.Hash = stream.ComputeSHA2Hash();
+
+ beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
+ beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
+
+ AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
+
+ updateHashAndMarkDirty(setInfo);
+
+ Realm.Write(r =>
+ {
+ var liveBeatmapSet = r.Find(setInfo.ID);
+
+ setInfo.CopyChangesToRealm(liveBeatmapSet);
+
+ if (transferCollections)
+ beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
+
+ ProcessBeatmap?.Invoke((liveBeatmapSet, false));
+ });
+ }
+
+ Debug.Assert(beatmapInfo.BeatmapSet != null);
+
+ static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
+ {
+ var metadata = beatmapInfo.Metadata;
+ return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
+ }
+ }
+
#region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index a38aa19cef..cf58d07b9e 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -556,8 +556,8 @@ namespace osu.Game
case JoystickHandler jh:
return new JoystickSettings(jh);
- case TouchHandler:
- return new InputSection.HandlerSection(handler);
+ case TouchHandler th:
+ return new TouchSettings(th);
}
}
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 671d649dcf..4cc38c41e4 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -5,12 +5,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests;
@@ -22,8 +24,6 @@ namespace osu.Game.Overlays
{
public partial class ChangelogOverlay : OnlineOverlay
{
- public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
-
public readonly Bindable Current = new Bindable();
private List builds;
@@ -81,6 +81,8 @@ namespace osu.Game.Overlays
ArgumentNullException.ThrowIfNull(updateStream);
ArgumentNullException.ThrowIfNull(version);
+ Show();
+
performAfterFetch(() =>
{
var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream)
@@ -89,8 +91,6 @@ namespace osu.Game.Overlays
if (build != null)
ShowBuild(build);
});
-
- Show();
}
public override bool OnPressed(KeyBindingPressEvent e)
@@ -127,11 +127,16 @@ namespace osu.Game.Overlays
private Task initialFetchTask;
- private void performAfterFetch(Action action) => Schedule(() =>
+ private void performAfterFetch(Action action)
{
- fetchListing()?.ContinueWith(_ =>
- Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
- });
+ Debug.Assert(State.Value == Visibility.Visible);
+
+ Schedule(() =>
+ {
+ fetchListing()?.ContinueWith(_ =>
+ Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
+ });
+ }
private Task fetchListing()
{
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
index 4d027cf8cb..9291dfe923 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
+ Keywords = new[] { @"touchscreen" },
Current = config.GetBindable(OsuSetting.GameplayCursorDuringTouch)
},
};
diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
index 55be06c765..2f68b3a82f 100644
--- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
@@ -70,6 +70,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(new SettingsButton
{
Text = GeneralSettingsStrings.OpenOsuFolder,
+ Keywords = new[] { @"logs", @"files", @"access", "directory" },
Action = () => storage.PresentExternally(),
});
diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
index d4fd78f0c8..c5274d6223 100644
--- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
@@ -34,6 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections
new SettingsButton
{
Text = GeneralSettingsStrings.RunSetupWizard,
+ Keywords = new[] { @"first run", @"initial", @"getting started" },
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
Action = () => firstRunSetupOverlay?.Show(),
},
diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
index dbd7949206..2b478f6af3 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings;
- public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" });
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" });
public BindingSettings(KeyBindingPanel keyConfig)
{
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs
new file mode 100644
index 0000000000..8d1b12d5b2
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Handlers.Touch;
+using osu.Framework.Localisation;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Input
+{
+ public partial class TouchSettings : SettingsSubsection
+ {
+ private readonly TouchHandler handler;
+
+ public TouchSettings(TouchHandler handler)
+ {
+ this.handler = handler;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = CommonStrings.Enabled,
+ Current = handler.Enabled
+ },
+ };
+ }
+
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" });
+
+ protected override LocalisableString Header => handler.Description;
+ }
+}
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 5091ddc2d0..a837444758 100644
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd . 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; }
+ public IEnumerable Keywords { get; set; } = Array.Empty();
+
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable IConditionalFilterable.CanBeShown => CanBeShown;
@@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings
get
{
if (TooltipText != default)
- return base.FilterTerms.Append(TooltipText);
+ yield return TooltipText;
- return base.FilterTerms;
+ foreach (string s in Keywords)
+ yield return s;
+
+ foreach (LocalisableString s in base.FilterTerms)
+ yield return s;
}
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs
index 62933ed556..9ab0fa7ad8 100644
--- a/osu.Game/Overlays/Settings/SettingsFooter.cs
+++ b/osu.Game/Overlays/Settings/SettingsFooter.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings
Text = game.Name,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
},
- new BuildDisplay(game.Version, DebugUtils.IsDebugBuild)
+ new BuildDisplay(game.Version)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings
private partial class BuildDisplay : OsuAnimatedButton
{
private readonly string version;
- private readonly bool isDebug;
[Resolved]
private OsuColour colours { get; set; }
- public BuildDisplay(string version, bool isDebug)
+ public BuildDisplay(string version)
{
this.version = version;
- this.isDebug = isDebug;
Content.RelativeSizeAxes = Axes.Y;
Content.AutoSizeAxes = AutoSizeAxes = Axes.X;
@@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader(true)]
private void load(ChangelogOverlay changelog)
{
- if (!isDebug)
- Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
+ Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
Add(new OsuSpriteText
{
@@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5),
- Colour = isDebug ? colours.Red : Color4.White,
+ Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
});
}
}
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 4e0e45e0f5..3c878ffd33 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using System.Linq;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty