mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 17:32:54 +08:00
Merge branch 'master' into json-converter
This commit is contained in:
commit
f7d26688f4
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.508.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.511.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -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<DrawableHitObject>().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<ScrollingDirection>)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<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);
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
[Cached(typeof(EditorBeatmap))]
|
||||
|
@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DefaultsApplied += () =>
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
TailCircle.HitWindows = new TestHitWindows();
|
||||
|
@ -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<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);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
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 |
@ -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<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
|
||||
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -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<HitSampleInfo> 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);
|
||||
|
@ -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<HitSampleInfo> 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<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
|
||||
|
149
osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
Normal file
149
osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -18,5 +18,6 @@ namespace osu.Game.Rulesets.Taiko
|
||||
TaikoExplosionMiss,
|
||||
TaikoExplosionGood,
|
||||
TaikoExplosionGreat,
|
||||
TaikoScroller
|
||||
}
|
||||
}
|
||||
|
@ -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<TaikoHitObject>
|
||||
{
|
||||
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<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();
|
||||
|
@ -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<string> 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)
|
||||
|
129
osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
Normal file
129
osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
Normal file
@ -0,0 +1,129 @@
|
||||
// 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;
|
||||
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<Storage>();
|
||||
|
||||
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<Storage>();
|
||||
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<bool> result, string failureMessage, int timeout = 60000)
|
||||
{
|
||||
Task task = Task.Run(() =>
|
||||
{
|
||||
while (!result()) Thread.Sleep(200);
|
||||
});
|
||||
|
||||
Assert.IsTrue(task.Wait(timeout), failureMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -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, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m);
|
||||
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(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, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m);
|
||||
}
|
||||
|
||||
|
@ -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<Mod> mods = null, TimeSpan? timeout = null)
|
||||
{
|
||||
using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)))
|
||||
using (var cancellationSource = createCancellationTokenSource(timeout))
|
||||
{
|
||||
mods ??= Array.Empty<Mod>();
|
||||
|
||||
@ -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<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
|
||||
{
|
||||
// Todo: Handle cancellation during beatmap parsing
|
||||
|
30
osu.Game/Configuration/StorageConfigManager.cs
Normal file
30
osu.Game/Configuration/StorageConfigManager.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// 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.Configuration;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public class StorageConfigManager : IniConfigManager<StorageConfig>
|
||||
{
|
||||
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,
|
||||
}
|
||||
}
|
26
osu.Game/IO/OsuStorage.cs
Normal file
26
osu.Game/IO/OsuStorage.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// 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.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<string>(StorageConfig.FullPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(customStoragePath))
|
||||
{
|
||||
ChangeTargetStorage(host.GetStorage(customStoragePath));
|
||||
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
osu.Game/IO/WrappedStorage.cs
Normal file
88
osu.Game/IO/WrappedStorage.cs
Normal file
@ -0,0 +1,88 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A storage which wraps another storage and delegates implementation, potentially mutating the lookup path.
|
||||
/// </summary>
|
||||
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<string> GetDirectories(string 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 = "*") =>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -97,6 +98,7 @@ namespace osu.Game
|
||||
|
||||
private MainMenu menuScreen;
|
||||
|
||||
[CanBeNull]
|
||||
private IntroScreen introScreen;
|
||||
|
||||
private Bindable<int> configRuleset;
|
||||
@ -914,7 +916,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;
|
||||
|
@ -132,6 +132,8 @@ namespace osu.Game
|
||||
|
||||
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
|
||||
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(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);
|
||||
|
@ -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<bool>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
[Cached(typeof(DrawableHitObject))]
|
||||
public abstract class DrawableHitObject : SkinReloadableDrawable
|
||||
{
|
||||
public event Action<DrawableHitObject> DefaultsApplied;
|
||||
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
/// <summary>
|
||||
@ -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)
|
||||
{
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// <summary>
|
||||
/// Invoked after <see cref="ApplyDefaults"/> has completed on this <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
public event Action DefaultsApplied;
|
||||
public event Action<HitObject> DefaultsApplied;
|
||||
|
||||
public readonly Bindable<double> 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)
|
||||
|
@ -487,6 +487,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
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>
|
||||
/// Sets a replay to be used, overriding local input.
|
||||
/// </summary>
|
||||
|
@ -16,17 +16,23 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
private readonly IBindable<double> timeRange = new BindableDouble();
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
|
||||
|
||||
[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<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
|
||||
|
||||
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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
|
||||
|
@ -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<BeatmapStatistic> GetStatistics() => PlayableBeatmap.GetStatistics();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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 =>
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
api.Unregister(this);
|
||||
api?.Unregister(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<TValue>(new Bindable<decimal>(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;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.508.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
||||
<PackageReference Include="Sentry" Version="2.1.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.25.0" />
|
||||
|
@ -70,7 +70,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.508.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.511.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
||||
</ItemGroup>
|
||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||
@ -80,7 +80,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.508.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.25.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user