1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 22:07:25 +08:00

Merge branch 'master' into migration-backend

This commit is contained in:
Dean Herbert 2020-05-11 18:52:27 +09:00
commit 36640f5dda
27 changed files with 415 additions and 33 deletions

View File

@ -10,10 +10,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit; 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;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -48,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.Tests
DrawableHitObject lastObject = null; DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero; Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Up);
AddStep("seek to last object", () => AddStep("seek to last object", () =>
{ {
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests
DrawableHitObject lastObject = null; DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero; Vector2 originalPosition = Vector2.Zero;
AddStep("set down scroll", () => ((Bindable<ScrollingDirection>)composer.Composer.ScrollingInfo.Direction).Value = ScrollingDirection.Down); setScrollStep(ScrollingDirection.Down);
AddStep("seek to last object", () => AddStep("seek to last object", () =>
{ {
@ -116,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Tests
DrawableHitObject lastObject = null; DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero; Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Down);
AddStep("seek to last object", () => AddStep("seek to last object", () =>
{ {
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); lastObject = this.ChildrenOfType<DrawableHitObject>().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); 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<DrawableHoldNote>().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<HoldNoteNoteSelectionBlueprint>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteSelectionBlueprint>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)
=> AddStep($"set scroll direction = {direction}", () => ((Bindable<ScrollingDirection>)composer.Composer.ScrollingInfo.Direction).Value = direction);
private class TestComposer : CompositeDrawable private class TestComposer : CompositeDrawable
{ {
[Cached(typeof(EditorBeatmap))] [Cached(typeof(EditorBeatmap))]

View File

@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public TestSlider() public TestSlider()
{ {
DefaultsApplied += () => DefaultsApplied += _ =>
{ {
HeadCircle.HitWindows = new TestHitWindows(); HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows(); TailCircle.HitWindows = new TestHitWindows();

View File

@ -28,8 +28,11 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType<SliderRepeat>().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); point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
} }
} }
} }

View File

@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void bindEvents(DrawableOsuHitObject drawableObject) private void bindEvents(DrawableOsuHitObject drawableObject)
{ {
drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh()); drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
drawableObject.HitObject.DefaultsApplied += scheduleRefresh; drawableObject.HitObject.DefaultsApplied += _ => scheduleRefresh();
} }
private void scheduleRefresh() private void scheduleRefresh()

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
typeof(TaikoHitTarget), typeof(TaikoHitTarget),
typeof(TaikoLegacyHitTarget), typeof(TaikoLegacyHitTarget),
typeof(PlayfieldBackgroundRight), typeof(PlayfieldBackgroundRight),
typeof(LegacyTaikoScroller),
}).ToList(); }).ToList();
[Cached(typeof(IScrollingInfo))] [Cached(typeof(IScrollingInfo))]
@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Height = 0.6f,
})); }));
AddRepeatStep("change height", () => this.ChildrenOfType<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50); AddRepeatStep("change height", () => this.ChildrenOfType<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
}
}
}

View File

@ -0,0 +1,149 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.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<JudgementResult>)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<ScrollerSprite>())
sprite.Passing = passing;
}, true);
}
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
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();
}
}
}
}
}

View File

@ -85,6 +85,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return new LegacyHitExplosion(sprite); return new LegacyHitExplosion(sprite);
return null; return null;
case TaikoSkinComponents.TaikoScroller:
if (GetTexture("taiko-slider") != null)
return new LegacyTaikoScroller();
return null;
} }
return source.GetDrawableComponent(component); return source.GetDrawableComponent(component);

View File

