From a0c1662fb7db323ff348aca6c4d1b50f3c868b34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Feb 2018 17:52:55 +0900 Subject: [PATCH 01/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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 6b35ef7063324d2b2b6f4bdf57b8a969575557e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Feb 2018 16:13:30 +0900 Subject: [PATCH 12/81] Update OpenTK version --- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Desktop/packages.config | 2 +- osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj | 2 +- osu.Game.Rulesets.Catch/packages.config | 2 +- osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj | 2 +- osu.Game.Rulesets.Mania/packages.config | 2 +- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 2 +- osu.Game.Rulesets.Osu/packages.config | 2 +- osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj | 2 +- osu.Game.Rulesets.Taiko/packages.config | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tests/packages.config | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.Game/packages.config | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3cc4e7f943..2ea2199a1f 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -136,7 +136,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Desktop/packages.config b/osu.Desktop/packages.config index 37014057a0..656e898d8b 100644 --- a/osu.Desktop/packages.config +++ b/osu.Desktop/packages.config @@ -6,7 +6,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index cdce598ce8..0362a897c2 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -41,7 +41,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Catch/packages.config b/osu.Game.Rulesets.Catch/packages.config index e67d3e9b34..33cc9e71ef 100644 --- a/osu.Game.Rulesets.Catch/packages.config +++ b/osu.Game.Rulesets.Catch/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index b9e7f8e60f..e9608b819c 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -41,7 +41,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Mania/packages.config b/osu.Game.Rulesets.Mania/packages.config index e67d3e9b34..33cc9e71ef 100644 --- a/osu.Game.Rulesets.Mania/packages.config +++ b/osu.Game.Rulesets.Mania/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 97a003513f..e89e465152 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -42,7 +42,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Osu/packages.config b/osu.Game.Rulesets.Osu/packages.config index e67d3e9b34..33cc9e71ef 100644 --- a/osu.Game.Rulesets.Osu/packages.config +++ b/osu.Game.Rulesets.Osu/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 5795048322..1cfd4de81b 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -41,7 +41,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Taiko/packages.config b/osu.Game.Rulesets.Taiko/packages.config index e67d3e9b34..33cc9e71ef 100644 --- a/osu.Game.Rulesets.Taiko/packages.config +++ b/osu.Game.Rulesets.Taiko/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 8301f1f734..df8a97de79 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -42,7 +42,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Game.Tests/packages.config b/osu.Game.Tests/packages.config index c16d10bf45..608c6a69d9 100644 --- a/osu.Game.Tests/packages.config +++ b/osu.Game.Tests/packages.config @@ -7,6 +7,6 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4944613828..6746d0e179 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -148,7 +148,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.11\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll True diff --git a/osu.Game/packages.config b/osu.Game/packages.config index 0216c8ae67..e6b4f83ac2 100644 --- a/osu.Game/packages.config +++ b/osu.Game/packages.config @@ -67,7 +67,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + From 7e56519d6a65a575cd0742ef72cd22b49b3055f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Feb 2018 16:13:39 +0900 Subject: [PATCH 13/81] Add setting for absolute input mapping --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index ab501906dc..16291ccb2a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Cursor Sensitivity", Bindable = config.GetBindable(FrameworkSetting.CursorSensitivity) }, + new SettingsCheckbox + { + LabelText = "Map absolute input to window", + Bindable = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) + }, new SettingsEnumDropdown { LabelText = "Confine mouse cursor to window", From 30b9439263eedf78ddd533dec7def72b85a8ae8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Feb 2018 20:00:36 +0900 Subject: [PATCH 14/81] Fix default mouse sensitivity not reverting correctly --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 16291ccb2a..c368b8fea7 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -93,6 +93,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input // this bindable will still act as the "interactive" bindable displayed during a drag. base.Bindable = new BindableDouble(doubleValue.Value) { + Default = doubleValue.Default, MinValue = doubleValue.MinValue, MaxValue = doubleValue.MaxValue }; From dfc344b47a96d393e213a0e89d81392dea24fab2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Feb 2018 13:47:54 +0900 Subject: [PATCH 15/81] Update OpenTK version --- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Desktop/packages.config | 2 +- osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj | 2 +- osu.Game.Rulesets.Catch/packages.config | 2 +- osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj | 2 +- osu.Game.Rulesets.Mania/packages.config | 2 +- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 2 +- osu.Game.Rulesets.Osu/packages.config | 2 +- osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj | 2 +- osu.Game.Rulesets.Taiko/packages.config | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tests/packages.config | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.Game/packages.config | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 2ea2199a1f..b0d9ea4e81 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -136,7 +136,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Desktop/packages.config b/osu.Desktop/packages.config index 656e898d8b..b5dc43267d 100644 --- a/osu.Desktop/packages.config +++ b/osu.Desktop/packages.config @@ -6,7 +6,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index 0362a897c2..31c225288b 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -41,7 +41,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Catch/packages.config b/osu.Game.Rulesets.Catch/packages.config index 33cc9e71ef..16fae25086 100644 --- a/osu.Game.Rulesets.Catch/packages.config +++ b/osu.Game.Rulesets.Catch/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index e9608b819c..38689fb19b 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -41,7 +41,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Mania/packages.config b/osu.Game.Rulesets.Mania/packages.config index 33cc9e71ef..16fae25086 100644 --- a/osu.Game.Rulesets.Mania/packages.config +++ b/osu.Game.Rulesets.Mania/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index e89e465152..d734fd70a9 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -42,7 +42,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Osu/packages.config b/osu.Game.Rulesets.Osu/packages.config index 33cc9e71ef..16fae25086 100644 --- a/osu.Game.Rulesets.Osu/packages.config +++ b/osu.Game.Rulesets.Osu/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 1cfd4de81b..74859f924d 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -41,7 +41,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Game.Rulesets.Taiko/packages.config b/osu.Game.Rulesets.Taiko/packages.config index 33cc9e71ef..16fae25086 100644 --- a/osu.Game.Rulesets.Taiko/packages.config +++ b/osu.Game.Rulesets.Taiko/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index df8a97de79..1c2cc58d26 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -42,7 +42,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Game.Tests/packages.config b/osu.Game.Tests/packages.config index 608c6a69d9..c0ac81ed79 100644 --- a/osu.Game.Tests/packages.config +++ b/osu.Game.Tests/packages.config @@ -7,6 +7,6 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6746d0e179..a5c3fc7f38 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -148,7 +148,7 @@ True - $(SolutionDir)\packages\ppy.OpenTK.3.0.12\lib\net45\OpenTK.dll + $(SolutionDir)\packages\ppy.OpenTK.3.0.13\lib\net45\OpenTK.dll True diff --git a/osu.Game/packages.config b/osu.Game/packages.config index e6b4f83ac2..6d46360b99 100644 --- a/osu.Game/packages.config +++ b/osu.Game/packages.config @@ -67,7 +67,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + From e0c52c69cf416cb12332e6caa00810bf5ddc2cf3 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 6 Feb 2018 22:31:30 +1030 Subject: [PATCH 16/81] Prevent revert-to-default OnHover from hiding visual settings at beatmap load --- osu.Game/Overlays/Settings/SettingsItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index adb7c509c0..5afc415d83 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Settings { hovering = true; UpdateState(); - return true; + return false; } protected override void OnHoverLost(InputState state) From d86ce816c73a962a5876dcc0400d2ebb1f5ca04d Mon Sep 17 00:00:00 2001 From: tgi74000 Date: Tue, 6 Feb 2018 21:40:52 +0100 Subject: [PATCH 17/81] Add support for country rank --- osu.Game/Users/User.cs | 4 ++-- osu.Game/Users/UserStatistics.cs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 8379e69869..c305cc004a 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -26,9 +26,9 @@ namespace osu.Game.Users [JsonProperty(@"age")] public int? Age; - public int GlobalRank; + public int GlobalRank { get => Statistics.Ranks.GlobalRank; set => Statistics.Ranks.GlobalRank = value; } - public int CountryRank; + public int CountryRank { get => Statistics.Ranks.CountryRank; set => Statistics.Ranks.CountryRank = value; } //public Team Team; diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 73d20eafb9..f047bd1980 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -23,7 +23,19 @@ namespace osu.Game.Users public decimal? PP; [JsonProperty(@"pp_rank")] - public int Rank; + public int Rank { get => Ranks.GlobalRank; set => Ranks.GlobalRank = value; } + + [JsonProperty(@"rank")] + public UserRank Ranks; + + public struct UserRank + { + [JsonProperty(@"global")] + public int GlobalRank; + + [JsonProperty(@"country")] + public int CountryRank; + } [JsonProperty(@"ranked_score")] public long RankedScore; From bcd568e6076968f5a9abf2cbfa09f15d57550a53 Mon Sep 17 00:00:00 2001 From: tgi74000 Date: Tue, 6 Feb 2018 23:00:52 +0100 Subject: [PATCH 18/81] Check for possible null ranks --- osu.Game.Tests/Visual/TestCaseUserProfile.cs | 2 +- osu.Game/Users/User.cs | 4 ++-- osu.Game/Users/UserStatistics.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/TestCaseUserProfile.cs b/osu.Game.Tests/Visual/TestCaseUserProfile.cs index da81de6a3a..2c3abe0049 100644 --- a/osu.Game.Tests/Visual/TestCaseUserProfile.cs +++ b/osu.Game.Tests/Visual/TestCaseUserProfile.cs @@ -42,12 +42,12 @@ namespace osu.Game.Tests.Visual LastVisit = DateTimeOffset.Now, Age = 1, ProfileOrder = new[] { "me" }, - CountryRank = 1, Statistics = new UserStatistics { Rank = 2148, PP = 4567.89m }, + CountryRank = 1, RankHistory = new User.RankHistoryData { Mode = @"osu", diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index c305cc004a..0be9600815 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -26,9 +26,9 @@ namespace osu.Game.Users [JsonProperty(@"age")] public int? Age; - public int GlobalRank { get => Statistics.Ranks.GlobalRank; set => Statistics.Ranks.GlobalRank = value; } + public int GlobalRank { get => Statistics?.Ranks.GlobalRank ?? 0; set => Statistics.Ranks.GlobalRank = value; } - public int CountryRank { get => Statistics.Ranks.CountryRank; set => Statistics.Ranks.CountryRank = value; } + public int CountryRank { get => Statistics?.Ranks.CountryRank ?? 0; set => Statistics.Ranks.CountryRank = value; } //public Team Team; diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index f047bd1980..f26db32cf0 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -23,7 +23,7 @@ namespace osu.Game.Users public decimal? PP; [JsonProperty(@"pp_rank")] - public int Rank { get => Ranks.GlobalRank; set => Ranks.GlobalRank = value; } + public int Rank { get => Ranks.GlobalRank ?? 0; set => Ranks.GlobalRank = value; } [JsonProperty(@"rank")] public UserRank Ranks; @@ -31,10 +31,10 @@ namespace osu.Game.Users public struct UserRank { [JsonProperty(@"global")] - public int GlobalRank; + public int? GlobalRank; [JsonProperty(@"country")] - public int CountryRank; + public int? CountryRank; } [JsonProperty(@"ranked_score")] From 406ec6e92d853dcf6d87a853cf9a21f70355a599 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Feb 2018 13:26:17 +0900 Subject: [PATCH 19/81] Make OsuSliderBar always use number of digits from precision --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 3c3939586e..f9d552042b 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterface var bindableDouble = CurrentNumber as BindableNumber; var bindableFloat = CurrentNumber as BindableNumber; var floatValue = bindableDouble?.Value ?? bindableFloat?.Value; + var floatPrecision = bindableDouble?.Precision ?? bindableFloat?.Precision; if (floatValue != null) { @@ -44,7 +45,11 @@ namespace osu.Game.Graphics.UserInterface if (floatMaxValue == 1 && (floatMinValue == 0 || floatMinValue == -1)) return floatValue.Value.ToString("P0"); - return floatValue.Value.ToString("N1"); + // We don't really care about more than 5 decimal digits + var decimalPrecision = normalize(Math.Round((decimal)floatPrecision, 5)); + var precisionDigits = (decimal.GetBits(decimalPrecision)[3] >> 16) & 255; + + return floatValue.Value.ToString($"N{precisionDigits}"); } var bindableInt = CurrentNumber as BindableNumber; @@ -52,6 +57,8 @@ namespace osu.Game.Graphics.UserInterface return bindableInt.Value.ToString("N0"); return Current.Value.ToString(CultureInfo.InvariantCulture); + + decimal normalize(decimal d) => decimal.Parse(d.ToString("0.############################", CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); } } From bd5db6fc8d74c241a670cb508e3b4a1a09327896 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Feb 2018 13:26:41 +0900 Subject: [PATCH 20/81] Make playback speed sliderbar use the tooltip text as its display --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 4 ++-- osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 15d8e73a76..3229b022de 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Text = "1x", + Text = "1.00x", Font = @"Exo2.0-Bold", } }, @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } }; - sliderbar.Bindable.ValueChanged += rateMultiplier => multiplierText.Text = $"{rateMultiplier}x"; + sliderbar.Bindable.ValueChanged += rateMultiplier => multiplierText.Text = $"{sliderbar.Bar.TooltipText}x"; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index 946669e3dd..43fe14cc24 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.Play.PlayerSettings public class PlayerSliderBar : SettingsSlider where T : struct, IEquatable, IComparable, IConvertible { + public OsuSliderBar Bar => (OsuSliderBar)Control; + protected override Drawable CreateControl() => new Sliderbar { Margin = new MarginPadding { Top = 5, Bottom = 5 }, @@ -21,8 +23,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private class Sliderbar : OsuSliderBar { - public override string TooltipText => $"{CurrentNumber.Value}"; - [BackgroundDependencyLoader] private void load(OsuColour colours) { From 8e280b6b0c0b9f580f790246af45680917e9e1ca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Feb 2018 13:28:10 +0900 Subject: [PATCH 21/81] Use 0.1 precision for playback speed --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 3229b022de..4da13cb872 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Text = "1.00x", + Text = "1.0x", Font = @"Exo2.0-Bold", } }, @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Default = 1, MinValue = 0.5, MaxValue = 2, - Precision = 0.01, + Precision = 0.1, }, } }; From 74016a14825b19343d6ca362b11fa297ae82c96e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Feb 2018 15:57:16 +0900 Subject: [PATCH 22/81] Make sure the import tests exit their hosts --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index ece1f626ec..5398fb3ff3 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + host.Exit(); } } @@ -64,6 +66,9 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); + + host.Exit(); + client.Exit(); } } @@ -86,6 +91,8 @@ namespace osu.Game.Tests.Beatmaps.IO File.Delete(temp); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); + + host.Exit(); } } From b66d089400134245656d076f35cfc5dd8e367d78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Feb 2018 17:12:12 +0900 Subject: [PATCH 23/81] Always put attributes on a separate line to their target --- osu.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 8767e5374a..3b62dbe579 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -173,7 +173,9 @@ NEXT_LINE True NEVER + NEVER False + NEVER False True False From 20c00720e5849f39a16244607095ae18915f31c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Feb 2018 17:11:10 +0900 Subject: [PATCH 24/81] Fix formatting --- osu.Game/Users/User.cs | 12 ++++++++++-- osu.Game/Users/UserStatistics.cs | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 0be9600815..46c5d9e282 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -26,9 +26,17 @@ namespace osu.Game.Users [JsonProperty(@"age")] public int? Age; - public int GlobalRank { get => Statistics?.Ranks.GlobalRank ?? 0; set => Statistics.Ranks.GlobalRank = value; } + public int GlobalRank + { + get => Statistics?.Ranks.GlobalRank ?? 0; + set => Statistics.Ranks.GlobalRank = value; + } - public int CountryRank { get => Statistics?.Ranks.CountryRank ?? 0; set => Statistics.Ranks.CountryRank = value; } + public int CountryRank + { + get => Statistics?.Ranks.CountryRank ?? 0; + set => Statistics.Ranks.CountryRank = value; + } //public Team Team; diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index f26db32cf0..48012b089b 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -23,7 +23,11 @@ namespace osu.Game.Users public decimal? PP; [JsonProperty(@"pp_rank")] - public int Rank { get => Ranks.GlobalRank ?? 0; set => Ranks.GlobalRank = value; } + public int Rank + { + get => Ranks.GlobalRank ?? 0; + set => Ranks.GlobalRank = value; + } [JsonProperty(@"rank")] public UserRank Ranks; From 23d4c207266c012649c336ce50b2f4fd069021b0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Feb 2018 17:31:18 +0900 Subject: [PATCH 25/81] Apply suggestions to normalisation function --- .../Graphics/UserInterface/OsuSliderBar.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index f9d552042b..8fc0aad55c 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -20,6 +20,11 @@ namespace osu.Game.Graphics.UserInterface public class OsuSliderBar : SliderBar, IHasTooltip, IHasAccentColour where T : struct, IEquatable, IComparable, IConvertible { + /// + /// Maximum number of decimal digits to be displayed in the tooltip. + /// + private const int max_decimal_digits = 5; + private SampleChannel sample; private double lastSampleTime; private T lastSampleValue; @@ -45,11 +50,12 @@ namespace osu.Game.Graphics.UserInterface if (floatMaxValue == 1 && (floatMinValue == 0 || floatMinValue == -1)) return floatValue.Value.ToString("P0"); - // We don't really care about more than 5 decimal digits - var decimalPrecision = normalize(Math.Round((decimal)floatPrecision, 5)); - var precisionDigits = (decimal.GetBits(decimalPrecision)[3] >> 16) & 255; + var decimalPrecision = normalise((decimal)floatPrecision, max_decimal_digits); - return floatValue.Value.ToString($"N{precisionDigits}"); + // Find the number of significant digits (we could have less than 5 after normalize()) + var significantDigits = (decimal.GetBits(decimalPrecision)[3] >> 16) & 255; + + return floatValue.Value.ToString($"N{significantDigits}"); } var bindableInt = CurrentNumber as BindableNumber; @@ -57,8 +63,6 @@ namespace osu.Game.Graphics.UserInterface return bindableInt.Value.ToString("N0"); return Current.Value.ToString(CultureInfo.InvariantCulture); - - decimal normalize(decimal d) => decimal.Parse(d.ToString("0.############################", CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); } } @@ -184,5 +188,14 @@ namespace osu.Game.Graphics.UserInterface { Nub.MoveToX(RangePadding + UsableWidth * value, 250, Easing.OutQuint); } + + /// + /// Removes all non-significant digits, keeping at most a requested number of decimal digits. + /// + /// The decimal to normalize. + /// The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value. + /// The normalised decimal. + private decimal normalise(decimal d, int sd) + => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); } } From 647cc4bdad78d95c197d5db2001f425e9fe7019a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Feb 2018 18:04:32 +0900 Subject: [PATCH 26/81] Remove in-between properties --- osu.Game.Tests/Visual/TestCaseDrawableRoom.cs | 14 +++++----- osu.Game.Tests/Visual/TestCaseRankGraph.cs | 8 +++--- .../Visual/TestCaseRoomInspector.cs | 22 +++++++-------- osu.Game.Tests/Visual/TestCaseUserProfile.cs | 5 ++-- osu.Game/Overlays/Profile/RankGraph.cs | 6 ++--- .../Screens/Multiplayer/ParticipantInfo.cs | 2 +- osu.Game/Users/User.cs | 12 --------- osu.Game/Users/UserStatistics.cs | 27 +++++++++---------- 8 files changed, 40 insertions(+), 56 deletions(-) diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs index 1bb72a5ab4..ec70253118 100644 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs @@ -63,8 +63,8 @@ namespace osu.Game.Tests.Visual { Value = new[] { - new User { GlobalRank = 1355 }, - new User { GlobalRank = 8756 }, + new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1355 } } }, + new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 8756 } } }, }, }, }), @@ -99,10 +99,10 @@ namespace osu.Game.Tests.Visual }, Participants = { - Value = new[] + Value = new[] { - new User { GlobalRank = 578975 }, - new User { GlobalRank = 24554 }, + new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 578975 } } }, + new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24554 } } }, }, }, }), @@ -116,8 +116,8 @@ namespace osu.Game.Tests.Visual AddStep(@"change beatmap", () => first.Room.Beatmap.Value = null); AddStep(@"change participants", () => first.Room.Participants.Value = new[] { - new User { GlobalRank = 1254 }, - new User { GlobalRank = 123189 }, + new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1254 } } }, + new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 123189 } } }, }); } diff --git a/osu.Game.Tests/Visual/TestCaseRankGraph.cs b/osu.Game.Tests/Visual/TestCaseRankGraph.cs index 54930c51a2..88631aa982 100644 --- a/osu.Game.Tests/Visual/TestCaseRankGraph.cs +++ b/osu.Game.Tests/Visual/TestCaseRankGraph.cs @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual { Statistics = new UserStatistics { - Rank = 123456, + Ranks = new UserStatistics.UserRanks { Global = 123456 }, PP = 12345, } }; @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual { Statistics = new UserStatistics { - Rank = 89000, + Ranks = new UserStatistics.UserRanks { Global = 89000 }, PP = 12345, }, RankHistory = new User.RankHistoryData @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual { Statistics = new UserStatistics { - Rank = 89000, + Ranks = new UserStatistics.UserRanks { Global = 89000 }, PP = 12345, }, RankHistory = new User.RankHistoryData @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual { Statistics = new UserStatistics { - Rank = 12000, + Ranks = new UserStatistics.UserRanks { Global = 12000 }, PP = 12345, }, RankHistory = new User.RankHistoryData diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs index e613d87500..8c4aa02a68 100644 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs @@ -54,12 +54,12 @@ namespace osu.Game.Tests.Visual { Value = new[] { - new User { Username = @"flyte", Id = 3103765, GlobalRank = 1425 }, - new User { Username = @"Cookiezi", Id = 124493, GlobalRank = 5466 }, - new User { Username = @"Angelsim", Id = 1777162, GlobalRank = 2873 }, - new User { Username = @"Rafis", Id = 2558286, GlobalRank = 4687 }, - new User { Username = @"hvick225", Id = 50265, GlobalRank = 3258 }, - new User { Username = @"peppy", Id = 2, GlobalRank = 6251 } + new User { Username = @"flyte", Id = 3103765, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 142 } } }, + new User { Username = @"Cookiezi", Id = 124493, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 546 } } }, + new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 287 } } }, + new User { Username = @"Rafis", Id = 2558286, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 468 } } }, + new User { Username = @"hvick225", Id = 50265, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 325 } } }, + new User { Username = @"peppy", Id = 2, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 625 } } }, } } }; @@ -80,8 +80,8 @@ namespace osu.Game.Tests.Visual AddStep(@"change max participants", () => room.MaxParticipants.Value = null); AddStep(@"change participants", () => room.Participants.Value = new[] { - new User { Username = @"filsdelama", Id = 2831793, GlobalRank = 8542 }, - new User { Username = @"_index", Id = 652457, GlobalRank = 15024 } + new User { Username = @"filsdelama", Id = 2831793, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 854 } } }, + new User { Username = @"_index", Id = 652457, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 150 } } } }); AddStep(@"change room", () => @@ -121,9 +121,9 @@ namespace osu.Game.Tests.Visual { Value = new[] { - new User { Username = @"Angelsim", Id = 1777162, GlobalRank = 4 }, - new User { Username = @"HappyStick", Id = 256802, GlobalRank = 752 }, - new User { Username = @"-Konpaku-", Id = 2258797, GlobalRank = 571 } + new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 4 } } }, + new User { Username = @"HappyStick", Id = 256802, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 752 } } }, + new User { Username = @"-Konpaku-", Id = 2258797, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 571 } } } } } }; diff --git a/osu.Game.Tests/Visual/TestCaseUserProfile.cs b/osu.Game.Tests/Visual/TestCaseUserProfile.cs index 2c3abe0049..8acc8d1b5b 100644 --- a/osu.Game.Tests/Visual/TestCaseUserProfile.cs +++ b/osu.Game.Tests/Visual/TestCaseUserProfile.cs @@ -44,10 +44,9 @@ namespace osu.Game.Tests.Visual ProfileOrder = new[] { "me" }, Statistics = new UserStatistics { - Rank = 2148, - PP = 4567.89m + Ranks = new UserStatistics.UserRanks { Global = 2148, Country = 1 }, + PP = 4567.89m, }, - CountryRank = 1, RankHistory = new User.RankHistoryData { Mode = @"osu", diff --git a/osu.Game/Overlays/Profile/RankGraph.cs b/osu.Game/Overlays/Profile/RankGraph.cs index 9d3183339e..e7e253df7c 100644 --- a/osu.Game/Overlays/Profile/RankGraph.cs +++ b/osu.Game/Overlays/Profile/RankGraph.cs @@ -105,7 +105,7 @@ namespace osu.Game.Overlays.Profile return; } - int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Rank }; + int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Ranks.Global }; ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); if (ranks.Length > 1) @@ -124,9 +124,9 @@ namespace osu.Game.Overlays.Profile private void updateRankTexts() { - rankText.Text = User.Value.Statistics.Rank > 0 ? $"#{User.Value.Statistics.Rank:#,0}" : "no rank"; + rankText.Text = User.Value.Statistics.Ranks.Global > 0 ? $"#{User.Value.Statistics.Ranks.Global:#,0}" : "no rank"; performanceText.Text = User.Value.Statistics.PP != null ? $"{User.Value.Statistics.PP:#,0}pp" : string.Empty; - relativeText.Text = $"{User.Value.Country?.FullName} #{User.Value.CountryRank:#,0}"; + relativeText.Text = $"{User.Value.Country?.FullName} #{User.Value.Statistics.Ranks.Country:#,0}"; } private void showHistoryRankTexts(int dayIndex) diff --git a/osu.Game/Screens/Multiplayer/ParticipantInfo.cs b/osu.Game/Screens/Multiplayer/ParticipantInfo.cs index ff00f53600..0fd4f4d08d 100644 --- a/osu.Game/Screens/Multiplayer/ParticipantInfo.cs +++ b/osu.Game/Screens/Multiplayer/ParticipantInfo.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Multiplayer { set { - var ranks = value.Select(u => u.GlobalRank); + var ranks = value.Select(u => u.Statistics.Ranks.Global); levelRangeLower.Text = ranks.Min().ToString(); levelRangeHigher.Text = ranks.Max().ToString(); } diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 46c5d9e282..777eb7beca 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -26,18 +26,6 @@ namespace osu.Game.Users [JsonProperty(@"age")] public int? Age; - public int GlobalRank - { - get => Statistics?.Ranks.GlobalRank ?? 0; - set => Statistics.Ranks.GlobalRank = value; - } - - public int CountryRank - { - get => Statistics?.Ranks.CountryRank ?? 0; - set => Statistics.Ranks.CountryRank = value; - } - //public Team Team; [JsonProperty(@"profile_colour")] diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 48012b089b..863293d847 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -23,23 +23,10 @@ namespace osu.Game.Users public decimal? PP; [JsonProperty(@"pp_rank")] - public int Rank - { - get => Ranks.GlobalRank ?? 0; - set => Ranks.GlobalRank = value; - } + private int rank { set => Ranks.Global = value; } [JsonProperty(@"rank")] - public UserRank Ranks; - - public struct UserRank - { - [JsonProperty(@"global")] - public int? GlobalRank; - - [JsonProperty(@"country")] - public int? CountryRank; - } + public UserRanks Ranks; [JsonProperty(@"ranked_score")] public long RankedScore; @@ -82,5 +69,15 @@ namespace osu.Game.Users [JsonProperty(@"a")] public int A; } + + public struct UserRanks + { + [JsonProperty(@"global")] + public int Global; + + [JsonProperty(@"country")] + public int Country; + } + } } From 93c4612f4f4d5229c8b8a5d036d1b01e8f5369d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Feb 2018 18:18:26 +0900 Subject: [PATCH 27/81] Add comment about deserialising helper --- osu.Game/Users/UserStatistics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 863293d847..c29bc91d17 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -22,7 +22,7 @@ namespace osu.Game.Users [JsonProperty(@"pp")] public decimal? PP; - [JsonProperty(@"pp_rank")] + [JsonProperty(@"pp_rank")] // the API sometimes only returns this value in condensed user responses private int rank { set => Ranks.Global = value; } [JsonProperty(@"rank")] From 4c3606f8fb209cbec1056db2ca5fc907ed9165f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Feb 2018 18:30:38 +0900 Subject: [PATCH 28/81] Handle non-present country rank --- osu.Game/Overlays/Profile/RankGraph.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/RankGraph.cs b/osu.Game/Overlays/Profile/RankGraph.cs index e7e253df7c..429049c7bc 100644 --- a/osu.Game/Overlays/Profile/RankGraph.cs +++ b/osu.Game/Overlays/Profile/RankGraph.cs @@ -124,9 +124,11 @@ namespace osu.Game.Overlays.Profile private void updateRankTexts() { - rankText.Text = User.Value.Statistics.Ranks.Global > 0 ? $"#{User.Value.Statistics.Ranks.Global:#,0}" : "no rank"; - performanceText.Text = User.Value.Statistics.PP != null ? $"{User.Value.Statistics.PP:#,0}pp" : string.Empty; - relativeText.Text = $"{User.Value.Country?.FullName} #{User.Value.Statistics.Ranks.Country:#,0}"; + var user = User.Value; + + performanceText.Text = user.Statistics.PP != null ? $"{user.Statistics.PP:#,0}pp" : string.Empty; + rankText.Text = user.Statistics.Ranks.Global > 0 ? $"#{user.Statistics.Ranks.Global:#,0}" : "no rank"; + relativeText.Text = user.Country != null && user.Statistics.Ranks.Country > 0 ? $"{user.Country.FullName} #{user.Statistics.Ranks.Country:#,0}" : "no rank"; } private void showHistoryRankTexts(int dayIndex) From ee93c0bc19a06c9c9a36b83f620411b41896be52 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Feb 2018 20:03:15 +0900 Subject: [PATCH 29/81] Use an endian-independent method to find precision --- .../Graphics/UserInterface/OsuSliderBar.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 8fc0aad55c..8f375d9885 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface var decimalPrecision = normalise((decimal)floatPrecision, max_decimal_digits); // Find the number of significant digits (we could have less than 5 after normalize()) - var significantDigits = (decimal.GetBits(decimalPrecision)[3] >> 16) & 255; + var significantDigits = findPrecision(decimalPrecision); return floatValue.Value.ToString($"N{significantDigits}"); } @@ -197,5 +197,22 @@ namespace osu.Game.Graphics.UserInterface /// The normalised decimal. private decimal normalise(decimal d, int sd) => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + + /// + /// Finds the number of digits after the decimal. + /// + /// The value to find the number of decimal digits for. + /// The number decimal digits. + private int findPrecision(decimal d) + { + int precision = 0; + while (d != Math.Round(d)) + { + d *= 10; + precision++; + } + + return precision; + } } } From 7a9dffd780b2bb385d106a0d730fc2de3ac71e15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Feb 2018 22:06:42 +0900 Subject: [PATCH 30/81] Update framework again --- osu-framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu-framework b/osu-framework index d89e6cd631..1440ae8538 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit d89e6cd63140c2b73631b79ff83b130a2b9958ed +Subproject commit 1440ae8538560b3c40883ec51ab39108d6a69e3b From a70989cb702075b18f25d110b279781a3f3ff4f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Feb 2018 11:12:05 +0900 Subject: [PATCH 31/81] Rely on bindable's formatting rather than setting a default --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 4da13cb872..6878bb098e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -42,7 +42,6 @@ namespace osu.Game.Screens.Play.PlayerSettings { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Text = "1.0x", Font = @"Exo2.0-Bold", } }, @@ -60,6 +59,7 @@ namespace osu.Game.Screens.Play.PlayerSettings }; sliderbar.Bindable.ValueChanged += rateMultiplier => multiplierText.Text = $"{sliderbar.Bar.TooltipText}x"; + sliderbar.Bindable.TriggerChange(); } protected override void LoadComplete() From a7aaaf90888e1dc473fa272e59a781048d635117 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 12:43:17 +0900 Subject: [PATCH 32/81] Update framework --- osu-framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu-framework b/osu-framework index 1440ae8538..2d6169fc07 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 1440ae8538560b3c40883ec51ab39108d6a69e3b +Subproject commit 2d6169fc07fdd50b8ce31d3a9124b4ec0123bdd1 From cfdeac64289c8450d4dacf964e789ecc8c63c7d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 13:38:31 +0900 Subject: [PATCH 33/81] 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 34/81] 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 35/81] 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 4f5bfdb888bce2c73afaf1578615d0ac32d83b17 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:03:37 +0900 Subject: [PATCH 36/81] Remove explicit .Exit on IPC test --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 5398fb3ff3..490d4ec4d3 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -68,7 +68,6 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); host.Exit(); - client.Exit(); } } From 48918b5324be01a24db5ead9fe7afed46d8ffd63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Feb 2018 14:07:12 +0900 Subject: [PATCH 37/81] Bring framework up-to-date --- osu-framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu-framework b/osu-framework index 2d6169fc07..eba12eb4a0 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 2d6169fc07fdd50b8ce31d3a9124b4ec0123bdd1 +Subproject commit eba12eb4a0fa6238873dd266deb35bfdece21a6a From 3d167c40ae28ae68c73b375a5b8b85a67a142ec5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Feb 2018 14:15:47 +0900 Subject: [PATCH 38/81] 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 39/81] 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 40/81] 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 41/81] 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 42/81] 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 43/81] 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 44/81] 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 45/81] 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 46/81] 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 47/81] 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 48/81] 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 3b7018fcd69be54032d0e27fb04c68cde875f461 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Feb 2018 17:22:48 +0900 Subject: [PATCH 49/81] 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 50/81] 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 51/81] 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 52/81] 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 53/81] 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 54/81] 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 55/81] 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 56/81] 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 57/81] 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 58/81] 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 59/81] 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 60/81] 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 61/81] 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 62/81] 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 63/81] 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 64/81] 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 c3ce015869c93b5e948fdbe1879bdeed6ea181f6 Mon Sep 17 00:00:00 2001 From: Aergwyn Date: Sun, 11 Feb 2018 11:03:01 +0100 Subject: [PATCH 65/81] 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 66/81] 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 67/81] 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 68/81] 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 69/81] 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 70/81] 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 71/81] 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 72/81] 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 73/81] 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 74/81] 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 75/81] 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 76/81] 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 77/81] 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 78/81] 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 79/81] 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 80/81] 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 81/81] 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();