diff --git a/osu.Android.props b/osu.Android.props
index 8214fa2f2c..69f897128c 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 84f215f930..19ed7ffcf5 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -18,7 +18,8 @@ namespace osu.Android
try
{
- string versionName = packageInfo.VersionCode.ToString();
+ // todo: needs checking before play store redeploy.
+ string versionName = packageInfo.VersionName;
// undo play store version garbling
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index f05ee48914..9351e17419 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -6,15 +6,14 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.Win32;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
using osuTK.Input;
-using Microsoft.Win32;
using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
-using osu.Framework.Platform.Windows;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
@@ -37,7 +36,11 @@ namespace osu.Desktop
try
{
if (Host is DesktopGameHost desktopHost)
- return new StableStorage(desktopHost);
+ {
+ string stablePath = getStableInstallPath();
+ if (!string.IsNullOrEmpty(stablePath))
+ return new DesktopStorage(stablePath, desktopHost);
+ }
}
catch (Exception)
{
@@ -47,6 +50,35 @@ namespace osu.Desktop
return null;
}
+ private string getStableInstallPath()
+ {
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+
+ string stableInstallPath;
+
+ try
+ {
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ return null;
+ }
+
protected override UpdateManager CreateUpdateManager()
{
switch (RuntimeInfo.OS)
@@ -111,45 +143,5 @@ namespace osu.Desktop
Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
}
-
- ///
- /// A method of accessing an osu-stable install in a controlled fashion.
- ///
- private class StableStorage : WindowsStorage
- {
- protected override string LocateBasePath()
- {
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
-
- string stableInstallPath;
-
- try
- {
- using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
-
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- return null;
- }
-
- public StableStorage(DesktopGameHost host)
- : base(string.Empty, host)
- {
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
index 286e3f6e50..48159c817d 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
@@ -10,10 +10,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@@ -48,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.Tests
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
+ setScrollStep(ScrollingDirection.Up);
+
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
@@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
- AddStep("set down scroll", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = ScrollingDirection.Down);
+ setScrollStep(ScrollingDirection.Down);
AddStep("seek to last object", () =>
{
@@ -116,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Tests
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
+ setScrollStep(ScrollingDirection.Down);
+
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
@@ -147,6 +154,46 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT);
}
+ [Test]
+ public void TestDragHoldNoteSelectionVertically()
+ {
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("setup beatmap", () =>
+ {
+ composer.EditorBeatmap.Clear();
+ composer.EditorBeatmap.Add(new HoldNote
+ {
+ Column = 1,
+ EndTime = 200
+ });
+ });
+
+ DrawableHoldNote holdNote = null;
+
+ AddStep("grab hold note", () =>
+ {
+ holdNote = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(holdNote);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move drag upwards", () =>
+ {
+ InputManager.MoveMouseTo(holdNote, new Vector2(0, -100));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
+
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ }
+
+ private void setScrollStep(ScrollingDirection direction)
+ => AddStep($"set scroll direction = {direction}", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = direction);
+
private class TestComposer : CompositeDrawable
{
[Cached(typeof(EditorBeatmap))]
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index b3dd392202..c63e30e98a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -7,6 +7,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@@ -49,6 +50,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override void OnMouseUp(MouseUpEvent e)
{
+ if (e.Button != MouseButton.Left)
+ return;
+
base.OnMouseUp(e);
EndPlacement(true);
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 400abb6380..3fb03d642f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@@ -46,6 +47,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
+ if (e.Button != MouseButton.Left)
+ return false;
+
if (Column == null)
return base.OnMouseDown(e);
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index 2b7b383dbe..a4c0791253 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@@ -30,6 +31,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
+ if (e.Button != MouseButton.Left)
+ return false;
+
base.OnMouseDown(e);
// Place the note immediately.
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index a6c3be7e5a..c3b4d2625e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
- DefaultsApplied += () =>
+ DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index cf6677a55d..e0577dd464 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -28,8 +28,11 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
- foreach (var point in slider.Path.ControlPoints)
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
+ foreach (var point in controlPoints)
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
+
+ slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 6f09bbcd57..8a0ef22c4a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void bindEvents(DrawableOsuHitObject drawableObject)
{
drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
- drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
+ drawableObject.HitObject.DefaultsApplied += _ => scheduleRefresh();
}
private void scheduleRefresh()
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider-fail@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider-fail@2x.png
new file mode 100644
index 0000000000..ac0fef8626
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider-fail@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider@2x.png
new file mode 100644
index 0000000000..cca9310322
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png
new file mode 100644
index 0000000000..2d9974a701
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png
new file mode 100644
index 0000000000..07b2f167e0
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider-fail.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider-fail.png
new file mode 100644
index 0000000000..78c6ef6e21
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider-fail.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider.png
new file mode 100644
index 0000000000..b824e4585b
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index ae5dd1e622..e02ad53ed8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
typeof(TaikoHitTarget),
typeof(TaikoLegacyHitTarget),
typeof(PlayfieldBackgroundRight),
+ typeof(LegacyTaikoScroller),
}).ToList();
[Cached(typeof(IScrollingInfo))]
@@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
+ Height = 0.6f,
}));
AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
new file mode 100644
index 0000000000..e26f410b71
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ public class TestSceneTaikoScroller : TaikoSkinnableTestScene
+ {
+ public TestSceneTaikoScroller()
+ {
+ AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty())));
+ AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value =
+ new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index caf645d5a2..d324441285 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -167,13 +167,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
default:
{
- bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
+ bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE;
+
+ bool isRim = samples.Any(isRimDefinition);
yield return new Hit
{
StartTime = obj.StartTime,
Type = isRim ? HitType.Rim : HitType.Centre,
- Samples = obj.Samples,
+ Samples = samples,
IsStrong = strong
};
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index d2671eadda..d332f90cd4 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
+using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
@@ -47,6 +49,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
: new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
+ protected override IEnumerable GetSamples()
+ {
+ // normal and claps are always handled by the drum (see DrumSampleMapping).
+ var samples = HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
+
+ if (HitObject.Type == HitType.Rim && HitObject.IsStrong)
+ {
+ // strong + rim always maps to whistle.
+ // TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken.
+ // when we add a taiko editor, this is probably not going to play nice.
+
+ var corrected = samples.ToList();
+
+ for (var i = 0; i < corrected.Count; i++)
+ {
+ var s = corrected[i];
+
+ if (s.Name != HitSampleInfo.HIT_FINISH)
+ continue;
+
+ var sClone = s.Clone();
+ sClone.Name = HitSampleInfo.HIT_WHISTLE;
+ corrected[i] = sClone;
+ }
+
+ return corrected;
+ }
+
+ return samples;
+ }
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 1be04f1760..90daf3950c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
- // Normal and clap samples are handled by the drum
- protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
+ // Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
+ protected override IEnumerable GetSamples() => Enumerable.Empty();
protected abstract SkinnableDrawable CreateMainPiece();
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
new file mode 100644
index 0000000000..1ecdb839fb
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
@@ -0,0 +1,149 @@
+// 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.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyTaikoScroller : CompositeDrawable
+ {
+ public LegacyTaikoScroller()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(GameplayBeatmap gameplayBeatmap)
+ {
+ if (gameplayBeatmap != null)
+ ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
+ }
+
+ private bool passing;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ LastResult.BindValueChanged(result =>
+ {
+ var r = result.NewValue;
+
+ // always ignore hitobjects that don't affect combo (drumroll ticks etc.)
+ if (r?.Judgement.AffectsCombo == false)
+ return;
+
+ passing = r == null || r.Type > HitResult.Miss;
+
+ foreach (var sprite in InternalChildren.OfType())
+ sprite.Passing = passing;
+ }, true);
+ }
+
+ public Bindable LastResult = new Bindable();
+
+ protected override void Update()
+ {
+ base.Update();
+
+ while (true)
+ {
+ float? additiveX = null;
+
+ foreach (var sprite in InternalChildren)
+ {
+ // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
+ sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f;
+
+ additiveX += sprite.DrawWidth - 1;
+
+ if (sprite.X + sprite.DrawWidth < 0)
+ sprite.Expire();
+ }
+
+ var last = InternalChildren.LastOrDefault();
+
+ // only break from this loop once we have saturated horizontal space completely.
+ if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X)
+ break;
+
+ AddInternal(new ScrollerSprite
+ {
+ Passing = passing
+ });
+ }
+ }
+
+ private class ScrollerSprite : CompositeDrawable
+ {
+ private Sprite passingSprite;
+ private Sprite failingSprite;
+
+ private bool passing = true;
+
+ public bool Passing
+ {
+ get => passing;
+ set
+ {
+ if (value == passing)
+ return;
+
+ passing = value;
+
+ if (IsLoaded)
+ updatePassing();
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ AutoSizeAxes = Axes.X;
+ RelativeSizeAxes = Axes.Y;
+
+ FillMode = FillMode.Fit;
+
+ InternalChildren = new Drawable[]
+ {
+ passingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider") },
+ failingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider-fail"), Alpha = 0 },
+ };
+
+ updatePassing();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ foreach (var c in InternalChildren)
+ c.Scale = new Vector2(DrawHeight / c.Height);
+ }
+
+ private void updatePassing()
+ {
+ if (passing)
+ {
+ passingSprite.Show();
+ failingSprite.FadeOut(200);
+ }
+ else
+ {
+ failingSprite.FadeIn(200);
+ passingSprite.Delay(200).FadeOut();
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 5dfc7ec0df..0212cdfd9e 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -85,6 +85,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return new LegacyHitExplosion(sprite);
return null;
+
+ case TaikoSkinComponents.TaikoScroller:
+ if (GetTexture("taiko-slider") != null)
+ return new LegacyTaikoScroller();
+
+ return null;
}
return source.GetDrawableComponent(component);
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index fd091f97d0..877351534a 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -18,5 +18,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionMiss,
TaikoExplosionGood,
TaikoExplosionGreat,
+ TaikoScroller
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index a6a00fe242..c0a6c4582c 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
@@ -16,11 +17,15 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public class DrawableTaikoRuleset : DrawableScrollingRuleset
{
+ private SkinnableDrawable scroller;
+
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false;
@@ -36,6 +41,20 @@ namespace osu.Game.Rulesets.Taiko.UI
private void load()
{
new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
+
+ AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty())
+ {
+ RelativeSizeAxes = Axes.X,
+ Depth = float.MaxValue
+ });
+ }
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
+ scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
}
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index bcc873b0b7..30331e98d2 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text;
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
@@ -28,14 +29,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu"));
[TestCaseSource(nameof(allBeatmaps))]
- public void TestBeatmap(string name)
+ public void TestEncodeDecodeStability(string name)
{
- var decoded = decode(name, out var encoded);
+ var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name));
+ var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded));
sort(decoded);
- sort(encoded);
+ sort(decodedAfterEncode);
- Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize()));
+ Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize()));
}
private void sort(IBeatmap beatmap)
@@ -48,27 +50,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private IBeatmap decode(string filename, out IBeatmap encoded)
+ private IBeatmap decodeFromLegacy(Stream stream)
{
- using (var stream = TestResources.GetStore().GetStream(filename))
- using (var sr = new LineBufferedReader(stream))
- {
- var legacyDecoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr));
+ using (var reader = new LineBufferedReader(stream))
+ return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader));
+ }
- using (var ms = new MemoryStream())
- using (var sw = new StreamWriter(ms))
- using (var sr2 = new LineBufferedReader(ms, true))
- {
- new LegacyBeatmapEncoder(legacyDecoded).Encode(sw);
+ private Stream encodeToLegacy(IBeatmap beatmap)
+ {
+ var stream = new MemoryStream();
- sw.Flush();
- ms.Position = 0;
+ using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
+ new LegacyBeatmapEncoder(beatmap).Encode(writer);
- encoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2));
+ stream.Position = 0;
- return legacyDecoded;
- }
- }
+ return stream;
}
private IBeatmap convert(IBeatmap beatmap)
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
new file mode 100644
index 0000000000..7c559ea6d2
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -0,0 +1,129 @@
+// 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.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Platform;
+using osu.Game.Configuration;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class CustomDataDirectoryTest
+ {
+ [Test]
+ public void TestDefaultDirectory()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+ var storage = osu.Dependencies.Get();
+
+ string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestDefaultDirectory));
+
+ Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ private string customPath => Path.Combine(Environment.CurrentDirectory, "custom-path");
+
+ [Test]
+ public void TestCustomDirectory()
+ {
+ using (var host = new HeadlessGameHost(nameof(TestCustomDirectory)))
+ {
+ string headlessPrefix = Path.Combine("headless", nameof(TestCustomDirectory));
+
+ // need access before the game has constructed its own storage yet.
+ Storage storage = new DesktopStorage(headlessPrefix, host);
+ // manual cleaning so we can prepare a config file.
+ storage.DeleteDirectory(string.Empty);
+
+ using (var storageConfig = new StorageConfigManager(storage))
+ storageConfig.Set(StorageConfig.FullPath, customPath);
+
+ try
+ {
+ var osu = loadOsu(host);
+
+ // switch to DI'd storage
+ storage = osu.Dependencies.Get();
+
+ Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestSubDirectoryLookup()
+ {
+ using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup)))
+ {
+ string headlessPrefix = Path.Combine("headless", nameof(TestSubDirectoryLookup));
+
+ // need access before the game has constructed its own storage yet.
+ Storage storage = new DesktopStorage(headlessPrefix, host);
+ // manual cleaning so we can prepare a config file.
+ storage.DeleteDirectory(string.Empty);
+
+ using (var storageConfig = new StorageConfigManager(storage))
+ storageConfig.Set(StorageConfig.FullPath, customPath);
+
+ try
+ {
+ var osu = loadOsu(host);
+
+ // switch to DI'd storage
+ storage = osu.Dependencies.Get();
+
+ string actualTestFile = Path.Combine(customPath, "rulesets", "test");
+
+ File.WriteAllText(actualTestFile, "test");
+
+ var rulesetStorage = storage.GetStorageForDirectory("rulesets");
+ var lookupPath = rulesetStorage.GetFiles(".").Single();
+
+ Assert.That(lookupPath, Is.EqualTo("test"));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ private OsuGameBase loadOsu(GameHost host)
+ {
+ var osu = new OsuGameBase();
+ Task.Run(() => host.Run(osu));
+ waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
+ return osu;
+ }
+
+ private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000)
+ {
+ Task task = Task.Run(() =>
+ {
+ while (!result()) Thread.Sleep(200);
+ });
+
+ Assert.IsTrue(task.Wait(timeout), failureMessage);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 685decf097..8deed75a56 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -152,11 +152,12 @@ namespace osu.Game.Tests.Skins
}
[Test]
- public void TestSetBeatmapVersionNoFallback()
+ public void TestSetBeatmapVersionFallsBackToUserSkin()
{
+ // completely ignoring beatmap versions for simplicity.
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m);
- AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m);
+ AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m);
}
[Test]
@@ -172,7 +173,6 @@ namespace osu.Game.Tests.Skins
public void TestIniWithNoVersionFallsBackTo1()
{
AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream())));
- AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m);
}
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index eefa9fcfe6..53ba597a7e 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -8,7 +8,6 @@ using Microsoft.Win32;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Platform;
-using osu.Framework.Platform.Windows;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
@@ -52,7 +51,12 @@ namespace osu.Game.Tournament.IPC
try
{
- Storage = new StableStorage(host as DesktopGameHost);
+ var path = findStablePath();
+
+ if (string.IsNullOrEmpty(path))
+ return null;
+
+ Storage = new DesktopStorage(path, host as DesktopGameHost);
const string file_ipc_filename = "ipc.txt";
const string file_ipc_state_filename = "ipc-state.txt";
@@ -145,64 +149,50 @@ namespace osu.Game.Tournament.IPC
return Storage;
}
- ///
- /// A method of accessing an osu-stable install in a controlled fashion.
- ///
- private class StableStorage : WindowsStorage
+ private string findStablePath()
{
- protected override string LocateBasePath()
- {
- static bool checkExists(string p)
- {
- return File.Exists(Path.Combine(p, "ipc.txt"));
- }
+ static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt"));
- string stableInstallPath = string.Empty;
+ string stableInstallPath = string.Empty;
+
+ try
+ {
+ try
+ {
+ stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
try
{
- try
- {
- stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- try
- {
- using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
-
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
if (checkExists(stableInstallPath))
return stableInstallPath;
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- return null;
}
- finally
+ catch
{
- Logger.Log($"Stable path for tourney usage: {stableInstallPath}");
}
- }
- public StableStorage(DesktopGameHost host)
- : base(string.Empty, host)
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ return null;
+ }
+ finally
{
+ Logger.Log($"Stable path for tourney usage: {stableInstallPath}");
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 90c100db05..3860f12baa 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -149,6 +149,11 @@ namespace osu.Game.Beatmaps
}
}
+ public string[] SearchableTerms => new[]
+ {
+ Version
+ }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
+
public override string ToString()
{
string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index d2804bdc05..bf2b9944a4 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -84,7 +85,7 @@ namespace osu.Game.Beatmaps
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null)
{
- using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)))
+ using (var cancellationSource = createCancellationTokenSource(timeout))
{
mods ??= Array.Empty();
@@ -181,6 +182,15 @@ namespace osu.Game.Beatmaps
beatmapLoadTask = null;
}
+ private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
+ {
+ if (Debugger.IsAttached)
+ // ignore timeout when debugger is attached (may be breakpointing / debugging).
+ return new CancellationTokenSource();
+
+ return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10));
+ }
+
private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
{
// Todo: Handle cancellation during beatmap parsing
diff --git a/osu.Game/Configuration/StorageConfigManager.cs b/osu.Game/Configuration/StorageConfigManager.cs
new file mode 100644
index 0000000000..929f8f22ad
--- /dev/null
+++ b/osu.Game/Configuration/StorageConfigManager.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Configuration;
+using osu.Framework.Platform;
+
+namespace osu.Game.Configuration
+{
+ public class StorageConfigManager : IniConfigManager
+ {
+ protected override string Filename => "storage.ini";
+
+ public StorageConfigManager(Storage storage)
+ : base(storage)
+ {
+ }
+
+ protected override void InitialiseDefaults()
+ {
+ base.InitialiseDefaults();
+
+ Set(StorageConfig.FullPath, string.Empty);
+ }
+ }
+
+ public enum StorageConfig
+ {
+ FullPath,
+ }
+}
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
new file mode 100644
index 0000000000..ee42c491d1
--- /dev/null
+++ b/osu.Game/IO/OsuStorage.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Game.Configuration;
+
+namespace osu.Game.IO
+{
+ public class OsuStorage : WrappedStorage
+ {
+ public OsuStorage(GameHost host)
+ : base(host.Storage, string.Empty)
+ {
+ var storageConfig = new StorageConfigManager(host.Storage);
+
+ var customStoragePath = storageConfig.Get(StorageConfig.FullPath);
+
+ if (!string.IsNullOrEmpty(customStoragePath))
+ {
+ ChangeTargetStorage(host.GetStorage(customStoragePath));
+ Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
+ }
+ }
+ }
+}
diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
index 64f1ebeb1a..f98fa05821 100644
--- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
+++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
@@ -9,12 +9,12 @@ using Newtonsoft.Json.Linq;
namespace osu.Game.IO.Serialization.Converters
{
///
- /// A type of that serializes a alongside
+ /// A type of that serializes an alongside
/// a lookup table for the types contained. The lookup table is used in deserialization to
/// reconstruct the objects with their original types.
///
- /// The type of objects contained in the this attribute is attached to.
- public class TypedListConverter : JsonConverter
+ /// The type of objects contained in the this attribute is attached to.
+ public class TypedListConverter : JsonConverter>
{
private readonly bool requiresTypeVersion;
@@ -36,9 +36,7 @@ namespace osu.Game.IO.Serialization.Converters
this.requiresTypeVersion = requiresTypeVersion;
}
- public override bool CanConvert(Type objectType) => objectType == typeof(List);
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override IReadOnlyList ReadJson(JsonReader reader, Type objectType, IReadOnlyList existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var list = new List();
@@ -59,14 +57,12 @@ namespace osu.Game.IO.Serialization.Converters
return list;
}
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, IReadOnlyList value, JsonSerializer serializer)
{
- var list = (IEnumerable)value;
-
var lookupTable = new List();
var objects = new List();
- foreach (var item in list)
+ foreach (var item in value)
{
var type = item.GetType();
var assemblyName = type.Assembly.GetName();
diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
index bf5edeef94..46447b607b 100644
--- a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
+++ b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
@@ -11,26 +11,22 @@ namespace osu.Game.IO.Serialization.Converters
///
/// A type of that serializes only the X and Y coordinates of a .
///
- public class Vector2Converter : JsonConverter
+ public class Vector2Converter : JsonConverter
{
- public override bool CanConvert(Type objectType) => objectType == typeof(Vector2);
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
return new Vector2((float)obj["x"], (float)obj["y"]);
}
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
{
- var vector = (Vector2)value;
-
writer.WriteStartObject();
writer.WritePropertyName("x");
- writer.WriteValue(vector.X);
+ writer.WriteValue(value.X);
writer.WritePropertyName("y");
- writer.WriteValue(vector.Y);
+ writer.WriteValue(value.Y);
writer.WriteEndObject();
}
diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs
new file mode 100644
index 0000000000..cc59e2cc28
--- /dev/null
+++ b/osu.Game/IO/WrappedStorage.cs
@@ -0,0 +1,88 @@
+// 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.IO;
+using osu.Framework.Platform;
+
+namespace osu.Game.IO
+{
+ ///
+ /// A storage which wraps another storage and delegates implementation, potentially mutating the lookup path.
+ ///
+ public class WrappedStorage : Storage
+ {
+ protected Storage UnderlyingStorage { get; private set; }
+
+ private readonly string subPath;
+
+ public WrappedStorage(Storage underlyingStorage, string subPath = null)
+ : base(string.Empty)
+ {
+ ChangeTargetStorage(underlyingStorage);
+
+ this.subPath = subPath;
+ }
+
+ protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path;
+
+ protected void ChangeTargetStorage(Storage newStorage)
+ {
+ UnderlyingStorage = newStorage;
+ }
+
+ public override string GetFullPath(string path, bool createIfNotExisting = false) =>
+ UnderlyingStorage.GetFullPath(MutatePath(path), createIfNotExisting);
+
+ public override bool Exists(string path) =>
+ UnderlyingStorage.Exists(MutatePath(path));
+
+ public override bool ExistsDirectory(string path) =>
+ UnderlyingStorage.ExistsDirectory(MutatePath(path));
+
+ public override void DeleteDirectory(string path) =>
+ UnderlyingStorage.DeleteDirectory(MutatePath(path));
+
+ public override void Delete(string path) =>
+ UnderlyingStorage.Delete(MutatePath(path));
+
+ public override IEnumerable GetDirectories(string path) =>
+ ToLocalRelative(UnderlyingStorage.GetDirectories(MutatePath(path)));
+
+ public IEnumerable ToLocalRelative(IEnumerable paths)
+ {
+ string localRoot = GetFullPath(string.Empty);
+
+ foreach (var path in paths)
+ yield return Path.GetRelativePath(localRoot, UnderlyingStorage.GetFullPath(path));
+ }
+
+ public override IEnumerable GetFiles(string path, string pattern = "*") =>
+ ToLocalRelative(UnderlyingStorage.GetFiles(MutatePath(path), pattern));
+
+ public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
+ UnderlyingStorage.GetStream(MutatePath(path), access, mode);
+
+ public override string GetDatabaseConnectionString(string name) =>
+ UnderlyingStorage.GetDatabaseConnectionString(MutatePath(name));
+
+ public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name));
+
+ public override void OpenInNativeExplorer() => UnderlyingStorage.OpenInNativeExplorer();
+
+ public override Storage GetStorageForDirectory(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ throw new ArgumentException("Must be non-null and not empty string", nameof(path));
+
+ if (!path.EndsWith(Path.DirectorySeparatorChar))
+ path += Path.DirectorySeparatorChar;
+
+ // create non-existing path.
+ GetFullPath(path, true);
+
+ return new WrappedStorage(this, path);
+ }
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 294180cb30..6c9daec9e2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
@@ -90,13 +91,14 @@ namespace osu.Game
protected BackButton BackButton;
- protected SettingsPanel Settings;
+ protected SettingsOverlay Settings;
private VolumeOverlay volume;
private OsuLogo osuLogo;
private MainMenu menuScreen;
+ [CanBeNull]
private IntroScreen introScreen;
private Bindable configRuleset;
@@ -767,13 +769,20 @@ namespace osu.Game
private Task asyncLoadStream;
- private T loadComponentSingleFile(T d, Action add, bool cache = false)
+ ///
+ /// Queues loading the provided component in sequential fashion.
+ /// This operation is limited to a single thread to avoid saturating all cores.
+ ///
+ /// The component to load.
+ /// An action to invoke on load completion (generally to add the component to the hierarchy).
+ /// Whether to cache the component as type into the game dependencies before any scheduling.
+ private T loadComponentSingleFile(T component, Action loadCompleteAction, bool cache = false)
where T : Drawable
{
if (cache)
- dependencies.Cache(d);
+ dependencies.CacheAs(component);
- if (d is OverlayContainer overlay)
+ if (component is OverlayContainer overlay)
overlays.Add(overlay);
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
@@ -791,12 +800,12 @@ namespace osu.Game
try
{
- Logger.Log($"Loading {d}...", level: LogLevel.Debug);
+ Logger.Log($"Loading {component}...", level: LogLevel.Debug);
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
Task task = null;
- var del = new ScheduledDelegate(() => task = LoadComponentAsync(d, add));
+ var del = new ScheduledDelegate(() => task = LoadComponentAsync(component, loadCompleteAction));
Scheduler.Add(del);
// The delegate won't complete if OsuGame has been disposed in the meantime
@@ -811,7 +820,7 @@ namespace osu.Game
await task;
- Logger.Log($"Loaded {d}!", level: LogLevel.Debug);
+ Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
}
catch (OperationCanceledException)
{
@@ -819,7 +828,7 @@ namespace osu.Game
});
});
- return d;
+ return component;
}
protected override bool OnScroll(ScrollEvent e)
@@ -916,7 +925,7 @@ namespace osu.Game
if (ScreenStack.CurrentScreen is Loader)
return false;
- if (introScreen.DidLoadMenu && !(ScreenStack.CurrentScreen is IntroScreen))
+ if (introScreen?.DidLoadMenu == true && !(ScreenStack.CurrentScreen is IntroScreen))
{
Scheduler.Add(introScreen.MakeCurrent);
return true;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 609b6ce98e..cf39c03f9d 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -132,6 +132,8 @@ namespace osu.Game
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
+ dependencies.CacheAs(Storage);
+
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")));
largeStore.AddStore(Host.CreateTextureLoaderStore(new OnlineStore()));
dependencies.Cache(largeStore);
@@ -300,8 +302,8 @@ namespace osu.Game
{
base.SetHost(host);
- if (Storage == null)
- Storage = host.Storage;
+ if (Storage == null) // may be non-null for certain tests
+ Storage = new OsuStorage(host);
if (LocalConfig == null)
LocalConfig = new OsuConfigManager(Storage);
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
index 589f2d5072..67782dfe3f 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
@@ -50,13 +50,6 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{
- if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
- {
- button.Enabled.Value = false;
- button.TooltipText = "this beatmap is currently not available for download.";
- return;
- }
-
noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo);
button.Action = () =>
@@ -81,6 +74,26 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break;
}
};
+
+ State.BindValueChanged(state =>
+ {
+ switch (state.NewValue)
+ {
+ case DownloadState.LocallyAvailable:
+ button.Enabled.Value = true;
+ button.TooltipText = string.Empty;
+ break;
+
+ default:
+ if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
+ {
+ button.Enabled.Value = false;
+ button.TooltipText = "this beatmap is currently not available for download.";
+ }
+
+ break;
+ }
+ }, true);
}
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs
index 1ff08aab2c..06e31277dd 100644
--- a/osu.Game/Overlays/BeatmapSet/Header.cs
+++ b/osu.Game/Overlays/BeatmapSet/Header.cs
@@ -264,7 +264,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
if (BeatmapSet.Value == null) return;
- if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false)
+ if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable)
{
downloadButtonsContainer.Clear();
return;
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index 58ca2143f9..01d5991d3e 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -250,6 +250,28 @@ namespace osu.Game.Overlays.KeyBinding
finalise();
}
+ protected override bool OnMidiDown(MidiDownEvent e)
+ {
+ if (!HasFocus)
+ return false;
+
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ finalise();
+
+ return true;
+ }
+
+ protected override void OnMidiUp(MidiUpEvent e)
+ {
+ if (!HasFocus)
+ {
+ base.OnMidiUp(e);
+ return;
+ }
+
+ finalise();
+ }
+
private void clear()
{
bindTarget.UpdateKeyCombination(InputKey.None);
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 0047142cbd..ba6571fe1a 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Cached(typeof(DrawableHitObject))]
public abstract class DrawableHitObject : SkinReloadableDrawable
{
+ public event Action DefaultsApplied;
+
public readonly HitObject HitObject;
///
@@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
samplesBindable.CollectionChanged += (_, __) => loadSamples();
updateState(ArmedState.Idle, true);
- onDefaultsApplied();
+ apply(HitObject);
}
private void loadSamples()
@@ -175,7 +177,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
AddInternal(Samples);
}
- private void onDefaultsApplied() => apply(HitObject);
+ private void onDefaultsApplied(HitObject hitObject)
+ {
+ apply(hitObject);
+ DefaultsApplied?.Invoke(this);
+ }
private void apply(HitObject hitObject)
{
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 9a8efdde84..cffbdbae08 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects
///
/// Invoked after has completed on this .
///
- public event Action DefaultsApplied;
+ public event Action DefaultsApplied;
public readonly Bindable StartTimeBindable = new BindableDouble();
@@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty);
- DefaultsApplied?.Invoke();
+ DefaultsApplied?.Invoke(this);
}
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs
index afd499cb9e..2e32b96084 100644
--- a/osu.Game/Rulesets/RulesetInfo.cs
+++ b/osu.Game/Rulesets/RulesetInfo.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Newtonsoft.Json;
namespace osu.Game.Rulesets
@@ -15,7 +16,20 @@ namespace osu.Game.Rulesets
public string ShortName { get; set; }
- public string InstantiationInfo { get; set; }
+ private string instantiationInfo;
+
+ public string InstantiationInfo
+ {
+ get => instantiationInfo;
+ set => instantiationInfo = abbreviateInstantiationInfo(value);
+ }
+
+ private string abbreviateInstantiationInfo(string value)
+ {
+ // exclude version onwards, matching only on namespace and type.
+ // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old.
+ return string.Join(',', value.Split(',').Take(2));
+ }
[JsonIgnore]
public bool Available { get; set; }
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index f302f8700f..b3026bf2b7 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -93,7 +93,9 @@ namespace osu.Game.Rulesets
// add any other modes
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
- if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
+ // todo: StartsWith can be changed to Equals on 2020-11-08
+ // This is to give users enough time to have their database use new abbreviated info).
+ if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}
@@ -104,13 +106,7 @@ namespace osu.Game.Rulesets
{
try
{
- var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo, asm =>
- {
- // for the time being, let's ignore the version being loaded.
- // this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version).
- asm.Version = null;
- return Assembly.Load(asm);
- }, null))).RulesetInfo;
+ var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo))).RulesetInfo;
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 0ffff0ec26..9a10b7d1b2 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -487,6 +487,11 @@ namespace osu.Game.Rulesets.UI
protected virtual ResumeOverlay CreateResumeOverlay() => null;
+ ///
+ /// Whether to display gameplay overlays, such as and .
+ ///
+ public virtual bool AllowGameplayOverlays => true;
+
///
/// Sets a replay to be used, overriding local input.
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 57f58be55a..15e625872d 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -16,17 +16,23 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
private readonly IBindable timeRange = new BindableDouble();
private readonly IBindable direction = new Bindable();
+ private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
- private readonly LayoutValue initialStateCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
+ // Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
+ private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
+
+ // A combined cache across all hit object states to reduce per-update iterations.
+ // When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
+ private readonly Cached combinedObjCache = new Cached();
public ScrollingHitObjectContainer()
{
RelativeSizeAxes = Axes.Both;
- AddLayout(initialStateCache);
+ AddLayout(layoutCache);
}
[BackgroundDependencyLoader]
@@ -35,13 +41,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
direction.BindTo(scrollingInfo.Direction);
timeRange.BindTo(scrollingInfo.TimeRange);
- direction.ValueChanged += _ => initialStateCache.Invalidate();
- timeRange.ValueChanged += _ => initialStateCache.Invalidate();
+ direction.ValueChanged += _ => layoutCache.Invalidate();
+ timeRange.ValueChanged += _ => layoutCache.Invalidate();
}
public override void Add(DrawableHitObject hitObject)
{
- initialStateCache.Invalidate();
+ combinedObjCache.Invalidate();
+ hitObject.DefaultsApplied += onDefaultsApplied;
base.Add(hitObject);
}
@@ -51,8 +58,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (result)
{
- initialStateCache.Invalidate();
+ combinedObjCache.Invalidate();
hitObjectInitialStateCache.Remove(hitObject);
+
+ hitObject.DefaultsApplied -= onDefaultsApplied;
}
return result;
@@ -60,23 +69,45 @@ namespace osu.Game.Rulesets.UI.Scrolling
public override void Clear(bool disposeChildren = true)
{
+ foreach (var h in Objects)
+ h.DefaultsApplied -= onDefaultsApplied;
+
base.Clear(disposeChildren);
- initialStateCache.Invalidate();
+ combinedObjCache.Invalidate();
hitObjectInitialStateCache.Clear();
}
+ private void onDefaultsApplied(DrawableHitObject drawableObject)
+ {
+ // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
+ // In such a case, combinedObjCache will take care of updating the hitobject.
+ if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache))
+ {
+ combinedObjCache.Invalidate();
+ objCache.Invalidate();
+ }
+ }
+
private float scrollLength;
protected override void Update()
{
base.Update();
- if (!initialStateCache.IsValid)
+ if (!layoutCache.IsValid)
{
foreach (var cached in hitObjectInitialStateCache.Values)
cached.Invalidate();
+ combinedObjCache.Invalidate();
+ scrollingInfo.Algorithm.Reset();
+
+ layoutCache.Validate();
+ }
+
+ if (!combinedObjCache.IsValid)
+ {
switch (direction.Value)
{
case ScrollingDirection.Up:
@@ -89,15 +120,21 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
- scrollingInfo.Algorithm.Reset();
-
foreach (var obj in Objects)
{
+ if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache))
+ objCache = hitObjectInitialStateCache[obj] = new Cached();
+
+ if (objCache.IsValid)
+ continue;
+
computeLifetimeStartRecursive(obj);
computeInitialStateRecursive(obj);
+
+ objCache.Validate();
}
- initialStateCache.Validate();
+ combinedObjCache.Validate();
}
}
@@ -109,8 +146,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
computeLifetimeStartRecursive(obj);
}
- private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
-
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{
float originAdjustment = 0.0f;
@@ -142,12 +177,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Cant use AddOnce() since the delegate is re-constructed every invocation
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{
- if (!hitObjectInitialStateCache.TryGetValue(hitObject, out var cached))
- cached = hitObjectInitialStateCache[hitObject] = new Cached();
-
- if (cached.IsValid)
- return;
-
if (hitObject.HitObject is IHasEndTime e)
{
switch (direction.Value)
@@ -171,8 +200,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime);
}
-
- cached.Validate();
});
protected override void UpdateAfterChildrenLife()
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 2e8e03bc73..23c8c9f605 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -201,6 +202,25 @@ namespace osu.Game.Screens.Edit
updateHitObject(null, true);
}
+ ///
+ /// Clears all from this .
+ ///
+ public void Clear()
+ {
+ var removed = HitObjects.ToList();
+
+ mutableHitObjects.Clear();
+
+ foreach (var b in startTimeBindables)
+ b.Value.UnbindAll();
+ startTimeBindables.Clear();
+
+ foreach (var h in removed)
+ HitObjectRemoved?.Invoke(h);
+
+ updateHitObject(null, true);
+ }
+
private void trackStartTime(HitObject hitObject)
{
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs
index d7f939a883..64894544f4 100644
--- a/osu.Game/Screens/Play/GameplayBeatmap.cs
+++ b/osu.Game/Screens/Play/GameplayBeatmap.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play
@@ -38,5 +40,11 @@ namespace osu.Game.Screens.Play
public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics();
public IBeatmap Clone() => PlayableBeatmap.Clone();
+
+ private readonly Bindable lastJudgementResult = new Bindable();
+
+ public IBindable LastJudgementResult => lastJudgementResult;
+
+ public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result;
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index ece4c6307e..a2735c8c55 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -184,6 +184,13 @@ namespace osu.Game.Screens.Play
addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
addOverlayComponents(GameplayClockContainer, Beatmap.Value);
+ if (!DrawableRuleset.AllowGameplayOverlays)
+ {
+ HUDOverlay.ShowHud.Value = false;
+ HUDOverlay.ShowHud.Disabled = true;
+ BreakOverlay.Hide();
+ }
+
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
// bind clock into components that require it
@@ -193,6 +200,7 @@ namespace osu.Game.Screens.Play
{
HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r);
+ gameplayBeatmap.ApplyResult(r);
};
DrawableRuleset.OnRevertResult += r =>
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 6d760df065..ed54c158db 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
@@ -55,10 +54,7 @@ namespace osu.Game.Screens.Select.Carousel
if (match)
{
- var terms = new List();
-
- terms.AddRange(Beatmap.Metadata.SearchableTerms);
- terms.Add(Beatmap.Version);
+ var terms = Beatmap.SearchableTerms;
foreach (var criteriaTerm in criteria.SearchTerms)
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs
index 20cdca858a..0dd3341a93 100644
--- a/osu.Game/Screens/Select/DifficultyRecommender.cs
+++ b/osu.Game/Screens/Select/DifficultyRecommender.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select
{
base.Dispose(isDisposing);
- api.Unregister(this);
+ api?.Unregister(this);
}
}
}
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index 21533e58cd..87bca856a3 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -27,9 +27,11 @@ namespace osu.Game.Skinning
switch (lookup)
{
case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version:
- if (Configuration.LegacyVersion is decimal version)
- return SkinUtils.As(new Bindable(version));
+ // For lookup simplicity, ignore beatmap-level versioning completely.
+ // If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject)
+ // needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin
+ // it should be returning the version for.
return null;
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9db5fe562c..c6dba8da13 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 82253a0418..f78fd2e4ff 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+