@ -18,5 +18,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionMiss, TaikoExplosionMiss,
TaikoExplosionGood, TaikoExplosionGood,
TaikoExplosionGreat, TaikoExplosionGreat,
TaikoScroller
} }
} }

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -16,11 +17,15 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject> public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject>
{ {
private SkinnableDrawable scroller;
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false; protected override bool UserScrollSpeedAdjustment => false;
@ -36,6 +41,20 @@ namespace osu.Game.Rulesets.Taiko.UI
private void load() private void load()
{ {
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); new BarLineGenerator<BarLine>(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(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
@ -78,6 +79,44 @@ namespace osu.Game.Tests.NonVisual
} }
} }
[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<Storage>();
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();
}
}
}
[Test] [Test]
public void TestMigration() public void TestMigration()
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -84,7 +85,7 @@ namespace osu.Game.Beatmaps
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null) public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
{ {
using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10))) using (var cancellationSource = createCancellationTokenSource(timeout))
{ {
mods ??= Array.Empty<Mod>(); mods ??= Array.Empty<Mod>();
@ -181,6 +182,15 @@ namespace osu.Game.Beatmaps
beatmapLoadTask = null; 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<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() => private Task<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
{ {
// Todo: Handle cancellation during beatmap parsing // Todo: Handle cancellation during beatmap parsing

View File

@ -48,10 +48,18 @@ namespace osu.Game.IO
UnderlyingStorage.Delete(MutatePath(path)); UnderlyingStorage.Delete(MutatePath(path));
public override IEnumerable<string> GetDirectories(string path) => public override IEnumerable<string> GetDirectories(string path) =>
UnderlyingStorage.GetDirectories(MutatePath(path)); ToLocalRelative(UnderlyingStorage.GetDirectories(MutatePath(path)));
public IEnumerable<string> ToLocalRelative(IEnumerable<string> paths)
{
string localRoot = GetFullPath(string.Empty);
foreach (var path in paths)
yield return Path.GetRelativePath(localRoot, UnderlyingStorage.GetFullPath(path));
}
public override IEnumerable<string> GetFiles(string path, string pattern = "*") => public override IEnumerable<string> GetFiles(string path, string pattern = "*") =>
UnderlyingStorage.GetFiles(MutatePath(path), pattern); ToLocalRelative(UnderlyingStorage.GetFiles(MutatePath(path), pattern));
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode); UnderlyingStorage.GetStream(MutatePath(path), access, mode);

View File

@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Development; using osu.Framework.Development;
@ -97,6 +98,7 @@ namespace osu.Game
private MainMenu menuScreen; private MainMenu menuScreen;
[CanBeNull]
private IntroScreen introScreen; private IntroScreen introScreen;
private Bindable<int> configRuleset; private Bindable<int> configRuleset;
@ -914,7 +916,7 @@ namespace osu.Game
if (ScreenStack.CurrentScreen is Loader) if (ScreenStack.CurrentScreen is Loader)
return false; return false;
if (introScreen.DidLoadMenu && !(ScreenStack.CurrentScreen is IntroScreen)) if (introScreen?.DidLoadMenu == true && !(ScreenStack.CurrentScreen is IntroScreen))
{ {
Scheduler.Add(introScreen.MakeCurrent); Scheduler.Add(introScreen.MakeCurrent);
return true; return true;

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Cached(typeof(DrawableHitObject))] [Cached(typeof(DrawableHitObject))]
public abstract class DrawableHitObject : SkinReloadableDrawable public abstract class DrawableHitObject : SkinReloadableDrawable
{ {
public event Action<DrawableHitObject> DefaultsApplied;
public readonly HitObject HitObject; public readonly HitObject HitObject;
/// <summary> /// <summary>
@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
samplesBindable.CollectionChanged += (_, __) => loadSamples(); samplesBindable.CollectionChanged += (_, __) => loadSamples();
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
onDefaultsApplied(); apply(HitObject);
} }
private void loadSamples() private void loadSamples()
@ -175,7 +177,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
AddInternal(Samples); AddInternal(Samples);
} }
private void onDefaultsApplied() => apply(HitObject); private void onDefaultsApplied(HitObject hitObject)
{
apply(hitObject);
DefaultsApplied?.Invoke(this);
}
private void apply(HitObject hitObject) private void apply(HitObject hitObject)
{ {

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects
/// <summary> /// <summary>
/// Invoked after <see cref="ApplyDefaults"/> has completed on this <see cref="HitObject"/>. /// Invoked after <see cref="ApplyDefaults"/> has completed on this <see cref="HitObject"/>.
/// </summary> /// </summary>
public event Action DefaultsApplied; public event Action<HitObject> DefaultsApplied;
public readonly Bindable<double> StartTimeBindable = new BindableDouble(); public readonly Bindable<double> StartTimeBindable = new BindableDouble();
@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
foreach (var h in nestedHitObjects) foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty); h.ApplyDefaults(controlPointInfo, difficulty);
DefaultsApplied?.Invoke(); DefaultsApplied?.Invoke(this);
} }
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)

View File

@ -487,6 +487,11 @@ namespace osu.Game.Rulesets.UI
protected virtual ResumeOverlay CreateResumeOverlay() => null; protected virtual ResumeOverlay CreateResumeOverlay() => null;
/// <summary>
/// Whether to display gameplay overlays, such as <see cref="HUDOverlay"/> and <see cref="BreakOverlay"/>.
/// </summary>
public virtual bool AllowGameplayOverlays => true;
/// <summary> /// <summary>
/// Sets a replay to be used, overriding local input. /// Sets a replay to be used, overriding local input.
/// </summary> /// </summary>

View File

