diff --git a/osu-framework b/osu-framework index 458ebc2d46..16a4bef775 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 458ebc2d4626c74bb8059cd28b44eb7adba74fbb +Subproject commit 16a4bef775a49166f38faa6e952d83d8823fe3e0 diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f37282366a..45ed66bad2 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -111,16 +111,11 @@ namespace osu.Desktop { var filePaths = new [] { e.FileName }; - if (filePaths.All(f => Path.GetExtension(f) == @".osz")) - Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning); - else if (filePaths.All(f => Path.GetExtension(f) == @".osr")) - Task.Run(() => - { - var score = ScoreStore.ReadReplayFile(filePaths.First()); - Schedule(() => LoadScore(score)); - }); - } + var firstExtension = Path.GetExtension(filePaths.First()); - private static readonly string[] allowed_extensions = { @".osz", @".osr" }; + if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; + + Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); + } } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 9760538197..048fe93c11 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -22,7 +22,7 @@ namespace osu.Desktop { if (!host.IsPrimaryInstance) { - var importer = new BeatmapIPCChannel(host); + var importer = new ArchiveImportIPCChannel(host); // Restore the cwd so relative paths given at the command line work correctly Directory.SetCurrentDirectory(cwd); foreach (var file in args) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 436d5c1ea6..732d5f4109 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.UI return null; } - protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); + protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs index 56efc25fa5..a8d895bc1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; +using OpenTK; namespace osu.Game.Rulesets.Osu.Edit { @@ -17,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Playfield CreatePlayfield() => new OsuEditPlayfield(); + protected override Vector2 PlayfieldArea => Vector2.One; + protected override CursorContainer CreateCursor() => null; } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 6652a5fde2..ae19706da3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -2,10 +2,12 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Edit @@ -25,5 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit new HitObjectCompositionTool(), new HitObjectCompositionTool() }; + + protected override ScalableContainer CreateLayerContainer() => new ScalableContainer(OsuPlayfield.BASE_SIZE.X) { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 79a4714e33..db704b0553 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -78,7 +78,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; List curve = drawableSlider.Body.CurrentCurve; - Position = isRepeatAtEnd ? end : start; + var positionOnCurve = isRepeatAtEnd ? end : start; + Position = positionOnCurve + drawableSlider.HitObject.StackOffset; if (curve.Count < 2) return; @@ -89,10 +90,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // find the next vector2 in the curve which is not equal to our current position to infer a rotation. for (int i = searchStart; i >= 0 && i < curve.Count; i += direction) { - if (curve[i] == Position) + if (curve[i] == positionOnCurve) continue; - Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - Position.Y, curve[i].X - Position.X)); + Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - positionOnCurve.Y, curve[i].X - positionOnCurve.X)); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5795bb8405..391e0ff023 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -7,10 +7,11 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Judgements; using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Objects.Types; +using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { var drawableTick = new DrawableSliderTick(tick) { - Position = tick.Position + Position = tick.StackedPosition }; ticks.Add(drawableTick); @@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { - Position = repeatPoint.Position + Position = repeatPoint.StackedPosition }; repeatPoints.Add(drawableRepeatPoint); @@ -87,7 +88,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - private int currentSpan; + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.SnakingInSliders, Body.SnakingIn); + config.BindWith(OsuSetting.SnakingOutSliders, Body.SnakingOut); + } + public bool Tracking; protected override void Update() @@ -96,19 +103,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking = Ball.Tracking; - double progress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); - - int span = slider.SpanAt(progress); - progress = slider.ProgressAt(progress); - - if (span > currentSpan) - currentSpan = span; + double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. if (!HeadCircle.IsHit) - HeadCircle.Position = slider.Curve.PositionAt(progress); + HeadCircle.Position = slider.StackedPositionAt(completionProgress); - foreach (var c in components.OfType()) c.UpdateProgress(progress, span); + foreach (var c in components.OfType()) c.UpdateProgress(completionProgress); foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0)); foreach (var t in components.OfType()) t.Tracking = Ball.Tracking; } @@ -157,6 +158,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => HeadCircle.ApproachCircle; + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos); + public override Vector2 SelectionPoint => ToScreenSpace(Body.Position); public override Quad SelectionQuad => Body.PathDrawQuad; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 2fda299389..61db10b694 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -139,9 +139,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public void UpdateProgress(double progress, int span) + public void UpdateProgress(double completionProgress) { - Position = slider.Curve.PositionAt(progress); + Position = slider.StackedPositionAt(completionProgress); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 901d1c568d..a83ee3a2e1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Textures; -using osu.Game.Configuration; using OpenTK; using OpenTK.Graphics.ES30; using OpenTK.Graphics; @@ -30,6 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { path.PathWidth = value; } } + public readonly Bindable SnakingIn = new Bindable(); + public readonly Bindable SnakingOut = new Bindable(); + public double? SnakedStart { get; private set; } public double? SnakedEnd { get; private set; } @@ -46,8 +48,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; accentColour = value; - if (LoadState == LoadState.Ready) - Schedule(reloadTexture); + if (LoadState >= LoadState.Ready) + reloadTexture(); + } + } + + private Color4 borderColour = Color4.White; + /// + /// Used to colour the path border. + /// + public new Color4 BorderColour + { + get { return borderColour; } + set + { + if (borderColour == value) + return; + borderColour = value; + + if (LoadState >= LoadState.Ready) + reloadTexture(); } } @@ -78,6 +98,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces container.Attach(RenderbufferInternalFormat.DepthComponent16); } + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => path.ReceiveMouseInputAt(screenSpacePos); + public void SetRange(double p0, double p1) { if (p0 > p1) @@ -95,15 +117,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - private Bindable snakingIn; - private Bindable snakingOut; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - snakingIn = config.GetBindable(OsuSetting.SnakingInSliders); - snakingOut = config.GetBindable(OsuSetting.SnakingOutSliders); - reloadTexture(); } @@ -128,10 +144,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (progress <= border_portion) { - bytes[i * 4] = 255; - bytes[i * 4 + 1] = 255; - bytes[i * 4 + 2] = 255; - bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * 255); + bytes[i * 4] = (byte)(BorderColour.R * 255); + bytes[i * 4 + 1] = (byte)(BorderColour.G * 255); + bytes[i * 4 + 2] = (byte)(BorderColour.B * 255); + bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * (BorderColour.A * 255)); } else { @@ -165,21 +181,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return true; } - public void UpdateProgress(double progress, int span) + public void UpdateProgress(double completionProgress) { + var span = slider.SpanAt(completionProgress); + var spanProgress = slider.ProgressAt(completionProgress); + double start = 0; - double end = snakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadein, 0, 1) : 1; + double end = SnakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadein, 0, 1) : 1; if (span >= slider.SpanCount() - 1) { if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1) { start = 0; - end = snakingOut ? progress : 1; + end = SnakingOut ? spanProgress : 1; } else { - start = snakingOut ? progress : 0; + start = SnakingOut ? spanProgress : 0; } } diff --git a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs index 54f783b664..a0566eaf17 100644 --- a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs +++ b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs @@ -5,6 +5,10 @@ namespace osu.Game.Rulesets.Osu.Objects { public interface ISliderProgress { - void UpdateProgress(double progress, int span); + /// + /// Updates the progress of this element along the slider. + /// + /// Amount of the slider completed. + void UpdateProgress(double completionProgress); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5dd3d7aa89..ce6c88a340 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -66,18 +66,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double SpanDuration => Duration / this.SpanCount(); - private int stackHeight; - - public override int StackHeight - { - get { return stackHeight; } - set - { - stackHeight = value; - Curve.Offset = StackOffset; - } - } - public double Velocity; public double TickDistance; diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index a22ac6aed1..274f7bff62 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -315,11 +315,11 @@ namespace osu.Game.Rulesets.Osu.Replays for (double j = FrameDelay; j < s.Duration; j += FrameDelay) { - Vector2 pos = s.PositionAt(j / s.Duration); + Vector2 pos = s.StackedPositionAt(j / s.Duration); AddFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); } - AddFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); + AddFrameToReplay(new ReplayFrame(s.EndTime, s.StackedEndPosition.X, s.StackedEndPosition.Y, button)); } // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs index 55fa37882d..90a0a450a7 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs @@ -88,10 +88,15 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Catmull Slider", () => testCatmull()); AddStep("Catmull Slider 1 Repeat", () => testCatmull(1)); AddStep("Catmull Slider 2 Repeats", () => testCatmull(2)); + + AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset()); + AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1)); } private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); + private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10); + private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); @@ -104,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); - private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2) + private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) { var slider = new Slider { @@ -118,7 +123,8 @@ namespace osu.Game.Rulesets.Osu.Tests }, Distance = distance, RepeatCount = repeats, - RepeatSamples = createEmptySamples(repeats) + RepeatSamples = createEmptySamples(repeats), + StackHeight = stackHeight }; addSlider(slider, circleSize, speedMultiplier); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 17521f8992..7f8cbce78e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -27,21 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); - public override Vector2 Size - { - get - { - if (Parent == null) - return Vector2.Zero; - - var parentSize = Parent.DrawSize; - var aspectSize = parentSize.X * 0.75f < parentSize.Y ? new Vector2(parentSize.X, parentSize.X * 0.75f) : new Vector2(parentSize.Y * 4f / 3f, parentSize.Y); - - return new Vector2(aspectSize.X / parentSize.X, aspectSize.Y / parentSize.Y) * base.Size; - } - } - - public OsuPlayfield() : base(BASE_SIZE.X) + public OsuPlayfield() + : base(BASE_SIZE.X) { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 526348062f..2af381dd71 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -50,7 +50,11 @@ namespace osu.Game.Rulesets.Osu.UI protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); - protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); + protected override Vector2 GetAspectAdjustedSize() + { + var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y); + return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y); + } protected override CursorContainer CreateCursor() => new GameplayCursor(); } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 1b9821d698..fd31f738ee 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - protected override Vector2 GetPlayfieldAspectAdjust() + protected override Vector2 GetAspectAdjustedSize() { const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; const float default_aspect = 16f / 9f; @@ -88,6 +88,8 @@ namespace osu.Game.Rulesets.Taiko.UI return new Vector2(1, default_relative_height * aspectAdjust); } + protected override Vector2 PlayfieldArea => Vector2.One; + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset); diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cade50a9f3..6428881b54 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -165,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.IO var temp = prepareTempCopy(osz_path); Assert.IsTrue(File.Exists(temp)); - var importer = new BeatmapIPCChannel(client); + var importer = new ArchiveImportIPCChannel(client); if (!importer.ImportAsync(temp).Wait(10000)) Assert.Fail(@"IPC took too long to send"); @@ -209,7 +209,11 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(File.Exists(temp)); - var imported = osu.Dependencies.Get().Import(temp); + var manager = osu.Dependencies.Get(); + + manager.Import(temp); + + var imported = manager.GetAllUsableBeatmapSets(); ensureLoaded(osu); diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 44eb385e22..7a1c6d9b89 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -5,9 +5,9 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.IO; using osu.Game.Tests.Resources; using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Archives; namespace osu.Game.Tests.Beatmaps.IO { @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Beatmaps.IO { using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz")) { - var reader = new OszArchiveReader(osz); + var reader = new ZipArchiveReader(osz); string[] expected = { "Soleily - Renatus (Deif) [Platter].osu", @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Beatmaps.IO { using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz")) { - var reader = new OszArchiveReader(osz); + var reader = new ZipArchiveReader(osz); BeatmapMetadata meta; using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps.IO { using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz")) { - var reader = new OszArchiveReader(osz); + var reader = new ZipArchiveReader(osz); using (var stream = new StreamReader( reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) { diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs index 4a65d12977..901d24e531 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs @@ -60,7 +60,9 @@ namespace osu.Game.Tests.Visual AddStep("Load Beatmaps", () => { carousel.BeatmapSets = beatmapSets; }); - AddUntilStep(() => carousel.BeatmapSets.Any(), "Wait for load"); + bool changed = false; + carousel.BeatmapSetsChanged = () => changed = true; + AddUntilStep(() => changed, "Wait for load"); testTraversal(); testFiltering(); diff --git a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs index 755800c4e1..5e0c0e165c 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs @@ -5,61 +5,51 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using OpenTK; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { public class TestCaseEditorSelectionLayer : OsuTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(SelectionLayer) }; + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SelectionBox), + typeof(SelectionLayer), + typeof(CaptureBox) + }; [BackgroundDependencyLoader] - private void load() + private void load(OsuGameBase osuGame) { - var playfield = new OsuEditPlayfield(); - - Children = new Drawable[] + osuGame.Beatmap.Value = new TestWorkingBeatmap(new Beatmap { - new Container + HitObjects = new List { - RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(new StopwatchClock()), - Child = playfield + new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }, + new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }, + new Slider + { + ControlPoints = new List + { + new Vector2(128, 256), + new Vector2(344, 256), + }, + Distance = 400, + Position = new Vector2(128, 256), + Velocity = 1, + TickDistance = 100, + Scale = 0.5f, + } }, - new SelectionLayer(playfield) - }; + }); - var hitCircle1 = new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }; - var hitCircle2 = new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }; - var slider = new Slider - { - ControlPoints = new List - { - new Vector2(128, 256), - new Vector2(344, 256), - }, - Distance = 400, - Position = new Vector2(128, 256), - Velocity = 1, - TickDistance = 100, - Scale = 0.5f, - }; - - hitCircle1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - hitCircle2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - playfield.Add(new DrawableHitCircle(hitCircle1)); - playfield.Add(new DrawableHitCircle(hitCircle2)); - playfield.Add(new DrawableSlider(slider)); + Child = new OsuHitObjectComposer(new OsuRuleset()); } } } diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 8bb0d152f6..13b2be9fdb 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual { if (deleteMaps) { - manager.DeleteAll(); + manager.Delete(manager.GetAllUsableBeatmapSets()); game.Beatmap.SetDefault(); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 47773528a6..1d6d8b6726 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -7,17 +7,14 @@ using System.IO; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using Ionic.Zip; using Microsoft.EntityFrameworkCore; using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; -using osu.Game.Beatmaps.IO; using osu.Game.Database; using osu.Game.Graphics; -using osu.Game.IO; -using osu.Game.IPC; +using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; @@ -28,23 +25,13 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public partial class BeatmapManager + public partial class BeatmapManager : ArchiveModelManager { - /// - /// Fired when a new becomes available in the database. - /// - public event Action BeatmapSetAdded; - /// /// Fired when a single difficulty has been hidden. /// public event Action BeatmapHidden; - /// - /// Fired when a is removed from the database. - /// - public event Action BeatmapSetRemoved; - /// /// Fired when a single difficulty has been restored. /// @@ -60,9 +47,7 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private readonly IDatabaseContextFactory contextFactory; - - private readonly FileStore files; + public override string[] HandledExtensions => new[] { ".osz" }; private readonly RulesetStore rulesets; @@ -72,150 +57,56 @@ namespace osu.Game.Beatmaps private readonly List currentDownloads = new List(); - // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) - private BeatmapIPCChannel ipc; - - /// - /// Set an endpoint for notifications to be posted to. - /// - public Action PostNotification { private get; set; } - /// /// Set a storage with access to an osu-stable install for import purposes. /// public Func GetStableStorage { private get; set; } public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) + : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) { - this.contextFactory = contextFactory; - - beatmaps = new BeatmapStore(contextFactory); - - beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + beatmaps = (BeatmapStore)ModelStore; beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - files = new FileStore(contextFactory, storage); - this.rulesets = rulesets; this.api = api; - - if (importHost != null) - ipc = new BeatmapIPCChannel(importHost, this); - - beatmaps.Cleanup(); } - /// - /// Import one or more from filesystem . - /// This will post notifications tracking progress. - /// - /// One or more beatmap locations on disk. - public List Import(params string[] paths) + protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) { - var notification = new ProgressNotification + model.Beatmaps = createBeatmapDifficulties(archive); + + // remove metadata from difficulties where it matches the set + foreach (BeatmapInfo b in model.Beatmaps) + if (model.Metadata.Equals(b.Metadata)) + b.Metadata = null; + } + + protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model) + { + // check if this beatmap has already been imported and exit early if so + var existingHashMatch = beatmaps.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + if (existingHashMatch != null) { - Text = "Beatmap import is initialising...", - CompletionText = "Import successful!", - Progress = 0, - State = ProgressNotificationState.Active, - }; + Undelete(existingHashMatch); + return existingHashMatch; + } - PostNotification?.Invoke(notification); - - List imported = new List(); - - int i = 0; - foreach (string path in paths) + // check if a set already exists with the same online id + if (model.OnlineBeatmapSetID != null) { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return imported; - - try + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); + if (existingOnlineId != null) { - notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; - using (ArchiveReader reader = getReaderFrom(path)) - imported.Add(Import(reader)); - - notification.Progress = (float)++i / paths.Length; - - // We may or may not want to delete the file depending on where it is stored. - // e.g. reconstructing/repairing database with beatmaps from default storage. - // Also, not always a single file, i.e. for LegacyFilesystemReader - // TODO: Add a check to prevent files from storage to be deleted. - try - { - if (File.Exists(path)) - File.Delete(path); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); - } - } - catch (Exception e) - { - e = e.InnerException ?? e; - Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); + Delete(existingOnlineId); + beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); } } - notification.State = ProgressNotificationState.Completed; - return imported; + return null; } - /// - /// Import a beatmap from an . - /// - /// The beatmap to be imported. - public BeatmapSetInfo Import(ArchiveReader archive) - { - using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. - { - // create a new set info (don't yet add to database) - var beatmapSet = createBeatmapSetInfo(archive); - - // check if this beatmap has already been imported and exit early if so - var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash); - if (existingHashMatch != null) - { - Undelete(existingHashMatch); - return existingHashMatch; - } - - // check if a set already exists with the same online id - if (beatmapSet.OnlineBeatmapSetID != null) - { - var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); - if (existingOnlineId != null) - { - Delete(existingOnlineId); - beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); - } - } - - beatmapSet.Files = createFileInfos(archive, files); - beatmapSet.Beatmaps = createBeatmapDifficulties(archive); - - // remove metadata from difficulties where it matches the set - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - if (beatmapSet.Metadata.Equals(b.Metadata)) - b.Metadata = null; - - // import to beatmap store - Import(beatmapSet); - return beatmapSet; - } - } - - /// - /// Import a beatmap from a . - /// - /// The beatmap to be imported. - public void Import(BeatmapSetInfo beatmapSet) => beatmaps.Add(beatmapSet); - /// /// Downloads a beatmap. /// This will post notifications tracking progress. @@ -260,7 +151,7 @@ namespace osu.Game.Beatmaps { // This gets scheduled back to the update thread, but we want the import to run in the background. using (var stream = new MemoryStream(data)) - using (var archive = new OszArchiveReader(stream)) + using (var archive = new ZipArchiveReader(stream, beatmapSetInfo.ToString())) Import(archive); downloadNotification.State = ProgressNotificationState.Completed; @@ -300,95 +191,6 @@ namespace osu.Game.Beatmaps /// The object if it exists, or null. public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID); - /// - /// Update a BeatmapSetInfo with all changes. TODO: This only supports very basic updates currently. - /// - /// The beatmap set to update. - public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap); - - /// - /// Delete a beatmap from the manager. - /// Is a no-op for already deleted beatmaps. - /// - /// The beatmap set to delete. - public void Delete(BeatmapSetInfo beatmapSet) - { - using (var usage = contextFactory.GetForWrite()) - { - var context = usage.Context; - - context.ChangeTracker.AutoDetectChangesEnabled = false; - - // re-fetch the beatmap set on the import context. - beatmapSet = context.BeatmapSetInfo.Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == beatmapSet.ID); - - if (beatmaps.Delete(beatmapSet)) - { - if (!beatmapSet.Protected) - files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - } - - context.ChangeTracker.AutoDetectChangesEnabled = true; - } - } - - /// - /// Restore all beatmaps that were previously deleted. - /// This will post notifications tracking progress. - /// - public void UndeleteAll() - { - var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList(); - - if (!deleteMaps.Any()) return; - - var notification = new ProgressNotification - { - CompletionText = "Restored all deleted beatmaps!", - Progress = 0, - State = ProgressNotificationState.Active, - }; - - PostNotification?.Invoke(notification); - - int i = 0; - - foreach (var bs in deleteMaps) - { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; - - notification.Text = $"Restoring ({i} of {deleteMaps.Count})"; - notification.Progress = (float)++i / deleteMaps.Count; - Undelete(bs); - } - - notification.State = ProgressNotificationState.Completed; - } - - /// - /// Restore a beatmap that was previously deleted. Is a no-op if the beatmap is not in a deleted state, or has its protected flag set. - /// - /// The beatmap to restore - public void Undelete(BeatmapSetInfo beatmapSet) - { - if (beatmapSet.Protected) - return; - - using (var usage = contextFactory.GetForWrite()) - { - usage.Context.ChangeTracker.AutoDetectChangesEnabled = false; - - if (!beatmaps.Undelete(beatmapSet)) return; - - if (!beatmapSet.Protected) - files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - - usage.Context.ChangeTracker.AutoDetectChangesEnabled = true; - } - } - /// /// Delete a beatmap difficulty. /// @@ -415,7 +217,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.Metadata == null) beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; - WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo); + WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, beatmapInfo); previous?.TransferTo(working); @@ -427,27 +229,20 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); - - /// - /// Refresh an existing instance of a from the store. - /// - /// A stale instance. - /// A fresh instance. - public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); /// /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); + public List GetAllUsableBeatmapSets() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected).ToList(); /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().Where(query); + public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().Where(query); /// /// Perform a lookup query on available s. @@ -484,54 +279,6 @@ namespace osu.Game.Beatmaps await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); } - /// - /// Delete all beatmaps. - /// This will post notifications tracking progress. - /// - public void DeleteAll() - { - var maps = GetAllUsableBeatmapSets(); - - if (maps.Count == 0) return; - - var notification = new ProgressNotification - { - Progress = 0, - CompletionText = "Deleted all beatmaps!", - State = ProgressNotificationState.Active, - }; - - PostNotification?.Invoke(notification); - - int i = 0; - - foreach (var b in maps) - { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; - - notification.Text = $"Deleting ({i} of {maps.Count})"; - notification.Progress = (float)++i / maps.Count; - Delete(b); - } - - notification.State = ProgressNotificationState.Completed; - } - - /// - /// Creates an from a valid storage path. - /// - /// A file or folder path resolving the beatmap content. - /// A reader giving access to the beatmap's content. - private ArchiveReader getReaderFrom(string path) - { - if (ZipFile.IsZipFile(path)) - // ReSharper disable once InconsistentlySynchronizedField - return new OszArchiveReader(files.Storage.GetStream(path)); - return new LegacyFilesystemReader(path); - } - /// /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// @@ -546,10 +293,7 @@ namespace osu.Game.Beatmaps return hashable.ComputeSHA2Hash(); } - /// - /// Create a from a provided archive. - /// - private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader) + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) { // let's make sure there are actually .osu files to import. string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); @@ -568,25 +312,6 @@ namespace osu.Game.Beatmaps }; } - /// - /// Create all required s for the provided archive, adding them to the global file store. - /// - private List createFileInfos(ArchiveReader reader, FileStore files) - { - List fileInfos = new List(); - - // import files to manager - foreach (string file in reader.Filenames) - using (Stream s = reader.GetStream(file)) - fileInfos.Add(new BeatmapSetFileInfo - { - Filename = file, - FileInfo = files.Add(s) - }); - - return fileInfos; - } - /// /// Create all required s for the provided archive. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 14a4028b44..a72c1adfcd 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -75,23 +75,32 @@ namespace osu.Game.Beatmaps protected override Storyboard GetStoryboard() { + Storyboard storyboard; try { using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) { Decoder decoder = Decoder.GetDecoder(beatmap); - if (BeatmapSetInfo?.StoryboardFile == null) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap); + // todo: support loading from both set-wide storyboard *and* beatmap specific. - using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard); + if (BeatmapSetInfo?.StoryboardFile == null) + storyboard = decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap); + else + { + using (var reader = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + storyboard = decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, reader); + } } } catch { - return new Storyboard(); + storyboard = new Storyboard(); } + + storyboard.BeatmapInfo = BeatmapInfo; + + return storyboard; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index ae4a6772a2..e88af6ed30 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -3,11 +3,12 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetFileInfo + public class BeatmapSetFileInfo : INamedFileInfo { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 982e41c92c..1736e3fa90 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -8,7 +8,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { - public class BeatmapSetInfo : IHasPrimaryKey + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 29373c0715..93ad1badd2 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -2,8 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Game.Database; @@ -12,11 +12,8 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// - public class BeatmapStore : DatabaseBackedStore + public class BeatmapStore : MutableDatabaseBackedStore { - public event Action BeatmapSetAdded; - public event Action BeatmapSetRemoved; - public event Action BeatmapHidden; public event Action BeatmapRestored; @@ -25,88 +22,6 @@ namespace osu.Game.Beatmaps { } - /// - /// Add a to the database. - /// - /// The beatmap to add. - public void Add(BeatmapSetInfo beatmapSet) - { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - - foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) - { - // If we detect a new metadata object it'll be attached to the current context so it can be reused - // to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local) - // of the corresponding table (.Set()) for matching entries to our criteria. - var contextMetadata = context.Set().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata)); - if (contextMetadata != null) - beatmap.Metadata = contextMetadata; - else - context.BeatmapMetadata.Attach(beatmap.Metadata); - } - - context.BeatmapSetInfo.Attach(beatmapSet); - - BeatmapSetAdded?.Invoke(beatmapSet); - } - } - - /// - /// Update a in the database. TODO: This only supports very basic updates currently. - /// - /// The beatmap to update. - public void Update(BeatmapSetInfo beatmapSet) - { - BeatmapSetRemoved?.Invoke(beatmapSet); - - using (var usage = ContextFactory.GetForWrite()) - usage.Context.BeatmapSetInfo.Update(beatmapSet); - - BeatmapSetAdded?.Invoke(beatmapSet); - } - - /// - /// Delete a from the database. - /// - /// The beatmap to delete. - /// Whether the beatmap's was changed. - public bool Delete(BeatmapSetInfo beatmapSet) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref beatmapSet, BeatmapSets); - - if (beatmapSet.DeletePending) return false; - - beatmapSet.DeletePending = true; - } - - BeatmapSetRemoved?.Invoke(beatmapSet); - return true; - } - - /// - /// Restore a previously deleted . - /// - /// The beatmap to restore. - /// Whether the beatmap's was changed. - public bool Undelete(BeatmapSetInfo beatmapSet) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref beatmapSet, BeatmapSets); - - if (!beatmapSet.DeletePending) return false; - - beatmapSet.DeletePending = false; - } - - BeatmapSetAdded?.Invoke(beatmapSet); - return true; - } - /// /// Hide a in the database. /// @@ -119,12 +34,10 @@ namespace osu.Game.Beatmaps Refresh(ref beatmap, Beatmaps); if (beatmap.Hidden) return false; - beatmap.Hidden = true; - - BeatmapHidden?.Invoke(beatmap); } + BeatmapHidden?.Invoke(beatmap); return true; } @@ -140,7 +53,6 @@ namespace osu.Game.Beatmaps Refresh(ref beatmap, Beatmaps); if (!beatmap.Hidden) return false; - beatmap.Hidden = false; } @@ -148,46 +60,38 @@ namespace osu.Game.Beatmaps return true; } - public override void Cleanup() => Cleanup(_ => true); + protected override IQueryable AddIncludesForDeletion(IQueryable query) => + base.AddIncludesForDeletion(query) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Metadata); - public void Cleanup(Expression> query) + protected override IQueryable AddIncludesForConsumption(IQueryable query) => + base.AddIncludesForConsumption(query) + .Include(s => s.Metadata) + .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Files).ThenInclude(f => f.FileInfo); + + protected override void Purge(List items, OsuDbContext context) { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; + // metadata is M-N so we can't rely on cascades + context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata)); + context.BeatmapMetadata.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); - var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) - .Where(query) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Metadata).ToList(); + // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. + context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); - if (!purgeable.Any()) return; - - // metadata is M-N so we can't rely on cascades - context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); - context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); - - // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. - context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); - - // cascades down to beatmaps. - context.BeatmapSetInfo.RemoveRange(purgeable); - } + base.Purge(items, context); } - public IQueryable BeatmapSets => ContextFactory.Get().BeatmapSetInfo - .Include(s => s.Metadata) - .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .Include(s => s.Files).ThenInclude(f => f.FileInfo); - - public IQueryable Beatmaps => ContextFactory.Get().BeatmapInfo - .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) - .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) - .Include(b => b.Metadata) - .Include(b => b.Ruleset) - .Include(b => b.BaseDifficulty); + public IQueryable Beatmaps => + ContextFactory.Get().BeatmapInfo + .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) + .Include(b => b.Metadata) + .Include(b => b.Ruleset) + .Include(b => b.BaseDifficulty); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index c633b94951..8a2a7b01a1 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); protected abstract Track GetTrack(); protected virtual Waveform GetWaveform() => new Waveform(); - protected virtual Storyboard GetStoryboard() => new Storyboard(); + protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; public bool BeatmapLoaded => beatmap.IsResultAvailable; public Beatmap Beatmap => beatmap.Value.Result; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c33dd91330..3d927ef67c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -14,6 +14,8 @@ namespace osu.Game.Configuration { // UI/selection defaults Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); + Set(OsuSetting.Skin, 0, 0, int.MaxValue); + Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details); Set(OsuSetting.ShowConvertedBeatmaps, true); @@ -122,6 +124,7 @@ namespace osu.Game.Configuration ChatDisplayHeight, Version, ShowConvertedBeatmaps, - SpeedChangeVisualisation + SpeedChangeVisualisation, + Skin } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs new file mode 100644 index 0000000000..a65593ff82 --- /dev/null +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -0,0 +1,337 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Ionic.Zip; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.IPC; +using osu.Game.Overlays.Notifications; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Database +{ + /// + /// Encapsulates a model store class to give it import functionality. + /// Adds cross-functionality with to give access to the central file store for the provided model. + /// + /// The model type. + /// The associated file join type. + public abstract class ArchiveModelManager : ICanAcceptFiles + where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete + where TFileModel : INamedFileInfo, new() + { + /// + /// Set an endpoint for notifications to be posted to. + /// + public Action PostNotification { protected get; set; } + + /// + /// Fired when a new becomes available in the database. + /// + public event Action ItemAdded; + + /// + /// Fired when a is removed from the database. + /// + public event Action ItemRemoved; + + public virtual string[] HandledExtensions => new[] { ".zip" }; + + protected readonly FileStore Files; + + protected readonly IDatabaseContextFactory ContextFactory; + + protected readonly MutableDatabaseBackedStore ModelStore; + + // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) + private ArchiveImportIPCChannel ipc; + + protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore modelStore, IIpcHost importHost = null) + { + ContextFactory = contextFactory; + + ModelStore = modelStore; + ModelStore.ItemAdded += s => ItemAdded?.Invoke(s); + ModelStore.ItemRemoved += s => ItemRemoved?.Invoke(s); + + Files = new FileStore(contextFactory, storage); + + if (importHost != null) + ipc = new ArchiveImportIPCChannel(importHost, this); + + ModelStore.Cleanup(); + } + + /// + /// Import one or more items from filesystem . + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + public void Import(params string[] paths) + { + var notification = new ProgressNotification + { + Text = "Import is initialising...", + CompletionText = "Import successful!", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + List imported = new List(); + + int i = 0; + foreach (string path in paths) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + try + { + notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; + using (ArchiveReader reader = getReaderFrom(path)) + imported.Add(Import(reader)); + + notification.Progress = (float)++i / paths.Length; + + // We may or may not want to delete the file depending on where it is stored. + // e.g. reconstructing/repairing database with items from default storage. + // Also, not always a single file, i.e. for LegacyFilesystemReader + // TODO: Add a check to prevent files from storage to be deleted. + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); + } + } + catch (Exception e) + { + e = e.InnerException ?? e; + Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Import an item from an . + /// + /// The archive to be imported. + public TModel Import(ArchiveReader archive) + { + using (ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + { + // create a new model (don't yet add to database) + var item = CreateModel(archive); + + var existing = CheckForExisting(item); + + if (existing != null) return existing; + + item.Files = createFileInfos(archive, Files); + + Populate(item, archive); + + // import to store + ModelStore.Add(item); + + return item; + } + } + + /// + /// Import an item from a . + /// + /// The model to be imported. + public void Import(TModel item) => ModelStore.Add(item); + + /// + /// Perform an update of the specified item. + /// TODO: Support file changes. + /// + /// The item to update. + public void Update(TModel item) => ModelStore.Update(item); + + /// + /// Delete an item from the manager. + /// Is a no-op for already deleted items. + /// + /// The item to delete. + public void Delete(TModel item) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + context.ChangeTracker.AutoDetectChangesEnabled = false; + + // re-fetch the model on the import context. + var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == item.ID); + + if (foundModel.DeletePending) return; + + if (ModelStore.Delete(foundModel)) + Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray()); + + context.ChangeTracker.AutoDetectChangesEnabled = true; + } + } + + /// + /// Delete multiple items. + /// This will post notifications tracking progress. + /// + public void Delete(List items) + { + if (items.Count == 0) return; + + var notification = new ProgressNotification + { + Progress = 0, + CompletionText = "Deleted all beatmaps!", + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + using (ContextFactory.GetForWrite()) + { + foreach (var b in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Deleting ({i} of {items.Count})"; + notification.Progress = (float)++i / items.Count; + Delete(b); + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + public void Undelete(List items) + { + if (!items.Any()) return; + + var notification = new ProgressNotification + { + CompletionText = "Restored all deleted items!", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + using (ContextFactory.GetForWrite()) + { + foreach (var item in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Restoring ({i} of {items.Count})"; + notification.Progress = (float)++i / items.Count; + Undelete(item); + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set. + /// + /// The item to restore + public void Undelete(TModel item) + { + using (var usage = ContextFactory.GetForWrite()) + { + usage.Context.ChangeTracker.AutoDetectChangesEnabled = false; + + if (!ModelStore.Undelete(item)) return; + + Files.Reference(item.Files.Select(f => f.FileInfo).ToArray()); + + usage.Context.ChangeTracker.AutoDetectChangesEnabled = true; + } + } + + /// + /// Create all required s for the provided archive, adding them to the global file store. + /// + private List createFileInfos(ArchiveReader reader, FileStore files) + { + var fileInfos = new List(); + + // import files to manager + foreach (string file in reader.Filenames) + using (Stream s = reader.GetStream(file)) + fileInfos.Add(new TFileModel + { + Filename = file, + FileInfo = files.Add(s) + }); + + return fileInfos; + } + + /// + /// Create a barebones model from the provided archive. + /// Actual expensive population should be done in ; this should just prepare for duplicate checking. + /// + /// The archive to create the model for. + /// A model populated with minimal information. + protected abstract TModel CreateModel(ArchiveReader archive); + + /// + /// Populate the provided model completely from the given archive. + /// After this method, the model should be in a state ready to commit to a store. + /// + /// The model to populate. + /// The archive to use as a reference for population. + protected virtual void Populate(TModel model, ArchiveReader archive) + { + } + + protected virtual TModel CheckForExisting(TModel model) => null; + + private DbSet queryModel() => ContextFactory.Get().Set(); + + /// + /// Creates an from a valid storage path. + /// + /// A file or folder path resolving the archive content. + /// A reader giving access to the archive's content. + private ArchiveReader getReaderFrom(string path) + { + if (ZipFile.IsZipFile(path)) + return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); + return new LegacyFilesystemReader(path); + } + } +} diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index cf46b66422..0fafb77339 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using osu.Framework.Platform; @@ -23,7 +22,7 @@ namespace osu.Game.Database /// The object to use as a reference when negotiating a local instance. /// An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes. /// A valid EF-stored type. - protected virtual void Refresh(ref T obj, IEnumerable lookupSource = null) where T : class, IHasPrimaryKey + protected virtual void Refresh(ref T obj, IQueryable lookupSource = null) where T : class, IHasPrimaryKey { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 2068d6bd8a..712ed2d0cc 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -26,7 +26,8 @@ namespace osu.Game.Database } /// - /// Get a context for read-only usage. + /// Get a context for the current thread for read-only usage. + /// If a is in progress, the existing write-safe context will be returned. /// public OsuDbContext Get() => threadContexts.Value; diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs new file mode 100644 index 0000000000..ab26525619 --- /dev/null +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + /// + /// A class which can accept files for importing. + /// + public interface ICanAcceptFiles + { + /// + /// Import the specified paths. + /// + /// The files which should be imported. + void Import(params string[] paths); + + /// + /// An array of accepted file extensions (in the standard format of ".abc"). + /// + string[] HandledExtensions { get; } + } +} diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs new file mode 100644 index 0000000000..deaf75360c --- /dev/null +++ b/osu.Game/Database/IHasFiles.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + /// The model representing a file. + public interface IHasFiles + { + List Files { get; set; } + } +} diff --git a/osu.Game/Database/INamedFileInfo.cs b/osu.Game/Database/INamedFileInfo.cs new file mode 100644 index 0000000000..8de451dd78 --- /dev/null +++ b/osu.Game/Database/INamedFileInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.IO; + +namespace osu.Game.Database +{ + /// + /// Represent a join model which gives a filename and scope to a . + /// + public interface INamedFileInfo + { + FileInfo FileInfo { get; set; } + string Filename { get; set; } + } +} diff --git a/osu.Game/Database/ISoftDelete.cs b/osu.Game/Database/ISoftDelete.cs new file mode 100644 index 0000000000..c884d7af00 --- /dev/null +++ b/osu.Game/Database/ISoftDelete.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + /// + /// A model that can be deleted from user's view without being instantly lost. + /// + public interface ISoftDelete + { + /// + /// Whether this model is marked for future deletion. + /// + bool DeletePending { get; set; } + } +} diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs new file mode 100644 index 0000000000..4ab55691f2 --- /dev/null +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -0,0 +1,149 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using osu.Framework.Platform; + +namespace osu.Game.Database +{ + /// + /// A typed store which supports basic addition, deletion and updating for soft-deletable models. + /// + /// The databased model. + public abstract class MutableDatabaseBackedStore : DatabaseBackedStore + where T : class, IHasPrimaryKey, ISoftDelete + { + public event Action ItemAdded; + public event Action ItemRemoved; + + protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) + : base(contextFactory, storage) + { + } + + /// + /// Access items pre-populated with includes for consumption. + /// + public IQueryable ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set()); + + /// + /// Add a to the database. + /// + /// The item to add. + public void Add(T item) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + context.Attach(item); + } + + ItemAdded?.Invoke(item); + } + + /// + /// Update a in the database. + /// + /// The item to update. + public void Update(T item) + { + ItemRemoved?.Invoke(item); + + using (var usage = ContextFactory.GetForWrite()) + usage.Context.Update(item); + + ItemAdded?.Invoke(item); + } + + /// + /// Delete a from the database. + /// + /// The item to delete. + public bool Delete(T item) + { + using (ContextFactory.GetForWrite()) + { + Refresh(ref item); + + if (item.DeletePending) return false; + item.DeletePending = true; + } + + ItemRemoved?.Invoke(item); + return true; + } + + /// + /// Restore a from a deleted state. + /// + /// The item to undelete. + public bool Undelete(T item) + { + using (ContextFactory.GetForWrite()) + { + Refresh(ref item, ConsumableItems); + + if (!item.DeletePending) return false; + item.DeletePending = false; + } + + ItemAdded?.Invoke(item); + return true; + } + + /// + /// Allow implementations to add database-side includes or constraints when querying for consumption of items. + /// + /// The input query. + /// A potentially modified output query. + protected virtual IQueryable AddIncludesForConsumption(IQueryable query) => query; + + /// + /// Allow implementations to add database-side includes or constraints when deleting items. + /// Included properties could then be subsequently deleted by overriding . + /// + /// The input query. + /// A potentially modified output query. + protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; + + /// + /// Called when removing an item completely from the database. + /// + /// The items to be purged. + /// The write context which can be used to perform subsequent deletions. + protected virtual void Purge(List items, OsuDbContext context) => context.RemoveRange(items); + + public override void Cleanup() + { + base.Cleanup(); + PurgeDeletable(); + } + + /// + /// Purge items in a pending delete state. + /// + /// An optional query limiting the scope of the purge. + public void PurgeDeletable(Expression> query = null) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + var lookup = context.Set().Where(s => s.DeletePending); + + if (query != null) lookup = lookup.Where(query); + + lookup = AddIncludesForDeletion(lookup); + + var purgeable = lookup.ToList(); + + if (!purgeable.Any()) return; + + Purge(purgeable, context); + } + } + } +} diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index e83b30595e..a4b0c30478 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -13,6 +13,7 @@ using osu.Game.IO; using osu.Game.Rulesets; using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding; using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using osu.Game.Skinning; namespace osu.Game.Database { @@ -26,6 +27,7 @@ namespace osu.Game.Database public DbSet DatabasedSetting { get; set; } public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } + public DbSet SkinInfo { get; set; } private readonly string connectionString; diff --git a/osu.Game/Beatmaps/IO/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs similarity index 76% rename from osu.Game/Beatmaps/IO/ArchiveReader.cs rename to osu.Game/IO/Archives/ArchiveReader.cs index 453a03b882..351a6dff39 100644 --- a/osu.Game/Beatmaps/IO/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using osu.Framework.IO.Stores; -namespace osu.Game.Beatmaps.IO +namespace osu.Game.IO.Archives { public abstract class ArchiveReader : IDisposable, IResourceStore { @@ -17,6 +17,16 @@ namespace osu.Game.Beatmaps.IO public abstract void Dispose(); + /// + /// The name of this archive (usually the containing filename). + /// + public readonly string Name; + + protected ArchiveReader(string name) + { + Name = name; + } + public abstract IEnumerable Filenames { get; } public virtual byte[] Get(string name) diff --git a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Game/IO/Archives/LegacyFilesystemReader.cs similarity index 86% rename from osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs rename to osu.Game/IO/Archives/LegacyFilesystemReader.cs index 4a85f6f526..d6d80783db 100644 --- a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs +++ b/osu.Game/IO/Archives/LegacyFilesystemReader.cs @@ -1,12 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.IO.File; using System.Collections.Generic; using System.IO; using System.Linq; +using osu.Framework.IO.File; -namespace osu.Game.Beatmaps.IO +namespace osu.Game.IO.Archives { /// /// Reads an extracted legacy beatmap from disk. @@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps.IO { private readonly string path; - public LegacyFilesystemReader(string path) + public LegacyFilesystemReader(string path) : base(Path.GetFileName(path)) { this.path = path; } diff --git a/osu.Game/Beatmaps/IO/OszArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs similarity index 83% rename from osu.Game/Beatmaps/IO/OszArchiveReader.cs rename to osu.Game/IO/Archives/ZipArchiveReader.cs index e5c971889b..a772382b5e 100644 --- a/osu.Game/Beatmaps/IO/OszArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -6,14 +6,15 @@ using System.IO; using System.Linq; using Ionic.Zip; -namespace osu.Game.Beatmaps.IO +namespace osu.Game.IO.Archives { - public sealed class OszArchiveReader : ArchiveReader + public sealed class ZipArchiveReader : ArchiveReader { private readonly Stream archiveStream; private readonly ZipFile archive; - public OszArchiveReader(Stream archiveStream) + public ZipArchiveReader(Stream archiveStream, string name = null) + : base(name) { this.archiveStream = archiveStream; archive = ZipFile.Read(archiveStream); diff --git a/osu.Game/IPC/BeatmapIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs similarity index 56% rename from osu.Game/IPC/BeatmapIPCChannel.cs rename to osu.Game/IPC/ArchiveImportIPCChannel.cs index 64e5d526e6..9d7bf17c77 100644 --- a/osu.Game/IPC/BeatmapIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -2,23 +2,25 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Diagnostics; +using System.IO; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Platform; -using osu.Game.Beatmaps; +using osu.Game.Database; namespace osu.Game.IPC { - public class BeatmapIPCChannel : IpcChannel + public class ArchiveImportIPCChannel : IpcChannel { - private readonly BeatmapManager beatmaps; + private readonly ICanAcceptFiles importer; - public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null) + public ArchiveImportIPCChannel(IIpcHost host, ICanAcceptFiles importer = null) : base(host) { - this.beatmaps = beatmaps; + this.importer = importer; MessageReceived += msg => { - Debug.Assert(beatmaps != null); + Debug.Assert(importer != null); ImportAsync(msg.Path).ContinueWith(t => { if (t.Exception != null) throw t.Exception; @@ -28,18 +30,19 @@ namespace osu.Game.IPC public async Task ImportAsync(string path) { - if (beatmaps == null) + if (importer == null) { //we want to contact a remote osu! to handle the import. - await SendMessageAsync(new BeatmapImportMessage { Path = path }); + await SendMessageAsync(new ArchiveImportMessage { Path = path }); return; } - beatmaps.Import(path); + if (importer.HandledExtensions.Contains(Path.GetExtension(path))) + importer.Import(path); } } - public class BeatmapImportMessage + public class ArchiveImportMessage { public string Path; } diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs new file mode 100644 index 0000000000..83b8d6cf8a --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs @@ -0,0 +1,379 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180219060912_AddSkins")] + partial class AddSkins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs new file mode 100644 index 0000000000..741fcf4079 --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class AddSkins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SkinInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Creator = table.Column(type: "TEXT", nullable: true), + DeletePending = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "SkinFileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FileInfoID = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false), + SkinInfoID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_SkinFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SkinFileInfo_SkinInfo_SkinInfoID", + column: x => x.SkinInfoID, + principalTable: "SkinInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_FileInfoID", + table: "SkinFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_SkinInfoID", + table: "SkinFileInfo", + column: "SkinInfoID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SkinFileInfo"); + + migrationBuilder.DropTable( + name: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 157125102f..1627627790 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -281,6 +281,43 @@ namespace osu.Game.Migrations b.ToTable("RulesetInfo"); }); + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => { b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") @@ -322,6 +359,19 @@ namespace osu.Game.Migrations .WithMany("BeatmapSets") .HasForeignKey("MetadataID"); }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); #pragma warning restore 612, 618 } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 1d657b8664..90f3999ddd 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Threading; -using osu.Framework; using osu.Framework.Configuration; using osu.Framework.Logging; using osu.Framework.Threading; @@ -16,7 +15,7 @@ using osu.Game.Users; namespace osu.Game.Online.API { - public class APIAccess : IUpdateable + public class APIAccess : IAPIProvider { private readonly OAuth authentication; @@ -34,7 +33,7 @@ namespace osu.Game.Online.API public string Password; - public Bindable LocalUser = new Bindable(createGuestUser()); + public Bindable LocalUser { get; } = new Bindable(createGuestUser()); public string Token { diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs new file mode 100644 index 0000000000..2dff07a847 --- /dev/null +++ b/osu.Game/Online/API/APIDownloadRequest.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API +{ + public abstract class APIDownloadRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var request = new WebRequest(Uri); + request.DownloadProgress += request_Progress; + return request; + } + + private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); }); + + protected APIDownloadRequest() + { + base.Success += onSuccess; + } + + private void onSuccess() + { + Success?.Invoke(WebRequest.ResponseData); + } + + public event APIProgressHandler Progress; + + public new event APISuccessHandler Success; + } +} diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index ce6f3c7c7d..35af8eefd7 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -27,32 +27,6 @@ namespace osu.Game.Online.API public new event APISuccessHandler Success; } - public abstract class APIDownloadRequest : APIRequest - { - protected override WebRequest CreateWebRequest() - { - var request = new WebRequest(Uri); - request.DownloadProgress += request_Progress; - return request; - } - - private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); }); - - protected APIDownloadRequest() - { - base.Success += onSuccess; - } - - private void onSuccess() - { - Success?.Invoke(WebRequest.ResponseData); - } - - public event APIProgressHandler Progress; - - public new event APISuccessHandler Success; - } - /// /// AN API request with no specified response type. /// diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs new file mode 100644 index 0000000000..fc0dc0ef8b --- /dev/null +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Game.Users; + +namespace osu.Game.Online.API +{ + public class DummyAPIAccess : IAPIProvider + { + public Bindable LocalUser { get; } = new Bindable(new User + { + Username = @"Dummy", + Id = 1, + }); + + public bool IsLoggedIn => true; + + public void Update() + { + } + + public virtual void Queue(APIRequest request) + { + } + + public void Register(IOnlineComponent component) + { + } + } +} diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs new file mode 100644 index 0000000000..b3c8774209 --- /dev/null +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework; +using osu.Framework.Configuration; +using osu.Game.Users; + +namespace osu.Game.Online.API +{ + public interface IAPIProvider : IUpdateable + { + /// + /// The local user. + /// + Bindable LocalUser { get; } + + /// + /// Returns whether the local user is logged in. + /// + bool IsLoggedIn { get; } + + /// + /// Queue a new request. + /// + /// The request to perform. + void Queue(APIRequest request); + + /// + /// Register a component to receive state changes. + /// + /// The component to register. + void Register(IOnlineComponent component); + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 624179cfe1..95eb88c5c8 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; using OpenTK.Graphics; namespace osu.Game @@ -79,6 +80,8 @@ namespace osu.Game private Bindable configRuleset; public Bindable Ruleset = new Bindable(); + private Bindable configSkin; + private readonly string[] args; private SettingsOverlay settings; @@ -105,6 +108,8 @@ namespace osu.Game { this.frameworkConfig = frameworkConfig; + ScoreStore.ScoreImported += score => Schedule(() => LoadScore(score)); + if (!Host.IsPrimaryInstance) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); @@ -114,15 +119,24 @@ namespace osu.Game if (args?.Length > 0) { var paths = args.Where(a => !a.StartsWith(@"-")); - Task.Run(() => BeatmapManager.Import(paths.ToArray())); + + Task.Run(() => Import(paths.ToArray())); } dependencies.CacheAs(this); + // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; + // bind config int to database SkinInfo + configSkin = LocalConfig.GetBindable(OsuSetting.Skin); + + SkinManager.CurrentSkinInfo.ValueChanged += s => configSkin.Value = s.ID; + configSkin.ValueChanged += id => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == id) ?? SkinInfo.Default; + configSkin.TriggerChange(); + LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust); } @@ -184,7 +198,9 @@ namespace osu.Game CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; // hook up notifications to components. + SkinManager.PostNotification = n => notifications?.Post(n); BeatmapManager.PostNotification = n => notifications?.Post(n); + BeatmapManager.GetStableStorage = GetStorageForStableInstall; AddRange(new Drawable[] diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 505577416d..94ed696e49 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -2,7 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Reflection; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -27,15 +30,18 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; namespace osu.Game { - public class OsuGameBase : Framework.Game, IOnlineComponent + public class OsuGameBase : Framework.Game, IOnlineComponent, ICanAcceptFiles { protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; + protected SkinManager SkinManager; + protected RulesetStore RulesetStore; protected FileStore FileStore; @@ -100,11 +106,14 @@ namespace osu.Game runMigrations(); + dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host)); + dependencies.Cache(API = new APIAccess { Username = LocalConfig.Get(OsuSetting.Username), Token = LocalConfig.Get(OsuSetting.Token) }); + dependencies.CacheAs(API); dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); @@ -114,6 +123,10 @@ namespace osu.Game dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(new OsuColour()); + fileImporters.Add(BeatmapManager); + fileImporters.Add(ScoreStore); + fileImporters.Add(SkinManager); + //this completely overrides the framework default. will need to change once we make a proper FontStore. dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }); @@ -257,5 +270,17 @@ namespace osu.Game base.Dispose(isDisposing); } + + private readonly List fileImporters = new List(); + + public void Import(params string[] paths) + { + var extension = Path.GetExtension(paths.First()); + + foreach (var importer in fileImporters) + if (importer.HandledExtensions.Contains(extension)) importer.Import(paths); + } + + public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); } } diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 36b6a9964a..3ce0dfee31 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -223,13 +223,13 @@ namespace osu.Game.Overlays.BeatmapSet tabsBg.Colour = colours.Gray3; this.beatmaps = beatmaps; - beatmaps.BeatmapSetAdded += handleBeatmapAdd; + beatmaps.ItemAdded += handleBeatmapAdd; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (beatmaps != null) beatmaps.BeatmapSetAdded -= handleBeatmapAdd; + if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd; } private void handleBeatmapAdd(BeatmapSetInfo beatmap) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index beb2b3b746..7c6e563c5b 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -477,7 +477,7 @@ namespace osu.Game.Overlays if (!api.IsLoggedIn) { - target.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); + target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); return; } diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 1d67bc2d90..0fb988ead7 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Direct public Track Preview { get; private set; } private BeatmapSetInfo beatmapSet; + public BeatmapSetInfo BeatmapSet { get { return beatmapSet; } @@ -199,8 +200,7 @@ namespace osu.Game.Overlays.Direct // add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy). config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); - if (!string.IsNullOrEmpty(preview)) - Preview = trackManager.Get(preview); + Preview = trackManager.Get(preview); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 05b5bba09c..8d8a4aebaa 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays resultCountsContainer.Colour = colours.Yellow; - beatmaps.BeatmapSetAdded += setAdded; + beatmaps.ItemAdded += setAdded; } private void setAdded(BeatmapSetInfo set) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 2125984785..ac7ec6257b 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.BeatmapSetAdded += list.AddBeatmapSet; - beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet; + beatmaps.ItemAdded += list.AddBeatmapSet; + beatmaps.ItemRemoved += list.RemoveBeatmapSet; list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index d959da52f3..a5d068adbd 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -208,7 +208,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { username = new OsuTextBox { - PlaceholderText = "Username", + PlaceholderText = "Email address", RelativeSizeAxes = Axes.X, Text = api?.Username ?? string.Empty, TabbableContentContainer = this @@ -222,12 +222,12 @@ namespace osu.Game.Overlays.Settings.Sections.General }, new SettingsCheckbox { - LabelText = "Remember username", + LabelText = "Remember email address", Bindable = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { - LabelText = "Stay logged in", + LabelText = "Stay signed in", Bindable = config.GetBindable(OsuSetting.SavePassword), }, new SettingsButton @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Settings.Sections.General }, new SettingsButton { - Text = "Register new account", + Text = "Register", //Action = registerLink } }; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 1223310c74..d9fedd0225 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => { deleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); })); } }, @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { undeleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index f6915896d7..bc0b8b4aaa 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,26 +1,33 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Overlays.Settings.Sections { public class SkinSection : SettingsSection { + private SettingsDropdown skinDropdown; + public override string Header => "Skin"; + public override FontAwesome Icon => FontAwesome.fa_paint_brush; [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, SkinManager skins) { FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { + skinDropdown = new SettingsDropdown(), new SettingsSlider { LabelText = "Menu cursor size", @@ -39,6 +46,14 @@ namespace osu.Game.Overlays.Settings.Sections Bindable = config.GetBindable(OsuSetting.AutoCursorSize) }, }; + + void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.Name, s.ID)); + skins.ItemAdded += _ => reloadSkins(); + skins.ItemRemoved += _ => reloadSkins(); + + reloadSkins(); + + skinDropdown.Bindable = config.GetBindable(OsuSetting.Skin); } private class SizeSlider : OsuSliderBar diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 7f22b3764c..1246127257 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -25,20 +25,24 @@ namespace osu.Game.Rulesets.Edit protected ICompositionTool CurrentTool { get; private set; } + private RulesetContainer rulesetContainer; + private readonly List layerContainers = new List(); + protected HitObjectComposer(Ruleset ruleset) { this.ruleset = ruleset; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(OsuGameBase osuGame) { - RulesetContainer rulesetContainer; try { rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value); + + // TODO: should probably be done at a RulesetContainer level to share logic with Player. + rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); } catch (Exception e) { @@ -46,6 +50,14 @@ namespace osu.Game.Rulesets.Edit return; } + ScalableContainer createLayerContainerWithContent(Drawable content) + { + var container = CreateLayerContainer(); + container.Child = content; + layerContainers.Add(container); + return container; + } + RadioButtonCollection toolboxCollection; InternalChild = new GridContainer { @@ -66,20 +78,21 @@ namespace osu.Game.Rulesets.Edit }, new Container { + Name = "Content", RelativeSizeAxes = Axes.Both, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, Children = new Drawable[] { - new Box + createLayerContainerWithContent(new Container { + Name = "Border", RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Child = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } + }), rulesetContainer, - new SelectionLayer(rulesetContainer.Playfield) + createLayerContainerWithContent(new SelectionLayer(rulesetContainer.Playfield)) } } }, @@ -90,8 +103,6 @@ namespace osu.Game.Rulesets.Edit } }; - rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); - toolboxCollection.Items = new[] { new RadioButton("Select", () => setCompositionTool(null)) } .Concat( @@ -102,10 +113,28 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items[0].Select(); } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + layerContainers.ForEach(l => + { + l.Anchor = rulesetContainer.Playfield.Anchor; + l.Origin = rulesetContainer.Playfield.Origin; + l.Position = rulesetContainer.Playfield.Position; + l.Size = rulesetContainer.Playfield.Size; + }); + } + private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); protected abstract IReadOnlyList CompositionTools { get; } + + /// + /// Creates a which provides a layer above or below the . + /// + protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/CaptureBox.cs b/osu.Game/Rulesets/Edit/Layers/Selection/CaptureBox.cs new file mode 100644 index 0000000000..269dd79bf7 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/CaptureBox.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + /// + /// A box which encloses s. + /// + public class CaptureBox : VisibilityContainer + { + private readonly IDrawable captureArea; + private readonly IReadOnlyList capturedObjects; + + public CaptureBox(IDrawable captureArea, IReadOnlyList capturedObjects) + { + this.captureArea = captureArea; + this.capturedObjects = capturedObjects; + + Masking = true; + BorderThickness = SelectionBox.BORDER_RADIUS; + + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0 + }; + + State = Visibility.Visible; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BorderColour = colours.Yellow; + + // Move the rectangle to cover the hitobjects + var topLeft = new Vector2(float.MaxValue, float.MaxValue); + var bottomRight = new Vector2(float.MinValue, float.MinValue); + + foreach (var obj in capturedObjects) + { + topLeft = Vector2.ComponentMin(topLeft, captureArea.ToLocalSpace(obj.SelectionQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, captureArea.ToLocalSpace(obj.SelectionQuad.BottomRight)); + } + + topLeft -= new Vector2(5); + bottomRight += new Vector2(5); + + Size = bottomRight - topLeft; + Position = topLeft; + } + + public override bool DisposeOnDeathRemoval => true; + + protected override void PopIn() => this.FadeIn(); + protected override void PopOut() => this.FadeOut(); + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs b/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs deleted file mode 100644 index d275022a15..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Game.Graphics; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// Represents a marker visible on the border of a which exposes - /// properties that are used to resize a . - /// - public class Handle : CompositeDrawable - { - private const float marker_size = 10; - - /// - /// Invoked when this requires the current drag rectangle. - /// - public Func GetDragRectangle; - - /// - /// Invoked when this wants to update the drag rectangle. - /// - public Action UpdateDragRectangle; - - /// - /// Invoked when this has finished updates to the drag rectangle. - /// - public Action FinishDrag; - - private Color4 normalColour; - private Color4 hoverColour; - - public Handle() - { - Size = new Vector2(marker_size); - - InternalChild = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Colour = normalColour = colours.Yellow; - hoverColour = colours.YellowDarker; - } - - protected override bool OnDragStart(InputState state) => true; - - protected override bool OnDrag(InputState state) - { - var currentRectangle = GetDragRectangle(); - - float left = currentRectangle.Left; - float right = currentRectangle.Right; - float top = currentRectangle.Top; - float bottom = currentRectangle.Bottom; - - // Apply modifications to the capture rectangle - if ((Anchor & Anchor.y0) > 0) - top += state.Mouse.Delta.Y; - else if ((Anchor & Anchor.y2) > 0) - bottom += state.Mouse.Delta.Y; - - if ((Anchor & Anchor.x0) > 0) - left += state.Mouse.Delta.X; - else if ((Anchor & Anchor.x2) > 0) - right += state.Mouse.Delta.X; - - UpdateDragRectangle(RectangleF.FromLTRB(left, top, right, bottom)); - return true; - } - - protected override bool OnDragEnd(InputState state) - { - FinishDrag(); - return true; - } - - protected override bool OnHover(InputState state) - { - this.FadeColour(hoverColour, 100); - return true; - } - - protected override void OnHoverLost(InputState state) - { - this.FadeColour(normalColour, 100); - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs deleted file mode 100644 index 359cdd009a..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// A that has s around its border. - /// - public class HandleContainer : CompositeDrawable - { - /// - /// Invoked when a requires the current drag rectangle. - /// - public Func GetDragRectangle; - - /// - /// Invoked when a wants to update the drag rectangle. - /// - public Action UpdateDragRectangle; - - /// - /// Invoked when a has finished updates to the drag rectangle. - /// - public Action FinishDrag; - - public HandleContainer() - { - InternalChildren = new Drawable[] - { - new Handle - { - Anchor = Anchor.TopLeft, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.TopRight, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.BottomRight, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre - }, - new OriginHandle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - } - }; - - InternalChildren.OfType().ForEach(m => - { - m.GetDragRectangle = () => GetDragRectangle(); - m.UpdateDragRectangle = r => UpdateDragRectangle(r); - m.FinishDrag = () => FinishDrag(); - }); - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs deleted file mode 100644 index fcb2f8f57f..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Configuration; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// A box that represents a drag selection. - /// - public class HitObjectSelectionBox : CompositeDrawable - { - public readonly Bindable Selection = new Bindable(); - - /// - /// The s that can be selected through a drag-selection. - /// - public IEnumerable CapturableObjects; - - private readonly Container borderMask; - private readonly Drawable background; - private readonly HandleContainer handles; - - private Color4 captureFinishedColour; - - private readonly Vector2 startPos; - - /// - /// Creates a new . - /// - /// The point at which the drag was initiated, in the parent's coordinates. - public HitObjectSelectionBox(Vector2 startPos) - { - this.startPos = startPos; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-1), - Child = borderMask = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, - MaskingSmoothness = 1, - Child = background = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f, - AlwaysPresent = true - }, - } - }, - handles = new HandleContainer - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - GetDragRectangle = () => dragRectangle, - UpdateDragRectangle = updateDragRectangle, - FinishDrag = FinishCapture - } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - captureFinishedColour = colours.Yellow; - } - - /// - /// The secondary corner of the drag selection box. A rectangle will be fit between the starting position and this value. - /// - public Vector2 DragEndPosition { set => updateDragRectangle(RectangleF.FromLTRB(startPos.X, startPos.Y, value.X, value.Y)); } - - private RectangleF dragRectangle; - private void updateDragRectangle(RectangleF rectangle) - { - dragRectangle = rectangle; - - Position = new Vector2( - Math.Min(rectangle.Left, rectangle.Right), - Math.Min(rectangle.Top, rectangle.Bottom)); - - Size = new Vector2( - Math.Max(rectangle.Left, rectangle.Right) - Position.X, - Math.Max(rectangle.Top, rectangle.Bottom) - Position.Y); - } - - private readonly List capturedHitObjects = new List(); - - /// - /// Processes hitobjects to determine which ones are captured by the drag selection. - /// Captured hitobjects will be enclosed by the drag selection upon . - /// - public void BeginCapture() - { - capturedHitObjects.Clear(); - - foreach (var obj in CapturableObjects) - { - if (!obj.IsAlive || !obj.IsPresent) - continue; - - if (ScreenSpaceDrawQuad.Contains(obj.SelectionPoint)) - capturedHitObjects.Add(obj); - } - } - - /// - /// Encloses hitobjects captured through in the drag selection box. - /// - public void FinishCapture() - { - if (capturedHitObjects.Count == 0) - { - Hide(); - return; - } - - // Move the rectangle to cover the hitobjects - var topLeft = new Vector2(float.MaxValue, float.MaxValue); - var bottomRight = new Vector2(float.MinValue, float.MinValue); - - foreach (var obj in capturedHitObjects) - { - topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.SelectionQuad.TopLeft)); - bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.SelectionQuad.BottomRight)); - } - - topLeft -= new Vector2(5); - bottomRight += new Vector2(5); - - this.MoveTo(topLeft, 200, Easing.OutQuint) - .ResizeTo(bottomRight - topLeft, 200, Easing.OutQuint); - - dragRectangle = RectangleF.FromLTRB(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y); - - borderMask.BorderThickness = 3; - borderMask.FadeColour(captureFinishedColour, 200); - - // Transform into markers to let the user modify the drag selection further. - background.Delay(50).FadeOut(200); - handles.FadeIn(200); - - Selection.Value = new SelectionInfo - { - Objects = capturedHitObjects, - SelectionQuad = Parent.ToScreenSpace(dragRectangle) - }; - } - - private bool isActive = true; - public override bool HandleKeyboardInput => isActive; - public override bool HandleMouseInput => isActive; - - public override void Hide() - { - isActive = false; - this.FadeOut(400, Easing.OutQuint).Expire(); - - Selection.Value = null; - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs b/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs deleted file mode 100644 index 6f8c946165..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using OpenTK; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// Represents the origin of a . - /// - public class OriginHandle : CompositeDrawable - { - private const float marker_size = 10; - private const float line_width = 2; - - public OriginHandle() - { - Size = new Vector2(marker_size); - - InternalChildren = new[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = line_width - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = line_width - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Colour = colours.Yellow; - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionBox.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionBox.cs new file mode 100644 index 0000000000..1c25846ee3 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionBox.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + /// + /// A box that represents a drag selection. + /// + public class SelectionBox : VisibilityContainer + { + public const float BORDER_RADIUS = 2; + + /// + /// Creates a new . + /// + public SelectionBox() + { + Masking = true; + BorderColour = Color4.White; + BorderThickness = BORDER_RADIUS; + + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + }; + } + + public void SetDragRectangle(RectangleF rectangle) + { + var topLeft = Parent.ToLocalSpace(rectangle.TopLeft); + var bottomRight = Parent.ToLocalSpace(rectangle.BottomRight); + + Position = topLeft; + Size = bottomRight - topLeft; + } + + public override bool DisposeOnDeathRemoval => true; + + protected override void PopIn() => this.FadeIn(250, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(250, Easing.OutQuint); + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs deleted file mode 100644 index beedb415c2..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - public class SelectionInfo - { - /// - /// The objects that are captured by the selection. - /// - public IEnumerable Objects; - - /// - /// The screen space quad of the selection box surrounding . - /// - public Quad SelectionQuad; - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs index 93755d400a..bda613f617 100644 --- a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs +++ b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs @@ -1,18 +1,20 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Configuration; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; +using OpenTK; namespace osu.Game.Rulesets.Edit.Layers.Selection { public class SelectionLayer : CompositeDrawable { - public readonly Bindable Selection = new Bindable(); - private readonly Playfield playfield; public SelectionLayer(Playfield playfield) @@ -22,40 +24,95 @@ namespace osu.Game.Rulesets.Edit.Layers.Selection RelativeSizeAxes = Axes.Both; } - private HitObjectSelectionBox selectionBoxBox; + private SelectionBox selectionBox; + private CaptureBox captureBox; + + private readonly List selectedHitObjects = new List(); + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + clearSelection(); + return true; + } protected override bool OnDragStart(InputState state) { - // Hide the previous drag box - we won't be working with it any longer - selectionBoxBox?.Hide(); - - AddInternal(selectionBoxBox = new HitObjectSelectionBox(ToLocalSpace(state.Mouse.NativeState.Position)) - { - CapturableObjects = playfield.HitObjects.Objects, - }); - - Selection.BindTo(selectionBoxBox.Selection); - + AddInternal(selectionBox = new SelectionBox()); return true; } protected override bool OnDrag(InputState state) { - selectionBoxBox.DragEndPosition = ToLocalSpace(state.Mouse.NativeState.Position); - selectionBoxBox.BeginCapture(); + selectionBox.Show(); + + var dragPosition = state.Mouse.NativeState.Position; + var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition; + + var screenSpaceDragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y); + + selectionBox.SetDragRectangle(screenSpaceDragQuad.AABBFloat); + selectQuad(screenSpaceDragQuad); + return true; } protected override bool OnDragEnd(InputState state) { - selectionBoxBox.FinishCapture(); + selectionBox.Hide(); + selectionBox.Expire(); + + finishSelection(); + return true; } protected override bool OnClick(InputState state) { - selectionBoxBox?.Hide(); + selectPoint(state.Mouse.NativeState.Position); + finishSelection(); + return true; } + + /// + /// Deselects all selected s. + /// + private void clearSelection() + { + selectedHitObjects.Clear(); + captureBox?.Hide(); + captureBox?.Expire(); + } + + /// + /// Selects all hitobjects that are present within the area of a . + /// + /// The selection . + private void selectQuad(Quad screenSpaceQuad) + { + foreach (var obj in playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint))) + selectedHitObjects.Add(obj); + } + + /// + /// Selects the top-most hitobject that is present under a specific point. + /// + /// The to select at. + private void selectPoint(Vector2 screenSpacePoint) + { + var selected = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint)); + if (selected == null) + return; + + selectedHitObjects.Add(selected); + } + + private void finishSelection() + { + if (selectedHitObjects.Count == 0) + return; + + AddInternal(captureBox = new CaptureBox(this, selectedHitObjects.ToList())); + } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index 7f03854ea9..c03bdb240e 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -30,21 +30,19 @@ namespace osu.Game.Rulesets.Objects.Types public static class HasCurveExtensions { /// - /// Computes the position on the curve at a given progress, accounting for repeat logic. - /// - /// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - /// + /// Computes the position on the curve relative to how much of the has been completed. /// /// The curve. - /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// The position on the curve. public static Vector2 PositionAt(this IHasCurve obj, double progress) => obj.Curve.PositionAt(obj.ProgressAt(progress)); /// - /// Finds the progress along the curve, accounting for repeat logic. + /// Computes the progress along the curve relative to how much of the has been completed. /// /// The curve. - /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. public static double ProgressAt(this IHasCurve obj, double progress) { diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index 8bde2747a2..7abee0b04f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using System.IO; using osu.Framework.Platform; @@ -14,7 +15,7 @@ using SharpCompress.Compressors.LZMA; namespace osu.Game.Rulesets.Scoring { - public class ScoreStore : DatabaseBackedStore + public class ScoreStore : DatabaseBackedStore, ICanAcceptFiles { private readonly Storage storage; @@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Scoring private const string replay_folder = @"replays"; + public event Action ScoreImported; + // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ScoreIPCChannel ipc; @@ -36,6 +39,18 @@ namespace osu.Game.Rulesets.Scoring ipc = new ScoreIPCChannel(importHost, this); } + public string[] HandledExtensions => new[] { ".osr" }; + + public void Import(params string[] paths) + { + foreach (var path in paths) + { + var score = ReadReplayFile(path); + if (score != null) + ScoreImported?.Invoke(score); + } + } + public Score ReadReplayFile(string replayFilename) { Score score; @@ -159,5 +174,6 @@ namespace osu.Game.Rulesets.Scoring return new Replay { Frames = frames }; } + } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index a7fed7059b..bbf20c2c26 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -3,52 +3,37 @@ using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; using osu.Framework.Allocation; namespace osu.Game.Rulesets.UI { - public abstract class Playfield : Container + public abstract class Playfield : ScalableContainer { /// /// The HitObjects contained in this Playfield. /// public HitObjectContainer HitObjects { get; private set; } - public Container ScaledContent; - - protected override Container Content => content; - private readonly Container content; - - private List nestedPlayfields; - /// /// All the s nested inside this playfield. /// public IReadOnlyList NestedPlayfields => nestedPlayfields; + private List nestedPlayfields; /// /// A container for keeping track of DrawableHitObjects. /// - /// Whether we want our internal coordinate system to be scaled to a specified width. - protected Playfield(float? customWidth = null) + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + protected Playfield(float? customWidth = null, float? customHeight = null) + : base(customWidth, customHeight) { RelativeSizeAxes = Axes.Both; - - AddInternal(ScaledContent = new ScaledContainer - { - CustomWidth = customWidth, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both, - } - } - }); } [BackgroundDependencyLoader] @@ -94,22 +79,5 @@ namespace osu.Game.Rulesets.UI /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); - - private class ScaledContainer : Container - { - /// - /// A value (in game pixels that we should scale our content to match). - /// - public float? CustomWidth; - - //dividing by the customwidth will effectively scale our content to the required container size. - protected override Vector2 DrawScale => CustomWidth.HasValue ? new Vector2(DrawSize.X / CustomWidth.Value) : base.DrawScale; - - protected override void Update() - { - base.Update(); - RelativeChildSize = new Vector2(DrawScale.X, RelativeChildSize.Y); - } - } } } diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 231250e858..05cb0f741b 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -33,11 +33,6 @@ namespace osu.Game.Rulesets.UI /// public abstract class RulesetContainer : Container { - /// - /// Whether to apply adjustments to the child based on our own size. - /// - public bool AspectAdjust = true; - /// /// The selected variant. /// @@ -324,7 +319,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - Playfield.Size = AspectAdjust ? GetPlayfieldAspectAdjust() : Vector2.One; + Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea; } /// @@ -335,10 +330,17 @@ namespace osu.Game.Rulesets.UI protected virtual BeatmapProcessor CreateBeatmapProcessor() => new BeatmapProcessor(); /// - /// In some cases we want to apply changes to the relative size of our contained based on custom conditions. + /// Computes the size of the in relative coordinate space after aspect adjustments. /// - /// - protected virtual Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); //a sane default + /// The aspect-adjusted size. + protected virtual Vector2 GetAspectAdjustedSize() => Vector2.One; + + /// + /// The area of this that is available for the to use. + /// Must be specified in relative coordinate space to this . + /// This affects the final size of the but does not affect the 's scale. + /// + protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default /// /// Creates a converter to convert Beatmap to a specific mode. diff --git a/osu.Game/Rulesets/UI/ScalableContainer.cs b/osu.Game/Rulesets/UI/ScalableContainer.cs new file mode 100644 index 0000000000..43ed770f77 --- /dev/null +++ b/osu.Game/Rulesets/UI/ScalableContainer.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A which can have its internal coordinate system scaled to a specific size. + /// + public class ScalableContainer : Container + { + /// + /// The scaled content. + /// + public readonly Container ScaledContent; + + protected override Container Content => content; + private readonly Container content; + + /// + /// A which can have its internal coordinate system scaled to a specific size. + /// + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + public ScalableContainer(float? customWidth = null, float? customHeight = null) + { + AddInternal(ScaledContent = new ScaledContainer + { + CustomWidth = customWidth, + CustomHeight = customHeight, + RelativeSizeAxes = Axes.Both, + Child = content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + + public class ScaledContainer : Container + { + /// + /// The value to scale the width of the content to match. + /// If null, is used. + /// + public float? CustomWidth; + + /// + /// The value to scale the height of the content to match. + /// if null, is used. + /// + public float? CustomHeight; + + /// + /// The scale that is required for the size of the content to match and . + /// + private Vector2 sizeScale + { + get + { + if (CustomWidth.HasValue && CustomHeight.HasValue) + return Vector2.Divide(DrawSize, new Vector2(CustomWidth.Value, CustomHeight.Value)); + if (CustomWidth.HasValue) + return new Vector2(DrawSize.X / CustomWidth.Value); + if (CustomHeight.HasValue) + return new Vector2(DrawSize.Y / CustomHeight.Value); + return Vector2.One; + } + } + + /// + /// Scale the content to the required container size by multiplying by . + /// + protected override Vector2 DrawScale => sizeScale * base.DrawScale; + + protected override void Update() + { + base.Update(); + RelativeChildSize = new Vector2(CustomWidth.HasValue ? sizeScale.X : RelativeChildSize.X, CustomHeight.HasValue ? sizeScale.Y : RelativeChildSize.Y); + } + } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index e168f6daec..1c1c8f7f61 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -62,9 +62,14 @@ namespace osu.Game.Rulesets.UI.Scrolling /// Creates a new . /// /// The direction in which s in this container should scroll. - /// Whether we want our internal coordinate system to be scaled to a specified width - protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null) - : base(customWidth) + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null) + : base(customWidth, customHeight) { this.direction = direction; } diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 10b08d704d..ce3c93ebcf 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -10,8 +10,8 @@ using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.IO; using osu.Game.Configuration; +using osu.Game.IO.Archives; using osu.Game.Screens.Backgrounds; using OpenTK; using OpenTK.Graphics; @@ -62,8 +62,10 @@ namespace osu.Game.Screens.Menu if (setInfo == null) { // we need to import the default menu background beatmap - setInfo = beatmaps.Import(new OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"))); + setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")); + setInfo.Protected = true; + beatmaps.Update(setInfo); } } @@ -73,9 +75,6 @@ namespace osu.Game.Screens.Menu welcome = audio.Sample.Get(@"welcome"); seeya = audio.Sample.Get(@"seeya"); - - if (setInfo.Protected) - beatmaps.Delete(setInfo); } protected override void OnEntering(Screen last) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index e6cf1f7982..5dba10ffc1 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), - VisualSettings = new VisualSettings() + VisualSettings = new VisualSettings { Expanded = false } } }; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index e8a4bc6b27..95b464154a 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -31,6 +31,28 @@ namespace osu.Game.Screens.Play.PlayerSettings private bool expanded = true; + public bool Expanded + { + get { return expanded; } + set + { + if (expanded == value) return; + expanded = value; + + content.ClearTransforms(); + + if (expanded) + content.AutoSizeAxes = Axes.Y; + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } + + button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); + } + } + private Color4 buttonActiveColour; protected PlayerSettingsGroup() @@ -82,7 +104,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Position = new Vector2(-15, 0), Icon = FontAwesome.fa_bars, Scale = new Vector2(0.75f), - Action = toggleContentVisibility, + Action = () => Expanded = !Expanded, }, } }, @@ -111,22 +133,5 @@ namespace osu.Game.Screens.Play.PlayerSettings } protected override Container Content => content; - - private void toggleContentVisibility() - { - content.ClearTransforms(); - - expanded = !expanded; - - if (expanded) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); - } } } diff --git a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs index 6be6523175..273cceeeda 100644 --- a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Select.Leaderboards replacePlaceholder(new MessagePlaceholder(@"No records yet!")); break; case PlaceholderState.NotLoggedIn: - replacePlaceholder(new MessagePlaceholder(@"Please login to view online leaderboards!")); + replacePlaceholder(new MessagePlaceholder(@"Please sign in to view online leaderboards!")); break; case PlaceholderState.NotSupporter: replacePlaceholder(new MessagePlaceholder(@"Please invest in a supporter tag to view this leaderboard!")); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2421a4fdfe..de6847d866 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -197,8 +197,8 @@ namespace osu.Game.Screens.Select if (osu != null) Ruleset.BindTo(osu.Ruleset); - this.beatmaps.BeatmapSetAdded += onBeatmapSetAdded; - this.beatmaps.BeatmapSetRemoved += onBeatmapSetRemoved; + this.beatmaps.ItemAdded += onBeatmapSetAdded; + this.beatmaps.ItemRemoved += onBeatmapSetRemoved; this.beatmaps.BeatmapHidden += onBeatmapHidden; this.beatmaps.BeatmapRestored += onBeatmapRestored; @@ -401,8 +401,8 @@ namespace osu.Game.Screens.Select if (beatmaps != null) { - beatmaps.BeatmapSetAdded -= onBeatmapSetAdded; - beatmaps.BeatmapSetRemoved -= onBeatmapSetRemoved; + beatmaps.ItemAdded -= onBeatmapSetAdded; + beatmaps.ItemRemoved -= onBeatmapSetRemoved; beatmaps.BeatmapHidden -= onBeatmapHidden; beatmaps.BeatmapRestored -= onBeatmapRestored; } @@ -448,7 +448,7 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { - if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false) + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false) { Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo); } diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs new file mode 100644 index 0000000000..e8caf8f44a --- /dev/null +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; +using osu.Game.IO; + +namespace osu.Game.Skinning +{ + public class SkinFileInfo : INamedFileInfo + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ID { get; set; } + + public int SkinInfoID { get; set; } + + public int FileInfoID { get; set; } + + public FileInfo FileInfo { get; set; } + + [Required] + public string Filename { get; set; } + } +} diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs new file mode 100644 index 0000000000..45c8b97f63 --- /dev/null +++ b/osu.Game/Skinning/SkinInfo.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; + +namespace osu.Game.Skinning +{ + public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ID { get; set; } + + public string Name { get; set; } + + public string Creator { get; set; } + + public List Files { get; set; } + + public bool DeletePending { get; set; } + + public static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", Creator = "team osu!" }; + + public bool Equals(SkinInfo other) => other != null && ID == other.ID; + } +} diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs new file mode 100644 index 0000000000..0031968b2b --- /dev/null +++ b/osu.Game/Skinning/SkinManager.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Configuration; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.IO.Archives; + +namespace osu.Game.Skinning +{ + public class SkinManager : ArchiveModelManager + { + public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; + + public override string[] HandledExtensions => new[] { ".osk" }; + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableSkins() + { + var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + userSkins.Insert(0, SkinInfo.Default); + return userSkins; + } + + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; + + private SkinStore store; + + public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost) + : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost) + { + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public SkinInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + } +} diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs new file mode 100644 index 0000000000..ffd9873901 --- /dev/null +++ b/osu.Game/Skinning/SkinStore.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Platform; +using osu.Game.Database; + +namespace osu.Game.Skinning +{ + public class SkinStore : MutableDatabaseBackedStore + { + public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null) + : base(contextFactory, storage) + { + } + + protected override IQueryable AddIncludesForConsumption(IQueryable query) => + base.AddIncludesForConsumption(query) + .Include(s => s.Files).ThenInclude(f => f.FileInfo); + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 2489369493..aaeaaabd55 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -5,7 +5,6 @@ using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.IO; @@ -15,13 +14,6 @@ namespace osu.Game.Storyboards.Drawables { public Storyboard Storyboard { get; private set; } - private readonly Background background; - public Texture BackgroundTexture - { - get { return background.Texture; } - set { background.Texture = value; } - } - private readonly Container content; protected override Container Content => content; @@ -52,11 +44,6 @@ namespace osu.Game.Storyboards.Drawables Anchor = Anchor.Centre; Origin = Anchor.Centre; - AddInternal(background = new Background - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); AddInternal(content = new Container { Size = new Vector2(640, 480), @@ -79,10 +66,5 @@ namespace osu.Game.Storyboards.Drawables foreach (var layer in Children) layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing; } - - private class Background : Sprite - { - protected override Vector2 DrawScale => Texture != null ? new Vector2(Parent.DrawHeight / Texture.DisplayHeight) : base.DrawScale; - } } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index e2587debc9..9d4efadc81 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -14,6 +14,8 @@ namespace osu.Game.Storyboards private readonly Dictionary layers = new Dictionary(); public IEnumerable Layers => layers.Values; + public BeatmapInfo BeatmapInfo = new BeatmapInfo(); + public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable)); public Storyboard() @@ -36,28 +38,22 @@ namespace osu.Game.Storyboards /// /// Whether the beatmap's background should be hidden while this storyboard is being displayed. /// - public bool ReplacesBackground(BeatmapInfo beatmapInfo) + public bool ReplacesBackground { - var backgroundPath = beatmapInfo.BeatmapSet?.Metadata?.BackgroundFile?.ToLowerInvariant(); - if (backgroundPath == null) - return false; + get + { + var backgroundPath = BeatmapInfo.BeatmapSet?.Metadata?.BackgroundFile?.ToLowerInvariant(); + if (backgroundPath == null) + return false; - return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + } } - public float AspectRatio(BeatmapInfo beatmapInfo) - => beatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f; - public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) { var drawable = new DrawableStoryboard(this); - if (working != null) - { - var beatmapInfo = working.Beatmap.BeatmapInfo; - drawable.Width = drawable.Height * AspectRatio(beatmapInfo); - if (!ReplacesBackground(beatmapInfo)) - drawable.BackgroundTexture = working.Background; - } + drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f); return drawable; } diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs index c531edb893..5b32433467 100644 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Please login to see online scores", + Text = "Please sign in to see online scores", }; } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c0194fc79a..fd0d0aa077 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -274,13 +274,25 @@ + + + + + + + + + + + + @@ -310,6 +322,10 @@ 20171209034410_AddRulesetInfoShortName.cs + + + 20180219060912_AddSkins.cs + @@ -336,6 +352,7 @@ + @@ -348,6 +365,7 @@ + @@ -357,11 +375,7 @@ - - - - - + @@ -372,8 +386,6 @@ - - @@ -388,7 +400,6 @@ - @@ -471,7 +482,7 @@ - + @@ -844,6 +855,10 @@ + + + +