diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..221e4746cb --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ +Add any details pertaining to developers above the break. + +- [ ] Depends on #PR +- Closes #ISSUE + +--- + +Add a sentence or two describing this change in plain english. This will be displayed on the [changelog](https://osu.ppy.sh/home/changelog). A single screenshot or short gif is also welcomed. \ No newline at end of file diff --git a/osu.Desktop.Deploy/.vscode/launch.json b/osu.Desktop.Deploy/.vscode/launch.json deleted file mode 100644 index 8c35d211bd..0000000000 --- a/osu.Desktop.Deploy/.vscode/launch.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [{ - "name": "Deploy (Debug)", - "request": "launch", - "type": "mono", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Desktop.Deploy.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "Deploy (Release)", - "request": "launch", - "type": "clr", - "program": "${workspaceRoot}/bin/Release/net471/osu.Desktop.Deploy.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - } - ] -} \ No newline at end of file diff --git a/osu.Desktop.Deploy/.vscode/tasks.json b/osu.Desktop.Deploy/.vscode/tasks.json deleted file mode 100644 index 35bf9e7a0e..0000000000 --- a/osu.Desktop.Deploy/.vscode/tasks.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "command": "msbuild", - "type": "shell", - "suppressTaskName": true, - "args": [ - "/property:GenerateFullPaths=true", - "/property:DebugType=portable", - "/verbosity:minimal", - "/m" //parallel compiling support. - ], - "tasks": [{ - "taskName": "Build (Debug)", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [ - "$msCompile" - ] - }, - { - "taskName": "Build (Release)", - "group": "build", - "args": [ - "/property:Configuration=Release" - ], - "problemMatcher": [ - "$msCompile" - ] - }, - { - "taskName": "Clean (Debug)", - "args": [ - "/target:Clean" - ], - "problemMatcher": [ - "$msCompile" - ] - }, - { - "taskName": "Clean (Release)", - "args": [ - "/target:Clean", - "/property:Configuration=Release" - ], - "problemMatcher": [ - "$msCompile" - ] - }, - { - "taskName": "Clean All", - "dependsOn": [ - "Clean (Debug)", - "Clean (Release)" - ], - "problemMatcher": [ - "$msCompile" - ] - } - ] -} \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 844db4a80f..64adcecba4 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -73,7 +73,7 @@ namespace osu.Desktop } public StableStorage() - : base(string.Empty) + : base(string.Empty, null) { } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 29d3b0e394..0289db20f0 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 5b34e46247..89e8361a72 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.MathUtils; using osu.Game.Rulesets.Catch.Objects; @@ -17,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Tests protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; [TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2232")] + [TestCase("spinner")] + [TestCase("spinner-and-circles")] public new void Test(string name) { base.Test(name); @@ -27,22 +30,15 @@ namespace osu.Game.Rulesets.Catch.Tests if (hitObject is JuiceStream stream) { foreach (var nested in stream.NestedHitObjects) - { - yield return new ConvertValue - { - StartTime = nested.StartTime, - Position = ((CatchHitObject)nested).X * CatchPlayfield.BASE_WIDTH - }; - } + yield return new ConvertValue((CatchHitObject)nested); + } + else if (hitObject is BananaShower shower) + { + foreach (var nested in shower.NestedHitObjects) + yield return new ConvertValue((CatchHitObject)nested); } else - { - yield return new ConvertValue - { - StartTime = hitObject.StartTime, - Position = ((CatchHitObject)hitObject).X * CatchPlayfield.BASE_WIDTH - }; - } + yield return new ConvertValue((CatchHitObject)hitObject); } protected override Ruleset CreateRuleset() => new CatchRuleset(); @@ -55,8 +51,31 @@ namespace osu.Game.Rulesets.Catch.Tests /// private const float conversion_lenience = 2; - public double StartTime; - public float Position; + [JsonIgnore] + public readonly CatchHitObject HitObject; + + public ConvertValue(CatchHitObject hitObject) + { + HitObject = hitObject; + startTime = 0; + position = 0; + } + + private double startTime; + + public double StartTime + { + get => HitObject?.StartTime ?? startTime; + set => startTime = value; + } + + private float position; + + public float Position + { + get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position; + set => position = value; + } public bool Equals(ConvertValue other) => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs index 5119260c53..0ba6398ced 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Tests { } - public void ToggleHyperDash(bool status) => MovableCatcher.HyperDashModifier = status ? 2 : 1; + public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperdashState(status ? 2 : 1); } } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index ad500606ed..46fe8454dc 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -27,22 +27,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps var comboData = obj as IHasCombo; var endTime = obj as IHasEndTime; - if (positionData == null) - { - if (endTime != null) - { - yield return new BananaShower - { - StartTime = obj.StartTime, - Samples = obj.Samples, - Duration = endTime.Duration, - NewCombo = comboData?.NewCombo ?? false - }; - } - - yield break; - } - if (curveData != null) { yield return new JuiceStream @@ -54,20 +38,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Distance = curveData.Distance, RepeatSamples = curveData.RepeatSamples, RepeatCount = curveData.RepeatCount, - X = positionData.X / CatchPlayfield.BASE_WIDTH, + X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, NewCombo = comboData?.NewCombo ?? false }; - - yield break; } - - yield return new Fruit + else if (endTime != null) { - StartTime = obj.StartTime, - Samples = obj.Samples, - NewCombo = comboData?.NewCombo ?? false, - X = positionData.X / CatchPlayfield.BASE_WIDTH - }; + yield return new BananaShower + { + StartTime = obj.StartTime, + Samples = obj.Samples, + Duration = endTime.Duration, + NewCombo = comboData?.NewCombo ?? false + }; + } + else + { + yield return new Fruit + { + StartTime = obj.StartTime, + Samples = obj.Samples, + NewCombo = comboData?.NewCombo ?? false, + X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH + }; + } } protected override Beatmap CreateBeatmap() => new CatchBeatmap(); diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index e16f5fcb60..8473f5a36c 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; using OpenTK; +using osu.Game.Rulesets.Catch.MathUtils; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -21,13 +22,54 @@ namespace osu.Game.Rulesets.Catch.Beatmaps public override void PostProcess() { + applyPositionOffsets(); + initialiseHyperDash((List)Beatmap.HitObjects); base.PostProcess(); int index = 0; foreach (var obj in Beatmap.HitObjects.OfType()) + { obj.IndexInBeatmap = index++; + if (obj.LastInCombo && obj.NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested) + lastNested.LastInCombo = true; + } + } + + public const int RNG_SEED = 1337; + + private void applyPositionOffsets() + { + var rng = new FastRandom(RNG_SEED); + // todo: HardRock displacement should be applied here + + foreach (var obj in Beatmap.HitObjects) + { + switch (obj) + { + case BananaShower bananaShower: + foreach (var nested in bananaShower.NestedHitObjects) + { + ((BananaShower.Banana)nested).X = (float)rng.NextDouble(); + rng.Next(); // osu!stable retrieved a random banana type + rng.Next(); // osu!stable retrieved a random banana rotation + rng.Next(); // osu!stable retrieved a random banana colour + } + break; + case JuiceStream juiceStream: + foreach (var nested in juiceStream.NestedHitObjects) + { + var hitObject = (CatchHitObject)nested; + if (hitObject is TinyDroplet) + hitObject.X += rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH; + else if (hitObject is Droplet) + rng.Next(); // osu!stable retrieved a random droplet rotation + hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1); + } + break; + } + } } private void initialiseHyperDash(List objects) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs new file mode 100644 index 0000000000..f6535380c8 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.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.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + public class CatchDifficultyAttributes : DifficultyAttributes + { + public double ApproachRate; + public int MaxCombo; + + public CatchDifficultyAttributes(Mod[] mods, double starRating) + : base(mods, starRating) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 562374087e..3d1013aad3 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -1,19 +1,146 @@ // 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.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyCalculator : DifficultyCalculator { + + /// + /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP. + /// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain. + /// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage. + /// + private const double strain_step = 750; + + /// + /// The weighting of each strain value decays to this number * it's previous value + /// + private const double decay_weight = 0.94; + + private const double star_scaling_factor = 0.145; + public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { } - protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => new DifficultyAttributes(mods, 0); + protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) + { + if (!beatmap.HitObjects.Any()) + return new CatchDifficultyAttributes(mods, 0); + + var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty); + float halfCatchWidth = catcher.CatchWidth * 0.5f; + + var difficultyHitObjects = new List(); + + foreach (var hitObject in beatmap.HitObjects) + { + // We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations. + if (hitObject is Fruit) + { + difficultyHitObjects.Add(new CatchDifficultyHitObject((CatchHitObject)hitObject, halfCatchWidth)); + } + if (hitObject is JuiceStream) + difficultyHitObjects.AddRange(hitObject.NestedHitObjects.OfType().Where(o => !(o is TinyDroplet)).Select(o => new CatchDifficultyHitObject(o, halfCatchWidth))); + } + + difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); + + if (!calculateStrainValues(difficultyHitObjects, timeRate)) + return new CatchDifficultyAttributes(mods, 0); + + // this is the same as osu!, so there's potential to share the implementation... maybe + double preEmpt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; + double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, timeRate)) * star_scaling_factor; + + return new CatchDifficultyAttributes(mods, starRating) + { + ApproachRate = preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0, + MaxCombo = difficultyHitObjects.Count + }; + } + + private bool calculateStrainValues(List objects, double timeRate) + { + CatchDifficultyHitObject lastObject = null; + + if (!objects.Any()) return false; + + // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. + foreach (var currentObject in objects) + { + if (lastObject != null) + currentObject.CalculateStrains(lastObject, timeRate); + + lastObject = currentObject; + } + + return true; + } + + private double calculateDifficulty(List objects, double timeRate) + { + // The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods + double actualStrainStep = strain_step * timeRate; + + // Find the highest strain value within each strain step + var highestStrains = new List(); + double intervalEndTime = actualStrainStep; + double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval + + CatchDifficultyHitObject previousHitObject = null; + foreach (CatchDifficultyHitObject hitObject in objects) + { + // While we are beyond the current interval push the currently available maximum to our strain list + while (hitObject.BaseHitObject.StartTime > intervalEndTime) + { + highestStrains.Add(maximumStrain); + + // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay + // until the beginning of the next interval. + if (previousHitObject == null) + { + maximumStrain = 0; + } + else + { + double decay = Math.Pow(CatchDifficultyHitObject.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); + maximumStrain = previousHitObject.Strain * decay; + } + + // Go to the next time interval + intervalEndTime += actualStrainStep; + } + + // Obtain maximum strain + maximumStrain = Math.Max(hitObject.Strain, maximumStrain); + + previousHitObject = hitObject; + } + + // Build the weighted sum over the highest strains for each interval + double difficulty = 0; + double weight = 1; + highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. + + foreach (double strain in highestStrains) + { + difficulty += weight * strain; + weight *= decay_weight; + } + + return difficulty; + } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs new file mode 100644 index 0000000000..720c1d8653 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs @@ -0,0 +1,130 @@ +// 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.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using OpenTK; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + public class CatchDifficultyHitObject + { + internal static readonly double DECAY_BASE = 0.20; + private const float normalized_hitobject_radius = 41.0f; + private const float absolute_player_positioning_error = 16f; + private readonly float playerPositioningError; + + internal CatchHitObject BaseHitObject; + + /// + /// Measures jump difficulty. CtB doesn't have something like button pressing speed or accuracy + /// + internal double Strain = 1; + + /// + /// This is required to keep track of lazy player movement (always moving only as far as necessary) + /// Without this quick repeat sliders / weirdly shaped streams might become ridiculously overrated + /// + internal float PlayerPositionOffset; + internal float LastMovement; + + internal float NormalizedPosition; + internal float ActualNormalizedPosition => NormalizedPosition + PlayerPositionOffset; + + internal CatchDifficultyHitObject(CatchHitObject baseHitObject, float catcherWidthHalf) + { + BaseHitObject = baseHitObject; + + // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. + float scalingFactor = normalized_hitobject_radius / catcherWidthHalf; + + playerPositioningError = absolute_player_positioning_error; // * scalingFactor; + NormalizedPosition = baseHitObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; + } + + private const double direction_change_bonus = 12.5; + internal void CalculateStrains(CatchDifficultyHitObject previousHitObject, double timeRate) + { + // Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make. + // See Taiko feedback thread. + double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; + double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000); + + // Update new position with lazy movement. + PlayerPositionOffset = + MathHelper.Clamp( + previousHitObject.ActualNormalizedPosition, + NormalizedPosition - (normalized_hitobject_radius - playerPositioningError), + NormalizedPosition + (normalized_hitobject_radius - playerPositioningError)) // Obtain new lazy position, but be stricter by allowing for an error of a certain degree of the player. + - NormalizedPosition; // Subtract HitObject position to obtain offset + + LastMovement = DistanceTo(previousHitObject); + double addition = spacingWeight(LastMovement); + + if (NormalizedPosition < previousHitObject.NormalizedPosition) + { + LastMovement = -LastMovement; + } + + CatchHitObject previousHitCircle = previousHitObject.BaseHitObject; + + double additionBonus = 0; + double sqrtTime = Math.Sqrt(Math.Max(timeElapsed, 25)); + + // Direction changes give an extra point! + if (Math.Abs(LastMovement) > 0.1) + { + if (Math.Abs(previousHitObject.LastMovement) > 0.1 && Math.Sign(LastMovement) != Math.Sign(previousHitObject.LastMovement)) + { + double bonus = direction_change_bonus / sqrtTime; + + // Weight bonus by how + double bonusFactor = Math.Min(playerPositioningError, Math.Abs(LastMovement)) / playerPositioningError; + + // We want time to play a role twice here! + addition += bonus * bonusFactor; + + // Bonus for tougher direction switches and "almost" hyperdashes at this point + if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) + { + additionBonus += 0.3 * bonusFactor; + } + } + + // Base bonus for every movement, giving some weight to streams. + addition += 7.5 * Math.Min(Math.Abs(LastMovement), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtTime; + } + + // Bonus for "almost" hyperdashes at corner points + if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) + { + if (!previousHitCircle.HyperDash) + { + additionBonus += 1.0; + } + else + { + // After a hyperdash we ARE in the correct position. Always! + PlayerPositionOffset = 0; + } + + addition *= 1.0 + additionBonus * ((10 - previousHitCircle.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10); + } + + addition *= 850.0 / Math.Max(timeElapsed, 25); + + Strain = previousHitObject.Strain * decay + addition; + } + + private static double spacingWeight(float distance) + { + return Math.Pow(distance, 1.3) / 500; + } + + internal float DistanceTo(CatchDifficultyHitObject other) + { + return Math.Abs(ActualNormalizedPosition - other.ActualNormalizedPosition); + } + } +} diff --git a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs new file mode 100644 index 0000000000..5b3835755a --- /dev/null +++ b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs @@ -0,0 +1,91 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; + +namespace osu.Game.Rulesets.Catch.MathUtils +{ + /// + /// A PRNG specified in http://heliosphan.org/fastrandom.html. + /// + public class FastRandom + { + private const double int_to_real = 1.0 / (int.MaxValue + 1.0); + private const uint int_mask = 0x7FFFFFFF; + private const uint y = 842502087; + private const uint z = 3579807591; + private const uint w = 273326509; + private uint _x, _y = y, _z = z, _w = w; + + public FastRandom(int seed) + { + _x = (uint)seed; + } + + public FastRandom() + : this(Environment.TickCount) + { + } + + /// + /// Generates a random unsigned integer within the range [, ). + /// + /// The random value. + public uint NextUInt() + { + uint t = _x ^ _x << 11; + _x = _y; + _y = _z; + _z = _w; + return _w = _w ^ _w >> 19 ^ t ^ t >> 8; + } + + /// + /// Generates a random integer value within the range [0, ). + /// + /// The random value. + public int Next() => (int)(int_mask & NextUInt()); + + /// + /// Generates a random integer value within the range [0, ). + /// + /// The upper bound. + /// The random value. + public int Next(int upperBound) => (int)(NextDouble() * upperBound); + + /// + /// Generates a random integer value within the range [, ). + /// + /// The lower bound of the range. + /// The upper bound of the range. + /// The random value. + public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound)); + + /// + /// Generates a random double value within the range [0, 1). + /// + /// The random value. + public double NextDouble() => int_to_real * Next(); + + private uint bitBuffer; + private int bitIndex = 32; + + /// + /// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls. + /// + /// The random value. + public bool NextBool() + { + if (bitIndex == 32) + { + bitBuffer = NextUInt(); + bitIndex = 1; + + return (bitBuffer & 1) == 1; + } + + bitIndex++; + return ((bitBuffer >>= 1) & 1) == 1; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index a6aba42f03..4dd491966c 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.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 osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects @@ -31,8 +30,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new Banana { Samples = Samples, - StartTime = i, - X = RNG.NextSingle() + StartTime = i }); } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 548813fbd2..d55cdac115 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects public int ComboIndex { get; set; } + /// + /// The distance for a fruit to to next hyper if it's not a hyper. + /// + public float DistanceToHyperDash { get; set; } + /// /// The next fruit starts a new combo. Used for explodey. /// diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index b2d8e3f8a5..6fe9692c26 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects protected override void CreateNestedHitObjects() { base.CreateNestedHitObjects(); - createTicks(); } @@ -124,9 +123,6 @@ namespace osu.Game.Rulesets.Catch.Objects X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH }); } - - if (NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested) - lastNested.LastInCombo = LastInCombo; } public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index 6a9d1bdbc7..8d3d898655 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Replays } } - public override List GetPendingStates() + public override List GetPendingInputs() { - if (!Position.HasValue) return new List(); + if (!Position.HasValue) return new List(); var actions = new List(); @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Replays else if (Position.Value < CurrentFrame.Position) actions.Add(CatchAction.MoveLeft); - return new List + return new List { new CatchReplayState { diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json index 9357d3b75c..b65d54a565 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -1,957 +1,1275 @@ { "Mappings": [{ - "StartTime": 500.0, - "Objects": [{ - "StartTime": 500.0, - "Position": 96.0 - }, { - "StartTime": 562.0, - "Position": 100.84 - }, { - "StartTime": 625.0, - "Position": 125.0 - }, { - "StartTime": 687.0, - "Position": 152.84 - }, { - "StartTime": 750.0, - "Position": 191.0 - }, { - "StartTime": 812.0, - "Position": 212.84 - }, { - "StartTime": 875.0, - "Position": 217.0 - }, { - "StartTime": 937.0, - "Position": 234.84 - }, { - "StartTime": 1000.0, - "Position": 256.0 - }, { - "StartTime": 1062.0, - "Position": 267.84 - }, { - "StartTime": 1125.0, - "Position": 284.0 - }, { - "StartTime": 1187.0, - "Position": 311.84 - }, { - "StartTime": 1250.0, - "Position": 350.0 - }, { - "StartTime": 1312.0, - "Position": 359.84 - }, { - "StartTime": 1375.0, - "Position": 367.0 - }, { - "StartTime": 1437.0, - "Position": 400.84 - }, { - "StartTime": 1500.0, - "Position": 416.0 - }, { - "StartTime": 1562.0, - "Position": 377.159973 - }, { - "StartTime": 1625.0, - "Position": 367.0 - }, { - "StartTime": 1687.0, - "Position": 374.159973 - }, { - "StartTime": 1750.0, - "Position": 353.0 - }, { - "StartTime": 1812.0, - "Position": 329.159973 - }, { - "StartTime": 1875.0, - "Position": 288.0 - }, { - "StartTime": 1937.0, - "Position": 259.159973 - }, { - "StartTime": 2000.0, - "Position": 256.0 - }, { - "StartTime": 2058.0, - "Position": 232.44 - }, { - "StartTime": 2116.0, - "Position": 222.879974 - }, { - "StartTime": 2174.0, - "Position": 185.319992 - }, { - "StartTime": 2232.0, - "Position": 177.76001 - }, { - "StartTime": 2290.0, - "Position": 162.200012 - }, { - "StartTime": 2348.0, - "Position": 158.639984 - }, { - "StartTime": 2406.0, - "Position": 111.079994 - }, { - "StartTime": 2500.0, - "Position": 96.0 - }] - }, { - "StartTime": 3000.0, - "Objects": [{ - "StartTime": 3000.0, - "Position": 18.0 - }, { - "StartTime": 3062.0, - "Position": 482.0 - }, { - "StartTime": 3125.0, - "Position": 243.0 - }, { - "StartTime": 3187.0, - "Position": 332.0 - }, { - "StartTime": 3250.0, - "Position": 477.0 - }, { - "StartTime": 3312.0, - "Position": 376.0 - }, { - "StartTime": 3375.0, - "Position": 104.0 - }, { - "StartTime": 3437.0, - "Position": 156.0 - }, { - "StartTime": 3500.0, - "Position": 135.0 - }, { - "StartTime": 3562.0, - "Position": 256.0 - }, { - "StartTime": 3625.0, - "Position": 360.0 - }, { - "StartTime": 3687.0, - "Position": 199.0 - }, { - "StartTime": 3750.0, - "Position": 239.0 - }, { - "StartTime": 3812.0, - "Position": 326.0 - }, { - "StartTime": 3875.0, - "Position": 393.0 - }, { - "StartTime": 3937.0, - "Position": 470.0 - }, { - "StartTime": 4000.0, - "Position": 136.0 - }] - }, { - "StartTime": 4500.0, - "Objects": [{ - "StartTime": 4500.0, - "Position": 317.0 - }, { - "StartTime": 4562.0, - "Position": 354.0 - }, { - "StartTime": 4625.0, - "Position": 414.0 - }, { - "StartTime": 4687.0, - "Position": 39.0 - }, { - "StartTime": 4750.0, - "Position": 172.0 - }, { - "StartTime": 4812.0, - "Position": 479.0 - }, { - "StartTime": 4875.0, - "Position": 18.0 - }, { - "StartTime": 4937.0, - "Position": 151.0 - }, { - "StartTime": 5000.0, - "Position": 342.0 - }, { - "StartTime": 5062.0, - "Position": 400.0 - }, { - "StartTime": 5125.0, - "Position": 420.0 - }, { - "StartTime": 5187.0, - "Position": 90.0 - }, { - "StartTime": 5250.0, - "Position": 220.0 - }, { - "StartTime": 5312.0, - "Position": 80.0 - }, { - "StartTime": 5375.0, - "Position": 421.0 - }, { - "StartTime": 5437.0, - "Position": 473.0 - }, { - "StartTime": 5500.0, - "Position": 97.0 - }] - }, { - "StartTime": 6000.0, - "Objects": [{ - "StartTime": 6000.0, - "Position": 105.0 - }, { - "StartTime": 6062.0, - "Position": 249.0 - }, { - "StartTime": 6125.0, - "Position": 163.0 - }, { - "StartTime": 6187.0, - "Position": 194.0 - }, { - "StartTime": 6250.0, - "Position": 106.0 - }, { - "StartTime": 6312.0, - "Position": 212.0 - }, { - "StartTime": 6375.0, - "Position": 257.0 - }, { - "StartTime": 6437.0, - "Position": 461.0 - }, { - "StartTime": 6500.0, - "Position": 79.0 - }] - }, { - "StartTime": 7000.0, - "Objects": [{ - "StartTime": 7000.0, - "Position": 256.0 - }, { - "StartTime": 7062.0, - "Position": 294.84 - }, { - "StartTime": 7125.0, - "Position": 279.0 - }, { - "StartTime": 7187.0, - "Position": 309.84 - }, { - "StartTime": 7250.0, - "Position": 336.0 - }, { - "StartTime": 7312.0, - "Position": 322.16 - }, { - "StartTime": 7375.0, - "Position": 308.0 - }, { - "StartTime": 7437.0, - "Position": 263.16 - }, { - "StartTime": 7500.0, - "Position": 256.0 - }, { - "StartTime": 7562.0, - "Position": 261.84 - }, { - "StartTime": 7625.0, - "Position": 277.0 - }, { - "StartTime": 7687.0, - "Position": 318.84 - }, { - "StartTime": 7750.0, - "Position": 336.0 - }, { - "StartTime": 7803.0, - "Position": 305.04 - }, { - "StartTime": 7857.0, - "Position": 307.76 - }, { - "StartTime": 7910.0, - "Position": 297.8 - }, { - "StartTime": 8000.0, - "Position": 256.0 - }] - }, { - "StartTime": 8500.0, - "Objects": [{ - "StartTime": 8500.0, - "Position": 32.0 - }, { - "StartTime": 8562.0, - "Position": 22.8515015 - }, { - "StartTime": 8625.0, - "Position": 28.5659637 - }, { - "StartTime": 8687.0, - "Position": 50.3433228 - }, { - "StartTime": 8750.0, - "Position": 56.58974 - }, { - "StartTime": 8812.0, - "Position": 64.23422 - }, { - "StartTime": 8875.0, - "Position": 67.7117844 - }, { - "StartTime": 8937.0, - "Position": 90.52607 - }, { - "StartTime": 9000.0, - "Position": 101.81015 - }, { - "StartTime": 9062.0, - "Position": 113.478188 - }, { - "StartTime": 9125.0, - "Position": 159.414444 - }, { - "StartTime": 9187.0, - "Position": 155.1861 - }, { - "StartTime": 9250.0, - "Position": 179.600418 - }, { - "StartTime": 9312.0, - "Position": 212.293015 - }, { - "StartTime": 9375.0, - "Position": 197.2076 - }, { - "StartTime": 9437.0, - "Position": 243.438324 - }, { - "StartTime": 9500.0, - "Position": 237.2304 - }, { - "StartTime": 9562.0, - "Position": 241.253983 - }, { - "StartTime": 9625.0, - "Position": 258.950623 - }, { - "StartTime": 9687.0, - "Position": 253.3786 - }, { - "StartTime": 9750.0, - "Position": 270.8865 - }, { - "StartTime": 9812.0, - "Position": 244.38974 - }, { - "StartTime": 9875.0, - "Position": 242.701874 - }, { - "StartTime": 9937.0, - "Position": 256.2331 - }, { - "StartTime": 10000.0, - "Position": 270.339874 - }, { - "StartTime": 10062.0, - "Position": 275.9349 - }, { - "StartTime": 10125.0, - "Position": 297.2969 - }, { - "StartTime": 10187.0, - "Position": 307.834137 - }, { - "StartTime": 10250.0, - "Position": 321.6449 - }, { - "StartTime": 10312.0, - "Position": 357.746338 - }, { - "StartTime": 10375.0, - "Position": 358.21875 - }, { - "StartTime": 10437.0, - "Position": 394.943 - }, { - "StartTime": 10500.0, - "Position": 401.0588 - }, { - "StartTime": 10558.0, - "Position": 418.21347 - }, { - "StartTime": 10616.0, - "Position": 424.6034 - }, { - "StartTime": 10674.0, - "Position": 455.835754 - }, { - "StartTime": 10732.0, - "Position": 477.5042 - }, { - "StartTime": 10790.0, - "Position": 476.290955 - }, { - "StartTime": 10848.0, - "Position": 470.943237 - }, { - "StartTime": 10906.0, - "Position": 503.3372 - }, { - "StartTime": 10999.0, - "Position": 508.166229 - }] - }, { - "StartTime": 11500.0, - "Objects": [{ - "StartTime": 11500.0, - "Position": 321.0 - }, { - "StartTime": 11562.0, - "Position": 17.0 - }, { - "StartTime": 11625.0, - "Position": 173.0 - }, { - "StartTime": 11687.0, - "Position": 170.0 - }, { - "StartTime": 11750.0, - "Position": 447.0 - }, { - "StartTime": 11812.0, - "Position": 218.0 - }, { - "StartTime": 11875.0, - "Position": 394.0 - }, { - "StartTime": 11937.0, - "Position": 46.0 - }, { - "StartTime": 12000.0, - "Position": 480.0 - }] - }, { - "StartTime": 12500.0, - "Objects": [{ - "StartTime": 12500.0, - "Position": 512.0 - }, { - "StartTime": 12562.0, - "Position": 491.3132 - }, { - "StartTime": 12625.0, - "Position": 484.3089 - }, { - "StartTime": 12687.0, - "Position": 454.6221 - }, { - "StartTime": 12750.0, - "Position": 433.617767 - }, { - "StartTime": 12812.0, - "Position": 399.930969 - }, { - "StartTime": 12875.0, - "Position": 395.926666 - }, { - "StartTime": 12937.0, - "Position": 361.239868 - }, { - "StartTime": 13000.0, - "Position": 353.235535 - }, { - "StartTime": 13062.0, - "Position": 314.548767 - }, { - "StartTime": 13125.0, - "Position": 315.544434 - }, { - "StartTime": 13187.0, - "Position": 288.857635 - }, { - "StartTime": 13250.0, - "Position": 254.853333 - }, { - "StartTime": 13312.0, - "Position": 239.166534 - }, { - "StartTime": 13375.0, - "Position": 240.1622 - }, { - "StartTime": 13437.0, - "Position": 212.4754 - }, { - "StartTime": 13500.0, - "Position": 194.471069 - }, { - "StartTime": 13562.0, - "Position": 161.784271 - }, { - "StartTime": 13625.0, - "Position": 145.779968 - }, { - "StartTime": 13687.0, - "Position": 129.09314 - }, { - "StartTime": 13750.0, - "Position": 104.088837 - }, { - "StartTime": 13812.0, - "Position": 95.40204 - }, { - "StartTime": 13875.0, - "Position": 61.3977356 - }, { - "StartTime": 13937.0, - "Position": 56.710907 - }, { - "StartTime": 14000.0, - "Position": 35.7066345 - }, { - "StartTime": 14062.0, - "Position": 5.019806 - }, { - "StartTime": 14125.0, - "Position": 0.0 - }, { - "StartTime": 14187.0, - "Position": 39.7696266 - }, { - "StartTime": 14250.0, - "Position": 23.0119171 - }, { - "StartTime": 14312.0, - "Position": 75.94882 - }, { - "StartTime": 14375.0, - "Position": 98.19112 - }, { - "StartTime": 14437.0, - "Position": 82.12803 - }, { - "StartTime": 14500.0, - "Position": 118.370323 - }, { - "StartTime": 14562.0, - "Position": 149.307236 - }, { - "StartTime": 14625.0, - "Position": 168.549515 - }, { - "StartTime": 14687.0, - "Position": 190.486435 - }, { - "StartTime": 14750.0, - "Position": 186.728714 - }, { - "StartTime": 14812.0, - "Position": 199.665634 - }, { - "StartTime": 14875.0, - "Position": 228.907928 - }, { - "StartTime": 14937.0, - "Position": 264.844849 - }, { - "StartTime": 15000.0, - "Position": 271.087128 - }, { - "StartTime": 15062.0, - "Position": 290.024017 - }, { - "StartTime": 15125.0, - "Position": 302.266327 - }, { - "StartTime": 15187.0, - "Position": 344.203247 - }, { - "StartTime": 15250.0, - "Position": 356.445526 - }, { - "StartTime": 15312.0, - "Position": 359.382446 - }, { - "StartTime": 15375.0, - "Position": 401.624725 - }, { - "StartTime": 15437.0, - "Position": 388.561646 - }, { - "StartTime": 15500.0, - "Position": 423.803925 - }, { - "StartTime": 15562.0, - "Position": 425.740845 - }, { - "StartTime": 15625.0, - "Position": 449.983124 - }, { - "StartTime": 15687.0, - "Position": 468.920044 - }, { - "StartTime": 15750.0, - "Position": 492.162323 - }, { - "StartTime": 15812.0, - "Position": 506.784332 - }, { - "StartTime": 15875.0, - "Position": 474.226227 - }, { - "StartTime": 15937.0, - "Position": 482.978638 - }, { - "StartTime": 16000.0, - "Position": 446.420532 - }, { - "StartTime": 16058.0, - "Position": 418.4146 - }, { - "StartTime": 16116.0, - "Position": 425.408844 - }, { - "StartTime": 16174.0, - "Position": 383.402924 - }, { - "StartTime": 16232.0, - "Position": 363.397156 - }, { - "StartTime": 16290.0, - "Position": 343.391235 - }, { - "StartTime": 16348.0, - "Position": 328.385468 - }, { - "StartTime": 16406.0, - "Position": 322.3797 - }, { - "StartTime": 16500.0, - "Position": 291.1977 - }] - }, { - "StartTime": 17000.0, - "Objects": [{ - "StartTime": 17000.0, - "Position": 256.0 - }, { - "StartTime": 17062.0, - "Position": 228.16 - }, { - "StartTime": 17125.0, - "Position": 234.0 - }, { - "StartTime": 17187.0, - "Position": 202.16 - }, { - "StartTime": 17250.0, - "Position": 176.0 - }, { - "StartTime": 17312.0, - "Position": 210.84 - }, { - "StartTime": 17375.0, - "Position": 221.0 - }, { - "StartTime": 17437.0, - "Position": 219.84 - }, { - "StartTime": 17500.0, - "Position": 256.0 - }, { - "StartTime": 17562.0, - "Position": 219.16 - }, { - "StartTime": 17625.0, - "Position": 228.0 - }, { - "StartTime": 17687.0, - "Position": 203.16 - }, { - "StartTime": 17750.0, - "Position": 176.0 - }, { - "StartTime": 17803.0, - "Position": 174.959991 - }, { - "StartTime": 17857.0, - "Position": 214.23999 - }, { - "StartTime": 17910.0, - "Position": 228.200012 - }, { - "StartTime": 18000.0, - "Position": 256.0 - }] - }, { - "StartTime": 18500.0, - "Objects": [{ - "StartTime": 18500.0, - "Position": 362.0 - }, { - "StartTime": 18559.0, - "Position": 249.0 - }, { - "StartTime": 18618.0, - "Position": 357.0 - }, { - "StartTime": 18678.0, - "Position": 167.0 - }, { - "StartTime": 18737.0, - "Position": 477.0 - }, { - "StartTime": 18796.0, - "Position": 411.0 - }, { - "StartTime": 18856.0, - "Position": 254.0 - }, { - "StartTime": 18915.0, - "Position": 308.0 - }, { - "StartTime": 18975.0, - "Position": 399.0 - }, { - "StartTime": 19034.0, - "Position": 176.0 - }, { - "StartTime": 19093.0, - "Position": 14.0 - }, { - "StartTime": 19153.0, - "Position": 258.0 - }, { - "StartTime": 19212.0, - "Position": 221.0 - }, { - "StartTime": 19271.0, - "Position": 481.0 - }, { - "StartTime": 19331.0, - "Position": 92.0 - }, { - "StartTime": 19390.0, - "Position": 211.0 - }, { - "StartTime": 19450.0, - "Position": 135.0 - }] - }, { - "StartTime": 19875.0, - "Objects": [{ - "StartTime": 19875.0, - "Position": 216.0 - }, { - "StartTime": 19937.0, - "Position": 215.307053 - }, { - "StartTime": 20000.0, - "Position": 236.036865 - }, { - "StartTime": 20062.0, - "Position": 236.312088 - }, { - "StartTime": 20125.0, - "Position": 235.838928 - }, { - "StartTime": 20187.0, - "Position": 269.9743 - }, { - "StartTime": 20250.0, - "Position": 285.999146 - }, { - "StartTime": 20312.0, - "Position": 283.669067 - }, { - "StartTime": 20375.0, - "Position": 317.446747 - }, { - "StartTime": 20437.0, - "Position": 330.750275 - }, { - "StartTime": 20500.0, - "Position": 344.0156 - }, { - "StartTime": 20562.0, - "Position": 318.472168 - }, { - "StartTime": 20625.0, - "Position": 309.165466 - }, { - "StartTime": 20687.0, - "Position": 317.044617 - }, { - "StartTime": 20750.0, - "Position": 280.457367 - }, { - "StartTime": 20812.0, - "Position": 272.220581 - }, { - "StartTime": 20875.0, - "Position": 270.3294 - }, { - "StartTime": 20937.0, - "Position": 262.57605 - }, { - "StartTime": 21000.0, - "Position": 244.803329 - }, { - "StartTime": 21062.0, - "Position": 215.958359 - }, { - "StartTime": 21125.0, - "Position": 177.79332 - }, { - "StartTime": 21187.0, - "Position": 190.948349 - }, { - "StartTime": 21250.0, - "Position": 158.78334 - }, { - "StartTime": 21312.0, - "Position": 136.93837 - }, { - "StartTime": 21375.0, - "Position": 119.121056 - }, { - "StartTime": 21437.0, - "Position": 132.387573 - }, { - "StartTime": 21500.0, - "Position": 124.503014 - }, { - "StartTime": 21562.0, - "Position": 118.749374 - }, { - "StartTime": 21625.0, - "Position": 123.165535 - }, { - "StartTime": 21687.0, - "Position": 96.02999 - }, { - "StartTime": 21750.0, - "Position": 118.547928 - }, { - "StartTime": 21812.0, - "Position": 128.856232 - }, { - "StartTime": 21875.0, - "Position": 124.28746 - }, { - "StartTime": 21937.0, - "Position": 150.754929 - }, { - "StartTime": 22000.0, - "Position": 149.528732 - }, { - "StartTime": 22062.0, - "Position": 145.1691 - }, { - "StartTime": 22125.0, - "Position": 182.802155 - }, { - "StartTime": 22187.0, - "Position": 178.6452 - }, { - "StartTime": 22250.0, - "Position": 213.892181 - }, { - "StartTime": 22312.0, - "Position": 218.713028 - }, { - "StartTime": 22375.0, - "Position": 240.4715 - }, { - "StartTime": 22437.0, - "Position": 239.371887 - }, { - "StartTime": 22500.0, - "Position": 261.907257 - }, { - "StartTime": 22562.0, - "Position": 314.353119 - }, { - "StartTime": 22625.0, - "Position": 299.273376 - }, { - "StartTime": 22687.0, - "Position": 356.98288 - }, { - "StartTime": 22750.0, - "Position": 339.078552 - }, { - "StartTime": 22812.0, - "Position": 377.8958 - }, { - "StartTime": 22875.0, - "Position": 398.054047 - }, { - "StartTime": 22937.0, - "Position": 398.739441 - }, { - "StartTime": 23000.0, - "Position": 407.178467 - }, { - "StartTime": 23062.0, - "Position": 444.8687 - }, { - "StartTime": 23125.0, - "Position": 417.069977 - }, { - "StartTime": 23187.0, - "Position": 454.688477 - }, { - "StartTime": 23250.0, - "Position": 428.9612 - }, { - "StartTime": 23312.0, - "Position": 441.92807 - }, { - "StartTime": 23375.0, - "Position": 439.749878 - }, { - "StartTime": 23433.0, - "Position": 455.644684 - }, { - "StartTime": 23491.0, - "Position": 440.7359 - }, { - "StartTime": 23549.0, - "Position": 430.0944 - }, { - "StartTime": 23607.0, - "Position": 420.796173 - }, { - "StartTime": 23665.0, - "Position": 435.897461 - }, { - "StartTime": 23723.0, - "Position": 418.462555 - }, { - "StartTime": 23781.0, - "Position": 405.53775 - }, { - "StartTime": 23874.0, - "Position": 408.720825 - }] - }] + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "Position": 96 + }, + { + "StartTime": 562, + "Position": 100.84 + }, + { + "StartTime": 625, + "Position": 125 + }, + { + "StartTime": 687, + "Position": 152.84 + }, + { + "StartTime": 750, + "Position": 191 + }, + { + "StartTime": 812, + "Position": 212.84 + }, + { + "StartTime": 875, + "Position": 217 + }, + { + "StartTime": 937, + "Position": 234.84 + }, + { + "StartTime": 1000, + "Position": 256 + }, + { + "StartTime": 1062, + "Position": 267.84 + }, + { + "StartTime": 1125, + "Position": 284 + }, + { + "StartTime": 1187, + "Position": 311.84 + }, + { + "StartTime": 1250, + "Position": 350 + }, + { + "StartTime": 1312, + "Position": 359.84 + }, + { + "StartTime": 1375, + "Position": 367 + }, + { + "StartTime": 1437, + "Position": 400.84 + }, + { + "StartTime": 1500, + "Position": 416 + }, + { + "StartTime": 1562, + "Position": 377.159973 + }, + { + "StartTime": 1625, + "Position": 367 + }, + { + "StartTime": 1687, + "Position": 374.159973 + }, + { + "StartTime": 1750, + "Position": 353 + }, + { + "StartTime": 1812, + "Position": 329.159973 + }, + { + "StartTime": 1875, + "Position": 288 + }, + { + "StartTime": 1937, + "Position": 259.159973 + }, + { + "StartTime": 2000, + "Position": 256 + }, + { + "StartTime": 2058, + "Position": 232.44 + }, + { + "StartTime": 2116, + "Position": 222.879974 + }, + { + "StartTime": 2174, + "Position": 185.319992 + }, + { + "StartTime": 2232, + "Position": 177.76001 + }, + { + "StartTime": 2290, + "Position": 162.200012 + }, + { + "StartTime": 2348, + "Position": 158.639984 + }, + { + "StartTime": 2406, + "Position": 111.079994 + }, + { + "StartTime": 2500, + "Position": 96 + } + ] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "Position": 18 + }, + { + "StartTime": 3062, + "Position": 249 + }, + { + "StartTime": 3125, + "Position": 184 + }, + { + "StartTime": 3187, + "Position": 477 + }, + { + "StartTime": 3250, + "Position": 43 + }, + { + "StartTime": 3312, + "Position": 494 + }, + { + "StartTime": 3375, + "Position": 135 + }, + { + "StartTime": 3437, + "Position": 30 + }, + { + "StartTime": 3500, + "Position": 11 + }, + { + "StartTime": 3562, + "Position": 239 + }, + { + "StartTime": 3625, + "Position": 505 + }, + { + "StartTime": 3687, + "Position": 353 + }, + { + "StartTime": 3750, + "Position": 136 + }, + { + "StartTime": 3812, + "Position": 135 + }, + { + "StartTime": 3875, + "Position": 346 + }, + { + "StartTime": 3937, + "Position": 39 + }, + { + "StartTime": 4000, + "Position": 300 + } + ] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "Position": 398 + }, + { + "StartTime": 4562, + "Position": 151 + }, + { + "StartTime": 4625, + "Position": 73 + }, + { + "StartTime": 4687, + "Position": 311 + }, + { + "StartTime": 4750, + "Position": 90 + }, + { + "StartTime": 4812, + "Position": 264 + }, + { + "StartTime": 4875, + "Position": 477 + }, + { + "StartTime": 4937, + "Position": 473 + }, + { + "StartTime": 5000, + "Position": 120 + }, + { + "StartTime": 5062, + "Position": 115 + }, + { + "StartTime": 5125, + "Position": 163 + }, + { + "StartTime": 5187, + "Position": 447 + }, + { + "StartTime": 5250, + "Position": 72 + }, + { + "StartTime": 5312, + "Position": 257 + }, + { + "StartTime": 5375, + "Position": 153 + }, + { + "StartTime": 5437, + "Position": 388 + }, + { + "StartTime": 5500, + "Position": 336 + } + ] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "Position": 13 + }, + { + "StartTime": 6062, + "Position": 429 + }, + { + "StartTime": 6125, + "Position": 381 + }, + { + "StartTime": 6187, + "Position": 186 + }, + { + "StartTime": 6250, + "Position": 267 + }, + { + "StartTime": 6312, + "Position": 305 + }, + { + "StartTime": 6375, + "Position": 456 + }, + { + "StartTime": 6437, + "Position": 26 + }, + { + "StartTime": 6500, + "Position": 238 + } + ] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "Position": 256 + }, + { + "StartTime": 7062, + "Position": 262.84 + }, + { + "StartTime": 7125, + "Position": 295 + }, + { + "StartTime": 7187, + "Position": 303.84 + }, + { + "StartTime": 7250, + "Position": 336 + }, + { + "StartTime": 7312, + "Position": 319.16 + }, + { + "StartTime": 7375, + "Position": 306 + }, + { + "StartTime": 7437, + "Position": 272.16 + }, + { + "StartTime": 7500, + "Position": 256 + }, + { + "StartTime": 7562, + "Position": 255.84 + }, + { + "StartTime": 7625, + "Position": 300 + }, + { + "StartTime": 7687, + "Position": 320.84 + }, + { + "StartTime": 7750, + "Position": 336 + }, + { + "StartTime": 7803, + "Position": 319.04 + }, + { + "StartTime": 7857, + "Position": 283.76 + }, + { + "StartTime": 7910, + "Position": 265.8 + }, + { + "StartTime": 8000, + "Position": 256 + } + ] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "Position": 32 + }, + { + "StartTime": 8562, + "Position": 21.8515015 + }, + { + "StartTime": 8625, + "Position": 44.5659637 + }, + { + "StartTime": 8687, + "Position": 33.3433228 + }, + { + "StartTime": 8750, + "Position": 63.58974 + }, + { + "StartTime": 8812, + "Position": 71.23422 + }, + { + "StartTime": 8875, + "Position": 62.7117844 + }, + { + "StartTime": 8937, + "Position": 65.52607 + }, + { + "StartTime": 9000, + "Position": 101.81015 + }, + { + "StartTime": 9062, + "Position": 134.47818 + }, + { + "StartTime": 9125, + "Position": 141.414444 + }, + { + "StartTime": 9187, + "Position": 164.1861 + }, + { + "StartTime": 9250, + "Position": 176.600418 + }, + { + "StartTime": 9312, + "Position": 184.293015 + }, + { + "StartTime": 9375, + "Position": 212.2076 + }, + { + "StartTime": 9437, + "Position": 236.438324 + }, + { + "StartTime": 9500, + "Position": 237.2304 + }, + { + "StartTime": 9562, + "Position": 241.253983 + }, + { + "StartTime": 9625, + "Position": 233.950623 + }, + { + "StartTime": 9687, + "Position": 265.3786 + }, + { + "StartTime": 9750, + "Position": 236.8865 + }, + { + "StartTime": 9812, + "Position": 273.38974 + }, + { + "StartTime": 9875, + "Position": 267.701874 + }, + { + "StartTime": 9937, + "Position": 263.2331 + }, + { + "StartTime": 10000, + "Position": 270.339874 + }, + { + "StartTime": 10062, + "Position": 291.9349 + }, + { + "StartTime": 10125, + "Position": 294.2969 + }, + { + "StartTime": 10187, + "Position": 307.834137 + }, + { + "StartTime": 10250, + "Position": 310.6449 + }, + { + "StartTime": 10312, + "Position": 344.746338 + }, + { + "StartTime": 10375, + "Position": 349.21875 + }, + { + "StartTime": 10437, + "Position": 373.943 + }, + { + "StartTime": 10500, + "Position": 401.0588 + }, + { + "StartTime": 10558, + "Position": 421.21347 + }, + { + "StartTime": 10616, + "Position": 431.6034 + }, + { + "StartTime": 10674, + "Position": 433.835754 + }, + { + "StartTime": 10732, + "Position": 452.5042 + }, + { + "StartTime": 10790, + "Position": 486.290955 + }, + { + "StartTime": 10848, + "Position": 488.943237 + }, + { + "StartTime": 10906, + "Position": 493.3372 + }, + { + "StartTime": 10999, + "Position": 508.166229 + } + ] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "Position": 97 + }, + { + "StartTime": 11562, + "Position": 267 + }, + { + "StartTime": 11625, + "Position": 116 + }, + { + "StartTime": 11687, + "Position": 451 + }, + { + "StartTime": 11750, + "Position": 414 + }, + { + "StartTime": 11812, + "Position": 88 + }, + { + "StartTime": 11875, + "Position": 257 + }, + { + "StartTime": 11937, + "Position": 175 + }, + { + "StartTime": 12000, + "Position": 38 + } + ] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "Position": 512 + }, + { + "StartTime": 12562, + "Position": 494.3132 + }, + { + "StartTime": 12625, + "Position": 461.3089 + }, + { + "StartTime": 12687, + "Position": 469.6221 + }, + { + "StartTime": 12750, + "Position": 441.617767 + }, + { + "StartTime": 12812, + "Position": 402.930969 + }, + { + "StartTime": 12875, + "Position": 407.926666 + }, + { + "StartTime": 12937, + "Position": 364.239868 + }, + { + "StartTime": 13000, + "Position": 353.235535 + }, + { + "StartTime": 13062, + "Position": 320.548767 + }, + { + "StartTime": 13125, + "Position": 303.544434 + }, + { + "StartTime": 13187, + "Position": 295.857635 + }, + { + "StartTime": 13250, + "Position": 265.853333 + }, + { + "StartTime": 13312, + "Position": 272.166534 + }, + { + "StartTime": 13375, + "Position": 240.1622 + }, + { + "StartTime": 13437, + "Position": 229.4754 + }, + { + "StartTime": 13500, + "Position": 194.471069 + }, + { + "StartTime": 13562, + "Position": 158.784271 + }, + { + "StartTime": 13625, + "Position": 137.779968 + }, + { + "StartTime": 13687, + "Position": 147.09314 + }, + { + "StartTime": 13750, + "Position": 122.088837 + }, + { + "StartTime": 13812, + "Position": 77.40204 + }, + { + "StartTime": 13875, + "Position": 79.3977356 + }, + { + "StartTime": 13937, + "Position": 56.710907 + }, + { + "StartTime": 14000, + "Position": 35.7066345 + }, + { + "StartTime": 14062, + "Position": 1.01980591 + }, + { + "StartTime": 14125, + "Position": 0 + }, + { + "StartTime": 14187, + "Position": 21.7696266 + }, + { + "StartTime": 14250, + "Position": 49.0119171 + }, + { + "StartTime": 14312, + "Position": 48.9488258 + }, + { + "StartTime": 14375, + "Position": 87.19112 + }, + { + "StartTime": 14437, + "Position": 97.12803 + }, + { + "StartTime": 14500, + "Position": 118.370323 + }, + { + "StartTime": 14562, + "Position": 130.307236 + }, + { + "StartTime": 14625, + "Position": 154.549515 + }, + { + "StartTime": 14687, + "Position": 190.486435 + }, + { + "StartTime": 14750, + "Position": 211.728714 + }, + { + "StartTime": 14812, + "Position": 197.665634 + }, + { + "StartTime": 14875, + "Position": 214.907928 + }, + { + "StartTime": 14937, + "Position": 263.844849 + }, + { + "StartTime": 15000, + "Position": 271.087128 + }, + { + "StartTime": 15062, + "Position": 270.024017 + }, + { + "StartTime": 15125, + "Position": 308.266327 + }, + { + "StartTime": 15187, + "Position": 313.203247 + }, + { + "StartTime": 15250, + "Position": 328.445526 + }, + { + "StartTime": 15312, + "Position": 370.382446 + }, + { + "StartTime": 15375, + "Position": 387.624725 + }, + { + "StartTime": 15437, + "Position": 421.561646 + }, + { + "StartTime": 15500, + "Position": 423.803925 + }, + { + "StartTime": 15562, + "Position": 444.740845 + }, + { + "StartTime": 15625, + "Position": 469.983124 + }, + { + "StartTime": 15687, + "Position": 473.920044 + }, + { + "StartTime": 15750, + "Position": 501.162323 + }, + { + "StartTime": 15812, + "Position": 488.784332 + }, + { + "StartTime": 15875, + "Position": 466.226227 + }, + { + "StartTime": 15937, + "Position": 445.978638 + }, + { + "StartTime": 16000, + "Position": 446.420532 + }, + { + "StartTime": 16058, + "Position": 428.4146 + }, + { + "StartTime": 16116, + "Position": 420.408844 + }, + { + "StartTime": 16174, + "Position": 374.402924 + }, + { + "StartTime": 16232, + "Position": 371.397156 + }, + { + "StartTime": 16290, + "Position": 350.391235 + }, + { + "StartTime": 16348, + "Position": 340.385468 + }, + { + "StartTime": 16406, + "Position": 337.3797 + }, + { + "StartTime": 16500, + "Position": 291.1977 + } + ] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "Position": 256 + }, + { + "StartTime": 17062, + "Position": 247.16 + }, + { + "StartTime": 17125, + "Position": 211 + }, + { + "StartTime": 17187, + "Position": 183.16 + }, + { + "StartTime": 17250, + "Position": 176 + }, + { + "StartTime": 17312, + "Position": 204.84 + }, + { + "StartTime": 17375, + "Position": 218 + }, + { + "StartTime": 17437, + "Position": 231.84 + }, + { + "StartTime": 17500, + "Position": 256 + }, + { + "StartTime": 17562, + "Position": 229.16 + }, + { + "StartTime": 17625, + "Position": 227 + }, + { + "StartTime": 17687, + "Position": 186.16 + }, + { + "StartTime": 17750, + "Position": 176 + }, + { + "StartTime": 17803, + "Position": 211.959991 + }, + { + "StartTime": 17857, + "Position": 197.23999 + }, + { + "StartTime": 17910, + "Position": 225.200012 + }, + { + "StartTime": 18000, + "Position": 256 + } + ] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "Position": 437 + }, + { + "StartTime": 18559, + "Position": 289 + }, + { + "StartTime": 18618, + "Position": 464 + }, + { + "StartTime": 18678, + "Position": 36 + }, + { + "StartTime": 18737, + "Position": 378 + }, + { + "StartTime": 18796, + "Position": 297 + }, + { + "StartTime": 18856, + "Position": 418 + }, + { + "StartTime": 18915, + "Position": 329 + }, + { + "StartTime": 18975, + "Position": 338 + }, + { + "StartTime": 19034, + "Position": 394 + }, + { + "StartTime": 19093, + "Position": 40 + }, + { + "StartTime": 19153, + "Position": 13 + }, + { + "StartTime": 19212, + "Position": 80 + }, + { + "StartTime": 19271, + "Position": 138 + }, + { + "StartTime": 19331, + "Position": 311 + }, + { + "StartTime": 19390, + "Position": 216 + }, + { + "StartTime": 19450, + "Position": 310 + } + ] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "Position": 216 + }, + { + "StartTime": 19937, + "Position": 228.307053 + }, + { + "StartTime": 20000, + "Position": 214.036865 + }, + { + "StartTime": 20062, + "Position": 224.312088 + }, + { + "StartTime": 20125, + "Position": 253.838928 + }, + { + "StartTime": 20187, + "Position": 259.9743 + }, + { + "StartTime": 20250, + "Position": 299.999146 + }, + { + "StartTime": 20312, + "Position": 289.669067 + }, + { + "StartTime": 20375, + "Position": 317.446747 + }, + { + "StartTime": 20437, + "Position": 344.750275 + }, + { + "StartTime": 20500, + "Position": 328.0156 + }, + { + "StartTime": 20562, + "Position": 331.472168 + }, + { + "StartTime": 20625, + "Position": 302.165466 + }, + { + "StartTime": 20687, + "Position": 303.044617 + }, + { + "StartTime": 20750, + "Position": 306.457367 + }, + { + "StartTime": 20812, + "Position": 265.220581 + }, + { + "StartTime": 20875, + "Position": 270.3294 + }, + { + "StartTime": 20937, + "Position": 257.57605 + }, + { + "StartTime": 21000, + "Position": 247.803329 + }, + { + "StartTime": 21062, + "Position": 225.958359 + }, + { + "StartTime": 21125, + "Position": 201.79332 + }, + { + "StartTime": 21187, + "Position": 170.948349 + }, + { + "StartTime": 21250, + "Position": 146.78334 + }, + { + "StartTime": 21312, + "Position": 149.93837 + }, + { + "StartTime": 21375, + "Position": 119.121056 + }, + { + "StartTime": 21437, + "Position": 133.387573 + }, + { + "StartTime": 21500, + "Position": 117.503014 + }, + { + "StartTime": 21562, + "Position": 103.749374 + }, + { + "StartTime": 21625, + "Position": 127.165535 + }, + { + "StartTime": 21687, + "Position": 113.029991 + }, + { + "StartTime": 21750, + "Position": 101.547928 + }, + { + "StartTime": 21812, + "Position": 133.856232 + }, + { + "StartTime": 21875, + "Position": 124.28746 + }, + { + "StartTime": 21937, + "Position": 121.754929 + }, + { + "StartTime": 22000, + "Position": 155.528732 + }, + { + "StartTime": 22062, + "Position": 142.1691 + }, + { + "StartTime": 22125, + "Position": 186.802155 + }, + { + "StartTime": 22187, + "Position": 198.6452 + }, + { + "StartTime": 22250, + "Position": 191.892181 + }, + { + "StartTime": 22312, + "Position": 232.713028 + }, + { + "StartTime": 22375, + "Position": 240.4715 + }, + { + "StartTime": 22437, + "Position": 278.3719 + }, + { + "StartTime": 22500, + "Position": 288.907257 + }, + { + "StartTime": 22562, + "Position": 297.353119 + }, + { + "StartTime": 22625, + "Position": 301.273376 + }, + { + "StartTime": 22687, + "Position": 339.98288 + }, + { + "StartTime": 22750, + "Position": 353.078552 + }, + { + "StartTime": 22812, + "Position": 363.8958 + }, + { + "StartTime": 22875, + "Position": 398.054047 + }, + { + "StartTime": 22937, + "Position": 419.739441 + }, + { + "StartTime": 23000, + "Position": 435.178467 + }, + { + "StartTime": 23062, + "Position": 420.8687 + }, + { + "StartTime": 23125, + "Position": 448.069977 + }, + { + "StartTime": 23187, + "Position": 425.688477 + }, + { + "StartTime": 23250, + "Position": 426.9612 + }, + { + "StartTime": 23312, + "Position": 454.92807 + }, + { + "StartTime": 23375, + "Position": 439.749878 + }, + { + "StartTime": 23433, + "Position": 440.644684 + }, + { + "StartTime": 23491, + "Position": 445.7359 + }, + { + "StartTime": 23549, + "Position": 432.0944 + }, + { + "StartTime": 23607, + "Position": 415.796173 + }, + { + "StartTime": 23665, + "Position": 407.897461 + }, + { + "StartTime": 23723, + "Position": 409.462555 + }, + { + "StartTime": 23781, + "Position": 406.53775 + }, + { + "StartTime": 23874, + "Position": 408.720825 + } + ] + } + ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json new file mode 100644 index 0000000000..dd81947f1c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json @@ -0,0 +1,65 @@ +{ + "Mappings": [{ + "StartTime": 2589, + "Objects": [{ + "StartTime": 2589, + "Position": 256 + }] + }, + { + "StartTime": 2915, + "Objects": [{ + "StartTime": 2915, + "Position": 65 + }, + { + "StartTime": 2916, + "Position": 482 + } + ] + }, + { + "StartTime": 3078, + "Objects": [{ + "StartTime": 3078, + "Position": 164 + }, + { + "StartTime": 3079, + "Position": 315 + } + ] + }, + { + "StartTime": 3241, + "Objects": [{ + "StartTime": 3241, + "Position": 145 + }, + { + "StartTime": 3242, + "Position": 159 + } + ] + }, + { + "StartTime": 3404, + "Objects": [{ + "StartTime": 3404, + "Position": 310 + }, + { + "StartTime": 3405, + "Position": 441 + } + ] + }, + { + "StartTime": 5197, + "Objects": [{ + "StartTime": 5197, + "Position": 256 + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles.osu new file mode 100644 index 0000000000..9a90768428 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles.osu @@ -0,0 +1,24 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:5 +CircleSize:2 +OverallDifficulty:5 +ApproachRate:8 +SliderMultiplier:1.4 +SliderTickRate:4 + +[TimingPoints] +2589,326.086956521739,4,2,1,70,1,0 + +[HitObjects] +256,192,2589,5,0,0:0:0:0: +256,192,2915,12,0,2916,0:0:0:0: +256,192,3078,12,0,3079,0:0:0:0: +256,192,3241,12,0,3242,0:0:0:0: +256,192,3404,12,0,3405,0:0:0:0: +256,192,5197,5,0,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json new file mode 100644 index 0000000000..b69b1ae056 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json @@ -0,0 +1,74 @@ +{ + "Mappings": [{ + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "Position": 65 + }, + { + "StartTime": 18559, + "Position": 482 + }, + { + "StartTime": 18618, + "Position": 164 + }, + { + "StartTime": 18678, + "Position": 315 + }, + { + "StartTime": 18737, + "Position": 145 + }, + { + "StartTime": 18796, + "Position": 159 + }, + { + "StartTime": 18856, + "Position": 310 + }, + { + "StartTime": 18915, + "Position": 441 + }, + { + "StartTime": 18975, + "Position": 428 + }, + { + "StartTime": 19034, + "Position": 243 + }, + { + "StartTime": 19093, + "Position": 422 + }, + { + "StartTime": 19153, + "Position": 481 + }, + { + "StartTime": 19212, + "Position": 104 + }, + { + "StartTime": 19271, + "Position": 473 + }, + { + "StartTime": 19331, + "Position": 135 + }, + { + "StartTime": 19390, + "Position": 360 + }, + { + "StartTime": 19450, + "Position": 123 + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner.osu new file mode 100644 index 0000000000..0fbd4dbf42 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner.osu @@ -0,0 +1,20 @@ +osu file format v14 + +[General] +Mode: 2 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +256,192,18500,12,0,19450,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index b62e9997d4..ceb05d349f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using OpenTK; using OpenTK.Graphics; @@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.UI { base.UpdateAfterChildren(); - var state = GetContainingInputManager().CurrentState as CatchFramedReplayInputHandler.CatchReplayState; + var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState; if (state?.CatcherX != null) MovableCatcher.X = state.CatcherX.Value; @@ -105,6 +106,11 @@ namespace osu.Game.Rulesets.Catch.UI public class Catcher : Container, IKeyBindingHandler { + /// + /// Width of the area that can be used to attempt catches during gameplay. + /// + internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X); + private Container caughtFruit; public Container ExplodingFruitTarget; @@ -232,63 +238,74 @@ namespace osu.Game.Rulesets.Catch.UI /// Whether the catch is possible. public bool AttemptCatch(CatchHitObject fruit) { - double halfCatcherWidth = CATCHER_SIZE * Math.Abs(Scale.X) * 0.5f; + float halfCatchWidth = CatchWidth * 0.5f; // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; var validCatch = - catchObjectPosition >= catcherPosition - halfCatcherWidth && - catchObjectPosition <= catcherPosition + halfCatcherWidth; + catchObjectPosition >= catcherPosition - halfCatchWidth && + catchObjectPosition <= catcherPosition + halfCatchWidth; if (validCatch && fruit.HyperDash) { - HyperDashModifier = Math.Abs(fruit.HyperDashTarget.X - fruit.X) / Math.Abs(fruit.HyperDashTarget.StartTime - fruit.StartTime) / BASE_SPEED; - HyperDashDirection = fruit.HyperDashTarget.X - fruit.X; + var target = fruit.HyperDashTarget; + double timeDifference = target.StartTime - fruit.StartTime; + double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; + double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + + SetHyperdashState(Math.Abs(velocity), target.X); } else - HyperDashModifier = 1; + { + SetHyperdashState(); + } return validCatch; } + private double hyperDashModifier = 1; + private int hyperDashDirection; + private float hyperDashTargetPosition; + /// /// Whether we are hypderdashing or not. /// public bool HyperDashing => hyperDashModifier != 1; - private double hyperDashModifier = 1; - /// - /// The direction in which hyperdash is allowed. 0 allows both directions. + /// Set hyperdash state. /// - public double HyperDashDirection; - - /// - /// The speed modifier resultant from hyperdash. Will trigger hyperdash when not equal to 1. - /// - public double HyperDashModifier + /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyperdashing state. + /// When this catcher crosses this position, this catcher ends hyperdashing. + public void SetHyperdashState(double modifier = 1, float targetPosition = -1) { - get { return hyperDashModifier; } - set + const float hyperdash_transition_length = 180; + + bool previouslyHyperDashing = HyperDashing; + if (modifier <= 1 || X == targetPosition) { - if (value == hyperDashModifier) return; - hyperDashModifier = value; + hyperDashModifier = 1; + hyperDashDirection = 0; - const float transition_length = 180; - - if (HyperDashing) + if (previouslyHyperDashing) { - this.FadeColour(Color4.OrangeRed, transition_length, Easing.OutQuint); - this.FadeTo(0.2f, transition_length, Easing.OutQuint); - Trail = true; + this.FadeColour(Color4.White, hyperdash_transition_length, Easing.OutQuint); + this.FadeTo(1, hyperdash_transition_length, Easing.OutQuint); } - else + } + else + { + hyperDashModifier = modifier; + hyperDashDirection = Math.Sign(targetPosition - X); + hyperDashTargetPosition = targetPosition; + + if (!previouslyHyperDashing) { - HyperDashDirection = 0; - this.FadeColour(Color4.White, transition_length, Easing.OutQuint); - this.FadeTo(1, transition_length, Easing.OutQuint); + this.FadeColour(Color4.OrangeRed, hyperdash_transition_length, Easing.OutQuint); + this.FadeTo(0.2f, hyperdash_transition_length, Easing.OutQuint); + Trail = true; } } } @@ -343,12 +360,18 @@ namespace osu.Game.Rulesets.Catch.UI var direction = Math.Sign(currentDirection); double dashModifier = Dashing ? 1 : 0.5; - - if (hyperDashModifier != 1 && (HyperDashDirection == 0 || direction == Math.Sign(HyperDashDirection))) - dashModifier = hyperDashModifier; + double speed = BASE_SPEED * dashModifier * hyperDashModifier; Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y); - X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * BASE_SPEED * dashModifier, 0, 1); + X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1); + + // Correct overshooting. + if (hyperDashDirection > 0 && hyperDashTargetPosition < X || + hyperDashDirection < 0 && hyperDashTargetPosition > X) + { + X = hyperDashTargetPosition; + SetHyperdashState(); + } } /// diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestCase.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestCase.cs new file mode 100644 index 0000000000..75c8fc7e79 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestCase.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public abstract class ManiaInputTestCase : OsuTestCase + { + private readonly Container content; + protected override Container Content => content ?? base.Content; + + protected ManiaInputTestCase(int keys) + { + base.Content.Add(content = new LocalInputManager(keys)); + } + + private class LocalInputManager : ManiaInputManager + { + public LocalInputManager(int variant) + : base(new ManiaRuleset().RulesetInfo, variant) + { + } + + protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new LocalKeyBindingContainer(ruleset, variant, unique); + + private class LocalKeyBindingContainer : RulesetKeyBindingContainer + { + public LocalKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override void ReloadMappings() + { + KeyBindings = DefaultKeyBindings; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs new file mode 100644 index 0000000000..78a98e83e8 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.Tests +{ + /// + /// A container which provides a to children. + /// + public class ScrollingTestContainer : Container + { + private readonly ScrollingDirection direction; + + public ScrollingTestContainer(ScrollingDirection direction) + { + this.direction = direction; + } + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs(new ScrollingInfo { Direction = { Value = direction }}); + return dependencies; + } + + private class ScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs new file mode 100644 index 0000000000..72f0b046b6 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs @@ -0,0 +1,111 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Rulesets.UI.Scrolling; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestCaseColumn : ManiaInputTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(Column), + typeof(ColumnBackground), + typeof(ColumnKeyArea), + typeof(ColumnHitObjectArea) + }; + + private readonly List columns = new List(); + + public TestCaseColumn() + : base(2) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(20, 0), + Children = new[] + { + createColumn(ScrollingDirection.Up, ManiaAction.Key1), + createColumn(ScrollingDirection.Down, ManiaAction.Key2) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("note", createNote); + AddStep("hold note", createHoldNote); + } + + private void createNote() + { + for (int i = 0; i < columns.Count; i++) + { + var obj = new Note { Column = i, StartTime = Time.Current + 2000 }; + obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + columns[i].Add(new DrawableNote(obj, columns[i].Action)); + } + } + + private void createHoldNote() + { + for (int i = 0; i < columns.Count; i++) + { + var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 }; + obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + columns[i].Add(new DrawableHoldNote(obj, columns[i].Action)); + } + } + + private Drawable createColumn(ScrollingDirection direction, ManiaAction action) + { + var column = new Column(direction) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 0.85f, + AccentColour = Color4.OrangeRed, + Action = action, + VisibleTimeRange = { Value = 2000 } + }; + + columns.Add(column); + + return new ScrollingTestContainer(direction) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Child = column + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs deleted file mode 100644 index a4109722d4..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Tests.Visual; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Game.Rulesets.Mania.Tests -{ - [TestFixture] - public class TestCaseManiaHitObjects : OsuTestCase - { - public TestCaseManiaHitObjects() - { - Note note1 = new Note(); - Note note2 = new Note(); - HoldNote holdNote = new HoldNote { StartTime = 1000 }; - - note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - Add(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - // Imagine that the containers containing the drawable notes are the "columns" - Children = new Drawable[] - { - new Container - { - Name = "Normal note column", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = 50, - Children = new[] - { - new Container - { - Name = "Timing section", - RelativeSizeAxes = Axes.Both, - RelativeChildSize = new Vector2(1, 10000), - Children = new[] - { - new DrawableNote(note1, ManiaAction.Key1) - { - Y = 5000, - LifetimeStart = double.MinValue, - LifetimeEnd = double.MaxValue, - AccentColour = Color4.Red - }, - new DrawableNote(note2, ManiaAction.Key1) - { - Y = 6000, - LifetimeStart = double.MinValue, - LifetimeEnd = double.MaxValue, - AccentColour = Color4.Red - } - } - } - } - }, - new Container - { - Name = "Hold note column", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = 50, - Children = new[] - { - new Container - { - Name = "Timing section", - RelativeSizeAxes = Axes.Both, - RelativeChildSize = new Vector2(1, 10000), - Children = new[] - { - new DrawableHoldNote(holdNote, ManiaAction.Key1) - { - Y = 5000, - Height = 1000, - LifetimeStart = double.MinValue, - LifetimeEnd = double.MaxValue, - AccentColour = Color4.Red, - } - } - } - } - } - } - }); - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs deleted file mode 100644 index b064d82a23..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Timing; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; -using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Configuration; -using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Scoring; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - [TestFixture] - public class TestCaseManiaPlayfield : OsuTestCase - { - private const double start_time = 500; - private const double duration = 500; - - protected override double TimePerAction => 200; - - private RulesetInfo maniaRuleset; - - public TestCaseManiaPlayfield() - { - var rng = new Random(1337); - - AddStep("1 column", () => createPlayfield(1)); - AddStep("4 columns", () => createPlayfield(4)); - AddStep("5 columns", () => createPlayfield(5)); - AddStep("8 columns", () => createPlayfield(8)); - AddStep("4 + 4 columns", () => - { - var stages = new List - { - new StageDefinition { Columns = 4 }, - new StageDefinition { Columns = 4 }, - }; - createPlayfield(stages); - }); - - AddStep("2 + 4 + 2 columns", () => - { - var stages = new List - { - new StageDefinition { Columns = 2 }, - new StageDefinition { Columns = 4 }, - new StageDefinition { Columns = 2 }, - }; - createPlayfield(stages); - }); - - AddStep("1 + 8 + 1 columns", () => - { - var stages = new List - { - new StageDefinition { Columns = 1 }, - new StageDefinition { Columns = 8 }, - new StageDefinition { Columns = 1 }, - }; - createPlayfield(stages); - }); - - AddStep("Reversed", () => createPlayfield(4, true)); - - AddStep("Notes with input", () => createPlayfieldWithNotes()); - AddStep("Notes with input (reversed)", () => createPlayfieldWithNotes(true)); - AddStep("Notes with gravity", () => createPlayfieldWithNotes()); - AddStep("Notes with gravity (reversed)", () => createPlayfieldWithNotes(true)); - - AddStep("Hit explosion", () => - { - var playfield = createPlayfield(4); - - int col = rng.Next(0, 4); - - var note = new Note { Column = col }; - note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - var drawableNote = new DrawableNote(note, ManiaAction.Key1) - { - AccentColour = playfield.Columns.ElementAt(col).AccentColour - }; - - playfield.OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect }); - playfield.Columns[col].OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect }); - }); - } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets, SettingsStore settings) - { - maniaRuleset = rulesets.GetRuleset(3); - - Dependencies.Cache(new ManiaConfigManager(settings, maniaRuleset, 4)); - } - - private ManiaPlayfield createPlayfield(int cols, bool inverted = false) - { - var stages = new List - { - new StageDefinition { Columns = cols }, - }; - - return createPlayfield(stages, inverted); - } - - private ManiaPlayfield createPlayfield(List stages, bool inverted = false) - { - Clear(); - - var inputManager = new ManiaInputManager(maniaRuleset, stages.Sum(g => g.Columns)) { RelativeSizeAxes = Axes.Both }; - Add(inputManager); - - ManiaPlayfield playfield; - - inputManager.Add(playfield = new ManiaPlayfield(stages) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - playfield.Inverted.Value = inverted; - - return playfield; - } - - private void createPlayfieldWithNotes(bool inverted = false) - { - Clear(); - - var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; - - var inputManager = new ManiaInputManager(maniaRuleset, 4) { RelativeSizeAxes = Axes.Both }; - Add(inputManager); - - ManiaPlayfield playfield; - var stages = new List - { - new StageDefinition { Columns = 4 }, - }; - - inputManager.Add(playfield = new ManiaPlayfield(stages) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Clock = new FramedClock(rateAdjustClock) - }); - - playfield.Inverted.Value = inverted; - - for (double t = start_time; t <= start_time + duration; t += 100) - { - var note1 = new Note { StartTime = t, Column = 0 }; - var note2 = new Note { StartTime = t, Column = 3 }; - - note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - playfield.Add(new DrawableNote(note1, ManiaAction.Key1)); - playfield.Add(new DrawableNote(note2, ManiaAction.Key4)); - } - - var holdNote1 = new HoldNote { StartTime = start_time, Duration = duration, Column = 1 }; - var holdNote2 = new HoldNote { StartTime = start_time, Duration = duration, Column = 2 }; - - holdNote1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - holdNote2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - playfield.Add(new DrawableHoldNote(holdNote1, ManiaAction.Key2)); - playfield.Add(new DrawableHoldNote(holdNote2, ManiaAction.Key3)); - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs new file mode 100644 index 0000000000..4fdfac93b7 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs @@ -0,0 +1,168 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestCaseNotes : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableNote), + typeof(DrawableHoldNote) + }; + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new[] + { + createNoteDisplay(ScrollingDirection.Down), + createNoteDisplay(ScrollingDirection.Up), + createHoldNoteDisplay(ScrollingDirection.Down), + createHoldNoteDisplay(ScrollingDirection.Up), + } + }; + } + + private Drawable createNoteDisplay(ScrollingDirection direction) + { + var note = new Note { StartTime = 999999999 }; + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return new ScrollingTestContainer(direction) + { + AutoSizeAxes = Axes.Both, + Child = new NoteContainer(direction, $"note, scrolling {direction.ToString().ToLower()}") + { + Child = new DrawableNote(note, ManiaAction.Key1) { AccentColour = Color4.OrangeRed } + } + }; + } + + private Drawable createHoldNoteDisplay(ScrollingDirection direction) + { + var note = new HoldNote { StartTime = 999999999, Duration = 1000 }; + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return new ScrollingTestContainer(direction) + { + AutoSizeAxes = Axes.Both, + Child = new NoteContainer(direction, $"hold note, scrolling {direction.ToString().ToLower()}") + { + Child = new DrawableHoldNote(note, ManiaAction.Key1) + { + RelativeSizeAxes = Axes.Both, + AccentColour = Color4.OrangeRed, + } + } + }; + } + + private class NoteContainer : Container + { + private readonly Container content; + protected override Container Content => content; + + private readonly ScrollingDirection direction; + + public NoteContainer(ScrollingDirection direction, string description) + { + this.direction = direction; + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(0, 10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 45, + Height = 100, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Width = 1.25f, + Colour = Color4.Black.Opacity(0.5f) + }, + content = new Container { RelativeSizeAxes = Axes.Both } + } + }, + new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextSize = 14, + Text = description + } + } + }; + } + + protected override void Update() + { + base.Update(); + + foreach (var obj in content.OfType()) + { + if (!(obj.HitObject is IHasEndTime endTime)) + continue; + + if (!obj.HasNestedHitObjects) + continue; + + foreach (var nested in obj.NestedHitObjects) + { + double finalPosition = (nested.HitObject.StartTime - obj.HitObject.StartTime) / endTime.Duration; + switch (direction) + { + case ScrollingDirection.Up: + nested.Y = (float)(finalPosition * content.DrawHeight); + break; + case ScrollingDirection.Down: + nested.Y = (float)(-finalPosition * content.DrawHeight); + break; + } + } + } + } + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs new file mode 100644 index 0000000000..9aff853ffd --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using OpenTK; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestCaseStage : ManiaInputTestCase + { + private const int columns = 4; + + private readonly List stages = new List(); + + public TestCaseStage() + : base(columns) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(20, 0), + Children = new[] + { + createStage(ScrollingDirection.Up, ManiaAction.Key1), + createStage(ScrollingDirection.Down, ManiaAction.Key3) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("note", createNote); + AddStep("hold note", createHoldNote); + AddStep("minor bar line", () => createBarLine(false)); + AddStep("major bar line", () => createBarLine(true)); + } + + private void createNote() + { + foreach (var stage in stages) + { + for (int i = 0; i < stage.Columns.Count; i++) + { + var obj = new Note { Column = i, StartTime = Time.Current + 2000 }; + obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + stage.Add(new DrawableNote(obj, stage.Columns[i].Action)); + } + } + } + + private void createHoldNote() + { + foreach (var stage in stages) + { + for (int i = 0; i < stage.Columns.Count; i++) + { + var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 }; + obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + stage.Add(new DrawableHoldNote(obj, stage.Columns[i].Action)); + } + } + } + + private void createBarLine(bool major) + { + foreach (var stage in stages) + { + var obj = new BarLine + { + StartTime = Time.Current + 2000, + ControlPoint = new TimingControlPoint(), + BeatIndex = major ? 0 : 1 + }; + + obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + stage.Add(obj); + } + } + + private Drawable createStage(ScrollingDirection direction, ManiaAction action) + { + var specialAction = ManiaAction.Special1; + + var stage = new ManiaStage(direction, 0, new StageDefinition { Columns = 2 }, ref action, ref specialAction) { VisibleTimeRange = { Value = 2000 } }; + stages.Add(stage); + + return new ScrollingTestContainer(direction) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Child = stage + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index f60958d581..c6fa465a0f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -329,7 +329,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy break; } - bool isDoubleSample(SampleInfo sample) => sample.Name == SampleInfo.HIT_CLAP && sample.Name == SampleInfo.HIT_FINISH; + bool isDoubleSample(SampleInfo sample) => sample.Name == SampleInfo.HIT_CLAP || sample.Name == SampleInfo.HIT_FINISH; bool canGenerateTwoNotes = (convertType & PatternType.LowProbability) == 0; canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample); diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs index d9e360081d..3e9c9feba1 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs @@ -4,6 +4,7 @@ using osu.Framework.Configuration.Tracking; using osu.Game.Configuration; using osu.Game.Rulesets.Configuration; +using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Configuration { @@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration base.InitialiseDefaults(); Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0); + Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down); } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings @@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration public enum ManiaSetting { - ScrollTime + ScrollTime, + ScrollDirection } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs new file mode 100644 index 0000000000..c7f6890b93 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + public class ManiaDifficultyAttributes : DifficultyAttributes + { + public double GreatHitWindow; + + public ManiaDifficultyAttributes(Mod[] mods, double starRating) + : base(mods, starRating) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 5fa113224d..7b4d4b12ed 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) { + if (!beatmap.HitObjects.Any()) + return new ManiaDifficultyAttributes(mods, 0); + var difficultyHitObjects = new List(); int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; @@ -50,9 +53,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (!calculateStrainValues(difficultyHitObjects, timeRate)) return new DifficultyAttributes(mods, 0); + double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; - return new DifficultyAttributes(mods, starRating); + return new ManiaDifficultyAttributes(mods, starRating) + { + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future + GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate + }; } private bool calculateStrainValues(List objects, double timeRate) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index b6089b830b..1f26bda1b2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaPerformanceCalculator : PerformanceCalculator { + protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes; + private Mod[] mods; // Score after being scaled by non-difficulty-increasing mods @@ -105,14 +107,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeAccuracyValue(double strainValue) { - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future - double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate; - if (hitWindowGreat <= 0) + if (Attributes.GreatHitWindow <= 0) return 0; // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Max(0.0, 0.2 - (hitWindowGreat - 34) * 0.006667) + double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667) * strainValue * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ac5fbfdde0..3d58e63da5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; @@ -155,6 +156,8 @@ namespace osu.Game.Rulesets.Mania public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaConfigManager(settings, RulesetInfo); + public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this); + public ManiaRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) { diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs new file mode 100644 index 0000000000..54a7bf954d --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania +{ + public class ManiaSettingsSubsection : RulesetSettingsSubsection + { + protected override string Header => "osu!mania"; + + public ManiaSettingsSubsection(ManiaRuleset ruleset) + : base(ruleset) + { + } + + [BackgroundDependencyLoader] + private void load(ManiaConfigManager config) + { + Children = new Drawable[] + { + new SettingsEnumDropdown + { + LabelText = "Scrolling direction", + Bindable = config.GetBindable(ManiaSetting.ScrollDirection) + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index e008fa952e..ce0276f759 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -75,6 +76,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AddNested(tail); } + protected override void OnDirectionChanged(ScrollingDirection direction) + { + base.OnDirectionChanged(direction); + + bodyPiece.Anchor = bodyPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + } + public override Color4 AccentColour { get { return base.AccentColour; } @@ -100,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.Update(); // Make the body piece not lie under the head note - bodyPiece.Y = head.Height / 2; + bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * head.Height / 2; bodyPiece.Height = DrawHeight - head.Height / 2 + tail.Height / 2; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index fbf1ee8460..1271fae0c1 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -1,8 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -16,18 +20,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public new TObject HitObject; + protected readonly IBindable Direction = new Bindable(); + protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null) : base(hitObject) { - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; - HitObject = hitObject; if (action != null) Action = action.Value; } + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + Direction.BindTo(scrollingInfo.Direction); + Direction.BindValueChanged(OnDirectionChanged, true); + } + + protected virtual void OnDirectionChanged(ScrollingDirection direction) + { + Anchor = Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + } + protected override void UpdateState(ArmedState state) { switch (state) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 3de0a9c5cc..fb4aa74ad1 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -28,14 +29,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables CornerRadius = 5; Masking = true; - InternalChildren = new Drawable[] - { - headPiece = new NotePiece - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - } - }; + InternalChild = headPiece = new NotePiece(); + } + + protected override void OnDirectionChanged(ScrollingDirection direction) + { + base.OnDirectionChanged(direction); + + headPiece.Anchor = headPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } public override Color4 AccentColour diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs index 9ebeb91e0c..2c74f5b168 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs @@ -1,12 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; +using osu.Framework.Configuration; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces { @@ -18,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces public const float NOTE_HEIGHT = 10; private const float head_colour_height = 6; + private readonly IBindable direction = new Bindable(); + private readonly Box colouredBox; public NotePiece() @@ -33,8 +39,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces }, colouredBox = new Box { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, Height = head_colour_height, Alpha = 0.2f @@ -42,6 +46,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces }; } + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(direction => + { + colouredBox.Anchor = colouredBox.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + }, true); + } + private Color4 accentColour; public Color4 AccentColour { diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index c71db745e0..29eeb1cab5 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -17,6 +17,6 @@ namespace osu.Game.Rulesets.Mania.Replays protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); - public override List GetPendingStates() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; + public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9c1830e642..e731ce9195 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,50 +1,51 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using OpenTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Colour; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using System; using System.Linq; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { - public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour + public class Column : ManiaScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { - private const float key_icon_size = 10; - private const float key_icon_corner_radius = 3; - private const float key_icon_border_radius = 2; - - private const float hit_target_height = 10; - private const float hit_target_bar_height = 2; - private const float column_width = 45; private const float special_column_width = 70; - public ManiaAction Action; + private ManiaAction action; - private readonly Box background; - private readonly Box backgroundOverlay; - private readonly Container hitTargetBar; - private readonly Container keyIcon; + public ManiaAction Action + { + get => action; + set + { + if (action == value) + return; + action = value; + + background.Action = value; + keyArea.Action = value; + } + } + + private readonly ColumnBackground background; + private readonly ColumnKeyArea keyArea; + private readonly ColumnHitObjectArea hitObjectArea; internal readonly Container TopLevelContainer; private readonly Container explosionContainer; - protected override Container Content => content; - private readonly Container content; + protected override Container Content => hitObjectArea; - public Column() - : base(ScrollingDirection.Up) + public Column(ScrollingDirection direction) + : base(direction) { RelativeSizeAxes = Axes.Y; Width = column_width; @@ -52,71 +53,21 @@ namespace osu.Game.Rulesets.Mania.UI Masking = true; CornerRadius = 5; - InternalChildren = new Drawable[] + background = new ColumnBackground { RelativeSizeAxes = Axes.Both }; + + Container hitTargetContainer; + + InternalChildren = new[] { - background = new Box - { - Name = "Background", - RelativeSizeAxes = Axes.Both, - Alpha = 0.3f - }, - backgroundOverlay = new Box - { - Name = "Background Gradient Overlay", - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Blending = BlendingMode.Additive, - Alpha = 0 - }, - new Container + // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements + background.CreateProxy(), + hitTargetContainer = new Container { Name = "Hit target + hit objects", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = ManiaStage.HIT_TARGET_POSITION }, Children = new Drawable[] { - new Container - { - Name = "Hit target", - RelativeSizeAxes = Axes.X, - Height = hit_target_height, - Children = new Drawable[] - { - new Box - { - Name = "Background", - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black - }, - hitTargetBar = new Container - { - Name = "Bar", - RelativeSizeAxes = Axes.X, - Height = hit_target_bar_height, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both - } - } - } - } - }, - content = new Container - { - Name = "Hit objects", - RelativeSizeAxes = Axes.Both, - }, - // For column lighting, we need to capture input events before the notes - new InputTarget - { - Pressed = onPressed, - Released = onReleased - }, + hitObjectArea = new ColumnHitObjectArea { RelativeSizeAxes = Axes.Both }, explosionContainer = new Container { Name = "Hit explosions", @@ -124,46 +75,27 @@ namespace osu.Game.Rulesets.Mania.UI } } }, - new Container + keyArea = new ColumnKeyArea { - Name = "Key", RelativeSizeAxes = Axes.X, Height = ManiaStage.HIT_TARGET_POSITION, - Children = new Drawable[] - { - new Box - { - Name = "Key gradient", - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), - Alpha = 0.5f - }, - keyIcon = new Container - { - Name = "Key icon", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(key_icon_size), - Masking = true, - CornerRadius = key_icon_corner_radius, - BorderThickness = 2, - BorderColour = Color4.White, // Not true - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } }, + background, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; TopLevelContainer.Add(explosionContainer.CreateProxy()); + + Direction.BindValueChanged(d => + { + hitTargetContainer.Padding = new MarginPadding + { + Top = d == ScrollingDirection.Up ? ManiaStage.HIT_TARGET_POSITION : 0, + Bottom = d == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0, + }; + + keyArea.Anchor = keyArea.Origin= d == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + }, true); } public override Axes RelativeSizeAxes => Axes.Y; @@ -192,22 +124,9 @@ namespace osu.Game.Rulesets.Mania.UI return; accentColour = value; - background.Colour = accentColour; - backgroundOverlay.Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.6f), accentColour.Opacity(0)); - - hitTargetBar.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Opacity(0.5f), - }; - - keyIcon.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Opacity(0.5f), - }; + background.AccentColour = value; + keyArea.AccentColour = value; + hitObjectArea.AccentColour = value; } } @@ -228,48 +147,10 @@ namespace osu.Game.Rulesets.Mania.UI if (!judgement.IsHit || !judgedObject.DisplayJudgement) return; - explosionContainer.Add(new HitExplosion(judgedObject)); - } - - private bool onPressed(ManiaAction action) - { - if (action == Action) + explosionContainer.Add(new HitExplosion(judgedObject) { - backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); - keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint); - } - - return false; - } - - private bool onReleased(ManiaAction action) - { - if (action == Action) - { - backgroundOverlay.FadeTo(0, 250, Easing.OutQuint); - keyIcon.ScaleTo(1f, 125, Easing.OutQuint); - } - - return false; - } - - /// - /// This is a simple container which delegates various input events that have to be captured before the notes. - /// - private class InputTarget : Container, IKeyBindingHandler - { - public Func Pressed; - public Func Released; - - public InputTarget() - { - RelativeSizeAxes = Axes.Both; - AlwaysPresent = true; - Alpha = 0; - } - - public bool OnPressed(ManiaAction action) => Pressed?.Invoke(action) ?? false; - public bool OnReleased(ManiaAction action) => Released?.Invoke(action) ?? false; + Anchor = Direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre + }); } public bool OnPressed(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs new file mode 100644 index 0000000000..9b744bd254 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs @@ -0,0 +1,106 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics; +using osu.Game.Rulesets.UI.Scrolling; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class ColumnBackground : CompositeDrawable, IKeyBindingHandler, IHasAccentColour + { + public ManiaAction Action; + + private Box background; + private Box backgroundOverlay; + + private readonly IBindable direction = new Bindable(); + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + InternalChildren = new[] + { + background = new Box + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + Alpha = 0.3f + }, + backgroundOverlay = new Box + { + Name = "Background Gradient Overlay", + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Blending = BlendingMode.Additive, + Alpha = 0 + } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(direction => + { + backgroundOverlay.Anchor = backgroundOverlay.Origin = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + updateColours(); + }, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColours(); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + accentColour = value; + + updateColours(); + } + } + + private void updateColours() + { + if (!IsLoaded) + return; + + background.Colour = AccentColour; + + var brightPoint = AccentColour.Opacity(0.6f); + var dimPoint = AccentColour.Opacity(0); + + backgroundOverlay.Colour = ColourInfo.GradientVertical( + direction.Value == ScrollingDirection.Up ? brightPoint : dimPoint, + direction.Value == ScrollingDirection.Up ? dimPoint : brightPoint); + } + + public bool OnPressed(ManiaAction action) + { + if (action == Action) + backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); + return false; + } + + public bool OnReleased(ManiaAction action) + { + if (action == Action) + backgroundOverlay.FadeTo(0, 250, Easing.OutQuint); + return false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs new file mode 100644 index 0000000000..b5dfb0949a --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.UI.Scrolling; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class ColumnHitObjectArea : Container, IHasAccentColour + { + private const float hit_target_height = 10; + private const float hit_target_bar_height = 2; + + private Container content; + protected override Container Content => content; + + private readonly IBindable direction = new Bindable(); + + private Container hitTargetLine; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + Drawable hitTargetBar; + + InternalChildren = new[] + { + hitTargetBar = new Box + { + RelativeSizeAxes = Axes.X, + Height = hit_target_height, + Colour = Color4.Black + }, + hitTargetLine = new Container + { + RelativeSizeAxes = Axes.X, + Height = hit_target_bar_height, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + content = new Container + { + Name = "Hit objects", + RelativeSizeAxes = Axes.Both, + }, + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(direction => + { + Anchor anchor = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + + hitTargetBar.Anchor = hitTargetBar.Origin = anchor; + hitTargetLine.Anchor = hitTargetLine.Origin = anchor; + }, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColours(); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + accentColour = value; + + updateColours(); + } + } + + private void updateColours() + { + if (!IsLoaded) + return; + + hitTargetLine.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Opacity(0.5f), + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs new file mode 100644 index 0000000000..4ce1614310 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs @@ -0,0 +1,122 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics; +using osu.Game.Rulesets.UI.Scrolling; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class ColumnKeyArea : CompositeDrawable, IKeyBindingHandler, IHasAccentColour + { + private const float key_icon_size = 10; + private const float key_icon_corner_radius = 3; + + public ManiaAction Action; + + private readonly IBindable direction = new Bindable(); + + private Container keyIcon; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + Drawable gradient; + + InternalChildren = new[] + { + gradient = new Box + { + Name = "Key gradient", + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f + }, + keyIcon = new Container + { + Name = "Key icon", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(key_icon_size), + Masking = true, + CornerRadius = key_icon_corner_radius, + BorderThickness = 2, + BorderColour = Color4.White, // Not true + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(direction => + { + gradient.Colour = ColourInfo.GradientVertical( + direction == ScrollingDirection.Up ? Color4.Black : Color4.Black.Opacity(0), + direction == ScrollingDirection.Up ? Color4.Black.Opacity(0) : Color4.Black); + }, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColours(); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + accentColour = value; + + updateColours(); + } + } + + private void updateColours() + { + if (!IsLoaded) + return; + + keyIcon.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Opacity(0.5f), + }; + } + + public bool OnPressed(ManiaAction action) + { + if (action == Action) + keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint); + return false; + } + + public bool OnReleased(ManiaAction action) + { + if (action == Action) + keyIcon.ScaleTo(1f, 125, Easing.OutQuint); + return false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs index bf8db63137..c74745868a 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Mania.UI { bool isTick = judgedObject is DrawableHoldNoteTick; - Anchor = Anchor.TopCentre; Origin = Anchor.Centre; RelativeSizeAxes = Axes.X; diff --git a/osu.Game.Rulesets.Mania/UI/IScrollingInfo.cs b/osu.Game.Rulesets.Mania/UI/IScrollingInfo.cs new file mode 100644 index 0000000000..ee65e9f1a5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/IScrollingInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.UI +{ + public interface IScrollingInfo + { + /// + /// The direction s should scroll in. + /// + IBindable Direction { get; } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 4b936fc7f9..2f8ad7b17e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; @@ -17,18 +16,13 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaPlayfield : ScrollingPlayfield + public class ManiaPlayfield : ManiaScrollingPlayfield { - /// - /// Whether this playfield should be inverted. This flips everything inside the playfield. - /// - public readonly Bindable Inverted = new Bindable(true); - public List Columns => stages.SelectMany(x => x.Columns).ToList(); private readonly List stages = new List(); - public ManiaPlayfield(List stageDefinitions) - : base(ScrollingDirection.Up) + public ManiaPlayfield(ScrollingDirection direction, List stageDefinitions) + : base(direction) { if (stageDefinitions == null) throw new ArgumentNullException(nameof(stageDefinitions)); @@ -36,8 +30,6 @@ namespace osu.Game.Rulesets.Mania.UI if (stageDefinitions.Count <= 0) throw new ArgumentException("Can't have zero or fewer stages."); - Inverted.Value = true; - GridContainer playfieldGrid; InternalChild = playfieldGrid = new GridContainer { @@ -50,9 +42,8 @@ namespace osu.Game.Rulesets.Mania.UI int firstColumnIndex = 0; for (int i = 0; i < stageDefinitions.Count; i++) { - var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); + var newStage = new ManiaStage(direction, firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); newStage.VisibleTimeRange.BindTo(VisibleTimeRange); - newStage.Inverted.BindTo(Inverted); playfieldGrid.Content[0][i] = newStage; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index a3145d6035..fa6fba0cd8 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input; @@ -12,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -33,6 +35,9 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; + private readonly Bindable configDirection = new Bindable(); + private ScrollingInfo scrollingInfo; + public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -65,12 +70,24 @@ namespace osu.Game.Rulesets.Mania.UI } [BackgroundDependencyLoader] - private void load() + private void load(ManiaConfigManager config) { BarLines.ForEach(Playfield.Add); + + config.BindWith(ManiaSetting.ScrollDirection, configDirection); + configDirection.BindValueChanged(d => scrollingInfo.Direction.Value = (ScrollingDirection)d, true); } - protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages) + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs(scrollingInfo = new ScrollingInfo()); + return dependencies; + } + + protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(scrollingInfo.Direction, Beatmap.Stages) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -100,5 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); + + private class ScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs new file mode 100644 index 0000000000..0fc9c1920a --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.UI +{ + public enum ManiaScrollingDirection + { + Up = ScrollingDirection.Up, + Down = ScrollingDirection.Down + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs new file mode 100644 index 0000000000..f1ff0665cd --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class ManiaScrollingPlayfield : ScrollingPlayfield + { + private readonly IBindable direction = new Bindable(); + + public ManiaScrollingPlayfield(ScrollingDirection direction) + : base(direction) + { + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(direction => Direction.Value = direction, true); + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index cb93613c7d..7b68582944 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,20 +23,15 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// - internal class ManiaStage : ScrollingPlayfield + internal class ManiaStage : ManiaScrollingPlayfield { public const float HIT_TARGET_POSITION = 50; - /// - /// Whether this playfield should be inverted. This flips everything inside the playfield. - /// - public readonly Bindable Inverted = new Bindable(true); - public IReadOnlyList Columns => columnFlow.Children; private readonly FillFlowContainer columnFlow; - protected override Container Content => content; - private readonly Container content; + protected override Container Content => barLineContainer; + private readonly Container barLineContainer; public Container Judgements => judgements; private readonly JudgementContainer judgements; @@ -49,8 +43,8 @@ namespace osu.Game.Rulesets.Mania.UI private readonly int firstColumnIndex; - public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) - : base(ScrollingDirection.Up) + public ManiaStage(ScrollingDirection direction, int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) + : base(direction) { this.firstColumnIndex = firstColumnIndex; @@ -106,13 +100,12 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = content = new Container + Child = barLineContainer = new Container { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding { Top = HIT_TARGET_POSITION } } }, judgements = new JudgementContainer @@ -131,7 +124,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { var isSpecial = definition.IsSpecialColumn(i); - var column = new Column + var column = new Column(direction) { IsSpecial = isSpecial, Action = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ @@ -140,14 +133,14 @@ namespace osu.Game.Rulesets.Mania.UI AddColumn(column); } - Inverted.ValueChanged += invertedChanged; - Inverted.TriggerChange(); - } - - private void invertedChanged(bool newValue) - { - Scale = new Vector2(1, newValue ? -1 : 1); - Judgements.Scale = Scale; + Direction.BindValueChanged(d => + { + barLineContainer.Padding = new MarginPadding + { + Top = d == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0, + Bottom = d == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0, + }; + }, true); } public void AddColumn(Column c) @@ -218,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.UI { // Due to masking differences, it is not possible to get the width of the columns container automatically // While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually - content.Width = columnFlow.Width; + barLineContainer.Width = columnFlow.Width; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 50a259ae55..2b42eed0c5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public double AimStrain; public double SpeedStrain; + public double ApproachRate; + public double OverallDifficulty; + public int MaxCombo; public OsuDifficultyAttributes(Mod[] mods, double starRating) : base(mods, starRating) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 400afbc043..62fafd8196 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) { + if (!beatmap.HitObjects.Any()) + return new OsuDifficultyAttributes(mods, 0); + OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast().ToList(), timeRate); Skill[] skills = { @@ -58,10 +61,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; + // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future + double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate; + double preEmpt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; + + int maxCombo = beatmap.HitObjects.Count(); + // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above) + maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); + return new OsuDifficultyAttributes(mods, starRating) { AimStrain = aimRating, - SpeedStrain = speedRating + SpeedStrain = speedRating, + ApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5, + OverallDifficulty = (80 - hitWindowGreat) / 6, + MaxCombo = maxCombo }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3ab3cc879a..d4a60dd52f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -22,16 +22,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private Mod[] mods; - /// - /// Approach rate adjusted by mods. - /// - private double realApproachRate; - - /// - /// Overall difficulty adjusted by mods. - /// - private double realOverallDifficulty; - private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -63,13 +53,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => !m.Ranked)) return 0; - // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future - double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate; - double preEmpt = (int)BeatmapDifficulty.DifficultyRange(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / TimeRate; - - realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5; - realOverallDifficulty = (80 - hitWindowGreat) / 6; - // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things @@ -94,8 +77,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty categoryRatings.Add("Aim", aimValue); categoryRatings.Add("Speed", speedValue); categoryRatings.Add("Accuracy", accuracyValue); - categoryRatings.Add("OD", realOverallDifficulty); - categoryRatings.Add("AR", realApproachRate); + categoryRatings.Add("OD", Attributes.OverallDifficulty); + categoryRatings.Add("AR", Attributes.ApproachRate); categoryRatings.Add("Max Combo", beatmapMaxCombo); } @@ -120,22 +103,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); double approachRateFactor = 1.0f; - if (realApproachRate > 10.33f) - approachRateFactor += 0.45f * (realApproachRate - 10.33f); - else if (realApproachRate < 8.0f) + if (Attributes.ApproachRate > 10.33f) + approachRateFactor += 0.45f * (Attributes.ApproachRate - 10.33f); + else if (Attributes.ApproachRate < 8.0f) { // HD is worth more with lower ar! if (mods.Any(h => h is OsuModHidden)) - approachRateFactor += 0.02f * (8.0f - realApproachRate); + approachRateFactor += 0.02f * (8.0f - Attributes.ApproachRate); else - approachRateFactor += 0.01f * (8.0f - realApproachRate); + approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate); } aimValue *= approachRateFactor; // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. if (mods.Any(h => h is OsuModHidden)) - aimValue *= 1.02 + (11.0f - realApproachRate) / 50.0; // Gives a 1.04 bonus for AR10, a 1.06 bonus for AR9, a 1.02 bonus for AR11. + aimValue *= 1.02 + (11.0f - Attributes.ApproachRate) / 50.0; // Gives a 1.04 bonus for AR10, a 1.06 bonus for AR9, a 1.02 bonus for AR11. if (mods.Any(h => h is OsuModFlashlight)) { @@ -146,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the aim value with accuracy _slightly_ aimValue *= 0.5f + accuracy / 2.0f; // It is important to also consider accuracy difficulty when doing that - aimValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500; + aimValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; return aimValue; } @@ -172,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy _slightly_ speedValue *= 0.5f + accuracy / 2.0f; // It is important to also consider accuracy difficulty when doing that - speedValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500; + speedValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; return speedValue; } @@ -194,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Pow(1.52163f, realOverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; + double accuracyValue = Math.Pow(1.52163f, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f)); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs index f9e5bfa89b..2eed41d13f 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs @@ -30,13 +30,16 @@ namespace osu.Game.Rulesets.Osu.Replays } } - public override List GetPendingStates() + public override List GetPendingInputs() { - return new List + return new List { + new MousePositionAbsoluteInput + { + Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) + }, new ReplayState { - Mouse = new ReplayMouseState(GamefieldToScreenSpace(Position ?? Vector2.Zero)), PressedActions = CurrentFrame.Actions } }; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs new file mode 100644 index 0000000000..d18d03b1db --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.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.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Taiko.Difficulty +{ + public class TaikoDifficultyAttributes : DifficultyAttributes + { + public double GreatHitWindow; + public int MaxCombo; + + public TaikoDifficultyAttributes(Mod[] mods, double starRating) + : base(mods, starRating) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 473c205293..8b527c2c82 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) { + if (!beatmap.HitObjects.Any()) + return new TaikoDifficultyAttributes(mods, 0); + var difficultyHitObjects = new List(); foreach (var hitObject in beatmap.HitObjects) @@ -47,7 +51,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; - return new DifficultyAttributes(mods, starRating); + return new TaikoDifficultyAttributes(mods, starRating) + { + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future + GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate, + MaxCombo = beatmap.HitObjects.Count(h => h is Hit) + }; } private bool calculateStrainValues(List objects, double timeRate) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 53cfb4fd0f..f530b6725c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -8,13 +8,12 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoPerformanceCalculator : PerformanceCalculator { - private readonly int beatmapMaxCombo; + protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes; private Mod[] mods; private int countGreat; @@ -25,7 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score) : base(ruleset, beatmap, score) { - beatmapMaxCombo = Beatmap.HitObjects.Count(h => h is Hit); } public override double Calculate(Dictionary categoryDifficulty = null) @@ -78,8 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty strainValue *= Math.Pow(0.985, countMiss); // Combo scaling - if (beatmapMaxCombo > 0) - strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(beatmapMaxCombo, 0.5), 1.0); + if (Attributes.MaxCombo > 0) + strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0); if (mods.Any(m => m is ModHidden)) strainValue *= 1.025; @@ -94,14 +92,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeAccuracyValue() { - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future - double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate; - if (hitWindowGreat <= 0) + if (Attributes.GreatHitWindow <= 0) return 0; // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accValue = Math.Pow(150.0 / hitWindowGreat, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; + double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs new file mode 100644 index 0000000000..608f1f9be2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Judgements +{ + public class TaikoIntermediateSwellJudgement : TaikoJudgement + { + public override HitResult MaxResult => HitResult.Perfect; + + public override bool AffectsCombo => false; + + public TaikoIntermediateSwellJudgement() + { + Final = false; + } + + /// + /// Computes the numeric result value for the combo portion of the score. + /// + /// The result to compute the value for. + /// The numeric result value. + protected override int NumericResultFor(HitResult result) => 0; + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 519b56a3ed..bb9cd02b14 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -86,6 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (State.Value) { case ArmedState.Idle: + SecondHitAllowed = false; + validKeyPressed = false; + UnproxyContent(); this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); break; @@ -95,8 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables break; case ArmedState.Hit: // If we're far enough away from the left stage, we should bring outselves in front of it - if (X >= -0.05f) - ProxyContent(); + ProxyContent(); var flash = circlePiece?.FlashBox; if (flash != null) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs index c416d50062..b431d35e16 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; @@ -46,6 +47,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.Great }); } + protected override void UpdateState(ArmedState state) + { + base.UpdateState(state); + + switch (state) + { + case ArmedState.Idle: + firstHitTime = 0; + firstKeyHeld = false; + break; + } + } + public override bool OnReleased(TaikoAction action) { if (action == firstHitAction) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index df36a475d6..408b37e377 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -20,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableSwell : DrawableTaikoHitObject { + /// + /// A judgement is only displayed when the user has complete the swell (either a hit or miss). + /// + public override bool DisplayJudgement => AllJudged; + private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; @@ -29,11 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - /// - /// The amount of times the user has hit this swell. - /// - private int userHits; - private readonly SwellSymbolPiece symbol; public DrawableSwell(Swell swell) @@ -129,9 +128,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { if (userTriggered) { - userHits++; + AddJudgement(new TaikoIntermediateSwellJudgement()); - var completion = (float)userHits / HitObject.RequiredHits; + var completion = (float)Judgements.Count / HitObject.RequiredHits; expandingRing .FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50) @@ -142,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); - if (userHits == HitObject.RequiredHits) + if (Judgements.Count == HitObject.RequiredHits) AddJudgement(new TaikoJudgement { Result = HitResult.Great }); } else @@ -151,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; //TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP - AddJudgement(userHits > HitObject.RequiredHits / 2 + AddJudgement(Judgements.Count > HitObject.RequiredHits / 2 ? new TaikoJudgement { Result = HitResult.Good } : new TaikoJudgement { Result = HitResult.Miss }); } @@ -162,23 +161,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables const float preempt = 100; const float out_transition_time = 300; - double untilStartTime = HitObject.StartTime - Time.Current; - double untilJudgement = untilStartTime + (Judgements.FirstOrDefault()?.TimeOffset ?? 0) + HitObject.Duration; - - targetRing.Delay(untilStartTime - preempt).ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint); - this.Delay(untilJudgement).FadeOut(out_transition_time, Easing.Out); - switch (state) { case ArmedState.Idle: UnproxyContent(); + expandingRing.FadeTo(0); + using (BeginAbsoluteSequence(HitObject.StartTime - preempt, true)) + targetRing.ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint); break; + case ArmedState.Miss: case ArmedState.Hit: - bodyContainer.Delay(untilJudgement).ScaleTo(1.4f, out_transition_time); + this.FadeOut(out_transition_time, Easing.Out); + bodyContainer.ScaleTo(1.4f, out_transition_time); + + Expire(); break; } - - Expire(); } protected override void Update() diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 6ccbd575e5..eae033401e 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -17,6 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Replays protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); - public override List GetPendingStates() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; + public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 313c205981..999e33e51a 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -69,11 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI bool isMajor = currentBeat % (int)currentPoint.TimeSignature == 0; Playfield.Add(isMajor ? new DrawableBarLineMajor(barLine) : new DrawableBarLine(barLine)); - double bl = currentPoint.BeatLength; - if (bl < 800) - bl *= (int)currentPoint.TimeSignature; - - time += bl; + time += currentPoint.BeatLength * (int)currentPoint.TimeSignature; currentBeat++; } } diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs index 94b99d483c..dace6e20ef 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs @@ -1,8 +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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,9 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Beatmaps; using OpenTK; @@ -20,10 +16,9 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorSeekSnapping : EditorClockTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(HitObjectComposer) }; - public TestCaseEditorSeekSnapping() { BeatDivisor.Value = 4; @@ -56,22 +51,13 @@ namespace osu.Game.Tests.Visual Beatmap.Value = new TestWorkingBeatmap(testBeatmap); Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; - - testSeekNoSnapping(); - testSeekSnappingOnBeat(); - testSeekSnappingInBetweenBeat(); - testSeekForwardNoSnapping(); - testSeekForwardSnappingOnBeat(); - testSeekForwardSnappingFromInBetweenBeat(); - testSeekBackwardSnappingOnBeat(); - testSeekBackwardSnappingFromInBetweenBeat(); - testSeekingWithFloatingPointBeatLength(); } /// /// Tests whether time is correctly seeked without snapping. /// - private void testSeekNoSnapping() + [Test] + public void TestSeekNoSnapping() { reset(); @@ -94,7 +80,8 @@ namespace osu.Game.Tests.Visual /// Tests whether seeking to exact beat times puts us on the beat time. /// These are the white/yellow ticks on the graph. /// - private void testSeekSnappingOnBeat() + [Test] + public void TestSeekSnappingOnBeat() { reset(); @@ -117,9 +104,9 @@ namespace osu.Game.Tests.Visual /// /// Tests whether seeking to somewhere in the middle between beats puts us on the expected beats. /// For example, snapping between a white/yellow beat should put us on either the yellow or white, depending on which one we're closer too. - /// If /// - private void testSeekSnappingInBetweenBeat() + [Test] + public void TestSeekSnappingInBetweenBeat() { reset(); @@ -140,7 +127,8 @@ namespace osu.Game.Tests.Visual /// /// Tests that when seeking forward with no beat snapping, beats are never explicitly snapped to, nor the next timing point (if we've skipped it). /// - private void testSeekForwardNoSnapping() + [Test] + public void TestSeekForwardNoSnapping() { reset(); @@ -159,7 +147,8 @@ namespace osu.Game.Tests.Visual /// /// Tests that when seeking forward with beat snapping, all beats are snapped to and timing points are never skipped. /// - private void testSeekForwardSnappingOnBeat() + [Test] + public void TestSeekForwardSnappingOnBeat() { reset(); @@ -181,7 +170,8 @@ namespace osu.Game.Tests.Visual /// Tests that when seeking forward from in-between two beats, the next beat or timing point is snapped to, and no beats are skipped. /// This will also test being extremely close to the next beat/timing point, to ensure rounding is not an issue. /// - private void testSeekForwardSnappingFromInBetweenBeat() + [Test] + public void TestSeekForwardSnappingFromInBetweenBeat() { reset(); @@ -214,21 +204,20 @@ namespace osu.Game.Tests.Visual /// /// Tests that when seeking backward with no beat snapping, beats are never explicitly snapped to, nor the next timing point (if we've skipped it). /// - private void testSeekBackwardNoSnapping() + [Test] + public void TestSeekBackwardNoSnapping() { reset(); AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 425", () => Clock.CurrentTime == 425); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 375", () => Clock.CurrentTime == 375); + AddAssert("Time = 350", () => Clock.CurrentTime == 350); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 325", () => Clock.CurrentTime == 325); + AddAssert("Time = 150", () => Clock.CurrentTime == 150); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 125", () => Clock.CurrentTime == 125); - AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 25", () => Clock.CurrentTime == 25); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); AddStep("SeekBackward", () => Clock.SeekBackward()); AddAssert("Time = 0", () => Clock.CurrentTime == 0); } @@ -236,7 +225,8 @@ namespace osu.Game.Tests.Visual /// /// Tests that when seeking backward with beat snapping, all beats are snapped to and timing points are never skipped. /// - private void testSeekBackwardSnappingOnBeat() + [Test] + public void TestSeekBackwardSnappingOnBeat() { reset(); @@ -259,7 +249,8 @@ namespace osu.Game.Tests.Visual /// Tests that when seeking backward from in-between two beats, the previous beat or timing point is snapped to, and no beats are skipped. /// This will also test being extremely close to the previous beat/timing point, to ensure rounding is not an issue. /// - private void testSeekBackwardSnappingFromInBetweenBeat() + [Test] + public void TestSeekBackwardSnappingFromInBetweenBeat() { reset(); @@ -280,7 +271,8 @@ namespace osu.Game.Tests.Visual /// /// Tests that there are no rounding issues when snapping to beats within a timing point with a floating-point beatlength. /// - private void testSeekingWithFloatingPointBeatLength() + [Test] + public void TestSeekingWithFloatingPointBeatLength() { reset(); @@ -288,7 +280,7 @@ namespace osu.Game.Tests.Visual AddStep("Seek(0)", () => Clock.Seek(0)); - for (int i = 0; i < 20; i++) + for (int i = 0; i < 9; i++) { AddStep("SeekForward, Snap", () => { @@ -298,7 +290,7 @@ namespace osu.Game.Tests.Visual AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime); } - for (int i = 0; i < 20; i++) + for (int i = 0; i < 9; i++) { AddStep("SeekBackward, Snap", () => { @@ -316,16 +308,6 @@ namespace osu.Game.Tests.Visual AddStep("Reset", () => Clock.Seek(0)); } - private class TestHitObjectComposer : HitObjectComposer - { - public TestHitObjectComposer(Ruleset ruleset) - : base(ruleset) - { - } - - protected override IReadOnlyList CompositionTools => new ICompositionTool[0]; - } - private class TimingPointVisualiser : CompositeDrawable { private readonly double length; diff --git a/osu.Game.Tests/Visual/TestCaseLounge.cs b/osu.Game.Tests/Visual/TestCaseLounge.cs index c5e0d1c4bf..174873b011 100644 --- a/osu.Game.Tests/Visual/TestCaseLounge.cs +++ b/osu.Game.Tests/Visual/TestCaseLounge.cs @@ -167,6 +167,7 @@ namespace osu.Game.Tests.Visual AddStep(@"set rooms", () => lounge.Rooms = rooms); selectAssert(1); AddStep(@"open room 1", () => clickRoom(1)); + AddUntilStep(() => lounge.ChildScreen?.IsCurrentScreen == true, "wait until room current"); AddStep(@"make lounge current", lounge.MakeCurrent); filterAssert(@"THE FINAL", LoungeTab.Public, 1); filterAssert(string.Empty, LoungeTab.Public, 2); diff --git a/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs b/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs new file mode 100644 index 0000000000..d711d501fe --- /dev/null +++ b/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs @@ -0,0 +1,127 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Containers; +using osu.Game.Audio; +using osu.Game.Beatmaps; + +namespace osu.Game.Tests.Visual +{ + public class TestCasePreviewTrackManager : OsuTestCase, IPreviewTrackOwner + { + private readonly PreviewTrackManager trackManager = new TestPreviewTrackManager(); + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs(trackManager); + dependencies.CacheAs(this); + return dependencies; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(trackManager); + } + + [Test] + public void TestStartStop() + { + PreviewTrack track = null; + + AddStep("get track", () => track = getOwnedTrack()); + AddStep("start", () => track.Start()); + AddAssert("started", () => track.IsRunning); + AddStep("stop", () => track.Stop()); + AddAssert("stopped", () => !track.IsRunning); + } + + [Test] + public void TestStartMultipleTracks() + { + PreviewTrack track1 = null; + PreviewTrack track2 = null; + + AddStep("get tracks", () => + { + track1 = getOwnedTrack(); + track2 = getOwnedTrack(); + }); + + AddStep("start track 1", () => track1.Start()); + AddStep("start track 2", () => track2.Start()); + AddAssert("track 1 stopped", () => !track1.IsRunning); + AddAssert("track 2 started", () => track2.IsRunning); + } + + [Test] + public void TestCancelFromOwner() + { + PreviewTrack track = null; + + AddStep("get track", () => track = getOwnedTrack()); + AddStep("start", () => track.Start()); + AddStep("stop by owner", () => trackManager.StopAnyPlaying(this)); + AddAssert("stopped", () => !track.IsRunning); + } + + [Test] + public void TestCancelFromNonOwner() + { + TestTrackOwner owner = null; + PreviewTrack track = null; + + AddStep("get track", () => AddInternal(owner = new TestTrackOwner(track = getTrack()))); + AddStep("start", () => track.Start()); + AddStep("attempt stop", () => trackManager.StopAnyPlaying(this)); + AddAssert("not stopped", () => track.IsRunning); + AddStep("stop by true owner", () => trackManager.StopAnyPlaying(owner)); + AddAssert("stopped", () => !track.IsRunning); + } + + private PreviewTrack getTrack() => trackManager.Get(null); + + private PreviewTrack getOwnedTrack() + { + var track = getTrack(); + + AddInternal(track); + + return track; + } + + private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner + { + public TestTrackOwner(PreviewTrack track) + { + AddInternal(track); + } + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs(this); + return dependencies; + } + } + + private class TestPreviewTrackManager : PreviewTrackManager + { + protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TestPreviewTrack(beatmapSetInfo, trackManager); + + protected class TestPreviewTrack : TrackManagerPreviewTrack + { + public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) + : base(beatmapSetInfo, trackManager) + { + } + + protected override Track GetTrack() => new TrackVirtual { Length = 100000 }; + } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs index 8f90b6d87e..9b48ec17bd 100644 --- a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual private class TestPlayfield : ScrollingPlayfield { - public readonly ScrollingDirection Direction; + public new readonly ScrollingDirection Direction; public TestPlayfield(ScrollingDirection direction) : base(direction) diff --git a/osu.Game/Audio/IPreviewTrackOwner.cs b/osu.Game/Audio/IPreviewTrackOwner.cs new file mode 100644 index 0000000000..f166096601 --- /dev/null +++ b/osu.Game/Audio/IPreviewTrackOwner.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Audio +{ + /// + /// Interface for objects that can own s. + /// + /// + /// s can cancel the currently playing through the + /// global if they're the owner of the playing . + /// + public interface IPreviewTrackOwner + { + } +} diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs new file mode 100644 index 0000000000..3c9122b941 --- /dev/null +++ b/osu.Game/Audio/PreviewTrack.cs @@ -0,0 +1,103 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Threading; + +namespace osu.Game.Audio +{ + public abstract class PreviewTrack : Component + { + /// + /// Invoked when this has stopped playing. + /// + public event Action Stopped; + + /// + /// Invoked when this has started playing. + /// + public event Action Started; + + private Track track; + private bool hasStarted; + + [BackgroundDependencyLoader] + private void load() + { + track = GetTrack(); + } + + /// + /// Length of the track. + /// + public double Length => track?.Length ?? 0; + + /// + /// The current track time. + /// + public double CurrentTime => track?.CurrentTime ?? 0; + + /// + /// Whether the track is loaded. + /// + public bool TrackLoaded => track?.IsLoaded ?? false; + + /// + /// Whether the track is playing. + /// + public bool IsRunning => track?.IsRunning ?? false; + + protected override void Update() + { + base.Update(); + + // Todo: Track currently doesn't signal its completion, so we have to handle it manually + if (hasStarted && track.HasCompleted) + Stop(); + } + + private ScheduledDelegate startDelegate; + + /// + /// Starts playing this . + /// + public void Start() => startDelegate = Schedule(() => + { + if (track == null) + return; + + if (hasStarted) + return; + hasStarted = true; + + track.Restart(); + Started?.Invoke(); + }); + + /// + /// Stops playing this . + /// + public void Stop() + { + startDelegate?.Cancel(); + + if (track == null) + return; + + if (!hasStarted) + return; + hasStarted = false; + + track.Stop(); + Stopped?.Invoke(); + } + + /// + /// Retrieves the audio track. + /// + protected abstract Track GetTrack(); + } +} diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs new file mode 100644 index 0000000000..07fbe86ff4 --- /dev/null +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -0,0 +1,107 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; + +namespace osu.Game.Audio +{ + /// + /// A central store for the retrieval of s. + /// + public class PreviewTrackManager : Component + { + private readonly BindableDouble muteBindable = new BindableDouble(); + + private AudioManager audio; + private TrackManager trackManager; + + private TrackManagerPreviewTrack current; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, FrameworkConfigManager config) + { + trackManager = new TrackManager(new OnlineStore()); + + this.audio = audio; + audio.AddItem(trackManager); + + config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); + } + + /// + /// Retrieves a for a . + /// + /// The to retrieve the preview track for. + /// The playable . + public PreviewTrack Get(BeatmapSetInfo beatmapSetInfo) + { + var track = CreatePreviewTrack(beatmapSetInfo, trackManager); + + track.Started += () => + { + current?.Stop(); + current = track; + audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable); + }; + + track.Stopped += () => + { + current = null; + audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); + }; + + return track; + } + + /// + /// Stops any currently playing . + /// + /// + /// Only the immediate owner (an object that implements ) of the playing + /// can globally stop the currently playing . The object holding a reference to the + /// can always stop the themselves through . + /// + /// The which may be the owner of the . + public void StopAnyPlaying(IPreviewTrackOwner source) + { + if (current == null || current.Owner != source) + return; + + current.Stop(); + current = null; + } + + /// + /// Creates the . + /// + protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TrackManagerPreviewTrack(beatmapSetInfo, trackManager); + + protected class TrackManagerPreviewTrack : PreviewTrack + { + public IPreviewTrackOwner Owner { get; private set; } + + private readonly BeatmapSetInfo beatmapSetInfo; + private readonly TrackManager trackManager; + + public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) + { + this.beatmapSetInfo = beatmapSetInfo; + this.trackManager = trackManager; + } + + [BackgroundDependencyLoader] + private void load(IPreviewTrackOwner owner) + { + Owner = owner; + } + + protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3"); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 5e3b66646b..50428cc5e6 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -389,6 +389,9 @@ namespace osu.Game.Beatmaps if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null) return true; + if (api.State != APIState.Online) + return false; + Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database); try diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 4310d9b7df..9b50aed077 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -116,9 +116,6 @@ namespace osu.Game.Beatmaps mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); } - // Post-process - rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess(); - // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed foreach (var obj in converted.HitObjects) obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); @@ -127,6 +124,9 @@ namespace osu.Game.Beatmaps foreach (var obj in converted.HitObjects) mod.ApplyToHitObject(obj); + // Post-process + rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess(); + return converted; } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index e270b5efbe..5f0190e7d7 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -8,22 +8,34 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using OpenTK; using osu.Framework.Configuration; +using osu.Game.Audio; using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { - public class OsuFocusedOverlayContainer : FocusedOverlayContainer + public class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner { private SampleChannel samplePopIn; private SampleChannel samplePopOut; protected virtual bool PlaySamplesOnStateChange => true; + private PreviewTrackManager previewTrackManager; + protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); - [BackgroundDependencyLoader(true)] - private void load(OsuGame osuGame, AudioManager audio) + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs(this); + return dependencies; + } + + [BackgroundDependencyLoader(true)] + private void load(OsuGame osuGame, AudioManager audio, PreviewTrackManager previewTrackManager) + { + this.previewTrackManager = previewTrackManager; + if (osuGame != null) OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); @@ -70,5 +82,11 @@ namespace osu.Game.Graphics.Containers break; } } + + protected override void PopOut() + { + base.PopOut(); + previewTrackManager.StopAnyPlaying(this); + } } } diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index 5454dd0c9f..57a2e5df6d 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -32,16 +32,14 @@ namespace osu.Game.Input.Handlers public override int Priority => 0; - public class ReplayState : InputState + public class ReplayState : IInput where T : struct { public List PressedActions; - public override InputState Clone() + public void Apply(InputState state, IInputStateChangeHandler handler) { - var clone = (ReplayState)base.Clone(); - clone.PressedActions = new List(PressedActions); - return clone; + handler.HandleCustomInput(state, this); } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8c5fc58878..12935a5ffe 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -148,7 +148,7 @@ namespace osu.Game.Online.API // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests // before actually going online. - while (State != APIState.Online) + while (State > APIState.Offline && State < APIState.Online) Thread.Sleep(500); break; @@ -158,7 +158,6 @@ namespace osu.Game.Online.API if (authentication.RequestAccessToken() == null) { Logout(false); - State = APIState.Offline; continue; } @@ -208,6 +207,14 @@ namespace osu.Game.Online.API { HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); + // special cases for un-typed but useful message responses. + switch (we.Message) + { + case "Unauthorized": + statusCode = HttpStatusCode.Unauthorized; + break; + } + switch (statusCode) { case HttpStatusCode.Unauthorized: @@ -292,6 +299,7 @@ namespace osu.Game.Online.API password = null; authentication.Clear(); LocalUser.Value = createGuestUser(); + State = APIState.Offline; } private static User createGuestUser() => new User diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 36c76851c6..ba8685b5b2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -101,6 +101,8 @@ namespace osu.Game public OsuGame(string[] args = null) { this.args = args; + + forwardLoggedErrorsToNotifications(); } public void ToggleSettings() => settings.ToggleVisibility(); @@ -305,8 +307,6 @@ namespace osu.Game Depth = -6, }, overlayContent.Add); - forwardLoggedErrorsToNotifications(); - dependencies.Cache(settings); dependencies.Cache(onscreenDisplay); dependencies.Cache(social); @@ -394,31 +394,40 @@ namespace osu.Game private void forwardLoggedErrorsToNotifications() { - int recentErrorCount = 0; + int recentLogCount = 0; const double debounce = 5000; Logger.NewEntry += entry => { - if (entry.Level < LogLevel.Error || entry.Target == null) return; + if (entry.Level < LogLevel.Important || entry.Target == null) return; - if (recentErrorCount < 2) + const int short_term_display_limit = 3; + + if (recentLogCount < short_term_display_limit) { - notifications.Post(new SimpleNotification + Schedule(() => notifications.Post(new SimpleNotification { - Icon = FontAwesome.fa_bomb, - Text = (recentErrorCount == 0 ? entry.Message : "Subsequent errors occurred and have been logged.") + "\nClick to view log files.", + Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb, + Text = entry.Message, + })); + } + else if (recentLogCount == short_term_display_limit) + { + Schedule(() => notifications.Post(new SimpleNotification + { + Icon = FontAwesome.fa_ellipsis_h, + Text = "Subsequent messages have been logged. Click to view log files.", Activated = () => { Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer(); return true; } - }); + })); } - Interlocked.Increment(ref recentErrorCount); - - Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentErrorCount), debounce); + Interlocked.Increment(ref recentLogCount); + Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce); }; } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a5779a2293..246229a794 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -21,6 +21,7 @@ using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; +using osu.Game.Audio; using osu.Game.Database; using osu.Game.Graphics.Textures; using osu.Game.Input; @@ -187,6 +188,10 @@ namespace osu.Game KeyBindingStore.Register(globalBinding); dependencies.Cache(globalBinding); + + PreviewTrackManager previewTrackManager; + dependencies.Cache(previewTrackManager = new PreviewTrackManager()); + Add(previewTrackManager); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 08a99f1aea..505b7a7540 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -2,13 +2,13 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly Box bg, progress; private readonly PlayButton playButton; - private Track preview => playButton.Preview; + private PreviewTrack preview => playButton.Preview; public Bindable Playing => playButton.Playing; public BeatmapSetInfo BeatmapSet @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons }, }; - Action = () => Playing.Value = !Playing.Value; + Action = () => playButton.TriggerOnClick(); Playing.ValueChanged += newValue => progress.FadeTo(newValue ? 1 : 0, 100); } @@ -89,12 +89,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons progress.Width = 0; } - protected override void Dispose(bool isDisposing) - { - Playing.Value = false; - base.Dispose(isDisposing); - } - protected override bool OnHover(InputState state) { bg.FadeColour(Color4.Black.Opacity(0.5f), 100); diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index 5264caf936..ccd0fa04ab 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -102,8 +102,6 @@ namespace osu.Game.Overlays.BeatmapSet updateDisplay(); } - public void StopPreview() => preview.Playing.Value = false; - private class DetailBox : Container { private readonly Container content; diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 096f7bb63c..88f0a72ddf 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -1,9 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using osu.Framework.Allocation; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,9 +14,10 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Rulesets; using osu.Game.Overlays.BeatmapSet.Scores; -using System.Linq; +using osu.Game.Rulesets; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Overlays { @@ -124,8 +124,6 @@ namespace osu.Game.Overlays protected override void PopOut() { base.PopOut(); - header.Details.StopPreview(); - FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out).OnComplete(_ => BeatmapSet = null); } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index e767f6ec83..e63c290ce5 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -4,22 +4,22 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; -using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using OpenTK.Graphics; -using osu.Framework.Input; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests; -using osu.Framework.Configuration; -using osu.Framework.Audio.Track; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Overlays.Direct { @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Direct private BeatmapManager beatmaps; private BeatmapSetOverlay beatmapSetOverlay; - public Track Preview => PlayButton.Preview; + public PreviewTrack Preview => PlayButton.Preview; public Bindable PreviewPlaying => PlayButton.Playing; protected abstract PlayButton PlayButton { get; } protected abstract Box PreviewBar { get; } @@ -113,7 +113,7 @@ namespace osu.Game.Overlays.Direct { base.Update(); - if (PreviewPlaying && Preview != null && Preview.IsLoaded) + if (PreviewPlaying && Preview != null && Preview.TrackLoaded) { PreviewBar.Width = (float)(Preview.CurrentTime / Preview.Length); } @@ -141,7 +141,6 @@ namespace osu.Game.Overlays.Direct protected override bool OnClick(InputState state) { ShowInformation(); - PreviewPlaying.Value = false; return true; } diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 131083c6ff..4b91a3d700 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -1,25 +1,23 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; -using osu.Framework.IO.Stores; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using OpenTK.Graphics; namespace osu.Game.Overlays.Direct { public class PlayButton : Container { - public readonly Bindable Playing = new Bindable(); - public Track Preview { get; private set; } + public readonly BindableBool Playing = new BindableBool(); + public PreviewTrack Preview { get; private set; } private BeatmapSetInfo beatmapSet; @@ -31,9 +29,11 @@ namespace osu.Game.Overlays.Direct if (value == beatmapSet) return; beatmapSet = value; - Playing.Value = false; - trackLoader = null; + Preview?.Stop(); + Preview?.Expire(); Preview = null; + + Playing.Value = false; } } @@ -41,8 +41,6 @@ namespace osu.Game.Overlays.Direct private readonly SpriteIcon icon; private readonly LoadingAnimation loadingAnimation; - private readonly BindableDouble muteBindable = new BindableDouble(); - private const float transition_duration = 500; private bool loading @@ -50,15 +48,9 @@ namespace osu.Game.Overlays.Direct set { if (value) - { loadingAnimation.Show(); - icon.FadeOut(transition_duration * 5, Easing.OutQuint); - } else - { loadingAnimation.Hide(); - icon.FadeIn(transition_duration, Easing.OutQuint); - } } } @@ -78,19 +70,22 @@ namespace osu.Game.Overlays.Direct loadingAnimation = new LoadingAnimation(), }); - Playing.ValueChanged += updatePreviewTrack; + Playing.ValueChanged += playingStateChanged; } + private PreviewTrackManager previewTrackManager; + [BackgroundDependencyLoader] - private void load(OsuColour colour, AudioManager audio) + private void load(OsuColour colour, PreviewTrackManager previewTrackManager) { + this.previewTrackManager = previewTrackManager; + hoverColour = colour.Yellow; - this.audio = audio; } protected override bool OnClick(InputState state) { - Playing.Value = !Playing.Value; + Playing.Toggle(); return true; } @@ -107,44 +102,44 @@ namespace osu.Game.Overlays.Direct base.OnHoverLost(state); } - protected override void Update() + private void playingStateChanged(bool playing) { - base.Update(); - - if (Preview?.HasCompleted ?? false) - { - Playing.Value = false; - Preview = null; - } - } - - private void updatePreviewTrack(bool playing) - { - if (playing && BeatmapSet == null) - { - Playing.Value = false; - return; - } - icon.Icon = playing ? FontAwesome.fa_stop : FontAwesome.fa_play; icon.FadeColour(playing || IsHovered ? hoverColour : Color4.White, 120, Easing.InOutQuint); if (playing) { - if (Preview == null) + if (BeatmapSet == null) { - beginAudioLoad(); + Playing.Value = false; return; } - Preview.Restart(); + if (Preview != null) + { + Preview.Start(); + return; + } - audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable); + loading = true; + + LoadComponentAsync(Preview = previewTrackManager.Get(beatmapSet), preview => + { + // beatmapset may have changed. + if (Preview != preview) + return; + + AddInternal(preview); + loading = false; + preview.Stopped += () => Playing.Value = false; + + // user may have changed their mind. + if (Playing) + preview.Start(); + }); } else { - audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); - Preview?.Stop(); loading = false; } @@ -155,64 +150,5 @@ namespace osu.Game.Overlays.Direct base.Dispose(isDisposing); Playing.Value = false; } - - private TrackLoader trackLoader; - private AudioManager audio; - - private void beginAudioLoad() - { - if (trackLoader != null) - { - Preview = trackLoader.Preview; - Playing.TriggerChange(); - return; - } - - loading = true; - - LoadComponentAsync(trackLoader = new TrackLoader($"https://b.ppy.sh/preview/{BeatmapSet.OnlineBeatmapSetID}.mp3"), - d => - { - // We may have been replaced by another loader - if (trackLoader != d) return; - - Preview = d?.Preview; - updatePreviewTrack(Playing); - loading = false; - - Add(trackLoader); - }); - } - - private class TrackLoader : Drawable - { - private readonly string preview; - - public Track Preview; - private TrackManager trackManager; - - public TrackLoader(string preview) - { - this.preview = preview; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio, FrameworkConfigManager config) - { - // create a local trackManager to bypass the mute we are applying above. - audio.AddItem(trackManager = new TrackManager(new OnlineStore())); - - // add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy). - config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); - - Preview = trackManager.Get(preview); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - trackManager?.Dispose(); - } - } } } diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index b33f271986..423211659d 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -4,12 +4,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using OpenTK; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays.Direct; using osu.Game.Overlays.SearchableList; using osu.Game.Rulesets; +using OpenTK; using OpenTK.Graphics; namespace osu.Game.Overlays @@ -32,7 +33,6 @@ namespace osu.Game.Overlays private readonly FillFlowContainer resultCountsContainer; private readonly OsuSpriteText resultCountsText; private FillFlowContainer panels; - private DirectPanel playing; protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); @@ -176,10 +176,11 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OsuColour colours, APIAccess api, RulesetStore rulesets) + private void load(OsuColour colours, APIAccess api, RulesetStore rulesets, PreviewTrackManager previewTrackManager) { this.api = api; this.rulesets = rulesets; + this.previewTrackManager = previewTrackManager; resultCountsContainer.Colour = colours.Yellow; } @@ -206,12 +207,6 @@ namespace osu.Game.Overlays panels.FadeOut(200); panels.Expire(); panels = null; - - if (playing != null) - { - playing.PreviewPlaying.Value = false; - playing = null; - } } if (BeatmapSets == null) return; @@ -242,17 +237,6 @@ namespace osu.Game.Overlays { if (panels != null) ScrollFlow.Remove(panels); ScrollFlow.Add(panels = newPanels); - - foreach (DirectPanel panel in p.Children) - panel.PreviewPlaying.ValueChanged += newValue => - { - if (newValue) - { - if (playing != null && playing != panel) - playing.PreviewPlaying.Value = false; - playing = panel; - } - }; }); } @@ -261,6 +245,7 @@ namespace osu.Game.Overlays private readonly Bindable currentQuery = new Bindable(); private ScheduledDelegate queryChangedDebounce; + private PreviewTrackManager previewTrackManager; private void updateSearch() { @@ -277,6 +262,8 @@ namespace osu.Game.Overlays if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return; + previewTrackManager.StopAnyPlaying(this); + getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty, ((FilterControl)Filter).Ruleset.Value, Filter.DisplayStyleControl.Dropdown.Current.Value, @@ -300,14 +287,6 @@ namespace osu.Game.Overlays api.Queue(getSetsRequest); } - protected override void PopOut() - { - base.PopOut(); - - if (playing != null) - playing.PreviewPlaying.Value = false; - } - private int distinctCount(List list) => list.Distinct().ToArray().Length; public class ResultCounts diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index ffe1560627..a12f9dee7e 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -186,7 +186,7 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget.IsHovered) { - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); + bindTarget.UpdateKeyCombination(new KeyCombination(KeyCombination.FromInputState(state).Keys.Append(state.Mouse.ScrollDelta.Y > 0 ? InputKey.MouseWheelUp : InputKey.MouseWheelDown))); finalise(); return true; } diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index a78bc8da81..25a832941e 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Notifications } }); - Content.Add(textDrawable = new OsuTextFlowContainer(t => t.TextSize = 16) + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.TextSize = 14) { Colour = OsuColour.Gray(128), AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 3fec9d8697..0b06acd426 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -1,14 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; -using OpenTK; +using System.Linq; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Direct; using osu.Game.Users; -using System.Linq; +using OpenTK; namespace osu.Game.Overlays.Profile.Sections.Beatmaps { @@ -18,10 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps private readonly BeatmapSetType type; - private DirectPanel currentlyPlaying; - - public event Action BeganPlayingPreview; - public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string header, string missing = "None... yet.") : base(user, header, missing) { @@ -56,28 +51,10 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets)); ItemsContainer.Add(panel); - - panel.PreviewPlaying.ValueChanged += isPlaying => - { - StopPlayingPreview(); - - if (isPlaying) - { - BeganPlayingPreview?.Invoke(this); - currentlyPlaying = panel; - } - }; } }; Api.Queue(req); } - - public void StopPlayingPreview() - { - if (currentlyPlaying == null) return; - currentlyPlaying.PreviewPlaying.Value = false; - currentlyPlaying = null; - } } } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index 92abd20f93..367d096c16 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.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.Linq; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Profile.Sections.Beatmaps; @@ -22,15 +21,6 @@ namespace osu.Game.Overlays.Profile.Sections new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps"), }; - - foreach (var paginatedBeatmapContainer in Children.OfType()) - { - paginatedBeatmapContainer.BeganPlayingPreview += _ => - { - foreach (var bc in Children.OfType()) - bc.StopPlayingPreview(); - }; - } } } } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index a4dd0c9ec3..745f2f3def 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -2,8 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -18,6 +16,8 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile.Sections; using osu.Game.Users; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Overlays { diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index b62c639ee3..d0aa58e668 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Volume protected override bool OnHover(InputState state) { this.TransformTo("BorderColour", hoveredColour, 500, Easing.OutQuint); - return true; + return false; } protected override void OnHoverLost(InputState state) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 0e43945f8c..1d392e6ee8 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.MathUtils; using osu.Game.Graphics; @@ -232,12 +233,13 @@ namespace osu.Game.Overlays.Volume { float amount = adjust_step * direction; - var mouse = GetContainingInputManager().CurrentState.Mouse; - if (mouse.HasPreciseScroll) + // handle the case where the OnPressed action was actually a mouse wheel. + // this allows for precise wheel handling. + var state = GetContainingInputManager().CurrentState; + if (state.Mouse?.ScrollDelta.Y != 0) { - float scrollDelta = mouse.ScrollDelta.Y; - if (scrollDelta != 0) - amount *= Math.Abs(scrollDelta / 10); + OnScroll(state); + return; } Volume += amount; @@ -260,6 +262,34 @@ namespace osu.Game.Overlays.Volume return false; } + // because volume precision is set to 0.01, this local is required to keep track of more precise adjustments and only apply when possible. + private double scrollAmount; + + protected override bool OnScroll(InputState state) + { + scrollAmount += adjust_step * state.Mouse.ScrollDelta.Y * (state.Mouse.HasPreciseScroll ? 0.1f : 1); + + if (Math.Abs(scrollAmount) < Bindable.Precision) + return true; + + Volume += scrollAmount; + scrollAmount = 0; + return true; + } + public bool OnReleased(GlobalAction action) => false; + + private const float transition_length = 500; + + protected override bool OnHover(InputState state) + { + this.ScaleTo(1.04f, transition_length, Easing.OutExpo); + return false; + } + + protected override void OnHoverLost(InputState state) + { + this.ScaleTo(1f, transition_length, Easing.OutExpo); + } } } diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index f922c507f7..1c9c615bbb 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Input.Bindings; @@ -86,16 +87,10 @@ namespace osu.Game.Overlays { base.LoadComplete(); - volumeMeterMaster.Bindable.ValueChanged += _ => settingChanged(); - volumeMeterEffect.Bindable.ValueChanged += _ => settingChanged(); - volumeMeterMusic.Bindable.ValueChanged += _ => settingChanged(); - muteButton.Current.ValueChanged += _ => settingChanged(); - } - - private void settingChanged() - { - Show(); - schedulePopOut(); + volumeMeterMaster.Bindable.ValueChanged += _ => Show(); + volumeMeterEffect.Bindable.ValueChanged += _ => Show(); + volumeMeterMusic.Bindable.ValueChanged += _ => Show(); + muteButton.Current.ValueChanged += _ => Show(); } public bool Adjust(GlobalAction action) @@ -127,6 +122,14 @@ namespace osu.Game.Overlays private ScheduledDelegate popOutDelegate; + public override void Show() + { + if (State == Visibility.Visible) + schedulePopOut(); + + base.Show(); + } + protected override void PopIn() { ClearTransforms(); @@ -140,10 +143,33 @@ namespace osu.Game.Overlays this.FadeOut(100); } + protected override bool OnMouseMove(InputState state) + { + // keep the scheduled event correctly timed as long as we have movement. + schedulePopOut(); + return base.OnMouseMove(state); + } + + protected override bool OnHover(InputState state) + { + schedulePopOut(); + return true; + } + + protected override void OnHoverLost(InputState state) + { + schedulePopOut(); + base.OnHoverLost(state); + } + private void schedulePopOut() { popOutDelegate?.Cancel(); - this.Delay(1000).Schedule(Hide, out popOutDelegate); + this.Delay(1000).Schedule(() => + { + if (!IsHovered) + Hide(); + }, out popOutDelegate); } } } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 0f5490a182..f13d96b35e 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Replays return true; } - public override List GetPendingStates() => new List(); + public override List GetPendingInputs() => new List(); public bool AtLastFrame => currentFrameIndex == Frames.Count - 1; public bool AtFirstFrame => currentFrameIndex == 0; @@ -119,7 +119,8 @@ namespace osu.Game.Rulesets.Replays { public ReplayKeyboardState(List keys) { - Keys = keys; + foreach (var key in keys) + Keys.Add(key); } } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 58a66a5224..8046e9f9b4 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -15,6 +15,7 @@ using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; using OpenTK.Input; +using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI { @@ -29,26 +30,43 @@ namespace osu.Game.Rulesets.UI } } + protected override InputState CreateInitialState() + { + var state = base.CreateInitialState(); + return new RulesetInputManagerInputState + { + Mouse = state.Mouse, + Keyboard = state.Keyboard, + Joystick = state.Joystick, + LastReplayState = null + }; + } + protected readonly KeyBindingContainer KeyBindingContainer; protected override Container Content => KeyBindingContainer; protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) { - InternalChild = KeyBindingContainer = new RulesetKeyBindingContainer(ruleset, variant, unique); + InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique); } #region Action mapping (for replays) private List lastPressedActions = new List(); - protected override void HandleNewState(InputState state) + public override void HandleCustomInput(InputState state, IInput input) { - base.HandleNewState(state); + if (!(input is ReplayState replayState)) + { + base.HandleCustomInput(state, input); + return; + } - var replayState = state as ReplayInputHandler.ReplayState; - - if (replayState == null) return; + if (state is RulesetInputManagerInputState inputState) + { + inputState.LastReplayState = replayState; + } // Here we handle states specifically coming from a replay source. // These have extra action information rather than keyboard keys or mouse buttons. @@ -80,7 +98,7 @@ namespace osu.Game.Rulesets.UI if (replayInputHandler != null) RemoveHandler(replayInputHandler); replayInputHandler = value; - UseParentState = replayInputHandler == null; + UseParentInput = replayInputHandler == null; if (replayInputHandler != null) AddHandler(replayInputHandler); @@ -123,7 +141,7 @@ namespace osu.Game.Rulesets.UI protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState; - private bool isAttached => replayInputHandler != null && !UseParentState; + private bool isAttached => replayInputHandler != null && !UseParentInput; private const int max_catch_up_updates_per_frame = 50; @@ -249,6 +267,9 @@ namespace osu.Game.Rulesets.UI } #endregion + + protected virtual RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new RulesetKeyBindingContainer(ruleset, variant, unique); } /// @@ -267,4 +288,10 @@ namespace osu.Game.Rulesets.UI { void Attach(KeyCounterCollection keyCounter); } + + public class RulesetInputManagerInputState : InputState + where T : struct + { + public ReplayState LastReplayState; + } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 36c6b07f54..c64fca6eff 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -29,17 +29,16 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected readonly SortedList ControlPoints = new SortedList(); - private readonly ScrollingDirection direction; + public readonly Bindable Direction = new Bindable(); private Cached initialStateCache = new Cached(); - public ScrollingHitObjectContainer(ScrollingDirection direction) + public ScrollingHitObjectContainer() { - this.direction = direction; - RelativeSizeAxes = Axes.Both; - TimeRange.ValueChanged += v => initialStateCache.Invalidate(); + TimeRange.ValueChanged += _ => initialStateCache.Invalidate(); + Direction.ValueChanged += _ => initialStateCache.Invalidate(); } private ISpeedChangeVisualiser speedChangeVisualiser; @@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!initialStateCache.IsValid) { - speedChangeVisualiser.ComputeInitialStates(Objects, direction, TimeRange, DrawSize); + speedChangeVisualiser.ComputeInitialStates(Objects, Direction, TimeRange, DrawSize); initialStateCache.Validate(); } } @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.UI.Scrolling base.UpdateAfterChildrenLife(); // We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions - speedChangeVisualiser.UpdatePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize); + speedChangeVisualiser.UpdatePositions(AliveObjects, Direction, Time.Current, TimeRange, DrawSize); } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 830214803c..172b866505 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects; - private readonly ScrollingDirection direction; + protected readonly Bindable Direction = new Bindable(); /// /// Creates a new . @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null) : base(customWidth, customHeight) { - this.direction = direction; + Direction.Value = direction; } [BackgroundDependencyLoader] @@ -99,6 +99,11 @@ namespace osu.Game.Rulesets.UI.Scrolling return false; } - protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction); + protected sealed override HitObjectContainer CreateHitObjectContainer() + { + var container = new ScrollingHitObjectContainer(); + container.Direction.BindTo(Direction); + return container; + } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs index e353c07e9f..745352a4d3 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs @@ -93,6 +93,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers /// A positive value indicating the position at . private double positionAt(double time, double timeRange) { + if (controlPoints.Count == 0) + return time / timeRange; + double length = 0; // We need to consider all timing points until the specified time and not just the currently-active one, diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2a2111c8d6..7eeabd3e5e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -185,9 +185,9 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(InputState state) { if (state.Mouse.ScrollDelta.X + state.Mouse.ScrollDelta.Y > 0) - clock.SeekBackward(true); + clock.SeekBackward(!clock.IsRunning); else - clock.SeekForward(true); + clock.SeekForward(!clock.IsRunning); return true; } diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs index e993d36551..e6a04bf1e7 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs @@ -62,7 +62,12 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline } /// - /// The track's time in the previous frame. + /// The timeline's scroll position in the last frame. + /// + private float lastScrollPosition; + + /// + /// The track time in the last frame. /// private double lastTrackTime; @@ -83,49 +88,51 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline // The extrema of track time should be positioned at the centre of the container when scrolled to the start or end Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 }; - if (handlingDragInput) - { - // The user is dragging - the track should always follow the timeline - seekTrackToCurrent(); - } - else if (adjustableClock.IsRunning) - { - // If the user hasn't provided mouse input but the track is running, always follow the track + // This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren + if (adjustableClock.IsRunning) scrollToTrackTime(); - } - else + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (handlingDragInput) + seekTrackToCurrent(); + else if (!adjustableClock.IsRunning) { - // The track isn't playing, so we want to smooth-scroll once more, and re-enable wheel scrolling - // There are two cases we have to be wary of: - // 1) The user scrolls on this timeline: We want the track to follow us + // The track isn't running. There are two cases we have to be wary of: + // 1) The user flick-drags on this timeline: We want the track to follow us // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time - // The simplest way to cover both cases is by checking that inter-frame track times are identical - if (adjustableClock.CurrentTime == lastTrackTime) - { - // The track hasn't been seeked externally + // The simplest way to cover both cases is by checking whether the scroll position has changed and the audio hasn't been changed externally + if (Current != lastScrollPosition && adjustableClock.CurrentTime == lastTrackTime) seekTrackToCurrent(); - } else - { - // The track has been seeked externally scrollToTrackTime(); - } } + lastScrollPosition = Current; lastTrackTime = adjustableClock.CurrentTime; + } - void seekTrackToCurrent() - { - if (!(Beatmap.Value.Track is TrackVirtual)) - adjustableClock.Seek(Current / Content.DrawWidth * Beatmap.Value.Track.Length); - } + private void seekTrackToCurrent() + { + var track = Beatmap.Value.Track; + if (track is TrackVirtual || !track.IsLoaded) + return; - void scrollToTrackTime() - { - if (!(Beatmap.Value.Track is TrackVirtual)) - ScrollTo((float)(adjustableClock.CurrentTime / Beatmap.Value.Track.Length) * Content.DrawWidth, false); - } + if (!(Beatmap.Value.Track is TrackVirtual)) + adjustableClock.Seek(Current / Content.DrawWidth * Beatmap.Value.Track.Length); + } + + private void scrollToTrackTime() + { + var track = Beatmap.Value.Track; + if (track is TrackVirtual || !track.IsLoaded) + return; + + ScrollTo((float)(adjustableClock.CurrentTime / Beatmap.Value.Track.Length) * Content.DrawWidth, false); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) diff --git a/osu.Game/Screens/Play/HUD/QuitButton.cs b/osu.Game/Screens/Play/HUD/QuitButton.cs index d0aa0dad92..29382c25f3 100644 --- a/osu.Game/Screens/Play/HUD/QuitButton.cs +++ b/osu.Game/Screens/Play/HUD/QuitButton.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -182,14 +183,14 @@ namespace osu.Game.Screens.Play.HUD protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - if (!pendingAnimation && state.Mouse.Buttons.Count == 1) + if (!pendingAnimation && state.Mouse.Buttons.Count() == 1) BeginConfirm(); return true; } protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) { - if (state.Mouse.Buttons.Count == 0) + if (!state.Mouse.Buttons.Any()) AbortConfirm(); return true; } diff --git a/osu.Game/Tests/Platform/TestStorage.cs b/osu.Game/Tests/Platform/TestStorage.cs index 883aae2184..5b31c7b4d0 100644 --- a/osu.Game/Tests/Platform/TestStorage.cs +++ b/osu.Game/Tests/Platform/TestStorage.cs @@ -7,7 +7,7 @@ namespace osu.Game.Tests.Platform { public class TestStorage : DesktopStorage { - public TestStorage(string baseName) : base(baseName) + public TestStorage(string baseName) : base(baseName, null) { } diff --git a/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs b/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs index 132a655c15..01676ad56e 100644 --- a/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs +++ b/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual /// protected void ReturnUserInput() { - AddStep("Return user input", () => InputManager.UseParentState = true); + AddStep("Return user input", () => InputManager.UseParentInput = true); } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f0bc330994..9668d40fd5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -14,12 +14,12 @@ - - - + + + - - + + diff --git a/osu.TestProject.props b/osu.TestProject.props index 8f7128f8b7..c2e6048a60 100644 --- a/osu.TestProject.props +++ b/osu.TestProject.props @@ -11,7 +11,7 @@ - +