@ -16,17 +16,23 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
private readonly IBindable<double> timeRange = new BindableDouble(); private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } 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() public ScrollingHitObjectContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AddLayout(initialStateCache); AddLayout(layoutCache);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -35,13 +41,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
timeRange.BindTo(scrollingInfo.TimeRange); timeRange.BindTo(scrollingInfo.TimeRange);
direction.ValueChanged += _ => initialStateCache.Invalidate(); direction.ValueChanged += _ => layoutCache.Invalidate();
timeRange.ValueChanged += _ => initialStateCache.Invalidate(); timeRange.ValueChanged += _ => layoutCache.Invalidate();
} }
public override void Add(DrawableHitObject hitObject) public override void Add(DrawableHitObject hitObject)
{ {
initialStateCache.Invalidate(); combinedObjCache.Invalidate();
hitObject.DefaultsApplied += onDefaultsApplied;
base.Add(hitObject); base.Add(hitObject);
} }
@ -51,8 +58,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (result) if (result)
{ {
initialStateCache.Invalidate(); combinedObjCache.Invalidate();
hitObjectInitialStateCache.Remove(hitObject); hitObjectInitialStateCache.Remove(hitObject);
hitObject.DefaultsApplied -= onDefaultsApplied;
} }
return result; return result;
@ -60,23 +69,45 @@ namespace osu.Game.Rulesets.UI.Scrolling
public override void Clear(bool disposeChildren = true) public override void Clear(bool disposeChildren = true)
{ {
foreach (var h in Objects)
h.DefaultsApplied -= onDefaultsApplied;
base.Clear(disposeChildren); base.Clear(disposeChildren);
initialStateCache.Invalidate(); combinedObjCache.Invalidate();
hitObjectInitialStateCache.Clear(); 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; private float scrollLength;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (!initialStateCache.IsValid) if (!layoutCache.IsValid)
{ {
foreach (var cached in hitObjectInitialStateCache.Values) foreach (var cached in hitObjectInitialStateCache.Values)
cached.Invalidate(); cached.Invalidate();
combinedObjCache.Invalidate();
scrollingInfo.Algorithm.Reset();
layoutCache.Validate();
}
if (!combinedObjCache.IsValid)
{
switch (direction.Value) switch (direction.Value)
{ {
case ScrollingDirection.Up: case ScrollingDirection.Up:
@ -89,15 +120,21 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
scrollingInfo.Algorithm.Reset();
foreach (var obj in Objects) foreach (var obj in Objects)
{ {
if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache))
objCache = hitObjectInitialStateCache[obj] = new Cached();
if (objCache.IsValid)
continue;
computeLifetimeStartRecursive(obj); computeLifetimeStartRecursive(obj);
computeInitialStateRecursive(obj); computeInitialStateRecursive(obj);
objCache.Validate();
} }
initialStateCache.Validate(); combinedObjCache.Validate();
} }
} }
@ -109,8 +146,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
computeLifetimeStartRecursive(obj); computeLifetimeStartRecursive(obj);
} }
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{ {
float originAdjustment = 0.0f; 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 // Cant use AddOnce() since the delegate is re-constructed every invocation
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => 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) if (hitObject.HitObject is IHasEndTime e)
{ {
switch (direction.Value) 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 // Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime); updatePosition(obj, hitObject.HitObject.StartTime);
} }
cached.Validate();
}); });
protected override void UpdateAfterChildrenLife() protected override void UpdateAfterChildrenLife()

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -201,6 +202,25 @@ namespace osu.Game.Screens.Edit
updateHitObject(null, true); updateHitObject(null, true);
} }
/// <summary>
/// Clears all <see cref="HitObjects"/> from this <see cref="EditorBeatmap"/>.
/// </summary>
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) private void trackStartTime(HitObject hitObject)
{ {
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy(); startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
@ -38,5 +40,11 @@ namespace osu.Game.Screens.Play
public IEnumerable<BeatmapStatistic> GetStatistics() => PlayableBeatmap.GetStatistics(); public IEnumerable<BeatmapStatistic> GetStatistics() => PlayableBeatmap.GetStatistics();
public IBeatmap Clone() => PlayableBeatmap.Clone(); public IBeatmap Clone() => PlayableBeatmap.Clone();
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
public IBindable<JudgementResult> LastJudgementResult => lastJudgementResult;
public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result;
} }
} }

View File

@ -184,6 +184,13 @@ namespace osu.Game.Screens.Play
addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
addOverlayComponents(GameplayClockContainer, Beatmap.Value); addOverlayComponents(GameplayClockContainer, Beatmap.Value);
if (!DrawableRuleset.AllowGameplayOverlays)
{
HUDOverlay.ShowHud.Value = false;
HUDOverlay.ShowHud.Disabled = true;
BreakOverlay.Hide();
}
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
// bind clock into components that require it // bind clock into components that require it
@ -193,6 +200,7 @@ namespace osu.Game.Screens.Play
{ {
HealthProcessor.ApplyResult(r); HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r);
}; };
DrawableRuleset.OnRevertResult += r => DrawableRuleset.OnRevertResult += r =>