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 @@ - +