From a0c1662fb7db323ff348aca6c4d1b50f3c868b34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 17:52:55 +0900 Subject: [PATCH 01/69] Move mania's HitWindows to osu.Game --- osu.Game.Rulesets.Mania/Objects/Note.cs | 2 +- osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj | 1 - .../Judgements => osu.Game/Rulesets/Objects}/HitWindows.cs | 2 +- osu.Game/osu.Game.csproj | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) rename {osu.Game.Rulesets.Mania/Judgements => osu.Game/Rulesets/Objects}/HitWindows.cs (96%) diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 9b40a320f9..faeee8d4ee 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Objects { diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index b9e7f8e60f..eeaef31874 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -64,7 +64,6 @@ - diff --git a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs similarity index 96% rename from osu.Game.Rulesets.Mania/Judgements/HitWindows.cs rename to osu.Game/Rulesets/Objects/HitWindows.cs index 43078a926e..ab2de7558a 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -4,7 +4,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Mania.Judgements +namespace osu.Game.Rulesets.Objects { public class HitWindows { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4944613828..58908570ff 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -339,6 +339,7 @@ + From 558c53a6baa0931e223a47c6f18433602d939e5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 18:47:10 +0900 Subject: [PATCH 02/69] Give HitObject some HitWindows --- osu.Game/Rulesets/Objects/HitObject.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 160d639e8e..ae9ed2b357 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -49,6 +49,15 @@ namespace osu.Game.Rulesets.Objects [JsonIgnore] public bool Kiai { get; private set; } + private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; + + private HitWindows hitWindows; + + /// + /// The keypress hit windows for this . + /// + public HitWindows HitWindows => hitWindows ?? (hitWindows = new HitWindows(overallDifficulty)); + private readonly SortedList nestedHitObjects = new SortedList((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); [JsonIgnore] @@ -75,6 +84,9 @@ namespace osu.Game.Rulesets.Objects Kiai = effectPoint.KiaiMode; SampleControlPoint = samplePoint; + + overallDifficulty = difficulty.OverallDifficulty; + hitWindows = null; } protected virtual void CreateNestedHitObjects() From acf20c079cbd201c6dd77135b581bc625491c184 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 18:47:54 +0900 Subject: [PATCH 03/69] General improvements around usage of HitWindows for mania --- .../Objects/Drawables/DrawableHoldNote.cs | 10 ++- .../Objects/Drawables/DrawableNote.cs | 10 ++- osu.Game/Rulesets/Objects/HitWindows.cs | 72 +++++++++++-------- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 57a4888b2b..9d1088f69d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.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; using System.Linq; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; @@ -212,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (!userTriggered) { - if (timeOffset > HitObject.HitWindows.Bad / 2) + if (!HitObject.HitWindows.CanBeHit(timeOffset)) { AddJudgement(new HoldNoteTailJudgement { @@ -224,14 +223,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; } - double offset = Math.Abs(timeOffset); - - if (offset > HitObject.HitWindows.Miss / 2) + var result = HitObject.HitWindows.ResultFor(timeOffset); + if (result == null) return; AddJudgement(new HoldNoteTailJudgement { - Result = HitObject.HitWindows.ResultFor(offset) ?? HitResult.Miss, + Result = result.Value, HasBroken = holdNote.hasBroken }); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 101db0205c..a9a0741370 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.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; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; @@ -63,17 +62,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (!userTriggered) { - if (timeOffset > HitObject.HitWindows.Bad / 2) + if (!HitObject.HitWindows.CanBeHit(timeOffset)) AddJudgement(new ManiaJudgement { Result = HitResult.Miss }); return; } - double offset = Math.Abs(timeOffset); - - if (offset > HitObject.HitWindows.Miss / 2) + var result = HitObject.HitWindows.ResultFor(timeOffset); + if (result == null) return; - AddJudgement(new ManiaJudgement { Result = HitObject.HitWindows.ResultFor(offset) ?? HitResult.Miss }); + AddJudgement(new ManiaJudgement { Result = result.Value }); } protected override void UpdateState(ArmedState state) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index ab2de7558a..57e3d0a976 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.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 osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; @@ -144,57 +145,68 @@ namespace osu.Game.Rulesets.Objects /// /// Retrieves the hit result for a time offset. /// - /// The time offset. - /// The hit result, or null if the time offset results in a miss. - public HitResult? ResultFor(double hitOffset) + /// The time offset. This should always be a positive value indicating the absolute time offset. + /// The hit result, or null if doesn't result in a judgement. + public HitResult? ResultFor(double timeOffset) { - if (hitOffset <= Perfect / 2) + timeOffset = Math.Abs(timeOffset); + + if (timeOffset <= Perfect / 2) return HitResult.Perfect; - if (hitOffset <= Great / 2) + if (timeOffset <= Great / 2) return HitResult.Great; - if (hitOffset <= Good / 2) + if (timeOffset <= Good / 2) return HitResult.Good; - if (hitOffset <= Ok / 2) + if (timeOffset <= Ok / 2) return HitResult.Ok; - if (hitOffset <= Bad / 2) + if (timeOffset <= Bad / 2) return HitResult.Meh; + if (timeOffset <= Miss / 2) + return HitResult.Miss; + return null; } /// - /// Constructs new hit windows which have been multiplied by a value. + /// Given a time offset, whether the can ever be hit in the future. + /// This happens if > . /// - /// The original hit windows. + /// The time offset. + /// Whether the can be hit at any point in the future from this time offset. + public bool CanBeHit(double timeOffset) => timeOffset <= Bad / 2; + + /// + /// Multiplies all hit windows by a value. + /// + /// The hit windows to multiply. /// The value to multiply each hit window by. public static HitWindows operator *(HitWindows windows, double value) { - return new HitWindows - { - Perfect = windows.Perfect * value, - Great = windows.Great * value, - Good = windows.Good * value, - Ok = windows.Ok * value, - Bad = windows.Bad * value, - Miss = windows.Miss * value - }; + windows.Perfect *= value; + windows.Great *= value; + windows.Good *= value; + windows.Ok *= value; + windows.Bad *= value; + windows.Miss *= value; + + return windows; } /// - /// Constructs new hit windows which have been divided by a value. + /// Divides all hit windows by a value. /// - /// The original hit windows. + /// The hit windows to divide. /// The value to divide each hit window by. public static HitWindows operator /(HitWindows windows, double value) { - return new HitWindows - { - Perfect = windows.Perfect / value, - Great = windows.Great / value, - Good = windows.Good / value, - Ok = windows.Ok / value, - Bad = windows.Bad / value, - Miss = windows.Miss / value - }; + windows.Perfect /= value; + windows.Great /= value; + windows.Good /= value; + windows.Ok /= value; + windows.Bad /= value; + windows.Miss /= value; + + return windows; } } } From 70462ebee3b39bc25902a3238a0cb8ca87f16c5d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 18:53:05 +0900 Subject: [PATCH 04/69] Make HitWindows settable by derived HitObjects --- osu.Game/Rulesets/Objects/HitObject.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ae9ed2b357..64dc94fe16 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -56,7 +56,11 @@ namespace osu.Game.Rulesets.Objects /// /// The keypress hit windows for this . /// - public HitWindows HitWindows => hitWindows ?? (hitWindows = new HitWindows(overallDifficulty)); + public HitWindows HitWindows + { + get => hitWindows ?? (hitWindows = new HitWindows(overallDifficulty)); + protected set => hitWindows = value; + } private readonly SortedList nestedHitObjects = new SortedList((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); From 15fe1a7966ebc7b654c0311ae35c217bf7442a4e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 18:53:18 +0900 Subject: [PATCH 05/69] Remove mania's custom storage of HitWindows --- osu.Game.Rulesets.Mania/Objects/Note.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index faeee8d4ee..438116b363 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -1,11 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using Newtonsoft.Json; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Objects; - namespace osu.Game.Rulesets.Mania.Objects { /// @@ -13,17 +8,5 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class Note : ManiaHitObject { - /// - /// The key-press hit window for this note. - /// - [JsonIgnore] - public HitWindows HitWindows { get; protected set; } = new HitWindows(); - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - HitWindows = new HitWindows(difficulty.OverallDifficulty); - } } } From 9bc4bf33a6c9f3183b8b7c67a7db28a9a14e3c96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 18:53:30 +0900 Subject: [PATCH 06/69] Use HitWindows for taiko --- .../Objects/Drawables/DrawableHit.cs | 17 +++++------- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 26 ------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 38188f89f3..1b8d95c0cf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -38,30 +38,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { if (!userTriggered) { - if (timeOffset > HitObject.HitWindowGood) + if (!HitObject.HitWindows.CanBeHit(timeOffset)) AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); return; } - double hitOffset = Math.Abs(timeOffset); - - if (hitOffset > HitObject.HitWindowMiss) + var result = HitObject.HitWindows.ResultFor(Math.Abs(timeOffset)); + if (result == null) return; - if (!validKeyPressed) + if (!validKeyPressed || result == HitResult.Miss) AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); - else if (hitOffset < HitObject.HitWindowGood) + else { AddJudgement(new TaikoJudgement { - Result = hitOffset < HitObject.HitWindowGreat ? HitResult.Great : HitResult.Good, + Result = result.Value, Final = !HitObject.IsStrong }); SecondHitAllowed = true; } - else - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); } public override bool OnPressed(TaikoAction action) @@ -90,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (State.Value) { case ArmedState.Idle: - this.Delay(HitObject.HitWindowMiss).Expire(); + this.Delay(HitObject.HitWindows.Miss / 2).Expire(); break; case ArmedState.Miss: this.FadeOut(100) diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 531f4b82f6..c91a1f1714 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -1,35 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; - namespace osu.Game.Rulesets.Taiko.Objects { public class Hit : TaikoHitObject { - /// - /// The hit window that results in a "GREAT" hit. - /// - public double HitWindowGreat = 35; - - /// - /// The hit window that results in a "GOOD" hit. - /// - public double HitWindowGood = 80; - - /// - /// The hit window that results in a "MISS". - /// - public double HitWindowMiss = 95; - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - HitWindowGreat = BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 50, 35, 20); - HitWindowGood = BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 120, 80, 50); - HitWindowMiss = BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 135, 95, 70); - } } } From d371425c875cbbdddf932b791163d9a688fa2f78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 18:56:44 +0900 Subject: [PATCH 07/69] BAD -> MEH --- osu.Game/Rulesets/Objects/HitWindows.cs | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 57e3d0a976..a7ffd5eb72 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -64,17 +64,17 @@ namespace osu.Game.Rulesets.Objects private const double ok_max = 254; /// - /// BAD hit window at OD = 10. + /// MEH hit window at OD = 10. /// - private const double bad_min = 242; + private const double meh_min = 242; /// - /// BAD hit window at OD = 5. + /// MEH hit window at OD = 5. /// - private const double bad_mid = 272; + private const double meh_mid = 272; /// - /// BAD hit window at OD = 0. + /// MEH hit window at OD = 0. /// - private const double bad_max = 302; + private const double meh_max = 302; /// /// MISS hit window at OD = 10. @@ -112,9 +112,9 @@ namespace osu.Game.Rulesets.Objects public double Ok = ok_mid; /// - /// Hit window for a BAD hit. + /// Hit window for a MEH hit. /// - public double Bad = bad_mid; + public double Meh = meh_mid; /// /// Hit window for a MISS hit. @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Objects Great = BeatmapDifficulty.DifficultyRange(difficulty, great_max, great_mid, great_min); Good = BeatmapDifficulty.DifficultyRange(difficulty, good_max, good_mid, good_min); Ok = BeatmapDifficulty.DifficultyRange(difficulty, ok_max, ok_mid, ok_min); - Bad = BeatmapDifficulty.DifficultyRange(difficulty, bad_max, bad_mid, bad_min); + Meh = BeatmapDifficulty.DifficultyRange(difficulty, meh_max, meh_mid, meh_min); Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min); } @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Objects return HitResult.Good; if (timeOffset <= Ok / 2) return HitResult.Ok; - if (timeOffset <= Bad / 2) + if (timeOffset <= Meh / 2) return HitResult.Meh; if (timeOffset <= Miss / 2) return HitResult.Miss; @@ -169,11 +169,11 @@ namespace osu.Game.Rulesets.Objects /// /// Given a time offset, whether the can ever be hit in the future. - /// This happens if > . + /// This happens if > . /// /// The time offset. /// Whether the can be hit at any point in the future from this time offset. - public bool CanBeHit(double timeOffset) => timeOffset <= Bad / 2; + public bool CanBeHit(double timeOffset) => timeOffset <= Meh / 2; /// /// Multiplies all hit windows by a value. @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.Objects windows.Great *= value; windows.Good *= value; windows.Ok *= value; - windows.Bad *= value; + windows.Meh *= value; windows.Miss *= value; return windows; @@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Objects windows.Great /= value; windows.Good /= value; windows.Ok /= value; - windows.Bad /= value; + windows.Meh /= value; windows.Miss /= value; return windows; From e45b26c742048cfe825005832e936d7115afa96c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 19:35:44 +0900 Subject: [PATCH 08/69] Cleanup/minify HitWindows --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 12 ++ osu.Game/Rulesets/Objects/HitWindows.cs | 168 +++++++----------------- 2 files changed, 57 insertions(+), 123 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 5be786a8e2..570faaea0a 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -40,5 +40,17 @@ namespace osu.Game.Beatmaps return mid - (mid - min) * (5 - difficulty) / 5; return mid; } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// The values that define the two linear ranges. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// Value to which the difficulty value maps in the specified range. + public static double DifficultyRange(double difficulty, (double min, double mid, double max) range) + => DifficultyRange(difficulty, range.min, range.mid, range.max); } } diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index a7ffd5eb72..8fa6bb5e8b 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; @@ -9,124 +10,45 @@ namespace osu.Game.Rulesets.Objects { public class HitWindows { - #region Constants - - /// - /// PERFECT hit window at OD = 10. - /// - private const double perfect_min = 27.8; - /// - /// PERFECT hit window at OD = 5. - /// - private const double perfect_mid = 38.8; - /// - /// PERFECT hit window at OD = 0. - /// - private const double perfect_max = 44.8; - - /// - /// GREAT hit window at OD = 10. - /// - private const double great_min = 68; - /// - /// GREAT hit window at OD = 5. - /// - private const double great_mid = 98; - /// - /// GREAT hit window at OD = 0. - /// - private const double great_max = 128; - - /// - /// GOOD hit window at OD = 10. - /// - private const double good_min = 134; - /// - /// GOOD hit window at OD = 5. - /// - private const double good_mid = 164; - /// - /// GOOD hit window at OD = 0. - /// - private const double good_max = 194; - - /// - /// OK hit window at OD = 10. - /// - private const double ok_min = 194; - /// - /// OK hit window at OD = 5. - /// - private const double ok_mid = 224; - /// - /// OK hit window at OD = 0. - /// - private const double ok_max = 254; - - /// - /// MEH hit window at OD = 10. - /// - private const double meh_min = 242; - /// - /// MEH hit window at OD = 5. - /// - private const double meh_mid = 272; - /// - /// MEH hit window at OD = 0. - /// - private const double meh_max = 302; - - /// - /// MISS hit window at OD = 10. - /// - private const double miss_min = 316; - /// - /// MISS hit window at OD = 5. - /// - private const double miss_mid = 346; - /// - /// MISS hit window at OD = 0. - /// - private const double miss_max = 376; - - #endregion - - /// - /// Hit window for a PERFECT hit. - /// - public double Perfect = perfect_mid; - - /// - /// Hit window for a GREAT hit. - /// - public double Great = great_mid; - - /// - /// Hit window for a GOOD hit. - /// - public double Good = good_mid; - - /// - /// Hit window for an OK hit. - /// - public double Ok = ok_mid; - - /// - /// Hit window for a MEH hit. - /// - public double Meh = meh_mid; - - /// - /// Hit window for a MISS hit. - /// - public double Miss = miss_mid; - - /// - /// Constructs default hit windows. - /// - public HitWindows() + private static readonly IReadOnlyDictionary base_ranges = new Dictionary { - } + { HitResult.Perfect, (44.8, 38.8, 27.8) }, + { HitResult.Great, (128, 98, 68 ) }, + { HitResult.Good, (194, 164, 134) }, + { HitResult.Ok, (254, 224, 194) }, + { HitResult.Meh, (382, 272, 242) }, + { HitResult.Miss, (376, 346, 316) }, + }; + + /// + /// Hit window for a hit. + /// + public double Perfect; + + /// + /// Hit window for a hit. + /// + public double Great; + + /// + /// Hit window for a hit. + /// + public double Good; + + /// + /// Hit window for an hit. + /// + public double Ok; + + /// + /// Hit window for a hit. + /// + public double Meh; + + /// + /// Hit window for a hit. + /// + public double Miss; /// /// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window. @@ -134,12 +56,12 @@ namespace osu.Game.Rulesets.Objects /// The parameter. public HitWindows(double difficulty) { - Perfect = BeatmapDifficulty.DifficultyRange(difficulty, perfect_max, perfect_mid, perfect_min); - Great = BeatmapDifficulty.DifficultyRange(difficulty, great_max, great_mid, great_min); - Good = BeatmapDifficulty.DifficultyRange(difficulty, good_max, good_mid, good_min); - Ok = BeatmapDifficulty.DifficultyRange(difficulty, ok_max, ok_mid, ok_min); - Meh = BeatmapDifficulty.DifficultyRange(difficulty, meh_max, meh_mid, meh_min); - Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min); + Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); + Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); + Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); + Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]); + Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); + Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); } /// From 6976347d64c2675901e4d95940a7917270da2b74 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 20:28:59 +0900 Subject: [PATCH 09/69] Protect hit window values --- osu.Game/Rulesets/Objects/HitWindows.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 8fa6bb5e8b..1d09a3ad51 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -23,32 +23,32 @@ namespace osu.Game.Rulesets.Objects /// /// Hit window for a hit. /// - public double Perfect; + public double Perfect { get; private set; } /// /// Hit window for a hit. /// - public double Great; + public double Great { get; private set; } /// /// Hit window for a hit. /// - public double Good; + public double Good { get; private set; } /// /// Hit window for an hit. /// - public double Ok; + public double Ok { get; private set; } /// /// Hit window for a hit. /// - public double Meh; + public double Meh { get; private set; } /// /// Hit window for a hit. /// - public double Miss; + public double Miss { get; private set; } /// /// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window. From 9225e883c10f036fe8d663a5667e0138e0006a8f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 20:29:50 +0900 Subject: [PATCH 10/69] Add + use HalfHitWindow --- .../Objects/Drawables/DrawableHit.cs | 2 +- osu.Game/Rulesets/Objects/HitWindows.cs | 45 +++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 1b8d95c0cf..349e8e8fb0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (State.Value) { case ArmedState.Idle: - this.Delay(HitObject.HitWindows.Miss / 2).Expire(); + this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); break; case ArmedState.Miss: this.FadeOut(100) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 1d09a3ad51..2762be4a54 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -65,37 +65,64 @@ namespace osu.Game.Rulesets.Objects } /// - /// Retrieves the hit result for a time offset. + /// Retrieves the for a time offset. /// - /// The time offset. This should always be a positive value indicating the absolute time offset. + /// The time offset. /// The hit result, or null if doesn't result in a judgement. public HitResult? ResultFor(double timeOffset) { timeOffset = Math.Abs(timeOffset); - if (timeOffset <= Perfect / 2) + if (timeOffset <= HalfWindowFor(HitResult.Perfect)) return HitResult.Perfect; - if (timeOffset <= Great / 2) + if (timeOffset <= HalfWindowFor(HitResult.Great)) return HitResult.Great; - if (timeOffset <= Good / 2) + if (timeOffset <= HalfWindowFor(HitResult.Good)) return HitResult.Good; - if (timeOffset <= Ok / 2) + if (timeOffset <= HalfWindowFor(HitResult.Ok)) return HitResult.Ok; - if (timeOffset <= Meh / 2) + if (timeOffset <= HalfWindowFor(HitResult.Meh)) return HitResult.Meh; - if (timeOffset <= Miss / 2) + if (timeOffset <= HalfWindowFor(HitResult.Miss)) return HitResult.Miss; return null; } + /// + /// Retrieves half the hit window for a . + /// This is useful if the of the hit window for one half of the hittable range of a is required. + /// + /// The expected . + /// One half of the hit window for . + public double HalfWindowFor(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return Perfect / 2; + case HitResult.Great: + return Great / 2; + case HitResult.Good: + return Good / 2; + case HitResult.Ok: + return Ok / 2; + case HitResult.Meh: + return Meh / 2; + case HitResult.Miss: + return Miss / 2; + default: + throw new ArgumentException(nameof(result)); + } + } + /// /// Given a time offset, whether the can ever be hit in the future. /// This happens if > . /// /// The time offset. /// Whether the can be hit at any point in the future from this time offset. - public bool CanBeHit(double timeOffset) => timeOffset <= Meh / 2; + public bool CanBeHit(double timeOffset) => timeOffset <= HalfWindowFor(HitResult.Meh); /// /// Multiplies all hit windows by a value. From b15f184261885ce7ca032737cdf0043fe8895938 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 20:31:39 +0900 Subject: [PATCH 11/69] Make osu! use HitWindows --- .../Objects/Drawables/DrawableHitCircle.cs | 10 ++++-- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 36 ------------------- .../Replays/OsuAutoGenerator.cs | 18 +++++----- 3 files changed, 16 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index fcae41f55b..41f50844ed 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -72,14 +72,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (!userTriggered) { - if (timeOffset > HitObject.HitWindowFor(HitResult.Meh)) + if (!HitObject.HitWindows.CanBeHit(timeOffset)) AddJudgement(new OsuJudgement { Result = HitResult.Miss }); return; } + var result = HitObject.HitWindows.ResultFor(timeOffset); + if (result == null) + return; + AddJudgement(new OsuJudgement { - Result = HitObject.ScoreResultForOffset(Math.Abs(timeOffset)), + Result = result.Value, PositionOffset = Vector2.Zero //todo: set to correct value }); } @@ -104,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Expire(true); // override lifetime end as FadeIn may have been changed externally, causing out expiration to be too early. - LifetimeEnd = HitObject.StartTime + HitObject.HitWindowFor(HitResult.Miss); + LifetimeEnd = HitObject.StartTime + HitObject.HitWindows.HalfWindowFor(HitResult.Miss); break; case ArmedState.Miss: ApproachCircle.FadeOut(50); diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index f217ae89e9..9b9d88f0f6 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -7,7 +7,6 @@ using OpenTK; using osu.Game.Rulesets.Objects.Types; using OpenTK.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { @@ -15,11 +14,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public const double OBJECT_RADIUS = 64; - private const double hittable_range = 300; - public double HitWindow50 = 150; - public double HitWindow100 = 80; - public double HitWindow300 = 30; - public double TimePreempt = 600; public double TimeFadein = 400; @@ -45,32 +39,6 @@ namespace osu.Game.Rulesets.Osu.Objects public virtual bool NewCombo { get; set; } public int IndexInCurrentCombo { get; set; } - public double HitWindowFor(HitResult result) - { - switch (result) - { - default: - return hittable_range; - case HitResult.Meh: - return HitWindow50; - case HitResult.Good: - return HitWindow100; - case HitResult.Great: - return HitWindow300; - } - } - - public HitResult ScoreResultForOffset(double offset) - { - if (offset < HitWindowFor(HitResult.Great)) - return HitResult.Great; - if (offset < HitWindowFor(HitResult.Good)) - return HitResult.Good; - if (offset < HitWindowFor(HitResult.Meh)) - return HitResult.Meh; - return HitResult.Miss; - } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -78,10 +46,6 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); TimeFadein = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1200, 800, 300); - HitWindow50 = BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 200, 150, 100); - HitWindow100 = BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 140, 100, 60); - HitWindow300 = BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 80, 50, 20); - Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index a1658a0de2..a22ac6aed1 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -89,20 +89,20 @@ namespace osu.Game.Rulesets.Osu.Replays double endTime = (prev as IHasEndTime)?.EndTime ?? prev.StartTime; // Make the cursor stay at a hitObject as long as possible (mainly for autopilot). - if (h.StartTime - h.HitWindowFor(HitResult.Miss) > endTime + h.HitWindowFor(HitResult.Meh) + 50) + if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Miss) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(HitResult.Miss), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); } - else if (h.StartTime - h.HitWindowFor(HitResult.Meh) > endTime + h.HitWindowFor(HitResult.Meh) + 50) + else if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); } - else if (h.StartTime - h.HitWindowFor(HitResult.Good) > endTime + h.HitWindowFor(HitResult.Good) + 50) + else if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(HitResult.Good), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(HitResult.Good), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); } } From cfdeac64289c8450d4dacf964e789ecc8c63c7d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 13:38:31 +0900 Subject: [PATCH 12/69] Make hit windows settable by derived classes --- osu.Game/Rulesets/Objects/HitWindows.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 2762be4a54..6d9461e3b9 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Objects { public class HitWindows { - private static readonly IReadOnlyDictionary base_ranges = new Dictionary + private static readonly IReadOnlyDictionary base_ranges = new Dictionary { { HitResult.Perfect, (44.8, 38.8, 27.8) }, { HitResult.Great, (128, 98, 68 ) }, @@ -23,32 +23,32 @@ namespace osu.Game.Rulesets.Objects /// /// Hit window for a hit. /// - public double Perfect { get; private set; } + public double Perfect { get; protected set; } /// /// Hit window for a hit. /// - public double Great { get; private set; } + public double Great { get; protected set; } /// /// Hit window for a hit. /// - public double Good { get; private set; } + public double Good { get; protected set; } /// /// Hit window for an hit. /// - public double Ok { get; private set; } + public double Ok { get; protected set; } /// /// Hit window for a hit. /// - public double Meh { get; private set; } + public double Meh { get; protected set; } /// /// Hit window for a hit. /// - public double Miss { get; private set; } + public double Miss { get; protected set; } /// /// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window. From 802aaefe35f34165e9276078e7ba2f4775970465 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 13:54:08 +0900 Subject: [PATCH 13/69] Give rulesets a way to disable/enable perfect/ok hit results --- .../Objects/ManiaHitObject.cs | 10 +++++++ osu.Game/Rulesets/Objects/HitWindows.cs | 28 +++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 22616fa0f9..be93471bcd 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects.Types; using osu.Game.Rulesets.Objects; @@ -9,5 +11,13 @@ namespace osu.Game.Rulesets.Mania.Objects public abstract class ManiaHitObject : HitObject, IHasColumn { public virtual int Column { get; set; } + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + HitWindows.AllowsPerfect = true; + HitWindows.AllowsOk = true; + } } } diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 6d9461e3b9..1b332ee80a 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -21,35 +21,47 @@ namespace osu.Game.Rulesets.Objects }; /// - /// Hit window for a hit. + /// Hit window for a result. + /// The user can only achieve receive this result if is true. /// public double Perfect { get; protected set; } /// - /// Hit window for a hit. + /// Hit window for a result. /// public double Great { get; protected set; } /// - /// Hit window for a hit. + /// Hit window for a result. /// public double Good { get; protected set; } /// - /// Hit window for an hit. + /// Hit window for an result. + /// The user can only achieve this result if is true. /// public double Ok { get; protected set; } /// - /// Hit window for a hit. + /// Hit window for a result. /// public double Meh { get; protected set; } /// - /// Hit window for a hit. + /// Hit window for a result. /// public double Miss { get; protected set; } + /// + /// Whether it's possible to achieve a result. + /// + public bool AllowsPerfect; + + /// + /// Whether it's possible to achieve a result. + /// + public bool AllowsOk; + /// /// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window. /// @@ -73,13 +85,13 @@ namespace osu.Game.Rulesets.Objects { timeOffset = Math.Abs(timeOffset); - if (timeOffset <= HalfWindowFor(HitResult.Perfect)) + if (AllowsPerfect && timeOffset <= HalfWindowFor(HitResult.Perfect)) return HitResult.Perfect; if (timeOffset <= HalfWindowFor(HitResult.Great)) return HitResult.Great; if (timeOffset <= HalfWindowFor(HitResult.Good)) return HitResult.Good; - if (timeOffset <= HalfWindowFor(HitResult.Ok)) + if (AllowsOk && timeOffset <= HalfWindowFor(HitResult.Ok)) return HitResult.Ok; if (timeOffset <= HalfWindowFor(HitResult.Meh)) return HitResult.Meh; From 17aa915c77d05581574f48de91d2dcaa972fd069 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 13:57:45 +0900 Subject: [PATCH 14/69] Rename DifficultyRange parameters --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 570faaea0a..3bfa70711b 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -46,11 +46,11 @@ namespace osu.Game.Beatmaps /// /// The difficulty value to be mapped. /// The values that define the two linear ranges. - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, (double min, double mid, double max) range) - => DifficultyRange(difficulty, range.min, range.mid, range.max); + public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) + => DifficultyRange(difficulty, range.od0, range.od5, range.od10); } } From 3d167c40ae28ae68c73b375a5b8b85a67a142ec5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:15:47 +0900 Subject: [PATCH 15/69] Remove now unneeded Math.Abs call --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 349e8e8fb0..bf327cb491 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.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; using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -43,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; } - var result = HitObject.HitWindows.ResultFor(Math.Abs(timeOffset)); + var result = HitObject.HitWindows.ResultFor(timeOffset); if (result == null) return; From a6f1a4689ea59e8448a7fb0c617b213593bc90b5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:16:31 +0900 Subject: [PATCH 16/69] Fix incorrect value copy-pasta --- osu.Game/Rulesets/Objects/HitWindows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 1b332ee80a..1c23505e17 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Objects { HitResult.Great, (128, 98, 68 ) }, { HitResult.Good, (194, 164, 134) }, { HitResult.Ok, (254, 224, 194) }, - { HitResult.Meh, (382, 272, 242) }, + { HitResult.Meh, (302, 272, 242) }, { HitResult.Miss, (376, 346, 316) }, }; From c537af02892415957618bfc4409b89efd10f7a80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:25:44 +0900 Subject: [PATCH 17/69] Fix/improve commends --- osu.Game/Rulesets/Objects/HitWindows.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 1c23505e17..e2f95f2cf2 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Objects /// /// Retrieves half the hit window for a . - /// This is useful if the of the hit window for one half of the hittable range of a is required. + /// This is useful if the hit window for one half of the hittable range of a is required. /// /// The expected . /// One half of the hit window for . @@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Objects } /// - /// Given a time offset, whether the can ever be hit in the future. - /// This happens if > . + /// Given a time offset, whether the can ever be hit in the future with a non- result. + /// This happens if . /// /// The time offset. /// Whether the can be hit at any point in the future from this time offset. From 46284c61aeee956daa39380e8a64a5a1c7e89e4f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:25:59 +0900 Subject: [PATCH 18/69] Return HitResult.None instead of null --- .../Objects/Drawables/DrawableHoldNote.cs | 4 ++-- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs | 4 ++-- .../Objects/Drawables/DrawableHitCircle.cs | 4 ++-- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 4 ++-- osu.Game/Rulesets/Objects/HitWindows.cs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 9d1088f69d..5a9ff592bc 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -224,12 +224,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == null) + if (result == HitResult.None) return; AddJudgement(new HoldNoteTailJudgement { - Result = result.Value, + Result = result, HasBroken = holdNote.hasBroken }); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index a9a0741370..8944978bdd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -68,10 +68,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == null) + if (result == HitResult.None) return; - AddJudgement(new ManiaJudgement { Result = result.Value }); + AddJudgement(new ManiaJudgement { Result = result }); } protected override void UpdateState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 41f50844ed..959c87bbba 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -78,12 +78,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == null) + if (result == HitResult.None) return; AddJudgement(new OsuJudgement { - Result = result.Value, + Result = result, PositionOffset = Vector2.Zero //todo: set to correct value }); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index bf327cb491..63e6cfb297 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == null) + if (result == HitResult.None) return; if (!validKeyPressed || result == HitResult.Miss) @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { AddJudgement(new TaikoJudgement { - Result = result.Value, + Result = result, Final = !HitObject.IsStrong }); diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index e2f95f2cf2..0ec8389b4f 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -80,8 +80,8 @@ namespace osu.Game.Rulesets.Objects /// Retrieves the for a time offset. /// /// The time offset. - /// The hit result, or null if doesn't result in a judgement. - public HitResult? ResultFor(double timeOffset) + /// The hit result, or if doesn't result in a judgement. + public HitResult ResultFor(double timeOffset) { timeOffset = Math.Abs(timeOffset); @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Objects if (timeOffset <= HalfWindowFor(HitResult.Miss)) return HitResult.Miss; - return null; + return HitResult.None; } /// From c213e58effd6edd44de3c298f5068616f3c5d80a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:40:35 +0900 Subject: [PATCH 19/69] Make slider tails not play hitsounds --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 3bde7e790b..5dd3d7aa89 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -123,9 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = EndTime, Position = StackedEndPosition, IndexInCurrentCombo = IndexInCurrentCombo, - ComboColour = ComboColour, - Samples = Samples, - SampleControlPoint = SampleControlPoint + ComboColour = ComboColour }; AddNested(HeadCircle); From cafa605b90e6d34c30bdc687bfd9de0733efbe7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Feb 2018 14:43:07 +0900 Subject: [PATCH 20/69] Fix visual settings checkboxes playing sounds in bindable binding Move sound binding to much later in the process to avoid programmatic checkbox changes triggering interaction sounds --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 5e7dda8713..f06313c261 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -76,6 +76,16 @@ namespace osu.Game.Graphics.UserInterface Nub.Current.BindTo(Current); + Current.DisabledChanged += disabled => + { + Alpha = disabled ? 0.3f : 1; + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Current.ValueChanged += newValue => { if (newValue) @@ -83,11 +93,6 @@ namespace osu.Game.Graphics.UserInterface else sampleUnchecked?.Play(); }; - - Current.DisabledChanged += disabled => - { - Alpha = disabled ? 0.3f : 1; - }; } protected override bool OnHover(InputState state) From 789e25069fb3860b69db9c579b178613dab7b32d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Feb 2018 17:07:18 +0900 Subject: [PATCH 21/69] Fix non-visual tests not cleaning up previous executions --- .../Beatmaps/IO/ImportBeatmapTest.cs | 9 ++++----- osu.Game/Tests/CleanRunHeadlessGameHost.cs | 19 +++++++++++++++++++ osu.Game/Tests/Visual/OsuTestCase.cs | 6 +----- osu.Game/osu.Game.csproj | 1 + 4 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 osu.Game/Tests/CleanRunHeadlessGameHost.cs diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 490d4ec4d3..1ee8f6728a 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Beatmaps.IO public void TestImportWhenClosed() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new HeadlessGameHost("TestImportWhenClosed")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed")) { var osu = loadOsu(host); @@ -44,11 +44,10 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] [NonParallelizable] - [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] public void TestImportOverIPC() { - using (HeadlessGameHost host = new HeadlessGameHost("host", true)) - using (HeadlessGameHost client = new HeadlessGameHost("client", true)) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true)) + using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true)) { Assert.IsTrue(host.IsPrimaryInstance); Assert.IsFalse(client.IsPrimaryInstance); @@ -74,7 +73,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestImportWhenFileOpen() { - using (HeadlessGameHost host = new HeadlessGameHost("TestImportWhenFileOpen")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen")) { var osu = loadOsu(host); diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs new file mode 100644 index 0000000000..b6ff6dcf84 --- /dev/null +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Platform; + +namespace osu.Game.Tests +{ + /// + /// A headless host which cleans up before running (removing any remnants from a previous execution). + /// + public class CleanRunHeadlessGameHost : HeadlessGameHost + { + public CleanRunHeadlessGameHost(string gameName = @"", bool bindIPC = false, bool realtime = true) + : base(gameName, bindIPC, realtime) + { + Storage.DeleteDirectory(string.Empty); + } + } +} diff --git a/osu.Game/Tests/Visual/OsuTestCase.cs b/osu.Game/Tests/Visual/OsuTestCase.cs index f9f198a5c1..97aada2971 100644 --- a/osu.Game/Tests/Visual/OsuTestCase.cs +++ b/osu.Game/Tests/Visual/OsuTestCase.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Platform; using osu.Framework.Testing; namespace osu.Game.Tests.Visual @@ -11,11 +10,8 @@ namespace osu.Game.Tests.Visual { public override void RunTest() { - using (var host = new HeadlessGameHost($"test-{Guid.NewGuid()}", realtime: false)) - { - host.Storage.DeleteDirectory(string.Empty); + using (var host = new CleanRunHeadlessGameHost($"test-{Guid.NewGuid()}", realtime: false)) host.Run(new OsuTestCaseTestRunner(this)); - } } public class OsuTestCaseTestRunner : OsuGameBase diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a5c3fc7f38..bb9925abbc 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -855,6 +855,7 @@ + From d8da68c55fc2bb5dd22ffb22610dc81b81092f70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Feb 2018 17:22:23 +0900 Subject: [PATCH 22/69] Disable test again (accidentally re-enabled) --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 1ee8f6728a..0b49bc8bb9 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -44,6 +44,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] [NonParallelizable] + [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] public void TestImportOverIPC() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true)) From 0511728fbe2e3b89ef100353483f0b31253cfd07 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 17:38:46 +0900 Subject: [PATCH 23/69] Remove "keypress" from comment --- osu.Game/Rulesets/Objects/HitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 64dc94fe16..75cb65eff0 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Objects private HitWindows hitWindows; /// - /// The keypress hit windows for this . + /// The hit windows for this . /// public HitWindows HitWindows { From e1075665753ca9c947365deafdfccbd4aa26d562 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Feb 2018 18:05:09 +0900 Subject: [PATCH 24/69] Update user object to match new standardised api --- osu.Game/Users/User.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 777eb7beca..21c59a6aeb 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -56,19 +56,19 @@ namespace osu.Game.Users public int? Id; } - [JsonProperty(@"isAdmin")] + [JsonProperty(@"is_admin")] public bool IsAdmin; - [JsonProperty(@"isSupporter")] + [JsonProperty(@"is_supporter")] public bool IsSupporter; - [JsonProperty(@"isGMT")] + [JsonProperty(@"is_gmt")] public bool IsGMT; - [JsonProperty(@"isQAT")] + [JsonProperty(@"is_qat")] public bool IsQAT; - [JsonProperty(@"isBNG")] + [JsonProperty(@"is_bng")] public bool IsBNG; [JsonProperty(@"is_active")] @@ -107,7 +107,7 @@ namespace osu.Game.Users [JsonProperty(@"playmode")] public string PlayMode; - [JsonProperty(@"profileOrder")] + [JsonProperty(@"profile_order")] public string[] ProfileOrder; [JsonProperty(@"kudosu")] From 4699b5ad7c3da198a66636068050d809a419446b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 23:47:03 +0900 Subject: [PATCH 25/69] Fix a few code styling issues These are present in the netstandard branch (the rules aren't working there - probably using the wrong configuration). --- osu.Game/Rulesets/Objects/HitWindows.cs | 4 ++-- osu.Game/Rulesets/Objects/Types/IHasCurve.cs | 3 +++ osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 0ec8389b4f..ddd9f9b5dc 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Objects public double Good { get; protected set; } /// - /// Hit window for an result. + /// Hit window for an result. /// The user can only achieve this result if is true. /// public double Ok { get; protected set; } @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Objects /// /// Given a time offset, whether the can ever be hit in the future with a non- result. - /// This happens if . + /// This happens if is less than what is required for a result. /// /// The time offset. /// Whether the can be hit at any point in the future from this time offset. diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index 0254a829f4..7f03854ea9 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Objects.Types /// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. /// /// + /// The curve. /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. public static Vector2 PositionAt(this IHasCurve obj, double progress) => obj.Curve.PositionAt(obj.ProgressAt(progress)); @@ -42,6 +43,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// Finds the progress along the curve, accounting for repeat logic. /// + /// 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 beginning of the curve and 1 is the end of the curve. public static double ProgressAt(this IHasCurve obj, double progress) @@ -55,6 +57,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// Determines which span of the curve the progress point is on. /// + /// The curve. /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. /// [0, SpanCount) where 0 is the first run. public static int SpanAt(this IHasCurve obj, double progress) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 287e917c7b..e168f6daec 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.UI.Scrolling protected virtual bool UserScrollSpeedAdjustment => true; /// - /// The container that contains the s and s. + /// The container that contains the s. /// public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects; @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Creates a new . /// - /// The axes on which s in this container should scroll. + /// 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) From 77a8b365292ecbae4bbc45a92b450af7a31ba270 Mon Sep 17 00:00:00 2001 From: TocoToucan Date: Thu, 8 Feb 2018 22:30:15 +0300 Subject: [PATCH 26/69] Schedule Resume method call without delay --- osu.Game/Screens/Play/PauseContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index ac497868bf..78ccd9a66a 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play AddInternal(pauseOverlay = new PauseOverlay { - OnResume = () => this.Delay(400).Schedule(Resume), + OnResume = () => Schedule(Resume), OnRetry = () => OnRetry(), OnQuit = () => OnQuit(), }); From cc6114b344d168ce9f7b6a08a515e30ceabed4bf Mon Sep 17 00:00:00 2001 From: Bang Sunghwan Date: Fri, 9 Feb 2018 09:21:18 +0900 Subject: [PATCH 27/69] Fix ApproachRate not inheriting OverallDifficulty when it is undefined --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 5be786a8e2..e2bbf7237d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -20,7 +20,18 @@ namespace osu.Game.Beatmaps public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY; - public float ApproachRate { get; set; } = DEFAULT_DIFFICULTY; + private float? approachRate = null; + public float ApproachRate + { + get + { + return approachRate ?? OverallDifficulty; + } + set + { + approachRate = DEFAULT_DIFFICULTY; + } + } public float SliderMultiplier { get; set; } = 1; public float SliderTickRate { get; set; } = 1; From f20e3ff31d8feb9984256416819f07a8312c57df Mon Sep 17 00:00:00 2001 From: Bang Sunghwan Date: Fri, 9 Feb 2018 09:45:43 +0900 Subject: [PATCH 28/69] Fix ApproachRate setting only DEFAULT_DIFFICULTY --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index e2bbf7237d..693b05123b 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps } set { - approachRate = DEFAULT_DIFFICULTY; + approachRate = value; } } public float SliderMultiplier { get; set; } = 1; From 3894622ddcb255346b49d23f65b6bd1009ca7bc3 Mon Sep 17 00:00:00 2001 From: Bang Sunghwan Date: Fri, 9 Feb 2018 09:55:02 +0900 Subject: [PATCH 29/69] Remove redundant initializing field by default --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 693b05123b..def19eb4e0 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY; - private float? approachRate = null; + private float? approachRate; public float ApproachRate { get From 3b7018fcd69be54032d0e27fb04c68cde875f461 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 17:22:48 +0900 Subject: [PATCH 30/69] Simplify beatmap import process --- osu.Game/Beatmaps/BeatmapManager.cs | 174 ++++++++++++++-------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 44289e2400..8b8a8e197a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -194,50 +194,74 @@ namespace osu.Game.Beatmaps } private readonly object importContextLock = new object(); - private Lazy importContext; /// /// Import a beatmap from an . /// - /// The beatmap to be imported. - public BeatmapSetInfo Import(ArchiveReader archiveReader) + /// The beatmap to be imported. + public BeatmapSetInfo Import(ArchiveReader archive) { - // let's only allow one concurrent import at a time for now. + // let's only allow one concurrent import at a time for now lock (importContextLock) { var context = importContext.Value; using (var transaction = context.BeginTransaction()) { - // create local stores so we can isolate and thread safely, and share a context/transaction. - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); + // create a new set info (don't yet add to database) + var beatmapSet = createBeatmapSetInfo(archive); - BeatmapSetInfo set = importToStorage(iFiles, iBeatmaps, archiveReader); - - if (set.ID == 0) + // 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) { - iBeatmaps.Add(set); - context.SaveChanges(); + undelete(beatmaps, files, 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); + } + + beatmapSet.Files = createFileInfos(archive, new FileStore(() => context, storage)); + 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, context); + context.SaveChanges(transaction); - return set; + return beatmapSet; } } } + /// /// Import a beatmap from a . /// /// The beatmap to be imported. public void Import(BeatmapSetInfo beatmapSetInfo) { - // If we have an ID then we already exist in the database. - if (beatmapSetInfo.ID != 0) return; + lock (importContextLock) + { + var context = importContext.Value; - createBeatmapStore(createContext).Add(beatmapSetInfo); + using (var transaction = context.BeginTransaction()) + { + import(beatmapSetInfo, context); + context.SaveChanges(transaction); + } + } } /// @@ -495,6 +519,8 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + private void import(BeatmapSetInfo beatmapSet, OsuDbContext context) => createBeatmapStore(() => context).Add(beatmapSet); + /// /// Creates an from a valid storage path. /// @@ -508,49 +534,43 @@ namespace osu.Game.Beatmaps return new LegacyFilesystemReader(path); } - /// - /// Import a beamap into our local storage. - /// If the beatmap is already imported, the existing instance will be returned. - /// - /// The store to import beatmap files to. - /// The store to import beatmaps to. - /// The beatmap archive to be read. - /// The imported beatmap, or an existing instance if it is already present. - private BeatmapSetInfo importToStorage(FileStore files, BeatmapStore beatmaps, ArchiveReader reader) + private string computeBeatmapSetHash(ArchiveReader reader) { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); - if (string.IsNullOrEmpty(mapName)) - throw new InvalidOperationException("No beatmap files found in the map folder."); - // for now, concatenate all .osu files in the set to create a unique hash. MemoryStream hashable = new MemoryStream(); foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) using (Stream s = reader.GetStream(file)) s.CopyTo(hashable); - var hash = hashable.ComputeSHA2Hash(); + return hashable.ComputeSHA2Hash(); + } - // check if this beatmap has already been imported and exit early if so. - var beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == hash); + /// + /// + /// + /// + /// + private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); + if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in the map folder."); - if (beatmapSet != null) + BeatmapMetadata metadata; + using (var stream = new StreamReader(reader.GetStream(mapName))) + metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; + + return new BeatmapSetInfo { - undelete(beatmaps, files, beatmapSet); - - // ensure all files are present and accessible - foreach (var f in beatmapSet.Files) - { - if (!storage.Exists(f.FileInfo.StoragePath)) - using (Stream s = reader.GetStream(f.Filename)) - files.Add(s, false); - } - - // todo: delete any files which shouldn't exist any more. - - return beatmapSet; - } + OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, + Beatmaps = new List(), + Hash = computeBeatmapSetHash(reader), + Metadata = metadata + }; + } + private List createFileInfos(ArchiveReader reader, FileStore files) + { List fileInfos = new List(); // import files to manager @@ -562,28 +582,20 @@ namespace osu.Game.Beatmaps FileInfo = files.Add(s) }); - BeatmapMetadata metadata; + return fileInfos; + } - using (var stream = new StreamReader(reader.GetStream(mapName))) - metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; + /// + /// Import a beamap into our local storage. + /// If the beatmap is already imported, the existing instance will be returned. + /// + /// The beatmap archive to be read. + /// The imported beatmap, or an existing instance if it is already present. + private List createBeatmapDifficulties(ArchiveReader reader) + { + var beatmapInfos = new List(); - // check if a set already exists with the same online id. - if (metadata.OnlineBeatmapSetID != null) - beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == metadata.OnlineBeatmapSetID); - - if (beatmapSet == null) - beatmapSet = new BeatmapSetInfo - { - OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, - Beatmaps = new List(), - Hash = hash, - Files = fileInfos, - Metadata = metadata - }; - - var mapNames = reader.Filenames.Where(f => f.EndsWith(".osu")); - - foreach (var name in mapNames) + foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu"))) { using (var raw = reader.GetStream(name)) using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit @@ -599,36 +611,24 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - var existing = beatmaps.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.BeatmapInfo.Hash || beatmap.BeatmapInfo.OnlineBeatmapID != null && b.OnlineBeatmapID == beatmap.BeatmapInfo.OnlineBeatmapID); + RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - if (existing == null) - { - // Exclude beatmap-metadata if it's equal to beatmapset-metadata - if (metadata.Equals(beatmap.Metadata)) - beatmap.BeatmapInfo.Metadata = null; + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.Ruleset = ruleset; + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; - RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.Ruleset = ruleset; - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; - - beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); - } + beatmapInfos.Add(beatmap.BeatmapInfo); } } - return beatmapSet; + return beatmapInfos; } /// /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets() - { - return beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); - } + public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap { From a1669324688c28e5006b36ca8f5a6f72d26a1102 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 17:51:29 +0900 Subject: [PATCH 31/69] Add deletion test --- .../Beatmaps/IO/ImportBeatmapTest.cs | 29 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 9 ++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0b49bc8bb9..a7ff308c6b 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -42,6 +42,35 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public void TestImportThenDelete() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete")) + { + var osu = loadOsu(host); + + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); + + var manager = osu.Dependencies.Get(); + + var imported = manager.Import(temp); + + ensureLoaded(osu); + + manager.Delete(imported.First()); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count() == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + host.Exit(); + } + } + [Test] [NonParallelizable] [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8b8a8e197a..049be49e44 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -141,7 +141,7 @@ namespace osu.Game.Beatmaps /// This will post a notification tracking import progress. /// /// One or more beatmap locations on disk. - public void Import(params string[] paths) + public List Import(params string[] paths) { var notification = new ProgressNotification { @@ -153,18 +153,20 @@ namespace osu.Game.Beatmaps PostNotification?.Invoke(notification); + List imported = new List(); + int i = 0; foreach (string path in paths) { if (notification.State == ProgressNotificationState.Cancelled) // user requested abort - return; + return imported; try { notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; using (ArchiveReader reader = getReaderFrom(path)) - Import(reader); + imported.Add(Import(reader)); notification.Progress = (float)++i / paths.Length; @@ -191,6 +193,7 @@ namespace osu.Game.Beatmaps } notification.State = ProgressNotificationState.Completed; + return imported; } private readonly object importContextLock = new object(); From 8140ffea15830580914d7bcbbdf5444407d5ac8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 17:58:53 +0900 Subject: [PATCH 32/69] Add test for deleting then reimporting --- .../Beatmaps/IO/ImportBeatmapTest.cs | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index a7ff308c6b..581d787242 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -71,6 +71,45 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public void TestImportThenDeleteThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) + { + var osu = loadOsu(host); + + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); + + var manager = osu.Dependencies.Get(); + + var imported = manager.Import(temp); + + ensureLoaded(osu); + + manager.Delete(imported.First()); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count() == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + + temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); + var importedSecondTime = manager.Import(temp); + + ensureLoaded(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.First().ID == importedSecondTime.First().ID); + Assert.IsTrue(imported.First().Beatmaps.First().ID == importedSecondTime.First().Beatmaps.First().ID); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + host.Exit(); + } + } + [Test] [NonParallelizable] [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] @@ -166,8 +205,8 @@ namespace osu.Game.Tests.Beatmaps.IO int countBeatmaps = 0; waitForOrAssert(() => - (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == - (countBeatmaps = queryBeatmaps().Count()), + (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == + (countBeatmaps = queryBeatmaps().Count()), $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); var set = queryBeatmapSets().First(); @@ -192,7 +231,10 @@ namespace osu.Game.Tests.Beatmaps.IO private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) { - Action waitAction = () => { while (!result()) Thread.Sleep(200); }; + Action waitAction = () => + { + while (!result()) Thread.Sleep(200); + }; Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), failureMessage); } } From a771ca4077eadc196cb810c32bd1b82608c57548 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 18:02:06 +0900 Subject: [PATCH 33/69] Add try-finally to ensure host is exited --- .../Beatmaps/IO/ImportBeatmapTest.cs | 172 +++++++++--------- 1 file changed, 89 insertions(+), 83 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 581d787242..591ad1680e 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -26,19 +26,24 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed")) { - var osu = loadOsu(host); + try + { + var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); + var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + Assert.IsTrue(File.Exists(temp)); - osu.Dependencies.Get().Import(temp); + osu.Dependencies.Get().Import(temp); - ensureLoaded(osu); + ensureLoaded(osu); - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - - host.Exit(); + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + } + finally + { + host.Exit(); + } } } @@ -48,26 +53,31 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete")) { - var osu = loadOsu(host); + try + { + var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); - var manager = osu.Dependencies.Get(); + var manager = osu.Dependencies.Get(); - var imported = manager.Import(temp); + var imported = manager.Import(temp); - ensureLoaded(osu); + ensureLoaded(osu); - manager.Delete(imported.First()); + manager.Delete(imported.First()); - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count() == 1); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - - host.Exit(); + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + } + finally + { + host.Exit(); + } } } @@ -77,36 +87,41 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) { - var osu = loadOsu(host); + try + { + var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); - var manager = osu.Dependencies.Get(); + var manager = osu.Dependencies.Get(); - var imported = manager.Import(temp); + var imported = manager.Import(temp); - ensureLoaded(osu); + ensureLoaded(osu); - manager.Delete(imported.First()); + manager.Delete(imported.First()); - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count() == 1); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count() == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); - temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); - var importedSecondTime = manager.Import(temp); + temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); + var importedSecondTime = manager.Import(temp); - ensureLoaded(osu); + ensureLoaded(osu); - // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. - Assert.IsTrue(imported.First().ID == importedSecondTime.First().ID); - Assert.IsTrue(imported.First().Beatmaps.First().ID == importedSecondTime.First().Beatmaps.First().ID); + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.First().ID == importedSecondTime.First().ID); + Assert.IsTrue(imported.First().Beatmaps.First().ID == importedSecondTime.First().Beatmaps.First().ID); - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - - host.Exit(); + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + } + finally + { + host.Exit(); + } } } @@ -118,24 +133,29 @@ namespace osu.Game.Tests.Beatmaps.IO using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true)) using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true)) { - Assert.IsTrue(host.IsPrimaryInstance); - Assert.IsFalse(client.IsPrimaryInstance); + try + { + Assert.IsTrue(host.IsPrimaryInstance); + Assert.IsFalse(client.IsPrimaryInstance); - var osu = loadOsu(host); + var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); + var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + Assert.IsTrue(File.Exists(temp)); - var importer = new BeatmapIPCChannel(client); - if (!importer.ImportAsync(temp).Wait(10000)) - Assert.Fail(@"IPC took too long to send"); + var importer = new BeatmapIPCChannel(client); + if (!importer.ImportAsync(temp).Wait(10000)) + Assert.Fail(@"IPC took too long to send"); - ensureLoaded(osu); + ensureLoaded(osu); - waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); - - host.Exit(); + waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); + } + finally + { + host.Exit(); + } } } @@ -144,22 +164,21 @@ namespace osu.Game.Tests.Beatmaps.IO { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen")) { - var osu = loadOsu(host); - - var temp = prepareTempCopy(osz_path); - - Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); - - using (File.OpenRead(temp)) - osu.Dependencies.Get().Import(temp); - - ensureLoaded(osu); - - File.Delete(temp); - - Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); - - host.Exit(); + try + { + var osu = loadOsu(host); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); + using (File.OpenRead(temp)) + osu.Dependencies.Get().Import(temp); + ensureLoaded(osu); + File.Delete(temp); + Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); + } + finally + { + host.Exit(); + } } } @@ -173,58 +192,44 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = new OsuGameBase(); Task.Run(() => host.Run(osu)); - waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - return osu; } private void ensureLoaded(OsuGameBase osu, int timeout = 60000) { IEnumerable resultSets = null; - var store = osu.Dependencies.Get(); - waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(), @"BeatmapSet did not import to the database in allocated time.", timeout); //ensure we were stored to beatmap database backing... Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1)."); - IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0); IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526); //if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout); - waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout); - int countBeatmapSetBeatmaps = 0; int countBeatmaps = 0; - waitForOrAssert(() => (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == (countBeatmaps = queryBeatmaps().Count()), $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); var set = queryBeatmapSets().First(); - foreach (BeatmapInfo b in set.Beatmaps) Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID)); - Assert.IsTrue(set.Beatmaps.Count > 0); - var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); } @@ -235,6 +240,7 @@ namespace osu.Game.Tests.Beatmaps.IO { while (!result()) Thread.Sleep(200); }; + Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), failureMessage); } } From 981fa379b7d61072e9b07597de11c21728343758 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 18:02:28 +0900 Subject: [PATCH 34/69] Count() -> Count --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 591ad1680e..7183afb70e 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Beatmaps.IO manager.Delete(imported.First()); Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count() == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); temp = prepareTempCopy(osz_path); From 623ba652ed003a4d323620317077ac8857fe0ed0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 18:20:18 +0900 Subject: [PATCH 35/69] Share more code between tests --- .../Beatmaps/IO/ImportBeatmapTest.cs | 82 ++++++++----------- 1 file changed, 33 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 7183afb70e..0438229252 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -28,17 +28,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); - - var temp = prepareTempCopy(osz_path); - - Assert.IsTrue(File.Exists(temp)); - - osu.Dependencies.Get().Import(temp); - - ensureLoaded(osu); - - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + loadOszIntoOsu(loadOsu(host)); } finally { @@ -57,22 +47,9 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + var imported = loadOszIntoOsu(osu); - var manager = osu.Dependencies.Get(); - - var imported = manager.Import(temp); - - ensureLoaded(osu); - - manager.Delete(imported.First()); - - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); - - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + deleteBeatmapSet(imported, osu); } finally { @@ -91,32 +68,15 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + var imported = loadOszIntoOsu(osu); - var manager = osu.Dependencies.Get(); + deleteBeatmapSet(imported, osu); - var imported = manager.Import(temp); - - ensureLoaded(osu); - - manager.Delete(imported.First()); - - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); - - temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); - var importedSecondTime = manager.Import(temp); - - ensureLoaded(osu); + var importedSecondTime = loadOszIntoOsu(osu); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. - Assert.IsTrue(imported.First().ID == importedSecondTime.First().ID); - Assert.IsTrue(imported.First().Beatmaps.First().ID == importedSecondTime.First().Beatmaps.First().ID); - - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); } finally { @@ -141,7 +101,6 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); var importer = new BeatmapIPCChannel(client); @@ -182,6 +141,31 @@ namespace osu.Game.Tests.Beatmaps.IO } } + private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu) + { + var temp = prepareTempCopy(osz_path); + + Assert.IsTrue(File.Exists(temp)); + + var imported = osu.Dependencies.Get().Import(temp); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return imported.FirstOrDefault(); + } + + private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) + { + var manager = osu.Dependencies.Get(); + manager.Delete(imported); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + } + private string prepareTempCopy(string path) { var temp = Path.GetTempFileName(); From 541068235d212056731b4e7337f6a8f3e6662753 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 18:23:06 +0900 Subject: [PATCH 36/69] Test import twice in a row --- .../Beatmaps/IO/ImportBeatmapTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0438229252..1e97dfefa4 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -58,6 +58,35 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public void TestImportThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) + { + try + { + var osu = loadOsu(host); + + var imported = loadOszIntoOsu(osu); + var importedSecondTime = loadOszIntoOsu(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + var manager = osu.Dependencies.Get(); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportThenDeleteThenImport() { From 5e0cb9d4b9e5cc6d752da0ff7ad317fd6ba72b40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:12:29 +0900 Subject: [PATCH 37/69] Simplify beatmap store retrieval --- osu.Game/Beatmaps/BeatmapManager.cs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 049be49e44..63d9874d53 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -67,7 +67,9 @@ namespace osu.Game.Beatmaps private readonly Storage storage; - private BeatmapStore createBeatmapStore(Func context) + private BeatmapStore getBeatmapStoreWithContext(OsuDbContext context) => getBeatmapStoreWithContext(() => context); + + private BeatmapStore getBeatmapStoreWithContext(Func context) { var store = new BeatmapStore(context); store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); @@ -123,7 +125,7 @@ namespace osu.Game.Beatmaps refreshImportContext(); - beatmaps = createBeatmapStore(context); + beatmaps = getBeatmapStoreWithContext(context); files = new FileStore(context, storage); this.storage = files.Storage; @@ -368,14 +370,10 @@ namespace osu.Game.Beatmaps // 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); - // create local stores so we can isolate and thread safely, and share a context/transaction. - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); - - if (iBeatmaps.Delete(beatmapSet)) + if (getBeatmapStoreWithContext(context).Delete(beatmapSet)) { if (!beatmapSet.Protected) - iFiles.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + new FileStore(() => context, storage).Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); } context.ChangeTracker.AutoDetectChangesEnabled = true; @@ -428,10 +426,7 @@ namespace osu.Game.Beatmaps { context.ChangeTracker.AutoDetectChangesEnabled = false; - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); - - undelete(iBeatmaps, iFiles, beatmapSet); + undelete(getBeatmapStoreWithContext(context), new FileStore(() => context, storage), beatmapSet); context.ChangeTracker.AutoDetectChangesEnabled = true; context.SaveChanges(transaction); @@ -522,7 +517,7 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - private void import(BeatmapSetInfo beatmapSet, OsuDbContext context) => createBeatmapStore(() => context).Add(beatmapSet); + private void import(BeatmapSetInfo beatmapSet, OsuDbContext context) => getBeatmapStoreWithContext(context).Add(beatmapSet); /// /// Creates an from a valid storage path. From c7de79caf6a00f9dd5db3de33af93506cc3988cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:24:17 +0900 Subject: [PATCH 38/69] Remove storage class variable --- osu.Game/Beatmaps/BeatmapManager.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 63d9874d53..08cf5aeff8 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -65,7 +65,7 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private readonly Storage storage; + private FileStore getFileStoreWithContext(OsuDbContext context) => new FileStore(() => context, files.Storage); private BeatmapStore getBeatmapStoreWithContext(OsuDbContext context) => getBeatmapStoreWithContext(() => context); @@ -128,7 +128,6 @@ namespace osu.Game.Beatmaps beatmaps = getBeatmapStoreWithContext(context); files = new FileStore(context, storage); - this.storage = files.Storage; this.rulesets = rulesets; this.api = api; @@ -233,7 +232,7 @@ namespace osu.Game.Beatmaps Delete(existingOnlineId); } - beatmapSet.Files = createFileInfos(archive, new FileStore(() => context, storage)); + beatmapSet.Files = createFileInfos(archive, getFileStoreWithContext(context)); beatmapSet.Beatmaps = createBeatmapDifficulties(archive); // remove metadata from difficulties where it matches the set @@ -373,7 +372,7 @@ namespace osu.Game.Beatmaps if (getBeatmapStoreWithContext(context).Delete(beatmapSet)) { if (!beatmapSet.Protected) - new FileStore(() => context, storage).Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + getFileStoreWithContext(context).Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); } context.ChangeTracker.AutoDetectChangesEnabled = true; @@ -426,7 +425,7 @@ namespace osu.Game.Beatmaps { context.ChangeTracker.AutoDetectChangesEnabled = false; - undelete(getBeatmapStoreWithContext(context), new FileStore(() => context, storage), beatmapSet); + undelete(getBeatmapStoreWithContext(context), getFileStoreWithContext(context), beatmapSet); context.ChangeTracker.AutoDetectChangesEnabled = true; context.SaveChanges(transaction); @@ -528,7 +527,7 @@ namespace osu.Game.Beatmaps { if (ZipFile.IsZipFile(path)) // ReSharper disable once InconsistentlySynchronizedField - return new OszArchiveReader(storage.GetStream(path)); + return new OszArchiveReader(files.Storage.GetStream(path)); return new LegacyFilesystemReader(path); } From fb6dc922c62534475445e279b8e8ad389fa02cf4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:25:55 +0900 Subject: [PATCH 39/69] Reorder file --- osu.Game/Beatmaps/BeatmapManager.cs | 38 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 08cf5aeff8..51d4d6cb22 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -65,20 +65,6 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private FileStore getFileStoreWithContext(OsuDbContext context) => new FileStore(() => context, files.Storage); - - private BeatmapStore getBeatmapStoreWithContext(OsuDbContext context) => getBeatmapStoreWithContext(() => context); - - private BeatmapStore getBeatmapStoreWithContext(Func context) - { - var store = new BeatmapStore(context); - store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); - store.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - return store; - } - private readonly Func createContext; private readonly FileStore files; @@ -495,6 +481,12 @@ namespace osu.Game.Beatmaps /// A fresh instance. public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); + /// /// Perform a lookup query on available s. /// @@ -621,11 +613,19 @@ namespace osu.Game.Beatmaps return beatmapInfos; } - /// - /// Returns a list of all usable s. - /// - /// A list of available . - public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); + private FileStore getFileStoreWithContext(OsuDbContext context) => new FileStore(() => context, files.Storage); + + private BeatmapStore getBeatmapStoreWithContext(OsuDbContext context) => getBeatmapStoreWithContext(() => context); + + private BeatmapStore getBeatmapStoreWithContext(Func context) + { + var store = new BeatmapStore(context); + store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); + store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + store.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); + return store; + } protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap { From db654004b72422d53898c58198c7f39647ed0ab0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:32:18 +0900 Subject: [PATCH 40/69] Move BeatmapManagerWorkingBeatmap to its own file --- osu.Game/Beatmaps/BeatmapManager.cs | 87 +---------------- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 95 +++++++++++++++++++ osu.Game/osu.Game.csproj | 1 + 3 files changed, 97 insertions(+), 86 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 51d4d6cb22..7252bad3c4 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,31 +9,26 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Ionic.Zip; using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio.Track; using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; 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.Graphics.Textures; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Storyboards; namespace osu.Game.Beatmaps { /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public class BeatmapManager + public partial class BeatmapManager { /// /// Fired when a new becomes available in the database. @@ -627,86 +622,6 @@ namespace osu.Game.Beatmaps return store; } - protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap - { - private readonly IResourceStore store; - - public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) - : base(beatmapInfo) - { - this.store = store; - } - - protected override Beatmap GetBeatmap() - { - try - { - using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) - { - Decoder decoder = Decoder.GetDecoder(stream); - return decoder.DecodeBeatmap(stream); - } - } - catch - { - return null; - } - } - - private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; - - protected override Texture GetBackground() - { - if (Metadata?.BackgroundFile == null) - return null; - - try - { - return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); - } - catch - { - return null; - } - } - - protected override Track GetTrack() - { - try - { - var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); - return trackData == null ? null : new TrackBass(trackData); - } - catch - { - return new TrackVirtual(); - } - } - - protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); - - protected override Storyboard GetStoryboard() - { - 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); - - using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard); - } - } - catch - { - return new Storyboard(); - } - } - } - public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs new file mode 100644 index 0000000000..2fbacca5e2 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps.Formats; +using osu.Game.Graphics.Textures; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps +{ + public partial class BeatmapManager + { + protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap + { + private readonly IResourceStore store; + + public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) + : base(beatmapInfo) + { + this.store = store; + } + + protected override Beatmap GetBeatmap() + { + try + { + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + { + Decoder decoder = Decoder.GetDecoder(stream); + return decoder.DecodeBeatmap(stream); + } + } + catch + { + return null; + } + } + + private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + + protected override Texture GetBackground() + { + if (Metadata?.BackgroundFile == null) + return null; + + try + { + return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); + } + catch + { + return null; + } + } + + protected override Track GetTrack() + { + try + { + var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new TrackBass(trackData); + } + catch + { + return new TrackVirtual(); + } + } + + protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); + + protected override Storyboard GetStoryboard() + { + 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); + + using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard); + } + } + catch + { + return new Storyboard(); + } + } + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6542160b97..c16767c02c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -248,6 +248,7 @@ + From c84cb0b33c215f011b6836c1175246025f5937b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:32:28 +0900 Subject: [PATCH 41/69] Fix/add some xmldoc --- osu.Game/Beatmaps/BeatmapManager.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7252bad3c4..143ae81fa6 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -503,6 +503,9 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + /// + /// Import a into the beatmap store. + /// private void import(BeatmapSetInfo beatmapSet, OsuDbContext context) => getBeatmapStoreWithContext(context).Add(beatmapSet); /// @@ -530,10 +533,8 @@ namespace osu.Game.Beatmaps } /// - /// + /// Create a from a provided archive. /// - /// - /// private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader) { // let's make sure there are actually .osu files to import. @@ -553,6 +554,9 @@ 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(); From 867b1b5f65889b483c1be93501a3d0bc1be5c707 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:33:10 +0900 Subject: [PATCH 42/69] Move public methods up --- osu.Game/Beatmaps/BeatmapManager.cs | 99 ++++++++++++++--------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 143ae81fa6..bb3a23548a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -230,7 +230,6 @@ namespace osu.Game.Beatmaps } } - /// /// Import a beatmap from a . /// @@ -503,6 +502,55 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + public async Task ImportFromStable() + { + var stable = GetStableStorage?.Invoke(); + + if (stable == null) + { + Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + return; + } + + await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); + } + + 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; + } + /// /// Import a into the beatmap store. /// @@ -625,54 +673,5 @@ namespace osu.Game.Beatmaps store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); return store; } - - public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; - - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// - public async Task ImportFromStable() - { - var stable = GetStableStorage?.Invoke(); - - if (stable == null) - { - Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); - return; - } - - await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); - } - - 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; - } } } From d547caa04ef88b0deaf8f56320ed65381c02b6e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 19:44:17 +0900 Subject: [PATCH 43/69] Further improve xmldoc --- osu.Game/Beatmaps/BeatmapManager.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bb3a23548a..c0a5a5b39b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -120,7 +120,7 @@ namespace osu.Game.Beatmaps /// /// Import one or more from filesystem . - /// This will post a notification tracking import progress. + /// This will post notifications tracking progress. /// /// One or more beatmap locations on disk. public List Import(params string[] paths) @@ -250,6 +250,7 @@ namespace osu.Game.Beatmaps /// /// Downloads a beatmap. + /// This will post notifications tracking progress. /// /// The to be downloaded. /// Whether the beatmap should be downloaded without video. Defaults to false. @@ -361,6 +362,10 @@ namespace osu.Game.Beatmaps } } + /// + /// Restore all beatmaps that were previously deleted. + /// This will post notifications tracking progress. + /// public void UndeleteAll() { var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList(); @@ -392,6 +397,10 @@ namespace osu.Game.Beatmaps 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) @@ -502,6 +511,9 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + /// + /// Denotes whether an osu-stable installation is present to perform automated imports from. + /// public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; /// @@ -520,6 +532,10 @@ 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(); @@ -569,6 +585,9 @@ namespace osu.Game.Beatmaps return new LegacyFilesystemReader(path); } + /// + /// Create a SHA-2 hash from the provided archive based on contained beatmap filenames. + /// private string computeBeatmapSetHash(ArchiveReader reader) { // for now, concatenate all .osu files in the set to create a unique hash. From a1513351c0454b3b060fc83a3e4aac090ae63923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 20:35:54 +0900 Subject: [PATCH 44/69] Add missing licence header --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 2fbacca5e2..14a4028b44 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; using System.IO; using System.Linq; using osu.Framework.Audio.Track; From 721bb7e4dd25b719128d5e2a33f38156257fe10a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 21:31:33 +0900 Subject: [PATCH 45/69] Add proper handling for OnlineBeatmapSetID conflicts Not yet working --- .../Beatmaps/IO/ImportBeatmapTest.cs | 34 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 11 ++++++ osu.Game/Beatmaps/BeatmapStore.cs | 28 ++++++++++++--- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 1e97dfefa4..4da9cba446 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -87,6 +87,40 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public void TestImportThenImportDifferentHash() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash")) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var imported = loadOszIntoOsu(osu); + + //var change = manager.QueryBeatmapSets(_ => true).First(); + imported.Hash += "-changed"; + manager.Update(imported); + + var importedSecondTime = loadOszIntoOsu(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportThenDeleteThenImport() { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c0a5a5b39b..cbaa8a1066 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -210,7 +210,12 @@ namespace osu.Game.Beatmaps { var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); if (existingOnlineId != null) + { + // {Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962…} + Delete(existingOnlineId); + beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); + } } beatmapSet.Files = createFileInfos(archive, getFileStoreWithContext(context)); @@ -332,6 +337,12 @@ 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. diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index df71c5c0d0..f2c3eddec9 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Game.Database; @@ -50,6 +51,22 @@ namespace osu.Game.Beatmaps 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); + + var context = GetContext(); + + context.BeatmapSetInfo.Update(beatmapSet); + context.SaveChanges(); + + BeatmapSetAdded?.Invoke(beatmapSet); + } + /// /// Delete a from the database. /// @@ -126,14 +143,17 @@ namespace osu.Game.Beatmaps return true; } - public override void Cleanup() + public override void Cleanup() => Cleanup(_ => true); + + public void Cleanup(Expression> query) { var context = GetContext(); var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Metadata); + .Where(query) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Metadata); // metadata is M-N so we can't rely on cascades context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); From a86a4f49cfdcd1b0ced0c40e6382d54e768fa8bf Mon Sep 17 00:00:00 2001 From: TocoToucan Date: Fri, 9 Feb 2018 21:20:49 +0300 Subject: [PATCH 46/69] Resolve "Pressing escape on continuing causes the game to return to song select" problem without removing Delay --- osu.Game/Screens/Play/PauseContainer.cs | 8 +++++++- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index 78ccd9a66a..669bcd600c 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -36,6 +36,7 @@ namespace osu.Game.Screens.Play public int Retries { set { pauseOverlay.Retries = value; } } public bool CanPause => (CheckCanPause?.Invoke() ?? true) && Time.Current >= lastPauseActionTime + pause_cooldown; + public bool IsResuming { get; private set; } public Action OnRetry; public Action OnQuit; @@ -54,7 +55,11 @@ namespace osu.Game.Screens.Play AddInternal(pauseOverlay = new PauseOverlay { - OnResume = () => Schedule(Resume), + OnResume = () => + { + IsResuming = true; + this.Delay(400).Schedule(Resume); + }, OnRetry = () => OnRetry(), OnQuit = () => OnQuit(), }); @@ -100,6 +105,7 @@ namespace osu.Game.Screens.Play pauseOverlay.Hide(); AudioClock.Start(); + IsResuming = false; } private OsuGameBase game; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 72864482d5..7028574997 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -351,7 +351,7 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { - if (!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) + if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) && (!pauseContainer?.IsResuming ?? false)) { // In the case of replays, we may have changed the playback rate. applyRateFromMods(); From 86f200e257e6e5b81e246bd4f6e9b2c3264d22f7 Mon Sep 17 00:00:00 2001 From: TocoToucan Date: Fri, 9 Feb 2018 22:04:39 +0300 Subject: [PATCH 47/69] Fix possible nullref --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7028574997..5df88b2b23 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -362,7 +362,7 @@ namespace osu.Game.Screens.Play if (loadedSuccessfully) { - pauseContainer.Pause(); + pauseContainer?.Pause(); } return true; From c3ce015869c93b5e948fdbe1879bdeed6ea181f6 Mon Sep 17 00:00:00 2001 From: Aergwyn Date: Sun, 11 Feb 2018 11:03:01 +0100 Subject: [PATCH 48/69] fade slider ticks with hidden mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 15 +++++++++------ .../Objects/Drawables/DrawableSliderTick.cs | 14 +++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index b4dd08eadb..beabeb0a19 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.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.Linq; using osu.Framework.Graphics; @@ -47,16 +48,20 @@ namespace osu.Game.Rulesets.Osu.Mods // fade out immediately after fade in. using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - { circle.FadeOut(fadeOutDuration); - } break; case DrawableSlider slider: using (slider.BeginAbsoluteSequence(fadeOutStartTime, true)) - { slider.Body.FadeOut(longFadeDuration, Easing.Out); - } + + break; + case DrawableSliderTick sliderTick: + // slider ticks fade out over up to one second + var tickFadeOutDuration = Math.Min(sliderTick.HitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); + + using (sliderTick.BeginAbsoluteSequence(sliderTick.HitObject.StartTime - tickFadeOutDuration, true)) + sliderTick.FadeOut(tickFadeOutDuration); break; case DrawableSpinner spinner: @@ -66,9 +71,7 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Background.Hide(); using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) - { spinner.FadeOut(fadeOutDuration); - } break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 41d73a745a..baa9eac1a3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking { - private const double anim_duration = 150; + public const double ANIM_DURATION = 150; public bool Tracking { get; set; } @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdatePreemptState() { this.Animate( - d => d.FadeIn(anim_duration), - d => d.ScaleTo(0.5f).ScaleTo(1f, anim_duration * 4, Easing.OutElasticHalf) + d => d.FadeIn(ANIM_DURATION), + d => d.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf) ); } @@ -64,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.Delay(HitObject.TimePreempt).FadeOut(); break; case ArmedState.Miss: - this.FadeOut(anim_duration) - .FadeColour(Color4.Red, anim_duration / 2); + this.FadeOut(ANIM_DURATION) + .FadeColour(Color4.Red, ANIM_DURATION / 2); break; case ArmedState.Hit: - this.FadeOut(anim_duration, Easing.OutQuint) - .ScaleTo(Scale * 1.5f, anim_duration, Easing.Out); + this.FadeOut(ANIM_DURATION, Easing.OutQuint) + .ScaleTo(Scale * 1.5f, ANIM_DURATION, Easing.Out); break; } } From efeffaf634bbfe10eb19f354bd973df82bcfaf15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 11:40:24 +0900 Subject: [PATCH 49/69] Update CFS version --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 9cf68803a2..b86082334d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ install: - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y - cmd: choco install nvika -y - - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.3/CodeFileSanity.exe + - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.4/CodeFileSanity.exe before_build: - cmd: CodeFileSanity.exe - cmd: nuget restore -verbosity quiet From 264a0f59e2d91ba963ec7c4a3fd74571b7beb30f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 12:22:13 +0900 Subject: [PATCH 50/69] Fix duplicate test name --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 4da9cba446..a0ca60f1f2 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Beatmaps.IO public void TestImportThenImport() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport")) { try { From e54de0c267a85003a78570c601fd18283c8c00d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 15:25:09 +0900 Subject: [PATCH 51/69] Remove sqlite-net migration Anyone that may have benefited from this already has. --- osu.Game/Database/OsuDbContext.cs | 82 ------------------------------- 1 file changed, 82 deletions(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 0fa1f238a9..cf29ae4496 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -186,8 +186,6 @@ namespace osu.Game.Database public void Migrate() { - migrateFromSqliteNet(); - try { Database.Migrate(); @@ -197,86 +195,6 @@ namespace osu.Game.Database throw new MigrationFailedException(e); } } - - private void migrateFromSqliteNet() - { - try - { - // will fail if the database isn't in a sane EF-migrated state. - Database.ExecuteSqlCommand("SELECT MetadataID FROM BeatmapSetInfo LIMIT 1"); - } - catch - { - try - { - Database.ExecuteSqlCommand("DROP TABLE IF EXISTS __EFMigrationsHistory"); - - // will fail (intentionally) if we don't have sqlite-net data present. - Database.ExecuteSqlCommand("SELECT OnlineBeatmapSetId FROM BeatmapMetadata LIMIT 1"); - - try - { - Logger.Log("Performing migration from sqlite-net to EF...", LoggingTarget.Database, Framework.Logging.LogLevel.Important); - - // we are good to perform messy migration of data!. - Database.ExecuteSqlCommand("ALTER TABLE BeatmapDifficulty RENAME TO BeatmapDifficulty_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapMetadata RENAME TO BeatmapMetadata_Old"); - Database.ExecuteSqlCommand("ALTER TABLE FileInfo RENAME TO FileInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE KeyBinding RENAME TO KeyBinding_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetInfo RENAME TO BeatmapSetInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapInfo RENAME TO BeatmapInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetFileInfo RENAME TO BeatmapSetFileInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE RulesetInfo RENAME TO RulesetInfo_Old"); - - Database.ExecuteSqlCommand("DROP TABLE StoreVersion"); - - // perform EF migrations to create sane table structure. - Database.Migrate(); - - // copy data table by table to new structure, dropping old tables as we go. - Database.ExecuteSqlCommand("INSERT INTO FileInfo SELECT * FROM FileInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE FileInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO KeyBinding SELECT ID, [Action], Keys, RulesetID, Variant FROM KeyBinding_Old"); - Database.ExecuteSqlCommand("DROP TABLE KeyBinding_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapMetadata SELECT ID, Artist, ArtistUnicode, AudioFile, Author, BackgroundFile, PreviewTime, Source, Tags, Title, TitleUnicode FROM BeatmapMetadata_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapMetadata_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapDifficulty SELECT `ID`, `ApproachRate`, `CircleSize`, `DrainRate`, `OverallDifficulty`, `SliderMultiplier`, `SliderTickRate` FROM BeatmapDifficulty_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapDifficulty_Old"); - - Database.ExecuteSqlCommand("INSERT INTO BeatmapSetInfo SELECT ID, DeletePending, Hash, BeatmapMetadataID, OnlineBeatmapSetID, Protected FROM BeatmapSetInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapSetInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO BeatmapSetFileInfo SELECT ID, BeatmapSetInfoID, FileInfoID, Filename FROM BeatmapSetFileInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapSetFileInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO RulesetInfo SELECT ID, Available, InstantiationInfo, Name FROM RulesetInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE RulesetInfo_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapInfo SELECT ID, AudioLeadIn, BaseDifficultyID, BeatDivisor, BeatmapSetInfoID, Countdown, DistanceSpacing, GridSize, Hash, IFNULL(Hidden, 0), LetterboxInBreaks, MD5Hash, NULLIF(BeatmapMetadataID, 0), NULLIF(OnlineBeatmapID, 0), Path, RulesetID, SpecialStyle, StackLeniency, StarDifficulty, StoredBookmarks, TimelineZoom, Version, WidescreenStoryboard FROM BeatmapInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapInfo_Old"); - - Logger.Log("Migration complete!", LoggingTarget.Database, Framework.Logging.LogLevel.Important); - } - catch (Exception e) - { - throw new MigrationFailedException(e); - } - } - catch (MigrationFailedException) - { - throw; - } - catch - { - } - } - } } public class MigrationFailedException : Exception From 9ed05543d7161dc0a3d71299db5f1a665bf2f571 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 15:39:00 +0900 Subject: [PATCH 52/69] Fix post-test conditionals from being inverse of what we want to test --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index a0ca60f1f2..cade50a9f3 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -107,9 +107,8 @@ namespace osu.Game.Tests.Beatmaps.IO var importedSecondTime = loadOszIntoOsu(osu); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); From cc948d688f90453e58cc2dcdceb8ca46bd0d338e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 15:39:13 +0900 Subject: [PATCH 53/69] Fix unrelated spacing issue --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 937b204c81..a7eac27056 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -218,7 +218,7 @@ namespace osu.Game CursorOverrideContainer.Child = globalBinding = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, - Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } + Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } }; base.Content.Add(new DrawSizePreservingFillContainer { Child = CursorOverrideContainer }); From edc36381752067292b9f2faeb3921794ea5e84bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 17:55:11 +0900 Subject: [PATCH 54/69] DatabaseWriteUsage --- .../Visual/TestCasePlaySongSelect.cs | 8 +- osu.Game/Beatmaps/BeatmapManager.cs | 188 ++++++------------ osu.Game/Beatmaps/BeatmapStore.cs | 124 ++++++------ osu.Game/Configuration/SettingsStore.cs | 21 +- osu.Game/Database/DatabaseBackedStore.cs | 44 ++-- osu.Game/Database/DatabaseContextFactory.cs | 60 +++++- osu.Game/Database/DatabaseWriteUsage.cs | 28 +++ osu.Game/Database/SingletonContextFactory.cs | 21 ++ osu.Game/IO/FileStore.cs | 109 +++++----- osu.Game/Input/KeyBindingStore.cs | 40 ++-- osu.Game/OsuGameBase.cs | 20 +- osu.Game/Rulesets/RulesetStore.cs | 71 +++---- osu.Game/Rulesets/Scoring/ScoreStore.cs | 3 +- osu.Game/osu.Game.csproj | 2 + 14 files changed, 385 insertions(+), 354 deletions(-) create mode 100644 osu.Game/Database/DatabaseWriteUsage.cs create mode 100644 osu.Game/Database/SingletonContextFactory.cs diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 809de2b8db..f54eb77c6b 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -63,12 +63,10 @@ namespace osu.Game.Tests.Visual var storage = new TestStorage(@"TestCasePlaySongSelect"); // this is by no means clean. should be replacing inside of OsuGameBase somehow. - var context = new OsuDbContext(); + DatabaseContextFactory factory = new SingletonContextFactory(new OsuDbContext()); - OsuDbContext contextFactory() => context; - - dependencies.Cache(rulesets = new RulesetStore(contextFactory)); - dependencies.Cache(manager = new BeatmapManager(storage, contextFactory, rulesets, null) + dependencies.Cache(rulesets = new RulesetStore(factory)); + dependencies.Cache(manager = new BeatmapManager(storage, factory, rulesets, null) { DefaultBeatmap = defaultBeatmap = game.Beatmap.Default }); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index cbaa8a1066..4ec153c78f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private readonly Func createContext; + private readonly DatabaseContextFactory contextFactory; private readonly FileStore files; @@ -85,29 +85,18 @@ namespace osu.Game.Beatmaps /// public Func GetStableStorage { private get; set; } - private void refreshImportContext() + public BeatmapManager(Storage storage, DatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) { - lock (importContextLock) - { - importContext?.Value?.Dispose(); + this.contextFactory = contextFactory; - importContext = new Lazy(() => - { - var c = createContext(); - c.Database.AutoTransactionsEnabled = false; - return c; - }); - } - } + beatmaps = new BeatmapStore(contextFactory); - public BeatmapManager(Storage storage, Func context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) - { - createContext = context; + beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); + beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - refreshImportContext(); - - beatmaps = getBeatmapStoreWithContext(context); - files = new FileStore(context, storage); + files = new FileStore(contextFactory, storage); this.rulesets = rulesets; this.api = api; @@ -170,7 +159,6 @@ namespace osu.Game.Beatmaps { e = e.InnerException ?? e; Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); - refreshImportContext(); } } @@ -178,80 +166,57 @@ namespace osu.Game.Beatmaps return imported; } - private readonly object importContextLock = new object(); - private Lazy importContext; - /// /// Import a beatmap from an . /// /// The beatmap to be imported. public BeatmapSetInfo Import(ArchiveReader archive) { - // let's only allow one concurrent import at a time for now - lock (importContextLock) + using ( contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { - var context = importContext.Value; + // create a new set info (don't yet add to database) + var beatmapSet = createBeatmapSetInfo(archive); - using (var transaction = context.BeginTransaction()) + // 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) { - // 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(beatmaps, files, 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) - { - // {Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962…} - - Delete(existingOnlineId); - beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); - } - } - - beatmapSet.Files = createFileInfos(archive, getFileStoreWithContext(context)); - 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, context); - - context.SaveChanges(transaction); - return beatmapSet; + 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) + { + // {Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962…} + + 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 beatmapSetInfo) - { - lock (importContextLock) - { - var context = importContext.Value; - - using (var transaction = context.BeginTransaction()) - { - import(beatmapSetInfo, context); - context.SaveChanges(transaction); - } - } - } + /// The beatmap to be imported. + public void Import(BeatmapSetInfo beatmapSet) => beatmaps.Add(beatmapSet); /// /// Downloads a beatmap. @@ -350,26 +315,22 @@ namespace osu.Game.Beatmaps /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - lock (importContextLock) + using (var db = contextFactory.GetForWrite()) { - var context = importContext.Value; + var context = db.Context; - using (var transaction = context.BeginTransaction()) + 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)) { - 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 (getBeatmapStoreWithContext(context).Delete(beatmapSet)) - { - if (!beatmapSet.Protected) - getFileStoreWithContext(context).Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - } - - context.ChangeTracker.AutoDetectChangesEnabled = true; - context.SaveChanges(transaction); + if (!beatmapSet.Protected) + files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); } + + context.ChangeTracker.AutoDetectChangesEnabled = true; } } @@ -417,19 +378,11 @@ namespace osu.Game.Beatmaps if (beatmapSet.Protected) return; - lock (importContextLock) + using (var db = contextFactory.GetForWrite()) { - var context = importContext.Value; - - using (var transaction = context.BeginTransaction()) - { - context.ChangeTracker.AutoDetectChangesEnabled = false; - - undelete(getBeatmapStoreWithContext(context), getFileStoreWithContext(context), beatmapSet); - - context.ChangeTracker.AutoDetectChangesEnabled = true; - context.SaveChanges(transaction); - } + db.Context.ChangeTracker.AutoDetectChangesEnabled = false; + undelete(beatmapSet); + db.Context.ChangeTracker.AutoDetectChangesEnabled = true; } } @@ -452,7 +405,7 @@ namespace osu.Game.Beatmaps /// The store to restore beatmaps from. /// The store to restore beatmap files from. /// The beatmap to restore. - private void undelete(BeatmapStore beatmaps, FileStore files, BeatmapSetInfo beatmapSet) + private void undelete(BeatmapSetInfo beatmapSet) { if (!beatmaps.Undelete(beatmapSet)) return; @@ -578,11 +531,6 @@ namespace osu.Game.Beatmaps notification.State = ProgressNotificationState.Completed; } - /// - /// Import a into the beatmap store. - /// - private void import(BeatmapSetInfo beatmapSet, OsuDbContext context) => getBeatmapStoreWithContext(context).Add(beatmapSet); - /// /// Creates an from a valid storage path. /// @@ -689,19 +637,5 @@ namespace osu.Game.Beatmaps return beatmapInfos; } - - private FileStore getFileStoreWithContext(OsuDbContext context) => new FileStore(() => context, files.Storage); - - private BeatmapStore getBeatmapStoreWithContext(OsuDbContext context) => getBeatmapStoreWithContext(() => context); - - private BeatmapStore getBeatmapStoreWithContext(Func context) - { - var store = new BeatmapStore(context); - store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); - store.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - return store; - } } } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index f2c3eddec9..67a2bbbd90 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps public event Action BeatmapHidden; public event Action BeatmapRestored; - public BeatmapStore(Func factory) + public BeatmapStore(DatabaseContextFactory factory) : base(factory) { } @@ -31,24 +31,25 @@ namespace osu.Game.Beatmaps /// The beatmap to add. public void Add(BeatmapSetInfo beatmapSet) { - var context = GetContext(); - - foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) + using (var db = ContextFactory.GetForWrite()) { - // 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); + var context = db.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); } - - context.BeatmapSetInfo.Attach(beatmapSet); - context.SaveChanges(); - - BeatmapSetAdded?.Invoke(beatmapSet); } /// @@ -59,10 +60,8 @@ namespace osu.Game.Beatmaps { BeatmapSetRemoved?.Invoke(beatmapSet); - var context = GetContext(); - - context.BeatmapSetInfo.Update(beatmapSet); - context.SaveChanges(); + using (var usage = ContextFactory.GetForWrite()) + usage.Context.BeatmapSetInfo.Update(beatmapSet); BeatmapSetAdded?.Invoke(beatmapSet); } @@ -74,13 +73,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Delete(BeatmapSetInfo beatmapSet) { - var context = GetContext(); + using ( ContextFactory.GetForWrite()) + { + Refresh(ref beatmapSet, BeatmapSets); - Refresh(ref beatmapSet, BeatmapSets); - - if (beatmapSet.DeletePending) return false; - beatmapSet.DeletePending = true; - context.SaveChanges(); + if (beatmapSet.DeletePending) return false; + beatmapSet.DeletePending = true; + } BeatmapSetRemoved?.Invoke(beatmapSet); return true; @@ -93,13 +92,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Undelete(BeatmapSetInfo beatmapSet) { - var context = GetContext(); + using ( ContextFactory.GetForWrite()) + { + Refresh(ref beatmapSet, BeatmapSets); - Refresh(ref beatmapSet, BeatmapSets); - - if (!beatmapSet.DeletePending) return false; - beatmapSet.DeletePending = false; - context.SaveChanges(); + if (!beatmapSet.DeletePending) return false; + beatmapSet.DeletePending = false; + } BeatmapSetAdded?.Invoke(beatmapSet); return true; @@ -112,15 +111,16 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Hide(BeatmapInfo beatmap) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmap, Beatmaps); - Refresh(ref beatmap, Beatmaps); + if (beatmap.Hidden) return false; + beatmap.Hidden = true; - if (beatmap.Hidden) return false; - beatmap.Hidden = true; - context.SaveChanges(); + BeatmapHidden?.Invoke(beatmap); + } - BeatmapHidden?.Invoke(beatmap); return true; } @@ -131,13 +131,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Restore(BeatmapInfo beatmap) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmap, Beatmaps); - Refresh(ref beatmap, Beatmaps); - - if (!beatmap.Hidden) return false; - beatmap.Hidden = false; - context.SaveChanges(); + if (!beatmap.Hidden) return false; + beatmap.Hidden = false; + } BeatmapRestored?.Invoke(beatmap); return true; @@ -147,34 +147,36 @@ namespace osu.Game.Beatmaps public void Cleanup(Expression> query) { - var context = GetContext(); + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; - 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); + 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); - // 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))); + // 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))); + // 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); - context.SaveChanges(); + // cascades down to beatmaps. + context.BeatmapSetInfo.RemoveRange(purgeable); + } } - public IQueryable BeatmapSets => GetContext().BeatmapSetInfo + 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 => GetContext().BeatmapInfo + 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) diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index 9b18151c84..7b66002a79 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -12,8 +12,8 @@ namespace osu.Game.Configuration { public event Action SettingChanged; - public SettingsStore(Func createContext) - : base(createContext) + public SettingsStore(DatabaseContextFactory contextFactory) + : base(contextFactory) { } @@ -24,19 +24,16 @@ namespace osu.Game.Configuration /// An optional variant. /// public List Query(int? rulesetId = null, int? variant = null) => - GetContext().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(DatabasedSetting setting) { - var context = GetContext(); - - var newValue = setting.Value; - - Refresh(ref setting); - - setting.Value = newValue; - - context.SaveChanges(); + using (ContextFactory.GetForWrite()) + { + var newValue = setting.Value; + Refresh(ref setting); + setting.Value = newValue; + } SettingChanged?.Invoke(); } diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index ec9967e097..da66167b14 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -1,10 +1,8 @@ // 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.Threading; using Microsoft.EntityFrameworkCore; using osu.Framework.Platform; @@ -17,9 +15,7 @@ namespace osu.Game.Database /// /// Create a new instance (separate from the shared context via for performing isolated operations. /// - protected readonly Func CreateContext; - - private readonly ThreadLocal queryContext; + protected readonly DatabaseContextFactory ContextFactory; /// /// Refresh an instance potentially from a different thread with a local context-tracked instance. @@ -29,33 +25,27 @@ namespace osu.Game.Database /// A valid EF-stored type. protected virtual void Refresh(ref T obj, IEnumerable lookupSource = null) where T : class, IHasPrimaryKey { - var context = GetContext(); - - if (context.Entry(obj).State != EntityState.Detached) return; - - var id = obj.ID; - var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); - if (foundObject != null) + using (var usage = ContextFactory.GetForWrite()) { - obj = foundObject; - context.Entry(obj).Reload(); + var context = usage.Context; + + if (context.Entry(obj).State != EntityState.Detached) return; + + var id = obj.ID; + var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); + if (foundObject != null) + { + obj = foundObject; + context.Entry(obj).Reload(); + } + else + context.Add(obj); } - else - context.Add(obj); } - /// - /// Retrieve a shared context for performing lookups (or write operations on the update thread, for now). - /// - protected OsuDbContext GetContext() => queryContext.Value; - - protected DatabaseBackedStore(Func createContext, Storage storage = null) + protected DatabaseBackedStore(DatabaseContextFactory contextFactory, Storage storage = null) { - CreateContext = createContext; - - // todo: while this seems to work quite well, we need to consider that contexts could enter a state where they are never cleaned up. - queryContext = new ThreadLocal(CreateContext); - + ContextFactory = contextFactory; Storage = storage; } diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index b1917d92c4..c092ed377f 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.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.Threading; using osu.Framework.Platform; namespace osu.Game.Database @@ -11,17 +12,70 @@ namespace osu.Game.Database private const string database_name = @"client"; + private ThreadLocal threadContexts; + + private readonly object writeLock = new object(); + + private OsuDbContext writeContext; + + private volatile int currentWriteUsages; + public DatabaseContextFactory(GameHost host) { this.host = host; + recycleThreadContexts(); } - public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + /// + /// Get a context for read-only usage. + /// + public OsuDbContext Get() => threadContexts.Value; + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + public DatabaseWriteUsage GetForWrite() + { + lock (writeLock) + { + var usage = new DatabaseWriteUsage(writeContext ?? (writeContext = threadContexts.Value), usageCompleted); + Interlocked.Increment(ref currentWriteUsages); + return usage; + } + } + + private void usageCompleted(DatabaseWriteUsage usage) + { + int usages = Interlocked.Decrement(ref currentWriteUsages); + if (usages == 0) + { + writeContext.Dispose(); + writeContext = null; + + // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. + recycleThreadContexts(); + } + } + + private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); + + protected virtual OsuDbContext CreateContext() + { + var ctx = new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + ctx.Database.AutoTransactionsEnabled = false; + + return ctx; + } public void ResetDatabase() { - // todo: we probably want to make sure there are no active contexts before performing this operation. - host.Storage.DeleteDatabase(database_name); + lock (writeLock) + { + recycleThreadContexts(); + host.Storage.DeleteDatabase(database_name); + } } } } diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs new file mode 100644 index 0000000000..0dc5a4cfe9 --- /dev/null +++ b/osu.Game/Database/DatabaseWriteUsage.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 Microsoft.EntityFrameworkCore.Storage; + +namespace osu.Game.Database +{ + public class DatabaseWriteUsage : IDisposable + { + public readonly OsuDbContext Context; + private readonly IDbContextTransaction transaction; + private readonly Action usageCompleted; + + public DatabaseWriteUsage(OsuDbContext context, Action onCompleted) + { + Context = context; + transaction = Context.BeginTransaction(); + usageCompleted = onCompleted; + } + + public void Dispose() + { + Context.SaveChanges(transaction); + usageCompleted?.Invoke(this); + } + } +} diff --git a/osu.Game/Database/SingletonContextFactory.cs b/osu.Game/Database/SingletonContextFactory.cs new file mode 100644 index 0000000000..88a43dc836 --- /dev/null +++ b/osu.Game/Database/SingletonContextFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + public class SingletonContextFactory : DatabaseContextFactory + { + private readonly OsuDbContext context; + + public SingletonContextFactory(OsuDbContext context) + : base(null) + { + this.context = context; + } + + protected override OsuDbContext CreateContext() + { + return context; + } + } +} diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 31c608a5f4..1bfe4db81a 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -21,86 +21,91 @@ namespace osu.Game.IO public new Storage Storage => base.Storage; - public FileStore(Func createContext, Storage storage) : base(createContext, storage.GetStorageForDirectory(@"files")) + public FileStore(DatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files")) { Store = new StorageBackedResourceStore(Storage); } public FileInfo Add(Stream data, bool reference = true) { - var context = GetContext(); - - string hash = data.ComputeSHA2Hash(); - - var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash); - - var info = existing ?? new FileInfo { Hash = hash }; - - string path = info.StoragePath; - - // we may be re-adding a file to fix missing store entries. - if (!Storage.Exists(path)) + using (var usage = ContextFactory.GetForWrite()) { - data.Seek(0, SeekOrigin.Begin); + var context = usage.Context; - using (var output = Storage.GetStream(path, FileAccess.Write)) - data.CopyTo(output); + string hash = data.ComputeSHA2Hash(); - data.Seek(0, SeekOrigin.Begin); + var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash); + + var info = existing ?? new FileInfo { Hash = hash }; + + string path = info.StoragePath; + + // we may be re-adding a file to fix missing store entries. + if (!Storage.Exists(path)) + { + data.Seek(0, SeekOrigin.Begin); + + using (var output = Storage.GetStream(path, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + } + + if (reference || existing == null) + Reference(info); + + return info; } - - if (reference || existing == null) - Reference(info); - - return info; } - public void Reference(params FileInfo[] files) => reference(GetContext(), files); - - private void reference(OsuDbContext context, FileInfo[] files) + public void Reference(params FileInfo[] files) { - foreach (var f in files.GroupBy(f => f.ID)) + using (var usage = ContextFactory.GetForWrite()) { - var refetch = context.Find(f.First().ID) ?? f.First(); - refetch.ReferenceCount += f.Count(); - context.FileInfo.Update(refetch); - } + var context = usage.Context; - context.SaveChanges(); + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.Find(f.First().ID) ?? f.First(); + refetch.ReferenceCount += f.Count(); + context.FileInfo.Update(refetch); + } + } } - public void Dereference(params FileInfo[] files) => dereference(GetContext(), files); - - private void dereference(OsuDbContext context, FileInfo[] files) + public void Dereference(params FileInfo[] files) { - foreach (var f in files.GroupBy(f => f.ID)) + using (var usage = ContextFactory.GetForWrite()) { - var refetch = context.FileInfo.Find(f.Key); - refetch.ReferenceCount -= f.Count(); - context.FileInfo.Update(refetch); + var context = usage.Context; + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.FileInfo.Find(f.Key); + refetch.ReferenceCount -= f.Count(); + context.FileInfo.Update(refetch); + } } - - context.SaveChanges(); } public override void Cleanup() { - var context = GetContext(); - - foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) + using (var usage = ContextFactory.GetForWrite()) { - try + var context = usage.Context; + + foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) { - Storage.Delete(f.StoragePath); - context.FileInfo.Remove(f); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete beatmap {f}"); + try + { + Storage.Delete(f.StoragePath); + context.FileInfo.Remove(f); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete beatmap {f}"); + } } } - - context.SaveChanges(); } } } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 92159ab491..4aad684959 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -16,14 +16,17 @@ namespace osu.Game.Input { public event Action KeyBindingChanged; - public KeyBindingStore(Func createContext, RulesetStore rulesets, Storage storage = null) - : base(createContext, storage) + public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + : base(contextFactory, storage) { - foreach (var info in rulesets.AvailableRulesets) + using (ContextFactory.GetForWrite()) { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + foreach (var info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } } } @@ -31,10 +34,10 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - var context = GetContext(); - - using (var transaction = context.BeginTransaction()) + using (var usage = ContextFactory.GetForWrite()) { + var context = usage.Context; + // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { @@ -54,8 +57,6 @@ namespace osu.Game.Input Variant = variant }); } - - context.SaveChanges(transaction); } } @@ -66,19 +67,16 @@ namespace osu.Game.Input /// An optional variant. /// public List Query(int? rulesetId = null, int? variant = null) => - GetContext().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(KeyBinding keyBinding) { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; - - var context = GetContext(); - - Refresh(ref dbKeyBinding); - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - - context.SaveChanges(); + using (ContextFactory.GetForWrite()) + { + var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + Refresh(ref dbKeyBinding); + dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + } KeyBindingChanged?.Invoke(); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a7eac27056..505577416d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -106,12 +106,12 @@ namespace osu.Game Token = LocalConfig.Get(OsuSetting.Token) }); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory.GetContext)); - dependencies.Cache(FileStore = new FileStore(contextFactory.GetContext, Host.Storage)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory.GetContext, RulesetStore, API, Host)); - dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory.GetContext, Host, BeatmapManager, RulesetStore)); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory.GetContext, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory.GetContext)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); + dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Host)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(new OsuColour()); //this completely overrides the framework default. will need to change once we make a proper FontStore. @@ -179,8 +179,8 @@ namespace osu.Game { try { - using (var context = contextFactory.GetContext()) - context.Migrate(); + using (var db = contextFactory.GetForWrite()) + db.Context.Migrate(); } catch (MigrationFailedException e) { @@ -191,8 +191,8 @@ namespace osu.Game contextFactory.ResetDatabase(); Logger.Log("Database purged successfully.", LoggingTarget.Database, LogLevel.Important); - using (var context = contextFactory.GetContext()) - context.Migrate(); + using (var db = contextFactory.GetForWrite()) + db.Context.Migrate(); } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 01e3b6848f..f66a126211 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets loadRulesetFromFile(file); } - public RulesetStore(Func factory) + public RulesetStore(DatabaseContextFactory factory) : base(factory) { AddMissingRulesets(); @@ -56,47 +56,50 @@ namespace osu.Game.Rulesets protected void AddMissingRulesets() { - var context = GetContext(); - - var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); - - //add all legacy modes in correct order - foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) + using (var usage = ContextFactory.GetForWrite()) { - if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) - context.RulesetInfo.Add(r.RulesetInfo); - } + var context = usage.Context; - context.SaveChanges(); + var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); - //add any other modes - foreach (var r in instances.Where(r => r.LegacyID < 0)) - if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) - context.RulesetInfo.Add(r.RulesetInfo); - - context.SaveChanges(); - - //perform a consistency check - foreach (var r in context.RulesetInfo) - { - try + //add all legacy modes in correct order + foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) { - var instance = r.CreateInstance(); - - r.Name = instance.Description; - r.ShortName = instance.ShortName; - - r.Available = true; + if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) + context.RulesetInfo.Add(r.RulesetInfo); } - catch + + context.SaveChanges(); + + //add any other modes + foreach (var r in instances.Where(r => r.LegacyID < 0)) + if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) + context.RulesetInfo.Add(r.RulesetInfo); + + context.SaveChanges(); + + //perform a consistency check + foreach (var r in context.RulesetInfo) { - r.Available = false; + try + { + var instance = r.CreateInstance(); + + r.Name = instance.Description; + r.ShortName = instance.ShortName; + + r.Available = true; + } + catch + { + r.Available = false; + } } + + context.SaveChanges(); + + AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } - - context.SaveChanges(); - - AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } private static void loadRulesetFromFile(string file) diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index d21ca79736..8bde2747a2 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.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; using System.Collections.Generic; using System.IO; using osu.Framework.Platform; @@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Scoring // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ScoreIPCChannel ipc; - public ScoreStore(Storage storage, Func factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) + public ScoreStore(Storage storage, DatabaseContextFactory factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) { this.storage = storage; this.beatmaps = beatmaps; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c16767c02c..71f1629c19 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -275,7 +275,9 @@ + + From 8b37fde15b51668f5bbe4e4c3b2d6cbaae2fb459 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 19:57:21 +0900 Subject: [PATCH 55/69] Only write when writes occur Also add finaliser logic for safety. Also better threading. Also more cleanup. --- osu.Game/Beatmaps/BeatmapManager.cs | 36 +++++++------------ osu.Game/Beatmaps/BeatmapStore.cs | 17 ++++++--- osu.Game/Database/DatabaseBackedStore.cs | 3 -- osu.Game/Database/DatabaseContextFactory.cs | 39 +++++++++++++++------ osu.Game/Database/DatabaseWriteUsage.cs | 22 ++++++++++-- osu.Game/Database/OsuDbContext.cs | 2 +- osu.Game/IO/FileStore.cs | 9 +++-- osu.Game/Input/KeyBindingStore.cs | 8 +++-- 8 files changed, 86 insertions(+), 50 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4ec153c78f..41ea293938 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -172,7 +172,7 @@ namespace osu.Game.Beatmaps /// 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. + 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); @@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash); if (existingHashMatch != null) { - undelete(existingHashMatch); + Undelete(existingHashMatch); return existingHashMatch; } @@ -315,9 +315,9 @@ namespace osu.Game.Beatmaps /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - using (var db = contextFactory.GetForWrite()) + using (var usage = contextFactory.GetForWrite()) { - var context = db.Context; + var context = usage.Context; context.ChangeTracker.AutoDetectChangesEnabled = false; @@ -378,11 +378,16 @@ namespace osu.Game.Beatmaps if (beatmapSet.Protected) return; - using (var db = contextFactory.GetForWrite()) + using (var usage = contextFactory.GetForWrite()) { - db.Context.ChangeTracker.AutoDetectChangesEnabled = false; - undelete(beatmapSet); - db.Context.ChangeTracker.AutoDetectChangesEnabled = true; + 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; } } @@ -398,21 +403,6 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); - /// - /// Returns a to a usable state if it has previously been deleted but not yet purged. - /// Is a no-op for already usable beatmaps. - /// - /// The store to restore beatmaps from. - /// The store to restore beatmap files from. - /// The beatmap to restore. - private void undelete(BeatmapSetInfo beatmapSet) - { - if (!beatmaps.Undelete(beatmapSet)) return; - - if (!beatmapSet.Protected) - files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - } - /// /// Retrieve a instance for the provided /// diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 67a2bbbd90..7a1dc763f0 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -31,9 +31,9 @@ namespace osu.Game.Beatmaps /// The beatmap to add. public void Add(BeatmapSetInfo beatmapSet) { - using (var db = ContextFactory.GetForWrite()) + using (var usage = ContextFactory.GetForWrite()) { - var context = db.Context; + var context = usage.Context; foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) { @@ -48,6 +48,7 @@ namespace osu.Game.Beatmaps } context.BeatmapSetInfo.Attach(beatmapSet); + BeatmapSetAdded?.Invoke(beatmapSet); } } @@ -73,11 +74,12 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Delete(BeatmapSetInfo beatmapSet) { - using ( ContextFactory.GetForWrite()) + using (ContextFactory.GetForWrite()) { Refresh(ref beatmapSet, BeatmapSets); if (beatmapSet.DeletePending) return false; + beatmapSet.DeletePending = true; } @@ -92,11 +94,12 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Undelete(BeatmapSetInfo beatmapSet) { - using ( ContextFactory.GetForWrite()) + using (ContextFactory.GetForWrite()) { Refresh(ref beatmapSet, BeatmapSets); if (!beatmapSet.DeletePending) return false; + beatmapSet.DeletePending = false; } @@ -116,6 +119,7 @@ namespace osu.Game.Beatmaps Refresh(ref beatmap, Beatmaps); if (beatmap.Hidden) return false; + beatmap.Hidden = true; BeatmapHidden?.Invoke(beatmap); @@ -136,6 +140,7 @@ namespace osu.Game.Beatmaps Refresh(ref beatmap, Beatmaps); if (!beatmap.Hidden) return false; + beatmap.Hidden = false; } @@ -155,7 +160,9 @@ namespace osu.Game.Beatmaps .Where(query) .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Metadata); + .Include(s => s.Metadata).ToList(); + + if (!purgeable.Any()) return; // metadata is M-N so we can't rely on cascades context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index da66167b14..0b2f34f6d1 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -34,10 +34,7 @@ namespace osu.Game.Database var id = obj.ID; var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); if (foundObject != null) - { obj = foundObject; - context.Entry(obj).Reload(); - } else context.Add(obj); } diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index c092ed377f..2291374e46 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.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.Diagnostics; using System.Threading; using osu.Framework.Platform; @@ -18,6 +19,7 @@ namespace osu.Game.Database private OsuDbContext writeContext; + private bool currentWriteDidWrite; private volatile int currentWriteUsages; public DatabaseContextFactory(GameHost host) @@ -38,24 +40,41 @@ namespace osu.Game.Database /// A usage containing a usable context. public DatabaseWriteUsage GetForWrite() { - lock (writeLock) - { - var usage = new DatabaseWriteUsage(writeContext ?? (writeContext = threadContexts.Value), usageCompleted); - Interlocked.Increment(ref currentWriteUsages); - return usage; - } + Monitor.Enter(writeLock); + + Trace.Assert(currentWriteUsages == 0, "Database writes in a bad state"); + Interlocked.Increment(ref currentWriteUsages); + + return new DatabaseWriteUsage(writeContext ?? (writeContext = threadContexts.Value), usageCompleted); } private void usageCompleted(DatabaseWriteUsage usage) { int usages = Interlocked.Decrement(ref currentWriteUsages); - if (usages == 0) + + try { - writeContext.Dispose(); + currentWriteDidWrite |= usage.PerformedWrite; + + if (usages > 0) return; + + + if (currentWriteDidWrite) + { + writeContext.Dispose(); + currentWriteDidWrite = false; + + // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. + recycleThreadContexts(); + } + + // always set to null (even when a write didn't occur) so we get the correct thread context on next write request. writeContext = null; - // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. - recycleThreadContexts(); + } + finally + { + Monitor.Exit(writeLock); } } diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index 0dc5a4cfe9..52dd0ee268 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -19,10 +19,28 @@ namespace osu.Game.Database usageCompleted = onCompleted; } + public bool PerformedWrite { get; private set; } + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) return; + isDisposed = true; + + PerformedWrite |= Context.SaveChanges(transaction) > 0; + usageCompleted?.Invoke(this); + } + public void Dispose() { - Context.SaveChanges(transaction); - usageCompleted?.Invoke(this); + Dispose(true); + GC.SuppressFinalize(this); + } + + ~DatabaseWriteUsage() + { + Dispose(false); } } } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index cf29ae4496..e83b30595e 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -111,7 +111,7 @@ namespace osu.Game.Database public int SaveChanges(IDbContextTransaction transaction = null) { var ret = base.SaveChanges(); - transaction?.Commit(); + if (ret > 0) transaction?.Commit(); return ret; } diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 1bfe4db81a..9889088dc4 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -30,11 +30,9 @@ namespace osu.Game.IO { using (var usage = ContextFactory.GetForWrite()) { - var context = usage.Context; - string hash = data.ComputeSHA2Hash(); - var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash); + var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash); var info = existing ?? new FileInfo { Hash = hash }; @@ -60,6 +58,8 @@ namespace osu.Game.IO public void Reference(params FileInfo[] files) { + if (files.Length == 0) return; + using (var usage = ContextFactory.GetForWrite()) { var context = usage.Context; @@ -75,9 +75,12 @@ namespace osu.Game.IO public void Dereference(params FileInfo[] files) { + if (files.Length == 0) return; + using (var usage = ContextFactory.GetForWrite()) { var context = usage.Context; + foreach (var f in files.GroupBy(f => f.ID)) { var refetch = context.FileInfo.Find(f.Key); diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 4aad684959..33cb0911a8 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -36,8 +36,6 @@ namespace osu.Game.Input { using (var usage = ContextFactory.GetForWrite()) { - var context = usage.Context; - // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { @@ -49,7 +47,7 @@ namespace osu.Game.Input foreach (var insertable in group.Skip(count).Take(aimCount - count)) // insert any defaults which are missing. - context.DatabasedKeyBinding.Add(new DatabasedKeyBinding + usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding { KeyCombination = insertable.KeyCombination, Action = insertable.Action, @@ -75,6 +73,10 @@ namespace osu.Game.Input { var dbKeyBinding = (DatabasedKeyBinding)keyBinding; Refresh(ref dbKeyBinding); + + if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) + return; + dbKeyBinding.KeyCombination = keyBinding.KeyCombination; } From 64cda9fd0f6ae85ea42838ff4144b41eb1ec2b56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 22:16:09 +0900 Subject: [PATCH 56/69] Remove incorrect assert assumption --- osu.Game/Database/DatabaseContextFactory.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 2291374e46..eaeea0b35e 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.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.Diagnostics; using System.Threading; using osu.Framework.Platform; @@ -42,7 +41,6 @@ namespace osu.Game.Database { Monitor.Enter(writeLock); - Trace.Assert(currentWriteUsages == 0, "Database writes in a bad state"); Interlocked.Increment(ref currentWriteUsages); return new DatabaseWriteUsage(writeContext ?? (writeContext = threadContexts.Value), usageCompleted); From a738664167a579f726828ea9b0cc445b0b1a939d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Feb 2018 23:10:05 +0900 Subject: [PATCH 57/69] Add interface for database context factory --- .../Visual/TestCasePlaySongSelect.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Beatmaps/BeatmapStore.cs | 2 +- osu.Game/Database/DatabaseBackedStore.cs | 4 ++-- osu.Game/Database/DatabaseContextFactory.cs | 2 +- osu.Game/Database/IDatabaseContextFactory.cs | 20 +++++++++++++++++++ osu.Game/Database/SingletonContextFactory.cs | 10 ++++------ osu.Game/IO/FileStore.cs | 2 +- osu.Game/Rulesets/RulesetStore.cs | 2 +- osu.Game/osu.Game.csproj | 1 + 10 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Database/IDatabaseContextFactory.cs diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index f54eb77c6b..8bb0d152f6 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual var storage = new TestStorage(@"TestCasePlaySongSelect"); // this is by no means clean. should be replacing inside of OsuGameBase somehow. - DatabaseContextFactory factory = new SingletonContextFactory(new OsuDbContext()); + IDatabaseContextFactory factory = new SingletonContextFactory(new OsuDbContext()); dependencies.Cache(rulesets = new RulesetStore(factory)); dependencies.Cache(manager = new BeatmapManager(storage, factory, rulesets, null) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 41ea293938..5748062fd5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private readonly DatabaseContextFactory contextFactory; + private readonly IDatabaseContextFactory contextFactory; private readonly FileStore files; @@ -85,7 +85,7 @@ namespace osu.Game.Beatmaps /// public Func GetStableStorage { private get; set; } - public BeatmapManager(Storage storage, DatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) { this.contextFactory = contextFactory; diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 7a1dc763f0..29373c0715 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps public event Action BeatmapHidden; public event Action BeatmapRestored; - public BeatmapStore(DatabaseContextFactory factory) + public BeatmapStore(IDatabaseContextFactory factory) : base(factory) { } diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index 0b2f34f6d1..cf46b66422 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -15,7 +15,7 @@ namespace osu.Game.Database /// /// Create a new instance (separate from the shared context via for performing isolated operations. /// - protected readonly DatabaseContextFactory ContextFactory; + protected readonly IDatabaseContextFactory ContextFactory; /// /// Refresh an instance potentially from a different thread with a local context-tracked instance. @@ -40,7 +40,7 @@ namespace osu.Game.Database } } - protected DatabaseBackedStore(DatabaseContextFactory contextFactory, Storage storage = null) + protected DatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) { ContextFactory = contextFactory; Storage = storage; diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index eaeea0b35e..002e9e456d 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -6,7 +6,7 @@ using osu.Framework.Platform; namespace osu.Game.Database { - public class DatabaseContextFactory + public class DatabaseContextFactory : IDatabaseContextFactory { private readonly GameHost host; diff --git a/osu.Game/Database/IDatabaseContextFactory.cs b/osu.Game/Database/IDatabaseContextFactory.cs new file mode 100644 index 0000000000..bc1bc0349c --- /dev/null +++ b/osu.Game/Database/IDatabaseContextFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + public interface IDatabaseContextFactory + { + /// + /// Get a context for read-only usage. + /// + OsuDbContext Get(); + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + DatabaseWriteUsage GetForWrite(); + } +} diff --git a/osu.Game/Database/SingletonContextFactory.cs b/osu.Game/Database/SingletonContextFactory.cs index 88a43dc836..067e4fd8eb 100644 --- a/osu.Game/Database/SingletonContextFactory.cs +++ b/osu.Game/Database/SingletonContextFactory.cs @@ -3,19 +3,17 @@ namespace osu.Game.Database { - public class SingletonContextFactory : DatabaseContextFactory + public class SingletonContextFactory : IDatabaseContextFactory { private readonly OsuDbContext context; public SingletonContextFactory(OsuDbContext context) - : base(null) { this.context = context; } - protected override OsuDbContext CreateContext() - { - return context; - } + public OsuDbContext Get() => context; + + public DatabaseWriteUsage GetForWrite() => new DatabaseWriteUsage(context, null); } } diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 9889088dc4..ab81ba4851 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -21,7 +21,7 @@ namespace osu.Game.IO public new Storage Storage => base.Storage; - public FileStore(DatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files")) + public FileStore(IDatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files")) { Store = new StorageBackedResourceStore(Storage); } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index f66a126211..92fbf25f04 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets loadRulesetFromFile(file); } - public RulesetStore(DatabaseContextFactory factory) + public RulesetStore(IDatabaseContextFactory factory) : base(factory) { AddMissingRulesets(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 71f1629c19..02801eb81f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -276,6 +276,7 @@ + From 8d313486b3e03dc05e8f27327bba14fc3a7f59ba Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 13 Feb 2018 00:40:34 +1030 Subject: [PATCH 58/69] Add a confirmation dialog to the Delete option in the beatmap context menu --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d8cfd79e12..6933f5503a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -25,10 +25,10 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu { - private Action deleteRequested; private Action restoreHiddenRequested; private Action viewDetails; + private DialogOverlay dialogOverlay; private readonly BeatmapSetInfo beatmapSet; public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) @@ -38,13 +38,13 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader(true)] - private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) + private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay, DialogOverlay overlay) { if (localisation == null) throw new ArgumentNullException(nameof(localisation)); restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); - deleteRequested = manager.Delete; + dialogOverlay = overlay; if (beatmapOverlay != null) viewDetails = beatmapOverlay.ShowBeatmapSet; @@ -89,6 +89,12 @@ namespace osu.Game.Screens.Select.Carousel }; } + private void delete(BeatmapSetInfo beatmap) + { + if (beatmap == null) return; + dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); + } + public MenuItem[] ContextMenuItems { get @@ -104,7 +110,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet))); - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => deleteRequested?.Invoke(beatmapSet))); + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => delete(beatmapSet))); return items.ToArray(); } From e8e093d6f2bda07dfb3cc596b553044db91c9262 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 14:54:01 +0900 Subject: [PATCH 59/69] Fix incorrect xmldoc --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 5748062fd5..be04a78034 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -535,7 +535,7 @@ namespace osu.Game.Beatmaps } /// - /// Create a SHA-2 hash from the provided archive based on contained beatmap filenames. + /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// private string computeBeatmapSetHash(ArchiveReader reader) { From 35613263064a696f5900b51de7bccc73139c2796 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 14:54:46 +0900 Subject: [PATCH 60/69] Remove fixed issue --- osu.Game/Beatmaps/BeatmapManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index be04a78034..40b63ffa39 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -191,8 +191,6 @@ namespace osu.Game.Beatmaps var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); if (existingOnlineId != null) { - // {Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962…} - Delete(existingOnlineId); beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); } From d603d032d59abcd32cbdce3bfa7503dca4541705 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 13 Feb 2018 16:26:05 +1030 Subject: [PATCH 61/69] Inlined delete beatmap dialog --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 6933f5503a..5204b7d787 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -89,12 +89,6 @@ namespace osu.Game.Screens.Select.Carousel }; } - private void delete(BeatmapSetInfo beatmap) - { - if (beatmap == null) return; - dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); - } - public MenuItem[] ContextMenuItems { get @@ -110,7 +104,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet))); - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => delete(beatmapSet))); + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); } From ab34123ba81fe37c62aaf8e500ec42346a17ea23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 14:58:15 +0900 Subject: [PATCH 62/69] Remove unnecessary class variable --- osu.Game/Database/DatabaseContextFactory.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 002e9e456d..d8044e6eb1 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -16,8 +16,6 @@ namespace osu.Game.Database private readonly object writeLock = new object(); - private OsuDbContext writeContext; - private bool currentWriteDidWrite; private volatile int currentWriteUsages; @@ -43,7 +41,7 @@ namespace osu.Game.Database Interlocked.Increment(ref currentWriteUsages); - return new DatabaseWriteUsage(writeContext ?? (writeContext = threadContexts.Value), usageCompleted); + return new DatabaseWriteUsage(threadContexts.Value, usageCompleted); } private void usageCompleted(DatabaseWriteUsage usage) @@ -56,19 +54,12 @@ namespace osu.Game.Database if (usages > 0) return; - if (currentWriteDidWrite) { - writeContext.Dispose(); currentWriteDidWrite = false; - // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. recycleThreadContexts(); } - - // always set to null (even when a write didn't occur) so we get the correct thread context on next write request. - writeContext = null; - } finally { @@ -76,7 +67,14 @@ namespace osu.Game.Database } } - private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); + private void recycleThreadContexts() + { + if (threadContexts != null) + foreach (var context in threadContexts.Values) + context.Dispose(); + + threadContexts = new ThreadLocal(CreateContext, true); + } protected virtual OsuDbContext CreateContext() { From 50cdb03cd9e55911c8727467dad7ebb18b4e35b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 15:08:45 +0900 Subject: [PATCH 63/69] Don't dispose read contexts --- osu.Game/Database/DatabaseContextFactory.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index d8044e6eb1..2068d6bd8a 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -56,7 +56,11 @@ namespace osu.Game.Database if (currentWriteDidWrite) { + // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). + usage.Context.Dispose(); + currentWriteDidWrite = false; + // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. recycleThreadContexts(); } @@ -67,14 +71,7 @@ namespace osu.Game.Database } } - private void recycleThreadContexts() - { - if (threadContexts != null) - foreach (var context in threadContexts.Values) - context.Dispose(); - - threadContexts = new ThreadLocal(CreateContext, true); - } + private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); protected virtual OsuDbContext CreateContext() { From 8c42225646402eb02079d9046e07f1370fc6f3f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 15:08:51 +0900 Subject: [PATCH 64/69] Fix outdated xmldoc --- osu.Game/Beatmaps/BeatmapManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 40b63ffa39..47773528a6 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -588,11 +588,8 @@ namespace osu.Game.Beatmaps } /// - /// Import a beamap into our local storage. - /// If the beatmap is already imported, the existing instance will be returned. + /// Create all required s for the provided archive. /// - /// The beatmap archive to be read. - /// The imported beatmap, or an existing instance if it is already present. private List createBeatmapDifficulties(ArchiveReader reader) { var beatmapInfos = new List(); From e2d11af4ef133317810f0bb0bd39419ff1183c74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 19:59:32 +0900 Subject: [PATCH 65/69] Fix incorrect MusicController drag handling --- osu.Game/Overlays/MusicController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b3140d8bd0..74f6e4435d 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -65,9 +64,12 @@ namespace osu.Game.Overlays AlwaysPresent = true; } + private Vector2 dragStart; + protected override bool OnDragStart(InputState state) { base.OnDragStart(state); + dragStart = state.Mouse.Position; return true; } @@ -75,9 +77,7 @@ namespace osu.Game.Overlays { if (base.OnDrag(state)) return true; - Trace.Assert(state.Mouse.PositionMouseDown != null, "state.Mouse.PositionMouseDown != null"); - - Vector2 change = state.Mouse.Position - state.Mouse.PositionMouseDown.Value; + Vector2 change = state.Mouse.Position - dragStart; // Diminish the drag distance as we go further to simulate "rubber band" feeling. change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; From f4ddc0717bfd31c0dbb8f4773ab9829e5b6822ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 19:59:39 +0900 Subject: [PATCH 66/69] Fix incorrect PlaylistItem drag handling --- osu.Game/Overlays/Music/PlaylistItem.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 34dcc36699..71fdcff6af 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -34,7 +34,19 @@ namespace osu.Game.Overlays.Music public Action OnSelect; - public bool IsDraggable => handle.IsHovered; + public bool IsDraggable { get; private set; } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + IsDraggable = handle.IsHovered; + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + IsDraggable = false; + return base.OnMouseUp(state, args); + } private bool selected; public bool Selected From 86acc8498878a4aa3fbdee928381c1aaa75b4f53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Feb 2018 20:23:51 +0900 Subject: [PATCH 67/69] Apply formatting --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 78cdf1b5ce..16e6692887 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -20,18 +20,15 @@ namespace osu.Game.Beatmaps public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY; + private float? approachRate; + public float ApproachRate { - get - { - return approachRate ?? OverallDifficulty; - } - set - { - approachRate = value; - } + get => approachRate ?? OverallDifficulty; + set => approachRate = value; } + public float SliderMultiplier { get; set; } = 1; public float SliderTickRate { get; set; } = 1; From bf6c93e77e3efbe75c23e359e8300b1cde774d26 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Feb 2018 21:40:07 +0900 Subject: [PATCH 68/69] Update framework --- osu-framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu-framework b/osu-framework index eba12eb4a0..63c9440bfb 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit eba12eb4a0fa6238873dd266deb35bfdece21a6a +Subproject commit 63c9440bfbd2bfb36f14c9ee0a521a6c46849cec From 20de3c421a0f395470982f04047eb5b141466630 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Feb 2018 17:43:45 +0900 Subject: [PATCH 69/69] Update framework --- osu-framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu-framework b/osu-framework index 63c9440bfb..52b97a5832 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 63c9440bfbd2bfb36f14c9ee0a521a6c46849cec +Subproject commit 52b97a5832410eb868421fd7c55cdb67620330d0