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 3cf95e9b3e..0289db20f0 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -29,7 +29,8 @@ - + + diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 5b34e46247..4b95a6754e 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; @@ -12,11 +13,14 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests { - internal class CatchBeatmapConversionTest : BeatmapConversionTest + [TestFixture] + public class CatchBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; - [TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2232")] + [TestCase("basic")] + [TestCase("spinner")] + [TestCase("spinner-and-circles")] public new void Test(string name) { base.Test(name); @@ -27,36 +31,52 @@ 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(); } - internal struct ConvertValue : IEquatable + public struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. /// 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.Tests/TestCaseFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs index e77dd76353..5c41e4136c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Tests private DrawableFruit createDrawable(int index) { Fruit fruit = index == 5 - ? new BananaShower.Banana + ? new Banana { StartTime = 1000000000000, IndexInBeatmap = index, diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index ad500606ed..68a8dfb7d3 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -26,22 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps var positionData = obj as IHasXPosition; 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; - } + var legacyOffset = obj as IHasLegacyLastTickOffset; if (curveData != null) { @@ -54,20 +39,31 @@ 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, + LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0 + }; + } + else if (endTime != null) + { + yield return new BananaShower + { + StartTime = obj.StartTime, + Samples = obj.Samples, + Duration = endTime.Duration, NewCombo = comboData?.NewCombo ?? false }; - - yield break; } - - yield return new Fruit + else { - StartTime = obj.StartTime, - Samples = obj.Samples, - NewCombo = comboData?.NewCombo ?? false, - X = positionData.X / CatchPlayfield.BASE_WIDTH - }; + 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..ab0afb08d7 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -9,11 +9,14 @@ 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 { public class CatchBeatmapProcessor : BeatmapProcessor { + public const int RNG_SEED = 1337; + public CatchBeatmapProcessor(IBeatmap beatmap) : base(beatmap) { @@ -21,13 +24,52 @@ namespace osu.Game.Rulesets.Catch.Beatmaps public override void PostProcess() { - initialiseHyperDash((List)Beatmap.HitObjects); - base.PostProcess(); + applyPositionOffsets(); + + initialiseHyperDash((List)Beatmap.HitObjects); + 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; + } + } + + 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 banana in bananaShower.NestedHitObjects.OfType()) + { + banana.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/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs new file mode 100644 index 0000000000..c39e663d75 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs @@ -0,0 +1,36 @@ +// 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.Catch.Judgements +{ + public class CatchBananaJudgement : CatchJudgement + { + public override bool AffectsCombo => false; + + public override bool ShouldExplode => true; + + protected override int NumericResultFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 1100; + } + } + + protected override float HealthIncreaseFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 8; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs new file mode 100644 index 0000000000..0df2305339 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs @@ -0,0 +1,32 @@ +// 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.Catch.Judgements +{ + public class CatchDropletJudgement : CatchJudgement + { + protected override int NumericResultFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 30; + } + } + + protected override float HealthIncreaseFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 7; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index bb2786f14f..51d7d3b5cd 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -2,11 +2,51 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Judgements { public class CatchJudgement : Judgement { - // todo: wangs + public override HitResult MaxResult => HitResult.Perfect; + + protected override int NumericResultFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 300; + } + } + + /// + /// The base health increase for the result achieved. + /// + public float HealthIncrease => HealthIncreaseFor(Result); + + /// + /// Whether fruit on the platter should explode or drop. + /// Note that this is only checked if the owning object is also + /// + public virtual bool ShouldExplode => IsHit; + + /// + /// Convert a to a base health increase. + /// + /// The value to convert. + /// The base health increase. + protected virtual float HealthIncreaseFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 10.2f; + } + } } } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs new file mode 100644 index 0000000000..8b77351027 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.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.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Judgements +{ + public class CatchTinyDropletJudgement : CatchJudgement + { + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 10; + } + } + + protected override float HealthIncreaseFor(HitResult result) + { + switch (result) + { + default: + return 0; + case HitResult.Perfect: + return 4; + } + } + } +} 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/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs new file mode 100644 index 0000000000..f7c60a7a47 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Catch.Objects +{ + public class Banana : Fruit + { + public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index a6aba42f03..25af7e4bdf 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,18 +30,12 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new Banana { Samples = Samples, - StartTime = i, - X = RNG.NextSingle() + StartTime = i }); } public double EndTime => StartTime + Duration; public double Duration { get; set; } - - public class Banana : Fruit - { - public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; - } } } 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/Drawable/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs new file mode 100644 index 0000000000..dd027abbe0 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.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.Game.Rulesets.Catch.Judgements; + +namespace osu.Game.Rulesets.Catch.Objects.Drawable +{ + public class DrawableBanana : DrawableFruit + { + public DrawableBanana(Banana h) + : base(h) + { + } + + protected override CatchJudgement CreateJudgement() => new CatchBananaJudgement(); + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs index 739cc6a59b..f039504600 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs @@ -5,9 +5,7 @@ using System; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -24,15 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable InternalChild = bananaContainer = new Container { RelativeSizeAxes = Axes.Both }; - foreach (var b in s.NestedHitObjects.Cast()) + foreach (var b in s.NestedHitObjects.Cast()) AddNested(getVisualRepresentation?.Invoke(b)); } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) - { - if (timeOffset >= 0) - AddJudgement(new Judgement { Result = NestedHitObjects.Cast().Any(n => n.Judgements.Any(j => j.IsHit)) ? HitResult.Perfect : HitResult.Miss }); - } + protected override bool ProvidesJudgement => false; protected override void AddNested(DrawableHitObject h) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index e3564b5967..6ce2e6a2ae 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -2,14 +2,14 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using OpenTK; +using OpenTK.Graphics; using osu.Framework.Graphics; -using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; -using OpenTK; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -58,9 +58,15 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable if (CheckPosition == null) return; if (timeOffset >= 0) - AddJudgement(new Judgement { Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss }); + { + var judgement = CreateJudgement(); + judgement.Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss; + AddJudgement(judgement); + } } + protected virtual CatchJudgement CreateJudgement() => new CatchJudgement(); + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs index 5c8a7c4a7c..11d5ed1f92 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using OpenTK; using OpenTK.Graphics; +using osu.Game.Rulesets.Catch.Judgements; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Masking = false; } + protected override CatchJudgement CreateJudgement() => new CatchDropletJudgement(); + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs new file mode 100644 index 0000000000..2232bb81a7 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.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 OpenTK; +using osu.Game.Rulesets.Catch.Judgements; + +namespace osu.Game.Rulesets.Catch.Objects.Drawable +{ + public class DrawableTinyDroplet : DrawableDroplet + { + public DrawableTinyDroplet(Droplet h) + : base(h) + { + Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 8; + } + + protected override CatchJudgement CreateJudgement() => new CatchTinyDropletJudgement(); + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index b2d8e3f8a5..0344189af5 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(); } @@ -78,6 +77,13 @@ namespace osu.Game.Rulesets.Catch.Objects double time = spanStartTime + timeProgress * spanDuration; + if (LegacyLastTickOffset != null) + { + // If we're the last tick, apply the legacy offset + if (span == this.SpanCount() - 1 && d + tickDistance > length) + time = Math.Max(StartTime + Duration / 2, time - LegacyLastTickOffset.Value); + } + double tinyTickInterval = time - lastDropletTime; while (tinyTickInterval > 100) tinyTickInterval /= 2; @@ -124,9 +130,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; @@ -156,5 +159,7 @@ namespace osu.Game.Rulesets.Catch.Objects get { return Curve.CurveType; } set { Curve.CurveType = value; } } + + public double? LegacyLastTickOffset { get; set; } } } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 936ab6a9d3..23b620248f 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Replays return; } - if (h is BananaShower.Banana) + if (h is Banana) { // auto bananas unrealistically warp to catch 100% combo. Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Replays { switch (nestedObj) { - case BananaShower.Banana _: + case Banana _: case TinyDroplet _: case Droplet _: case Fruit _: 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/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index ce1aee5c34..5b69d836a3 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -17,28 +19,57 @@ namespace osu.Game.Rulesets.Catch.Scoring { } + private float hpDrainRate; + protected override void SimulateAutoplay(Beatmap beatmap) { + hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; + foreach (var obj in beatmap.HitObjects) { switch (obj) { case JuiceStream stream: - foreach (var _ in stream.NestedHitObjects.Cast()) - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + foreach (var nestedObject in stream.NestedHitObjects) + switch (nestedObject) + { + case TinyDroplet _: + AddJudgement(new CatchTinyDropletJudgement { Result = HitResult.Perfect }); + break; + case Droplet _: + AddJudgement(new CatchDropletJudgement { Result = HitResult.Perfect }); + break; + case Fruit _: + AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + break; + } break; case BananaShower shower: foreach (var _ in shower.NestedHitObjects.Cast()) - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + AddJudgement(new CatchBananaJudgement { Result = HitResult.Perfect }); break; case Fruit _: AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); break; } } + } - base.SimulateAutoplay(beatmap); + private const double harshness = 0.01; + + protected override void OnNewJudgement(Judgement judgement) + { + base.OnNewJudgement(judgement); + + if (judgement.Result == HitResult.Miss) + { + if (!judgement.IsBonus) + Health.Value -= hpDrainRate * (harshness * 2); + return; + } + + if (judgement is CatchJudgement catchJudgement) + Health.Value += Math.Max(catchJudgement.HealthIncrease - hpDrainRate, 0) * harshness; } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 52763e09af..1ac052de4d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -38,14 +38,16 @@ namespace osu.Game.Rulesets.Catch.UI { switch (h) { + case Banana banana: + return new DrawableBanana(banana); case Fruit fruit: return new DrawableFruit(fruit); case JuiceStream stream: return new DrawableJuiceStream(stream, GetVisualRepresentation); - case BananaShower banana: - return new DrawableBananaShower(banana, GetVisualRepresentation); + case BananaShower shower: + return new DrawableBananaShower(shower, GetVisualRepresentation); case TinyDroplet tiny: - return new DrawableDroplet(tiny) { Scale = new Vector2(0.5f) }; + return new DrawableTinyDroplet(tiny); case Droplet droplet: return new DrawableDroplet(droplet); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index b62e9997d4..9c376f340a 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -11,11 +11,13 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.MathUtils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; 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; @@ -77,12 +79,11 @@ namespace osu.Game.Rulesets.Catch.UI if (!fruit.StaysOnPlate) runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); - } if (fruit.HitObject.LastInCombo) { - if (judgement.IsHit) + if (((CatchJudgement)judgement).ShouldExplode) runAfterLoaded(() => MovableCatcher.Explode()); else MovableCatcher.Drop(); @@ -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/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index 5ae899f6d6..73555dcecb 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -12,7 +12,8 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests { - internal class ManiaBeatmapConversionTest : BeatmapConversionTest + [TestFixture] + public class ManiaBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; @@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests protected override Ruleset CreateRuleset() => new ManiaRuleset(); } - internal struct ConvertValue : IEquatable + public struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. 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..de2bfaed9c --- /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)); + } + } + + 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)); + } + } + + 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 = { Value = 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..e827fa1fdc --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs @@ -0,0 +1,173 @@ +// 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.Configuration; +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) { 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) + { + 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 IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs>(new Bindable()); + return dependencies; + } + + protected override void Update() + { + base.Update(); + + foreach (var obj in content.OfType()) + { + if (!(obj.HitObject is IHasEndTime endTime)) + 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..8046c46fc1 --- /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)); + } + } + } + + 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)); + } + } + } + + 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/Judgements/HoldNoteJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs index 9630ba9273..9055e48a4c 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs @@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Mania.Judgements public class HoldNoteJudgement : ManiaJudgement { public override bool AffectsCombo => false; + protected override int NumericResultFor(HitResult result) => 0; } } 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..597450f223 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 { @@ -37,8 +38,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tickContainer; - public DrawableHoldNote(HoldNote hitObject, ManiaAction action) - : base(hitObject, action) + public DrawableHoldNote(HoldNote hitObject) + : base(hitObject) { RelativeSizeAxes = Axes.X; @@ -56,12 +57,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables HoldStartTime = () => holdStartTime }) }, - head = new DrawableHeadNote(this, action) + head = new DrawableHeadNote(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - tail = new DrawableTailNote(this, action) + tail = new DrawableTailNote(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre @@ -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; } @@ -110,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) return false; - if (action != Action) + if (action != Action.Value) return false; // The user has pressed during the body of the hold note, after the head note and its hit windows have passed @@ -127,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!holdStartTime.HasValue) return false; - if (action != Action) + if (action != Action.Value) return false; holdStartTime = null; @@ -146,8 +154,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { private readonly DrawableHoldNote holdNote; - public DrawableHeadNote(DrawableHoldNote holdNote, ManiaAction action) - : base(holdNote.HitObject.Head, action) + public DrawableHeadNote(DrawableHoldNote holdNote) + : base(holdNote.HitObject.Head) { this.holdNote = holdNote; } @@ -183,8 +191,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly DrawableHoldNote holdNote; - public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action) - : base(holdNote.HitObject.Tail, action) + public DrawableTailNote(DrawableHoldNote holdNote) + : base(holdNote.HitObject.Tail) { this.holdNote = holdNote; } @@ -227,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!holdNote.holdStartTime.HasValue) return false; - if (action != Action) + if (action != Action.Value) return false; UpdateJudgement(true); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index fbf1ee8460..cb6196a890 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -1,31 +1,55 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using JetBrains.Annotations; +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 { - public abstract class DrawableManiaHitObject : DrawableHitObject - where TObject : ManiaHitObject + public abstract class DrawableManiaHitObject : DrawableHitObject { /// - /// The key that will trigger input for this hit object. + /// The which causes this to be hit. /// - protected ManiaAction Action { get; } + protected readonly IBindable Action = new Bindable(); - public new TObject HitObject; + protected readonly IBindable Direction = new Bindable(); - protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null) + protected DrawableManiaHitObject(ManiaHitObject hitObject) : base(hitObject) { - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; - - HitObject = hitObject; + } + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] IBindable action, [NotNull] IScrollingInfo scrollingInfo) + { if (action != null) - Action = action.Value; + Action.BindTo(action); + + Direction.BindTo(scrollingInfo.Direction); + Direction.BindValueChanged(OnDirectionChanged, true); + } + + protected virtual void OnDirectionChanged(ScrollingDirection direction) + { + Anchor = Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + } + } + + public abstract class DrawableManiaHitObject : DrawableManiaHitObject + where TObject : ManiaHitObject + { + public new readonly TObject HitObject; + + protected DrawableManiaHitObject(TObject hitObject) + : base(hitObject) + { + HitObject = hitObject; } protected override void UpdateState(ArmedState state) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 3de0a9c5cc..18084c4c08 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 { @@ -19,8 +20,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { private readonly NotePiece headPiece; - public DrawableNote(Note hitObject, ManiaAction action) - : base(hitObject, action) + public DrawableNote(Note hitObject) + : base(hitObject) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -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 @@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public virtual bool OnPressed(ManiaAction action) { - if (action != Action) + if (action != Action.Value) return false; return UpdateJudgement(true); 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..a19a6fb5d4 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,50 +1,39 @@ // 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.Allocation; +using osu.Framework.Configuration; 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; + public readonly Bindable Action = new Bindable(); - private readonly Box background; - private readonly Box backgroundOverlay; - private readonly Container hitTargetBar; - private readonly Container keyIcon; + 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 +41,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 +63,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,25 +112,19 @@ 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; } } + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + dependencies.CacheAs>(Action); + return dependencies; + } + /// /// Adds a DrawableHitObject to this Playfield. /// @@ -228,48 +142,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..19cc8fffef --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs @@ -0,0 +1,108 @@ +// 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 + { + private readonly IBindable action = new Bindable(); + + private Box background; + private Box backgroundOverlay; + + private readonly IBindable direction = new Bindable(); + + [BackgroundDependencyLoader] + private void load(IBindable action, IScrollingInfo scrollingInfo) + { + this.action.BindTo(action); + + 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 == this.action.Value) + backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); + return false; + } + + public bool OnReleased(ManiaAction action) + { + if (action == this.action.Value) + 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..e30a033831 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs @@ -0,0 +1,123 @@ +// 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; + + private readonly IBindable action = new Bindable(); + private readonly IBindable direction = new Bindable(); + + private Container keyIcon; + + [BackgroundDependencyLoader] + private void load(IBindable action, IScrollingInfo scrollingInfo) + { + this.action.BindTo(action); + + 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 == this.action.Value) + keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint); + return false; + } + + public bool OnReleased(ManiaAction action) + { + if (action == this.action.Value) + 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..e6ebf43c67 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, @@ -84,21 +101,25 @@ namespace osu.Game.Rulesets.Mania.UI protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) { - ManiaAction action = Playfield.Columns.ElementAt(h.Column).Action; - - var holdNote = h as HoldNote; - if (holdNote != null) - return new DrawableHoldNote(holdNote, action); - - var note = h as Note; - if (note != null) - return new DrawableNote(note, action); - - return null; + switch (h) + { + case HoldNote holdNote: + return new DrawableHoldNote(holdNote); + case Note note: + return new DrawableNote(note); + default: + return null; + } } 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..4c7deb4567 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,23 +124,23 @@ 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++ + Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ } }; 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.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index 386ae5eb05..3fa039d946 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -8,17 +8,19 @@ using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Tests.Beatmaps; -using OpenTK; namespace osu.Game.Rulesets.Osu.Tests { - internal class OsuBeatmapConversionTest : BeatmapConversionTest + [TestFixture] + public class OsuBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; [TestCase("basic")] [TestCase("colinear-perfect-curve")] + [TestCase("slider-ticks")] public new void Test(string name) { base.Test(name); @@ -26,24 +28,30 @@ namespace osu.Game.Rulesets.Osu.Tests protected override IEnumerable CreateConvertValue(HitObject hitObject) { - var startPosition = (hitObject as IHasPosition)?.Position ?? new Vector2(256, 192); - var endPosition = (hitObject as Slider)?.EndPosition ?? startPosition; - - yield return new ConvertValue + switch (hitObject) { - StartTime = hitObject.StartTime, - EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, - StartX = startPosition.X, - StartY = startPosition.Y, - EndX = endPosition.X, - EndY = endPosition.Y + case Slider slider: + foreach (var nested in slider.NestedHitObjects) + yield return createConvertValue(nested); + break; + default: + yield return createConvertValue(hitObject); + break; + } + + ConvertValue createConvertValue(HitObject obj) => new ConvertValue + { + StartTime = obj.StartTime, + EndTime = (obj as IHasEndTime)?.EndTime ?? obj.StartTime, + X = (obj as IHasPosition)?.X ?? OsuPlayfield.BASE_SIZE.X / 2, + Y = (obj as IHasPosition)?.Y ?? OsuPlayfield.BASE_SIZE.Y / 2, }; } protected override Ruleset CreateRuleset() => new OsuRuleset(); } - internal struct ConvertValue : IEquatable + public struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. @@ -52,17 +60,13 @@ namespace osu.Game.Rulesets.Osu.Tests public double StartTime; public double EndTime; - public float StartX; - public float StartY; - public float EndX; - public float EndY; + public float X; + public float Y; public bool Equals(ConvertValue other) - => Precision.AlmostEquals(StartTime, other.StartTime) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) - && Precision.AlmostEquals(StartX, other.StartX) - && Precision.AlmostEquals(StartY, other.StartY, conversion_lenience) - && Precision.AlmostEquals(EndX, other.EndX, conversion_lenience) - && Precision.AlmostEquals(EndY, other.EndY, conversion_lenience); + && Precision.AlmostEquals(X, other.X, conversion_lenience) + && Precision.AlmostEquals(Y, other.Y, conversion_lenience); } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 80eb808f6e..405493cde4 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps var endTimeData = original as IHasEndTime; var positionData = original as IHasPosition; var comboData = original as IHasCombo; + var legacyOffset = original as IHasLegacyLastTickOffset; if (curveData != null) { @@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps RepeatSamples = curveData.RepeatSamples, RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false + NewCombo = comboData?.NewCombo ?? false, + LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset }; } else if (endTimeData != null) diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index c7c9f4a01a..bbe2d67baa 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -15,10 +15,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { } - public override void PostProcess() + public override void PreProcess() { + base.PreProcess(); applyStacking((Beatmap)Beatmap); - base.PostProcess(); } private void applyStacking(Beatmap beatmap) @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps continue; double endTime = (stackBaseObject as IHasEndTime)?.EndTime ?? stackBaseObject.StartTime; - double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo?.StackLeniency ?? 0.7f; + double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency; if (objectN.StartTime - endTime > stackThreshold) //We are no longer within stacking range of the next object. @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps OsuHitObject objectI = beatmap.HitObjects[i]; if (objectI.StackHeight != 0 || objectI is Spinner) continue; - double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo?.StackLeniency ?? 0.7f; + double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency; /* If this object is a hitcircle, then we enter this "special" case. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. 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/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index b7c4470592..26becfdec9 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.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 OpenTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -12,11 +11,6 @@ namespace osu.Game.Rulesets.Osu.Judgements { public override HitResult MaxResult => HitResult.Great; - /// - /// The positional hit offset. - /// - public Vector2 PositionOffset; - protected override int NumericResultFor(HitResult result) { switch (result) diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs index c4e265aac9..d52de9f971 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs @@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Osu.Judgements public class OsuSliderTailJudgement : OsuJudgement { public override bool AffectsCombo => false; + protected override int NumericResultFor(HitResult result) => 0; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 9fe6dcd46c..421c93d485 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AddJudgement(new OsuJudgement { Result = result, - PositionOffset = Vector2.Zero //todo: set to correct value }); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index dfab123038..f1907a92a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -93,6 +93,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.AccentColour = value; Body.AccentColour = AccentColour; Ball.AccentColour = AccentColour; + + foreach (var drawableHitObject in NestedHitObjects) + drawableHitObject.AccentColour = AccentColour; } } @@ -133,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (!userTriggered && Time.Current >= slider.EndTime) { - var judgementsCount = NestedHitObjects.Count; + var judgementsCount = NestedHitObjects.Count(); var judgementsHit = NestedHitObjects.Count(h => h.IsHit); var hitFraction = (double)judgementsHit / judgementsCount; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 54126b934f..befbc01f3c 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Osu.Objects public virtual bool NewCombo { get; set; } - public int IndexInCurrentCombo { get; set; } + public virtual int IndexInCurrentCombo { get; set; } - public int ComboIndex { get; set; } + public virtual int ComboIndex { get; set; } public bool LastInCombo { get; set; } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 7f4407370f..698f9de787 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using OpenTK; using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; @@ -25,6 +26,28 @@ namespace osu.Game.Rulesets.Osu.Objects public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); public override Vector2 EndPosition => Position + this.CurvePositionAt(1); + public override int ComboIndex + { + get => base.ComboIndex; + set + { + base.ComboIndex = value; + foreach (var n in NestedHitObjects.OfType()) + n.ComboIndex = value; + } + } + + public override int IndexInCurrentCombo + { + get => base.IndexInCurrentCombo; + set + { + base.IndexInCurrentCombo = value; + foreach (var n in NestedHitObjects.OfType()) + n.IndexInCurrentCombo = value; + } + } + public SliderCurve Curve { get; } = new SliderCurve(); public List ControlPoints @@ -45,6 +68,8 @@ namespace osu.Game.Rulesets.Osu.Objects set { Curve.Distance = value; } } + public double? LegacyLastTickOffset { get; set; } + /// /// The position of the cursor at the point of completion of this if it was hit /// with as few movements as possible. This is set and used by difficulty calculation. @@ -91,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Objects createSliderEnds(); createTicks(); createRepeatPoints(); + + if (LegacyLastTickOffset != null) + TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value); } private void createSliderEnds() @@ -141,7 +169,8 @@ namespace osu.Game.Rulesets.Osu.Objects var distanceProgress = d / length; var timeProgress = reversed ? 1 - distanceProgress : distanceProgress; - var firstSample = Samples.FirstOrDefault(s => s.Name == SampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) + var firstSample = Samples.FirstOrDefault(s => s.Name == SampleInfo.HIT_NORMAL) + ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) var sampleList = new List(); if (firstSample != null) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 324d0502c1..bb0bd891b3 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -6,6 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using System; using System.Collections.Generic; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Users; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Replays /// /// Constants (for spinners). /// - protected static readonly Vector2 SPINNER_CENTRE = new Vector2(256, 192); + protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; protected const float SPIN_RADIUS = 50; /// 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.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json index b82fddbe79..491adc2b50 100644 --- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -1,124 +1,256 @@ { "Mappings": [{ - "StartTime": 500, - "Objects": [{ - "StartTime": 500, - "EndTime": 2500, - "StartX": 96, - "StartY": 192, - "EndX": 96, - "EndY": 192 - }] - }, - { - "StartTime": 3000, - "Objects": [{ - "StartTime": 3000, - "EndTime": 4000, - "StartX": 256, - "StartY": 192, - "EndX": 256, - "EndY": 192 - }] - }, - { - "StartTime": 4500, - "Objects": [{ - "StartTime": 4500, - "EndTime": 5500, - "StartX": 256, - "StartY": 192, - "EndX": 256, - "EndY": 192 - }] - }, - { - "StartTime": 6000, - "Objects": [{ - "StartTime": 6000, - "EndTime": 6500, - "StartX": 256, - "StartY": 192, - "EndX": 256, - "EndY": 192 - }] - }, - { - "StartTime": 7000, - "Objects": [{ - "StartTime": 7000, - "EndTime": 8000, - "StartX": 256, - "StartY": 128, - "EndX": 256, - "EndY": 128 - }] - }, - { - "StartTime": 8500, - "Objects": [{ - "StartTime": 8500, - "EndTime": 10999, - "StartX": 32, - "StartY": 192, - "EndX": 508.166229, - "EndY": 153.299271 - }] - }, - { - "StartTime": 11500, - "Objects": [{ - "StartTime": 11500, - "EndTime": 12000, - "StartX": 256, - "StartY": 192, - "EndX": 256, - "EndY": 192 - }] - }, - { - "StartTime": 12500, - "Objects": [{ - "StartTime": 12500, - "EndTime": 16500, - "StartX": 512, - "StartY": 320, - "EndX": 291.1977, - "EndY": 40.799427 - }] - }, - { - "StartTime": 17000, - "Objects": [{ - "StartTime": 17000, - "EndTime": 18000, - "StartX": 256, - "StartY": 256, - "EndX": 256, - "EndY": 256 - }] - }, - { - "StartTime": 18500, - "Objects": [{ - "StartTime": 18500, - "EndTime": 19450, - "StartX": 256, - "StartY": 192, - "EndX": 256, - "EndY": 192 - }] - }, - { - "StartTime": 19875, - "Objects": [{ - "StartTime": 19875, - "EndTime": 23874, - "StartX": 216, - "StartY": 231, - "EndX": 408.720825, - "EndY": 339.810455 - }] - } - ] + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "EndTime": 500.0, + "X": 96.0, + "Y": 192.0 + }, { + "StartTime": 1000.0, + "EndTime": 1000.0, + "X": 256.0, + "Y": 192.0 + }, { + "StartTime": 1500.0, + "EndTime": 1500.0, + "X": 416.0, + "Y": 192.0 + }, { + "StartTime": 2000.0, + "EndTime": 2000.0, + "X": 256.0, + "Y": 192.0 + }, { + "StartTime": 2464.0, + "EndTime": 2464.0, + "X": 96.0, + "Y": 192.0 + }] + }, { + "StartTime": 3000.0, + "Objects": [{ + "StartTime": 3000.0, + "EndTime": 4000.0, + "X": 256.0, + "Y": 192.0 + }] + }, { + "StartTime": 4500.0, + "Objects": [{ + "StartTime": 4500.0, + "EndTime": 5500.0, + "X": 256.0, + "Y": 192.0 + }] + }, { + "StartTime": 6000.0, + "Objects": [{ + "StartTime": 6000.0, + "EndTime": 6500.0, + "X": 256.0, + "Y": 192.0 + }] + }, { + "StartTime": 7000.0, + "Objects": [{ + "StartTime": 7000.0, + "EndTime": 7000.0, + "X": 256.0, + "Y": 128.0 + }, { + "StartTime": 7250.0, + "EndTime": 7250.0, + "X": 336.0, + "Y": 128.0 + }, { + "StartTime": 7500.0, + "EndTime": 7500.0, + "X": 256.0, + "Y": 128.0 + }, { + "StartTime": 7750.0, + "EndTime": 7750.0, + "X": 336.0, + "Y": 128.0 + }, { + "StartTime": 7964.0, + "EndTime": 7964.0, + "X": 256.0, + "Y": 128.0 + }] + }, { + "StartTime": 8500.0, + "Objects": [{ + "StartTime": 8500.0, + "EndTime": 8500.0, + "X": 32.0, + "Y": 192.0 + }, { + "StartTime": 9000.0, + "EndTime": 9000.0, + "X": 101.81015, + "Y": 326.4915 + }, { + "StartTime": 9500.0, + "EndTime": 9500.0, + "X": 237.2304, + "Y": 276.282928 + }, { + "StartTime": 10000.0, + "EndTime": 10000.0, + "X": 270.339874, + "Y": 121.1423 + }, { + "StartTime": 10500.0, + "EndTime": 10500.0, + "X": 401.0588, + "Y": 49.1515045 + }, { + "StartTime": 10964.0, + "EndTime": 10964.0, + "X": 508.166229, + "Y": 153.299271 + }] + }, { + "StartTime": 11500.0, + "Objects": [{ + "StartTime": 11500.0, + "EndTime": 12000.0, + "X": 256.0, + "Y": 192.0 + }] + }, { + "StartTime": 12500.0, + "Objects": [{ + "StartTime": 12500.0, + "EndTime": 12500.0, + "X": 512.0, + "Y": 320.0 + }, { + "StartTime": 13000.0, + "EndTime": 13000.0, + "X": 353.235535, + "Y": 300.154449 + }, { + "StartTime": 13500.0, + "EndTime": 13500.0, + "X": 194.471069, + "Y": 280.3089 + }, { + "StartTime": 14000.0, + "EndTime": 14000.0, + "X": 35.7066345, + "Y": 260.463318 + }, { + "StartTime": 14500.0, + "EndTime": 14500.0, + "X": 118.370323, + "Y": 219.009277 + }, { + "StartTime": 15000.0, + "EndTime": 15000.0, + "X": 271.087128, + "Y": 171.285278 + }, { + "StartTime": 15500.0, + "EndTime": 15500.0, + "X": 423.803925, + "Y": 123.561279 + }, { + "StartTime": 16000.0, + "EndTime": 16000.0, + "X": 446.420532, + "Y": 79.60513 + }, { + "StartTime": 16464.0, + "EndTime": 16464.0, + "X": 291.1977, + "Y": 40.799427 + }] + }, { + "StartTime": 17000.0, + "Objects": [{ + "StartTime": 17000.0, + "EndTime": 17000.0, + "X": 256.0, + "Y": 256.0 + }, { + "StartTime": 17250.0, + "EndTime": 17250.0, + "X": 176.0, + "Y": 256.0 + }, { + "StartTime": 17500.0, + "EndTime": 17500.0, + "X": 256.0, + "Y": 256.0 + }, { + "StartTime": 17750.0, + "EndTime": 17750.0, + "X": 176.0, + "Y": 256.0 + }, { + "StartTime": 17964.0, + "EndTime": 17964.0, + "X": 256.0, + "Y": 256.0 + }] + }, { + "StartTime": 18500.0, + "Objects": [{ + "StartTime": 18500.0, + "EndTime": 19450.0, + "X": 256.0, + "Y": 192.0 + }] + }, { + "StartTime": 19875.0, + "Objects": [{ + "StartTime": 19875.0, + "EndTime": 19875.0, + "X": 216.0, + "Y": 231.0 + }, { + "StartTime": 20375.0, + "EndTime": 20375.0, + "X": 317.446747, + "Y": 171.345245 + }, { + "StartTime": 20875.0, + "EndTime": 20875.0, + "X": 270.3294, + "Y": 310.4395 + }, { + "StartTime": 21375.0, + "EndTime": 21375.0, + "X": 119.121056, + "Y": 322.8657 + }, { + "StartTime": 21875.0, + "EndTime": 21875.0, + "X": 124.28746, + "Y": 165.224731 + }, { + "StartTime": 22375.0, + "EndTime": 22375.0, + "X": 240.4715, + "Y": 62.65587 + }, { + "StartTime": 22875.0, + "EndTime": 22875.0, + "X": 398.054047, + "Y": 39.064167 + }, { + "StartTime": 23375.0, + "EndTime": 23375.0, + "X": 439.749878, + "Y": 183.668091 + }, { + "StartTime": 23839.0, + "EndTime": 23839.0, + "X": 408.720825, + "Y": 339.810455 + }] + }] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json index 7fe038658c..96e4bf1637 100644 --- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json @@ -1,13 +1,16 @@ { - "Mappings": [{ - "StartTime": 118858, - "Objects": [{ - "StartTime": 118858, - "EndTime": 119088, - "StartX": 219, - "StartY": 215, - "EndX": 239.6507, - "EndY": 29.1437378 + "Mappings": [{ + "StartTime": 118858.0, + "Objects": [{ + "StartTime": 118858.0, + "EndTime": 118858.0, + "X": 219.0, + "Y": 215.0 + }, { + "StartTime": 119052.0, + "EndTime": 119052.0, + "X": 239.6507, + "Y": 29.1437378 + }] }] - }] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-expected-conversion.json new file mode 100644 index 0000000000..9c049c7cd6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-expected-conversion.json @@ -0,0 +1,331 @@ +{ + "Mappings": [{ + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "EndTime": 500.0, + "X": 96.0, + "Y": 192.0 + }, { + "StartTime": 624.0, + "EndTime": 624.0, + "X": 105.921242, + "Y": 192.0 + }, { + "StartTime": 749.0, + "EndTime": 749.0, + "X": 115.922493, + "Y": 192.0 + }, { + "StartTime": 874.0, + "EndTime": 874.0, + "X": 125.923737, + "Y": 192.0 + }, { + "StartTime": 999.0, + "EndTime": 999.0, + "X": 135.924988, + "Y": 192.0 + }, { + "StartTime": 1124.0, + "EndTime": 1124.0, + "X": 145.926239, + "Y": 192.0 + }, { + "StartTime": 1249.0, + "EndTime": 1249.0, + "X": 155.92749, + "Y": 192.0 + }, { + "StartTime": 1374.0, + "EndTime": 1374.0, + "X": 165.928741, + "Y": 192.0 + }, { + "StartTime": 1499.0, + "EndTime": 1499.0, + "X": 175.93, + "Y": 192.0 + }, { + "StartTime": 1624.0, + "EndTime": 1624.0, + "X": 185.931244, + "Y": 192.0 + }, { + "StartTime": 1749.0, + "EndTime": 1749.0, + "X": 195.9325, + "Y": 192.0 + }, { + "StartTime": 1874.0, + "EndTime": 1874.0, + "X": 205.933746, + "Y": 192.0 + }, { + "StartTime": 1999.0, + "EndTime": 1999.0, + "X": 215.935, + "Y": 192.0 + }, { + "StartTime": 2124.0, + "EndTime": 2124.0, + "X": 225.936234, + "Y": 192.0 + }, { + "StartTime": 2249.0, + "EndTime": 2249.0, + "X": 235.9375, + "Y": 192.0 + }, { + "StartTime": 2374.0, + "EndTime": 2374.0, + "X": 245.938751, + "Y": 192.0 + }, { + "StartTime": 2499.0, + "EndTime": 2499.0, + "X": 255.94, + "Y": 192.0 + }, { + "StartTime": 2624.0, + "EndTime": 2624.0, + "X": 265.941223, + "Y": 192.0 + }, { + "StartTime": 2749.0, + "EndTime": 2749.0, + "X": 275.9425, + "Y": 192.0 + }, { + "StartTime": 2874.0, + "EndTime": 2874.0, + "X": 285.943756, + "Y": 192.0 + }, { + "StartTime": 2999.0, + "EndTime": 2999.0, + "X": 295.945, + "Y": 192.0 + }, { + "StartTime": 3124.0, + "EndTime": 3124.0, + "X": 305.946259, + "Y": 192.0 + }, { + "StartTime": 3249.0, + "EndTime": 3249.0, + "X": 315.9475, + "Y": 192.0 + }, { + "StartTime": 3374.0, + "EndTime": 3374.0, + "X": 325.94873, + "Y": 192.0 + }, { + "StartTime": 3499.0, + "EndTime": 3499.0, + "X": 335.949982, + "Y": 192.0 + }, { + "StartTime": 3624.0, + "EndTime": 3624.0, + "X": 345.951233, + "Y": 192.0 + }, { + "StartTime": 3749.0, + "EndTime": 3749.0, + "X": 355.952484, + "Y": 192.0 + }, { + "StartTime": 3874.0, + "EndTime": 3874.0, + "X": 365.953766, + "Y": 192.0 + }, { + "StartTime": 3999.0, + "EndTime": 3999.0, + "X": 375.955, + "Y": 192.0 + }, { + "StartTime": 4124.0, + "EndTime": 4124.0, + "X": 385.956238, + "Y": 192.0 + }, { + "StartTime": 4249.0, + "EndTime": 4249.0, + "X": 395.9575, + "Y": 192.0 + }, { + "StartTime": 4374.0, + "EndTime": 4374.0, + "X": 405.95874, + "Y": 192.0 + }, { + "StartTime": 4499.0, + "EndTime": 4499.0, + "X": 415.960022, + "Y": 192.0 + }, { + "StartTime": 4624.0, + "EndTime": 4624.0, + "X": 406.038757, + "Y": 192.0 + }, { + "StartTime": 4749.0, + "EndTime": 4749.0, + "X": 396.0375, + "Y": 192.0 + }, { + "StartTime": 4874.0, + "EndTime": 4874.0, + "X": 386.036255, + "Y": 192.0 + }, { + "StartTime": 4999.0, + "EndTime": 4999.0, + "X": 376.035034, + "Y": 192.0 + }, { + "StartTime": 5124.0, + "EndTime": 5124.0, + "X": 366.033752, + "Y": 192.0 + }, { + "StartTime": 5249.0, + "EndTime": 5249.0, + "X": 356.0325, + "Y": 192.0 + }, { + "StartTime": 5374.0, + "EndTime": 5374.0, + "X": 346.03125, + "Y": 192.0 + }, { + "StartTime": 5499.0, + "EndTime": 5499.0, + "X": 336.030029, + "Y": 192.0 + }, { + "StartTime": 5624.0, + "EndTime": 5624.0, + "X": 326.028748, + "Y": 192.0 + }, { + "StartTime": 5749.0, + "EndTime": 5749.0, + "X": 316.0275, + "Y": 192.0 + }, { + "StartTime": 5874.0, + "EndTime": 5874.0, + "X": 306.026245, + "Y": 192.0 + }, { + "StartTime": 5999.0, + "EndTime": 5999.0, + "X": 296.025, + "Y": 192.0 + }, { + "StartTime": 6124.0, + "EndTime": 6124.0, + "X": 286.023773, + "Y": 192.0 + }, { + "StartTime": 6249.0, + "EndTime": 6249.0, + "X": 276.022522, + "Y": 192.0 + }, { + "StartTime": 6374.0, + "EndTime": 6374.0, + "X": 266.02124, + "Y": 192.0 + }, { + "StartTime": 6499.0, + "EndTime": 6499.0, + "X": 256.02, + "Y": 192.0 + }, { + "StartTime": 6624.0, + "EndTime": 6624.0, + "X": 246.018768, + "Y": 192.0 + }, { + "StartTime": 6749.0, + "EndTime": 6749.0, + "X": 236.017517, + "Y": 192.0 + }, { + "StartTime": 6874.0, + "EndTime": 6874.0, + "X": 226.016251, + "Y": 192.0 + }, { + "StartTime": 6999.0, + "EndTime": 6999.0, + "X": 216.014984, + "Y": 192.0 + }, { + "StartTime": 7124.0, + "EndTime": 7124.0, + "X": 206.013733, + "Y": 192.0 + }, { + "StartTime": 7249.0, + "EndTime": 7249.0, + "X": 196.012512, + "Y": 192.0 + }, { + "StartTime": 7374.0, + "EndTime": 7374.0, + "X": 186.011261, + "Y": 192.0 + }, { + "StartTime": 7499.0, + "EndTime": 7499.0, + "X": 176.01, + "Y": 192.0 + }, { + "StartTime": 7624.0, + "EndTime": 7624.0, + "X": 166.008728, + "Y": 192.0 + }, { + "StartTime": 7749.0, + "EndTime": 7749.0, + "X": 156.0075, + "Y": 192.0 + }, { + "StartTime": 7874.0, + "EndTime": 7874.0, + "X": 146.006256, + "Y": 192.0 + }, { + "StartTime": 7999.0, + "EndTime": 7999.0, + "X": 136.005, + "Y": 192.0 + }, { + "StartTime": 8124.0, + "EndTime": 8124.0, + "X": 126.003738, + "Y": 192.0 + }, { + "StartTime": 8249.0, + "EndTime": 8249.0, + "X": 116.002518, + "Y": 192.0 + }, { + "StartTime": 8374.0, + "EndTime": 8374.0, + "X": 106.001259, + "Y": 192.0 + }, { + "StartTime": 8463.0, + "EndTime": 8463.0, + "X": 96.0, + "Y": 192.0 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks.osu new file mode 100644 index 0000000000..fd835efbc8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks.osu @@ -0,0 +1,20 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:0.400000005960464 +SliderTickRate:4 + +[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] +96,192,500,6,0,L|416:192,2,320.000004768372 diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index f2d5631e93..04724931ae 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -11,7 +11,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using System.Linq; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.UI { @@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.UI DrawableOsuJudgement explosion = new DrawableOsuJudgement(judgement, judgedObject) { Origin = Anchor.Centre, - Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition + ((OsuJudgement)judgement).PositionOffset + Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition }; judgementLayer.Add(explosion); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 11586e340b..db8480c742 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -12,7 +12,8 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Taiko.Tests { - internal class TaikoBeatmapConversionTest : BeatmapConversionTest + [TestFixture] + public class TaikoBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; @@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override Ruleset CreateRuleset() => new TaikoRuleset(); } - internal struct ConvertValue : IEquatable + public struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. 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/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 7b4978694b..609fd27bb4 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -27,14 +27,12 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { + new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), + new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), new KeyBinding(InputKey.D, TaikoAction.LeftRim), new KeyBinding(InputKey.F, TaikoAction.LeftCentre), new KeyBinding(InputKey.J, TaikoAction.RightCentre), new KeyBinding(InputKey.K, TaikoAction.RightRim), - new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), - new KeyBinding(InputKey.MouseLeft, TaikoAction.RightCentre), - new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), - new KeyBinding(InputKey.MouseRight, TaikoAction.RightRim), }; public override IEnumerable ConvertLegacyMods(LegacyMods mods) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 313c205981..e04b4d45f6 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI private void loadBarLines() { TaikoHitObject lastObject = Beatmap.HitObjects[Beatmap.HitObjects.Count - 1]; - double lastHitTime = 1 + (lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime; + double lastHitTime = 1 + ((lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime); var timingPoints = Beatmap.ControlPointInfo.TimingPoints.ToList(); @@ -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/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index f52fecfd02..ee66f53ddc 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -65,17 +65,19 @@ namespace osu.Game.Tests.Visual foreach (var rulesetInfo in rulesets.AvailableRulesets) { - var ruleset = rulesetInfo.CreateInstance(); + var instance = rulesetInfo.CreateInstance(); var testBeatmap = createTestBeatmap(rulesetInfo); beatmaps.Add(testBeatmap); + AddStep("set ruleset", () => Ruleset.Value = rulesetInfo); + selectBeatmap(testBeatmap); - testBeatmapLabels(ruleset); + testBeatmapLabels(instance); // TODO: adjust cases once more info is shown for other gamemodes - switch (ruleset) + switch (instance) { case OsuRuleset _: testInfoLabels(5); 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/TestCaseLeaderboard.cs b/osu.Game.Tests/Visual/TestCaseLeaderboard.cs index 7f5bce3b84..e8ac19e7fc 100644 --- a/osu.Game.Tests/Visual/TestCaseLeaderboard.cs +++ b/osu.Game.Tests/Visual/TestCaseLeaderboard.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Graphics; using osu.Game.Rulesets.Scoring; @@ -17,6 +19,12 @@ namespace osu.Game.Tests.Visual [Description("PlaySongSelect leaderboard")] public class TestCaseLeaderboard : OsuTestCase { + public override IReadOnlyList RequiredTypes => new[] { + typeof(Placeholder), + typeof(MessagePlaceholder), + typeof(RetrievalFailurePlaceholder), + }; + private RulesetStore rulesets; private readonly FailableLeaderboard leaderboard; diff --git a/osu.Game.Tests/Visual/TestCaseLoadingAnimation.cs b/osu.Game.Tests/Visual/TestCaseLoadingAnimation.cs new file mode 100644 index 0000000000..ebbc673d36 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseLoadingAnimation.cs @@ -0,0 +1,63 @@ +// 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.Shapes; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseLoadingAnimation : GridTestCase + { + public TestCaseLoadingAnimation() + : base(2, 2) + { + LoadingAnimation loading; + + Cell(0).AddRange(new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both + }, + loading = new LoadingAnimation() + }); + + loading.Show(); + + Cell(1).AddRange(new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both + }, + loading = new LoadingAnimation() + }); + + loading.Show(); + + Cell(2).AddRange(new Drawable[] + { + new Box + { + Colour = Color4.Gray, + RelativeSizeAxes = Axes.Both + }, + loading = new LoadingAnimation() + }); + + loading.Show(); + + Cell(3).AddRange(new Drawable[] + { + loading = new LoadingAnimation() + }); + + Scheduler.AddDelayed(() => loading.ToggleVisibility(), 200, true); + } + } +} 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/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs index 3255478bea..73c37348d5 100644 --- a/osu.Game.Tests/Visual/TestCaseMods.cs +++ b/osu.Game.Tests/Visual/TestCaseMods.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual foreach (var rulesetInfo in rulesets.AvailableRulesets) { Ruleset ruleset = rulesetInfo.CreateInstance(); - AddStep($"switch to {ruleset.Description}", () => modSelect.Ruleset.Value = rulesetInfo); + AddStep($"switch to {ruleset.Description}", () => Ruleset.Value = rulesetInfo); switch (ruleset) { diff --git a/osu.Game.Tests/Visual/TestCaseParallaxContainer.cs b/osu.Game.Tests/Visual/TestCaseParallaxContainer.cs new file mode 100644 index 0000000000..8c12589c6f --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseParallaxContainer.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.Graphics.Containers; +using osu.Game.Screens.Backgrounds; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseParallaxContainer : OsuTestCase + { + public TestCaseParallaxContainer() + { + ParallaxContainer parallax; + + Add(parallax = new ParallaxContainer + { + Child = new BackgroundScreenDefault { Alpha = 0.8f } + }); + + AddStep("default parallax", () => parallax.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT); + AddStep("high parallax", () => parallax.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * 10); + AddStep("no parallax", () => parallax.ParallaxAmount = 0); + AddStep("negative parallax", () => parallax.ParallaxAmount = -ParallaxContainer.DEFAULT_PARALLAX_AMOUNT); + } + } +} 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/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3afc3c4d32..303a19aab3 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -66,8 +66,8 @@ namespace osu.Game.Beatmaps // General public int AudioLeadIn { get; set; } - public bool Countdown { get; set; } - public float StackLeniency { get; set; } + public bool Countdown { get; set; } = true; + public float StackLeniency { get; set; } = 0.7f; public bool SpecialStyle { get; set; } public int RulesetID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 5e3b66646b..e488dacf80 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -138,7 +138,7 @@ namespace osu.Game.Beatmaps PostNotification?.Invoke(new SimpleNotification { Icon = FontAwesome.fa_superpowers, - Text = "You gotta be a supporter to download for now 'yo" + Text = "You gotta be an osu!supporter to download for now 'yo" }); return; } @@ -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/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 71406c6034..43ae30f780 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -69,11 +69,22 @@ namespace osu.Game.Beatmaps } catch { - return new TrackVirtual(); + return null; } } - protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); + protected override Waveform GetWaveform() + { + try + { + var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new Waveform(trackData); + } + catch + { + return null; + } + } protected override Storyboard GetStoryboard() { diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index bf1cd7d4ee..0173125e8b 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -6,23 +6,9 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps { - public interface IBeatmapProcessor - { - IBeatmap Beatmap { get; } - - /// - /// Post-processes to add mode-specific components that aren't added during conversion. - /// - /// An example of such a usage is for combo colours. - /// - /// - void PostProcess(); - } - /// - /// Processes a post-converted Beatmap. + /// Provides functionality to alter a after it has been converted. /// - /// The type of HitObject contained in the Beatmap. public class BeatmapProcessor : IBeatmapProcessor { public IBeatmap Beatmap { get; } @@ -32,13 +18,7 @@ namespace osu.Game.Beatmaps Beatmap = beatmap; } - /// - /// Post-processes a Beatmap to add mode-specific components that aren't added during conversion. - /// - /// An example of such a usage is for combo colours. - /// - /// - public virtual void PostProcess() + public virtual void PreProcess() { IHasComboInformation lastObj = null; @@ -62,5 +42,9 @@ namespace osu.Game.Beatmaps lastObj = obj; } } + + public virtual void PostProcess() + { + } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index ed8fbdbb26..ebebe42097 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -13,7 +13,13 @@ namespace osu.Game.Beatmaps [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } - public int? OnlineBeatmapSetID { get; set; } + private int? onlineBeatmapSetID; + + public int? OnlineBeatmapSetID + { + get { return onlineBeatmapSetID; } + set { onlineBeatmapSetID = value > 0 ? value : null; } + } public BeatmapMetadata Metadata { get; set; } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 265c6832b2..25a76b52a7 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -45,7 +45,7 @@ namespace osu.Game.Beatmaps protected override Texture GetBackground() => game?.Textures.Get(@"Backgrounds/bg4"); - protected override Track GetTrack() => new TrackVirtual(); + protected override Track GetTrack() => new TrackVirtual { Length = 1000 }; private class DummyRulesetInfo : RulesetInfo { diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index 00566093b8..cbf9d184ac 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -7,6 +7,9 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Beatmaps { + /// + /// Provides functionality to convert a for a . + /// public interface IBeatmapConverter { /// diff --git a/osu.Game/Beatmaps/IBeatmapProcessor.cs b/osu.Game/Beatmaps/IBeatmapProcessor.cs new file mode 100644 index 0000000000..282662373a --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapProcessor.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Beatmaps +{ + /// + /// Provides functionality to alter a after it has been converted. + /// + public interface IBeatmapProcessor + { + /// + /// The to process. This should already be converted to the applicable . + /// + IBeatmap Beatmap { get; } + + /// + /// Processes the converted prior to being invoked. + /// + /// Nested s generated during will not be present by this point, + /// and no mods will have been applied to the s. + /// + /// + /// + /// This can only be used to add alterations to s generated directly through the conversion process. + /// + void PreProcess(); + + /// + /// Processes the converted after has been invoked. + /// + /// Nested s generated during will be present by this point, + /// and mods will have been applied to all s. + /// + /// + /// + /// This should be used to add alterations to s while they are in their most playable state. + /// + void PostProcess(); + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 4310d9b7df..74da978d9c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -19,7 +19,7 @@ using osu.Game.Skinning; namespace osu.Game.Beatmaps { - public abstract class WorkingBeatmap : IDisposable + public abstract partial class WorkingBeatmap : IDisposable { public readonly BeatmapInfo BeatmapInfo; @@ -116,8 +116,9 @@ namespace osu.Game.Beatmaps mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); } - // Post-process - rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess(); + IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); + + processor?.PreProcess(); // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed foreach (var obj in converted.HitObjects) @@ -127,6 +128,8 @@ namespace osu.Game.Beatmaps foreach (var obj in converted.HitObjects) mod.ApplyToHitObject(obj); + processor?.PostProcess(); + return converted; } @@ -145,7 +148,7 @@ namespace osu.Game.Beatmaps private Track populateTrack() { // we want to ensure that we always have a track, even if it's a fake one. - var t = GetTrack() ?? new TrackVirtual(); + var t = GetTrack() ?? new VirtualBeatmapTrack(Beatmap); applyRateAdjustments(t); return t; } diff --git a/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs b/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs new file mode 100644 index 0000000000..0e0a9a3bb9 --- /dev/null +++ b/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs @@ -0,0 +1,38 @@ +// 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.Audio.Track; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Beatmaps +{ + public partial class WorkingBeatmap + { + /// + /// A type of which provides a valid length based on the s of an . + /// + protected class VirtualBeatmapTrack : TrackVirtual + { + private const double excess_length = 1000; + + public VirtualBeatmapTrack(IBeatmap beatmap) + { + var lastObject = beatmap.HitObjects.LastOrDefault(); + + switch (lastObject) + { + case null: + Length = excess_length; + break; + case IHasEndTime endTime: + Length = endTime.EndTime + excess_length; + break; + default: + Length = lastObject.StartTime + excess_length; + break; + } + } + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 0528f7b3ae..39369350ef 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -8,20 +8,36 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using OpenTK; using osu.Framework.Configuration; +using osu.Framework.Input.Bindings; +using osu.Game.Audio; +using osu.Game.Input.Bindings; using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { - public class OsuFocusedOverlayContainer : FocusedOverlayContainer + public class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler { 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); @@ -51,20 +67,41 @@ namespace osu.Game.Graphics.Containers return base.OnClick(state); } + public bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Back) + { + State = Visibility.Hidden; + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; + private void onStateChanged(Visibility visibility) { switch (visibility) { case Visibility.Visible: if (OverlayActivationMode != OverlayActivation.Disabled) - samplePopIn?.Play(); + { + if (PlaySamplesOnStateChange) samplePopIn?.Play(); + } else State = Visibility.Hidden; break; case Visibility.Hidden: - samplePopOut?.Play(); + if (PlaySamplesOnStateChange) samplePopOut?.Play(); break; } } + + protected override void PopOut() + { + base.PopOut(); + previewTrackManager.StopAnyPlaying(this); + } } } diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs index 119af4d762..e77e075fe2 100644 --- a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; @@ -16,6 +17,8 @@ namespace osu.Game.Graphics.Containers protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + public void AddArbitraryDrawable(Drawable drawable) => AddInternal(drawable); + public void AddIcon(FontAwesome icon, Action creationParameters = null) => AddText(((char)icon).ToString(), creationParameters); } } diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs index dc635ce7e7..8e1e5d54fa 100644 --- a/osu.Game/Graphics/Containers/ParallaxContainer.cs +++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs @@ -16,6 +16,9 @@ namespace osu.Game.Graphics.Containers { public const float DEFAULT_PARALLAX_AMOUNT = 0.02f; + /// + /// The amount of parallax movement. Negative values will reverse the direction of parallax relative to user input. + /// public float ParallaxAmount = DEFAULT_PARALLAX_AMOUNT; private Bindable parallaxEnabled; @@ -45,7 +48,7 @@ namespace osu.Game.Graphics.Containers if (!parallaxEnabled) { content.MoveTo(Vector2.Zero, firstUpdate ? 0 : 1000, Easing.OutQuint); - content.Scale = new Vector2(1 + ParallaxAmount); + content.Scale = new Vector2(1 + System.Math.Abs(ParallaxAmount)); } }; } @@ -69,7 +72,7 @@ namespace osu.Game.Graphics.Containers double elapsed = MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000); content.Position = Interpolation.ValueAt(elapsed, content.Position, offset, 0, 1000, Easing.OutQuint); - content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + ParallaxAmount), 0, 1000, Easing.OutQuint); + content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + System.Math.Abs(ParallaxAmount)), 0, 1000, Easing.OutQuint); } firstUpdate = false; diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 5f57fb76b0..0bb33930b0 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -11,9 +11,9 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Game.Configuration; using System; -using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Graphics.Textures; +using OpenTK.Input; namespace osu.Game.Graphics.Cursor { @@ -25,9 +25,8 @@ namespace osu.Game.Graphics.Cursor protected override Drawable CreateCursor() => new Cursor(); private Bindable cursorRotate; - private bool dragging; - - private bool startRotation; + private DragRotationState dragRotationState; + private Vector2 positionMouseDown; [BackgroundDependencyLoader(true)] private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager) @@ -40,18 +39,18 @@ namespace osu.Game.Graphics.Cursor protected override bool OnMouseMove(InputState state) { - if (cursorRotate && dragging) + if (dragRotationState != DragRotationState.NotDragging) { - Debug.Assert(state.Mouse.PositionMouseDown != null); - + var position = state.Mouse.Position; + var distance = Vector2Extensions.Distance(position, positionMouseDown); // don't start rotating until we're moved a minimum distance away from the mouse down location, // else it can have an annoying effect. - // ReSharper disable once PossibleInvalidOperationException - startRotation |= Vector2Extensions.Distance(state.Mouse.Position, state.Mouse.PositionMouseDown.Value) > 30; - - if (startRotation) + if (dragRotationState == DragRotationState.DragStarted && distance > 30) + dragRotationState = DragRotationState.Rotating; + // don't rotate when distance is zero to avoid NaN + if (dragRotationState == DragRotationState.Rotating && distance > 0) { - Vector2 offset = state.Mouse.Position - state.Mouse.PositionMouseDown.Value; + Vector2 offset = state.Mouse.Position - positionMouseDown; float degrees = (float)MathHelper.RadiansToDegrees(Math.Atan2(-offset.X, offset.Y)) + 24.3f; // Always rotate in the direction of least distance @@ -67,12 +66,6 @@ namespace osu.Game.Graphics.Cursor return base.OnMouseMove(state); } - protected override bool OnDragStart(InputState state) - { - dragging = true; - return base.OnDragStart(state); - } - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { ActiveCursor.Scale = new Vector2(1); @@ -80,6 +73,12 @@ namespace osu.Game.Graphics.Cursor ((Cursor)ActiveCursor).AdditiveLayer.Alpha = 0; ((Cursor)ActiveCursor).AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); + + if (args.Button == MouseButton.Left && cursorRotate) + { + dragRotationState = DragRotationState.DragStarted; + positionMouseDown = state.Mouse.Position; + } return base.OnMouseDown(state, args); } @@ -87,14 +86,16 @@ namespace osu.Game.Graphics.Cursor { if (!state.Mouse.HasMainButtonPressed) { - dragging = false; - startRotation = false; - ((Cursor)ActiveCursor).AdditiveLayer.FadeOut(500, Easing.OutQuint); - ActiveCursor.RotateTo(0, 600 * (1 + Math.Abs(ActiveCursor.Rotation / 720)), Easing.OutElasticHalf); ActiveCursor.ScaleTo(1, 500, Easing.OutElastic); } + if (args.Button == MouseButton.Left) + { + if (dragRotationState == DragRotationState.Rotating) + ActiveCursor.RotateTo(0, 600 * (1 + Math.Abs(ActiveCursor.Rotation / 720)), Easing.OutElasticHalf); + dragRotationState = DragRotationState.NotDragging; + } return base.OnMouseUp(state, args); } @@ -160,5 +161,12 @@ namespace osu.Game.Graphics.Cursor cursorScale.TriggerChange(); } } + + private enum DragRotationState + { + NotDragging, + DragStarted, + Rotating, + } } } diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 763e57e397..406fc2ffd2 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -12,14 +12,14 @@ namespace osu.Game.Graphics { public class DrawableDate : OsuSpriteText, IHasTooltip { - private readonly DateTimeOffset date; + protected readonly DateTimeOffset Date; public DrawableDate(DateTimeOffset date) { AutoSizeAxes = Axes.Both; Font = "Exo2.0-RegularItalic"; - this.date = date.ToLocalTime(); + Date = date.ToLocalTime(); } [BackgroundDependencyLoader] @@ -38,7 +38,7 @@ namespace osu.Game.Graphics { updateTime(); - var diffToNow = DateTimeOffset.Now.Subtract(date); + var diffToNow = DateTimeOffset.Now.Subtract(Date); double timeUntilNextUpdate = 1000; if (diffToNow.TotalSeconds > 60) @@ -58,8 +58,10 @@ namespace osu.Game.Graphics public override bool HandleMouseInput => true; - private void updateTime() => Text = date.Humanize(); + protected virtual string Format() => Date.Humanize(); - public string TooltipText => date.ToString(); + private void updateTime() => Text = Format(); + + public virtual string TooltipText => string.Format($"{Date:MMMM d, yyyy h:mm tt \"UTC\"z}"); } } diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 6500578de3..28d04c9a82 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -2,9 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK.Graphics; -using OpenTK.Input; using osu.Framework.Input; using System; +using osu.Game.Input.Bindings; +using OpenTK.Input; namespace osu.Game.Graphics.UserInterface { @@ -19,6 +20,7 @@ namespace osu.Game.Graphics.UserInterface public Action Exit; private bool focus; + public bool HoldFocus { get { return focus; } @@ -26,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface { focus = value; if (!focus && HasFocus) - GetContainingInputManager().ChangeFocus(null); + base.KillFocus(); } } @@ -41,18 +43,34 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (!args.Repeat && args.Key == Key.Escape) - { - if (Text.Length > 0) - Text = string.Empty; - else - Exit?.Invoke(); - return true; - } + if (!HasFocus) return false; + + if (args.Key == Key.Escape) + return false; // disable the framework-level handling of escape key for confority (we use GlobalAction.Back). return base.OnKeyDown(state, args); } + public override bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Back) + { + if (Text.Length > 0) + { + Text = string.Empty; + return true; + } + } + + return base.OnPressed(action); + } + + protected override void KillFocus() + { + base.KillFocus(); + Exit?.Invoke(); + } + public override bool RequestsFocus => HoldFocus; } } diff --git a/osu.Game/Graphics/UserInterface/LoadingAnimation.cs b/osu.Game/Graphics/UserInterface/LoadingAnimation.cs index 5ea6bce432..8d45b03a43 100644 --- a/osu.Game/Graphics/UserInterface/LoadingAnimation.cs +++ b/osu.Game/Graphics/UserInterface/LoadingAnimation.cs @@ -4,12 +4,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Graphics.UserInterface { public class LoadingAnimation : VisibilityContainer { private readonly SpriteIcon spinner; + private readonly SpriteIcon spinnerShadow; + + private const float spin_duration = 600; + private const float transition_duration = 200; public LoadingAnimation() { @@ -20,12 +25,22 @@ namespace osu.Game.Graphics.UserInterface Children = new Drawable[] { - spinner = new SpriteIcon + spinnerShadow = new SpriteIcon { - Size = new Vector2(20), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.fa_spinner + RelativeSizeAxes = Axes.Both, + Position = new Vector2(1, 1), + Colour = Color4.Black, + Alpha = 0.4f, + Icon = FontAwesome.fa_circle_o_notch + }, + spinner = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.fa_circle_o_notch } }; } @@ -34,12 +49,11 @@ namespace osu.Game.Graphics.UserInterface { base.LoadComplete(); - spinner.Spin(2000, RotationDirection.Clockwise); + spinner.Spin(spin_duration, RotationDirection.Clockwise); + spinnerShadow.Spin(spin_duration, RotationDirection.Clockwise); } - private const float transition_duration = 500; - - protected override void PopIn() => this.FadeIn(transition_duration * 5, Easing.OutQuint); + protected override void PopIn() => this.FadeIn(transition_duration * 2, Easing.OutQuint); protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint); } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index d34d2b2a7c..07920865c0 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,7 @@ namespace osu.Game.Graphics.UserInterface { protected override Drawable GetDrawableCharacter(char c) => new PasswordMaskChar(CalculatedTextSize); - public override bool AllowClipboardExport => false; + protected override bool AllowClipboardExport => false; private readonly CapsWarning warning; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 6021af2028..88b0543de0 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -9,10 +9,12 @@ using osu.Framework.Input; using osu.Game.Graphics.Sprites; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface { - public class OsuTextBox : TextBox + public class OsuTextBox : TextBox, IKeyBindingHandler { protected override Color4 BackgroundUnfocused => Color4.Black.Opacity(0.5f); protected override Color4 BackgroundFocused => OsuColour.Gray(0.3f).Opacity(0.8f); @@ -33,10 +35,7 @@ namespace osu.Game.Graphics.UserInterface TextContainer.Height = 0.5f; CornerRadius = 5; - Current.DisabledChanged += disabled => - { - Alpha = disabled ? 0.3f : 1; - }; + Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; }; } [BackgroundDependencyLoader] @@ -59,5 +58,18 @@ namespace osu.Game.Graphics.UserInterface } protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), TextSize = CalculatedTextSize }; + + public virtual bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Back) + { + KillFocus(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; } } diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index e50539e120..7d53c9e9d9 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -34,8 +34,6 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (HandlePendingText(state)) return true; - if (!state.Keyboard.ControlPressed && !state.Keyboard.ShiftPressed) { switch (args.Key) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index fced2e5d95..83ffd415ae 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -45,7 +45,9 @@ namespace osu.Game.Input.Bindings public IEnumerable InGameKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), - new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry) + new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry), + new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), }; protected override IEnumerable KeyBindingInputQueue => @@ -85,6 +87,12 @@ namespace osu.Game.Input.Bindings ToggleGameplayMouseButtons, [Description("Go back")] - Back + Back, + + [Description("Increase scroll speed")] + IncreaseScrollSpeed, + + [Description("Decrease scroll speed")] + DecreaseScrollSpeed, } } 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/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs new file mode 100644 index 0000000000..aaa11e88b6 --- /dev/null +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs @@ -0,0 +1,376 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180621044111_UpdateTaikoDefaultBindings")] + partial class UpdateTaikoDefaultBindings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs new file mode 100644 index 0000000000..98ce5def08 --- /dev/null +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using osu.Framework.Logging; + +namespace osu.Game.Migrations +{ + public partial class UpdateTaikoDefaultBindings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM KeyBinding WHERE RulesetID = 1"); + Logger.Log("osu!taiko bindings have been reset due to new defaults", LoggingTarget.Runtime, LogLevel.Important); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // we can't really tell if these should be restored or not, so let's just not do so. + } + } +} diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs new file mode 100644 index 0000000000..7eeacd56d7 --- /dev/null +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs @@ -0,0 +1,376 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180628011956_RemoveNegativeSetIDs")] + partial class RemoveNegativeSetIDs + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs new file mode 100644 index 0000000000..c38cd19b42 --- /dev/null +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RemoveNegativeSetIDs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // There was a change that baetmaps were being loaded with "-1" online IDs, which is completely incorrect. + // This ensures there will not be unique key conflicts as a result of these incorrectly imported beatmaps. + migrationBuilder.Sql("UPDATE BeatmapSetInfo SET OnlineBeatmapSetID = null WHERE OnlineBeatmapSetID <= 0"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index d750d50b5b..bd80cb743b 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -1,11 +1,9 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using osu.Game.Database; -using System; namespace osu.Game.Migrations { @@ -16,7 +14,7 @@ namespace osu.Game.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.3-rtm-10026"); + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => { 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/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 3b6bb565b0..bbaaa0c756 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -49,7 +49,7 @@ namespace osu.Game.Online.API.Requests.Responses private DateTimeOffset submitted { get; set; } [JsonProperty(@"ranked_date")] - private DateTimeOffset ranked { get; set; } + private DateTimeOffset? ranked { get; set; } [JsonProperty(@"last_updated")] private DateTimeOffset lastUpdated { get; set; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a9d03effd7..5c12a7a64a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -85,7 +85,7 @@ namespace osu.Game private OnScreenDisplay onscreenDisplay; private Bindable configRuleset; - public Bindable Ruleset = new Bindable(); + private readonly Bindable ruleset = new Bindable(); private Bindable configSkin; @@ -101,6 +101,8 @@ namespace osu.Game public OsuGame(string[] args = null) { this.args = args; + + forwardLoggedErrorsToNotifications(); } public void ToggleSettings() => settings.ToggleVisibility(); @@ -145,10 +147,13 @@ namespace osu.Game dependencies.CacheAs(this); + dependencies.CacheAs(ruleset); + dependencies.CacheAs>(ruleset); + // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); - Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); - Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; + ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); + ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; // bind config int to database SkinInfo configSkin = LocalConfig.GetBindable(OsuSetting.Skin); @@ -214,7 +219,7 @@ namespace osu.Game return; } - Ruleset.Value = s.Ruleset; + ruleset.Value = s.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(s.Beatmap); Beatmap.Value.Mods.Value = s.Mods; @@ -305,8 +310,6 @@ namespace osu.Game Depth = -6, }, overlayContent.Add); - forwardLoggedErrorsToNotifications(); - dependencies.Cache(settings); dependencies.Cache(onscreenDisplay); dependencies.Cache(social); @@ -394,31 +397,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); }; } @@ -541,7 +553,7 @@ namespace osu.Game // the use case for not applying is in visual/unit tests. bool applyBeatmapRulesetRestrictions = !currentScreen?.AllowBeatmapRulesetChange ?? false; - Ruleset.Disabled = applyBeatmapRulesetRestrictions; + ruleset.Disabled = applyBeatmapRulesetRestrictions; Beatmap.Disabled = applyBeatmapRulesetRestrictions; mainContent.Padding = new MarginPadding { Top = ToolbarOffset }; 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/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 5f90ec67ad..cd0913a25e 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -6,16 +6,16 @@ using System.Linq; 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.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; using OpenTK.Input; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Dialog { @@ -23,6 +23,9 @@ namespace osu.Game.Overlays.Dialog { public static readonly float ENTER_DURATION = 500; public static readonly float EXIT_DURATION = 200; + + protected override bool BlockPassThroughMouse => false; + private readonly Vector2 ringSize = new Vector2(100f); private readonly Vector2 ringMinifiedSize = new Vector2(20f); private readonly Vector2 buttonsEnterSpacing = new Vector2(0f, 50f); @@ -34,26 +37,28 @@ namespace osu.Game.Overlays.Dialog private readonly SpriteText header; private readonly TextFlowContainer body; + private bool actionInvoked; + public FontAwesome Icon { - get { return icon.Icon; } - set { icon.Icon = value; } + get => icon.Icon; + set => icon.Icon = value; } public string HeaderText { - get { return header.Text; } - set { header.Text = value; } + get => header.Text; + set => header.Text = value; } public string BodyText { - set { body.Text = value; } + set => body.Text = value; } public IEnumerable Buttons { - get { return buttonsContainer.Children; } + get => buttonsContainer.Children; set { buttonsContainer.ChildrenEnumerable = value; @@ -62,71 +67,17 @@ namespace osu.Game.Overlays.Dialog var action = b.Action; b.Action = () => { - Hide(); + if (actionInvoked) return; + + actionInvoked = true; action?.Invoke(); + + Hide(); }; } } } - private void pressButtonAtIndex(int index) - { - if (index < Buttons.Count()) - Buttons.Skip(index).First().TriggerOnClick(); - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (args.Repeat) return false; - - if (args.Key == Key.Enter || args.Key == Key.KeypadEnter) - { - Buttons.OfType().FirstOrDefault()?.TriggerOnClick(); - return true; - } - - // press button at number if 1-9 on number row or keypad are pressed - var k = args.Key; - if (k >= Key.Number1 && k <= Key.Number9) - { - pressButtonAtIndex(k - Key.Number1); - return true; - } - - if (k >= Key.Keypad1 && k <= Key.Keypad9) - { - pressButtonAtIndex(k - Key.Keypad1); - return true; - } - - return base.OnKeyDown(state, args); - } - - protected override void PopIn() - { - base.PopIn(); - - // Reset various animations but only if the dialog animation fully completed - if (content.Alpha == 0) - { - buttonsContainer.TransformSpacingTo(buttonsEnterSpacing); - buttonsContainer.MoveToY(buttonsEnterSpacing.Y); - ring.ResizeTo(ringMinifiedSize); - } - - content.FadeIn(ENTER_DURATION, Easing.OutQuint); - ring.ResizeTo(ringSize, ENTER_DURATION, Easing.OutQuint); - buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, Easing.OutQuint); - buttonsContainer.MoveToY(0, ENTER_DURATION, Easing.OutQuint); - } - - protected override void PopOut() - { - base.PopOut(); - - content.FadeOut(EXIT_DURATION, Easing.InSine); - } - public PopupDialog() { RelativeSizeAxes = Axes.Both; @@ -136,9 +87,6 @@ namespace osu.Game.Overlays.Dialog content = new Container { RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Width = 0.4f, Alpha = 0f, Children = new Drawable[] { @@ -243,5 +191,69 @@ namespace osu.Game.Overlays.Dialog }, }; } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat) return false; + + if (args.Key == Key.Enter || args.Key == Key.KeypadEnter) + { + Buttons.OfType().FirstOrDefault()?.TriggerOnClick(); + return true; + } + + // press button at number if 1-9 on number row or keypad are pressed + var k = args.Key; + if (k >= Key.Number1 && k <= Key.Number9) + { + pressButtonAtIndex(k - Key.Number1); + return true; + } + + if (k >= Key.Keypad1 && k <= Key.Keypad9) + { + pressButtonAtIndex(k - Key.Keypad1); + return true; + } + + return base.OnKeyDown(state, args); + } + + protected override void PopIn() + { + base.PopIn(); + + actionInvoked = false; + + // Reset various animations but only if the dialog animation fully completed + if (content.Alpha == 0) + { + buttonsContainer.TransformSpacingTo(buttonsEnterSpacing); + buttonsContainer.MoveToY(buttonsEnterSpacing.Y); + ring.ResizeTo(ringMinifiedSize); + } + + content.FadeIn(ENTER_DURATION, Easing.OutQuint); + ring.ResizeTo(ringSize, ENTER_DURATION, Easing.OutQuint); + buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, Easing.OutQuint); + buttonsContainer.MoveToY(0, ENTER_DURATION, Easing.OutQuint); + } + + protected override void PopOut() + { + if (!actionInvoked) + // In the case a user did not choose an action before a hide was triggered, press the last button. + // This is presumed to always be a sane default "cancel" action. + buttonsContainer.Last().TriggerOnClick(); + + base.PopOut(); + content.FadeOut(EXIT_DURATION, Easing.InSine); + } + + private void pressButtonAtIndex(int index) + { + if (index < Buttons.Count()) + Buttons.Skip(index).First().TriggerOnClick(); + } } } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 7caab0dd6c..c40f517023 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -1,12 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Dialog; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; namespace osu.Game.Overlays @@ -16,6 +13,20 @@ namespace osu.Game.Overlays private readonly Container dialogContainer; private PopupDialog currentDialog; + public DialogOverlay() + { + RelativeSizeAxes = Axes.Both; + + Child = dialogContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }; + + Width = 0.4f; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + } + public void Push(PopupDialog dialog) { if (dialog == currentDialog) return; @@ -30,6 +41,10 @@ namespace osu.Game.Overlays State = Visibility.Visible; } + protected override bool PlaySamplesOnStateChange => false; + + protected override bool BlockPassThroughKeyboard => true; + private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v) { if (v != Visibility.Hidden) return; @@ -50,32 +65,14 @@ namespace osu.Game.Overlays protected override void PopOut() { base.PopOut(); - this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); - } - public DialogOverlay() - { - RelativeSizeAxes = Axes.Both; - - Children = new Drawable[] + if (currentDialog?.State == Visibility.Visible) { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - }, - }, - }, - dialogContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }; + currentDialog.Hide(); + return; + } + + this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); } } } 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/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 4f4348c131..df98cc3c32 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -35,15 +35,13 @@ namespace osu.Game.Overlays.Direct } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, RulesetStore rulesets, OsuColour colours) + private void load(RulesetStore rulesets, OsuColour colours, Bindable ruleset) { DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark; - Ruleset.Value = game?.Ruleset.Value ?? rulesets.GetRuleset(0); + Ruleset.Value = ruleset ?? rulesets.GetRuleset(0); foreach (var r in rulesets.AvailableRulesets) - { modeButtons.Add(new RulesetToggleButton(Ruleset, r)); - } } private class RulesetToggleButton : OsuClickableContainer diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 131083c6ff..28cc484109 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -1,25 +1,24 @@ // 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; +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 +30,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 +42,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 @@ -51,13 +50,13 @@ namespace osu.Game.Overlays.Direct { if (value) { + icon.FadeTo(0.5f, transition_duration, Easing.OutQuint); loadingAnimation.Show(); - icon.FadeOut(transition_duration * 5, Easing.OutQuint); } else { + icon.FadeTo(1, transition_duration, Easing.OutQuint); loadingAnimation.Hide(); - icon.FadeIn(transition_duration, Easing.OutQuint); } } } @@ -75,22 +74,28 @@ namespace osu.Game.Overlays.Direct RelativeSizeAxes = Axes.Both, Icon = FontAwesome.fa_play, }, - loadingAnimation = new LoadingAnimation(), + loadingAnimation = new LoadingAnimation + { + Size = new Vector2(15), + }, }); - 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 +112,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 +160,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..29eb1094cf 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; } @@ -202,9 +202,6 @@ namespace osu.Game.Overlays.KeyBinding switch (args.Key) { - case Key.Escape: - finalise(); - return true; case Key.Delete: { if (state.Keyboard.ShiftPressed) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f1624721da..c584a32a82 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -40,7 +40,7 @@ namespace osu.Game.Overlays.Mods public readonly Bindable> SelectedMods = new Bindable>(); - public readonly Bindable Ruleset = new Bindable(); + public readonly IBindable Ruleset = new Bindable(); private void rulesetChanged(RulesetInfo newRuleset) { @@ -51,8 +51,8 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, OsuGame osu, RulesetStore rulesets, AudioManager audio) + [BackgroundDependencyLoader] + private void load(OsuColour colours, IBindable ruleset, AudioManager audio) { SelectedMods.ValueChanged += selectedModsChanged; @@ -60,13 +60,8 @@ namespace osu.Game.Overlays.Mods HighMultiplierColour = colours.Green; UnrankedLabel.Colour = colours.Blue; - if (osu != null) - Ruleset.BindTo(osu.Ruleset); - else - Ruleset.Value = rulesets.AvailableRulesets.First(); - - Ruleset.ValueChanged += rulesetChanged; - Ruleset.TriggerChange(); + Ruleset.BindTo(ruleset); + Ruleset.BindValueChanged(rulesetChanged, true); sampleOn = audio.Sample.Get(@"UI/check-on"); sampleOff = audio.Sample.Get(@"UI/check-off"); 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/Components/DrawableJoinDate.cs b/osu.Game/Overlays/Profile/Components/DrawableJoinDate.cs new file mode 100644 index 0000000000..11ee329f33 --- /dev/null +++ b/osu.Game/Overlays/Profile/Components/DrawableJoinDate.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Profile.Components +{ + public class DrawableJoinDate : DrawableDate + { + public DrawableJoinDate(DateTimeOffset date) + : base(date) + { + } + + protected override string Format() => Text = Date.ToUniversalTime().Year < 2008 ? "Here since the beginning" : $"{Date:MMMM yyyy}"; + + public override string TooltipText => $"{Date:MMMM d, yyyy}"; + } +} diff --git a/osu.Game/Overlays/Profile/Components/GradeBadge.cs b/osu.Game/Overlays/Profile/Components/GradeBadge.cs new file mode 100644 index 0000000000..14a47e8d03 --- /dev/null +++ b/osu.Game/Overlays/Profile/Components/GradeBadge.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Overlays.Profile.Components +{ + public class GradeBadge : Container + { + private const float width = 50; + private readonly string grade; + private readonly Sprite badge; + private readonly SpriteText numberText; + + public int DisplayCount + { + set => numberText.Text = value.ToString(@"#,0"); + } + + public GradeBadge(string grade) + { + this.grade = grade; + Width = width; + Height = 41; + Add(badge = new Sprite + { + Width = width, + Height = 26 + }); + Add(numberText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + TextSize = 14, + Font = @"Exo2.0-Bold" + }); + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + badge.Texture = textures.Get($"Grades/{grade}"); + } + } +} diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 067144c26e..c72ff6131b 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Profile.Components; using osu.Game.Overlays.Profile.Header; using osu.Game.Users; @@ -375,12 +376,12 @@ namespace osu.Game.Overlays.Profile if (user.JoinDate.ToUniversalTime().Year < 2008) { - infoTextLeft.AddText("Here since the beginning", boldItalic); + infoTextLeft.AddText(new DrawableJoinDate(user.JoinDate), lightText); } else { infoTextLeft.AddText("Joined ", lightText); - infoTextLeft.AddText(new DrawableDate(user.JoinDate), boldItalic); + infoTextLeft.AddText(new DrawableJoinDate(user.JoinDate), boldItalic); } infoTextLeft.NewLine(); @@ -470,43 +471,5 @@ namespace osu.Game.Overlays.Profile infoTextRight.NewLine(); } - - private class GradeBadge : Container - { - private const float width = 50; - private readonly string grade; - private readonly Sprite badge; - private readonly SpriteText numberText; - - public int DisplayCount - { - set { numberText.Text = value.ToString(@"#,0"); } - } - - public GradeBadge(string grade) - { - this.grade = grade; - Width = width; - Height = 41; - Add(badge = new Sprite - { - Width = width, - Height = 26 - }); - Add(numberText = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - TextSize = 14, - Font = @"Exo2.0-Bold" - }); - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - badge.Texture = textures.Get($"Grades/{grade}"); - } - } } } 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/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 046c1b1a33..fefb289d17 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -141,11 +141,11 @@ namespace osu.Game.Overlays.Profile.Sections.Recent break; case RecentActivityType.UserSupportFirst: - message = $"{userLinkTemplate()} has become an osu! supporter - thanks for your generosity!"; + message = $"{userLinkTemplate()} has become an osu!supporter - thanks for your generosity!"; break; case RecentActivityType.UserSupportGift: - message = $"{userLinkTemplate()} has received the gift of osu! supporter!"; + message = $"{userLinkTemplate()} has received the gift of osu!supporter!"; break; case RecentActivityType.UsernameChange: diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 930b3c1eaa..18a371e904 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -56,7 +56,13 @@ namespace osu.Game.Overlays.Settings.Sections reloadSkins(); - skinDropdown.Bindable = config.GetBindable(OsuSetting.Skin); + var skinBindable = config.GetBindable(OsuSetting.Skin); + + // Todo: This should not be necessary when OsuConfigManager is databased + if (skinDropdown.Items.All(s => s.Value != skinBindable.Value)) + skinBindable.Value = 0; + + skinDropdown.Bindable = skinBindable; } private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); diff --git a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs index 3078c44844..dae4f84b1a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs @@ -67,8 +67,8 @@ namespace osu.Game.Overlays.Toolbar }; } - [BackgroundDependencyLoader(true)] - private void load(RulesetStore rulesets, OsuGame game) + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets, Bindable parentRuleset) { this.rulesets = rulesets; foreach (var r in rulesets.AvailableRulesets) @@ -82,11 +82,7 @@ namespace osu.Game.Overlays.Toolbar ruleset.ValueChanged += rulesetChanged; ruleset.DisabledChanged += disabledChanged; - - if (game != null) - ruleset.BindTo(game.Ruleset); - else - ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(); + ruleset.BindTo(parentRuleset); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) 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/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 587f2c8d15..129dd07c3e 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -45,11 +45,15 @@ namespace osu.Game.Rulesets.Judgements public double TimeOffset { get; set; } /// - /// Whether the should affect the combo portion of the score. - /// If false, the will be considered for the bonus portion of the score. + /// Whether the should affect the current combo. /// public virtual bool AffectsCombo => true; + /// + /// Whether the should be counted as base (combo) or bonus score. + /// + public virtual bool IsBonus => !AffectsCombo; + /// /// The numeric representation for the result achieved. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 88990d435c..1f857cb7a8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -33,8 +33,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected virtual IEnumerable GetSamples() => HitObject.Samples; private readonly Lazy> nestedHitObjects = new Lazy>(); - public bool HasNestedHitObjects => nestedHitObjects.IsValueCreated; - public IReadOnlyList NestedHitObjects => nestedHitObjects.Value; + public IEnumerable NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : Enumerable.Empty(); public event Action OnJudgement; public event Action OnJudgementRemoved; @@ -50,12 +49,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Whether this and all of its nested s have been hit. /// - public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (!HasNestedHitObjects || NestedHitObjects.All(n => n.IsHit)); + public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && NestedHitObjects.All(n => n.IsHit); /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (!HasNestedHitObjects || NestedHitObjects.All(h => h.AllJudged)); + public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && NestedHitObjects.All(h => h.AllJudged); /// /// Whether this can be judged. @@ -206,9 +205,8 @@ namespace osu.Game.Rulesets.Objects.Drawables if (AllJudged) return false; - if (HasNestedHitObjects) - foreach (var d in NestedHitObjects) - judgementOccurred |= d.UpdateJudgement(userTriggered); + foreach (var d in NestedHitObjects) + judgementOccurred |= d.UpdateJudgement(userTriggered); if (!ProvidesJudgement || judgementFinalized || judgementOccurred) return judgementOccurred; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 17848d1e6e..ef1eecec3d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -10,7 +10,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : HitObject, IHasCurve + internal abstract class ConvertSlider : HitObject, IHasCurve, IHasLegacyLastTickOffset { /// /// Scoring distance with a speed-adjusted beat length of 1 second. @@ -45,5 +45,7 @@ namespace osu.Game.Rulesets.Objects.Legacy Velocity = scoringDistance / timingPoint.BeatLength; } + + public double LegacyLastTickOffset => 36; } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs b/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs deleted file mode 100644 index c5d0152ae7..0000000000 --- a/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -namespace osu.Game.Rulesets.Objects.Types -{ - /// - /// A HitObject that is part of a combo and has extended information about its position relative to other combo objects. - /// - public interface IHasComboIndex : IHasCombo - { - /// - /// The offset of this hitobject in the current combo. - /// - int IndexInCurrentCombo { get; set; } - - /// - /// The offset of this hitobject in the current combo. - /// - int ComboIndex { get; set; } - - /// - /// Whether this is the last object in the current combo. - /// - bool LastInCombo { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs b/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs new file mode 100644 index 0000000000..ab2573e933 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A type of which may require the last tick to be offset. + /// This is specific to osu!stable conversion, and should not be used elsewhere. + /// + public interface IHasLegacyLastTickOffset + { + double LegacyLastTickOffset { get; } + } +} 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/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index f818523a3d..82d9945ef7 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -57,8 +57,18 @@ namespace osu.Game.Rulesets /// public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap); + /// + /// Creates a to convert a to one that is applicable for this . + /// + /// The to be converted. + /// The . public abstract IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap); + /// + /// Optionally creates a to alter a after it has been converted. + /// + /// The to be processed. + /// The . public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null; public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 8d267f48e9..513173ef2f 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -84,10 +84,17 @@ namespace osu.Game.Rulesets { try { - var instance = r.CreateInstance(); + var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo, asm => + { + // for the time being, let's ignore the version being loaded. + // this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version). + asm.Version = null; + return Assembly.Load(asm); + }, null), (RulesetInfo)null)).RulesetInfo; - r.Name = instance.Description; - r.ShortName = instance.ShortName; + r.Name = instanceInfo.Name; + r.ShortName = instanceInfo.ShortName; + r.InstantiationInfo = instanceInfo.InstantiationInfo; r.Available = true; } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index dd4120f2fb..9e8ea0f7c2 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -261,13 +261,19 @@ namespace osu.Game.Rulesets.Scoring break; } - baseScore += judgement.NumericResult; - rollingMaxBaseScore += judgement.MaxNumericResult; - JudgedHits++; } - else if (judgement.IsHit) - bonusScore += judgement.NumericResult; + + if (judgement.IsBonus) + { + if (judgement.IsHit) + bonusScore += judgement.NumericResult; + } + else + { + baseScore += judgement.NumericResult; + rollingMaxBaseScore += judgement.MaxNumericResult; + } } /// @@ -280,14 +286,18 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = judgement.HighestComboAtJudgement; if (judgement.AffectsCombo) + JudgedHits--; + + if (judgement.IsBonus) + { + if (judgement.IsHit) + bonusScore -= judgement.NumericResult; + } + else { baseScore -= judgement.NumericResult; rollingMaxBaseScore -= judgement.MaxNumericResult; - - JudgedHits--; } - else if (judgement.IsHit) - bonusScore -= judgement.NumericResult; } private void updateScore() 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..cfcfca157b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -4,30 +4,33 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; -using OpenTK.Input; namespace osu.Game.Rulesets.UI.Scrolling { /// /// A type of specialized towards scrolling s. /// - public abstract class ScrollingPlayfield : Playfield + public abstract class ScrollingPlayfield : Playfield, IKeyBindingHandler { /// /// The default span of time visible by the length of the scrolling axes. /// This is clamped between and . /// private const double time_span_default = 1500; + /// /// The minimum span of time that may be visible by the length of the scrolling axes. /// private const double time_span_min = 50; + /// /// The maximum span of time that may be visible by the length of the scrolling axes. /// private const double time_span_max = 10000; + /// /// The step increase/decrease of the span of time visible by the length of the scrolling axes. /// @@ -54,7 +57,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 +72,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] @@ -78,27 +81,31 @@ namespace osu.Game.Rulesets.UI.Scrolling HitObjects.TimeRange.BindTo(VisibleTimeRange); } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public bool OnPressed(GlobalAction action) { if (!UserScrollSpeedAdjustment) return false; - if (state.Keyboard.ControlPressed) + switch (action) { - switch (args.Key) - { - case Key.Minus: - this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 600, Easing.OutQuint); - break; - case Key.Plus: - this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 600, Easing.OutQuint); - break; - } + case GlobalAction.IncreaseScrollSpeed: + this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint); + return true; + case GlobalAction.DecreaseScrollSpeed: + this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint); + return true; } return false; } - protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction); + public bool OnReleased(GlobalAction action) => false; + + protected sealed override HitObjectContainer CreateHitObjectContainer() + { + var container = new ScrollingHitObjectContainer(); + container.Direction.BindTo(Direction); + return container; + } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs index 35a91275a7..d2b79e2fa7 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs @@ -47,13 +47,10 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers } } - if (obj.HasNestedHitObjects) - { - ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); + ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - // Nested hitobjects don't need to scroll, but they do need accurate positions - UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); - } + // Nested hitobjects don't need to scroll, but they do need accurate positions + UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); } } diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs index e353c07e9f..25dea8dfbf 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs @@ -48,13 +48,10 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers } } - if (obj.HasNestedHitObjects) - { - ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); + ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - // Nested hitobjects don't need to scroll, but they do need accurate positions - UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); - } + // Nested hitobjects don't need to scroll, but they do need accurate positions + UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); } } @@ -93,6 +90,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/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index 5e9863f642..9d9432b2ce 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -40,7 +40,14 @@ namespace osu.Game.Screens while (screen.LoadState < LoadState.Ready) Thread.Sleep(1); - base.Push(screen); + try + { + base.Push(screen); + } + catch (ScreenAlreadyExitedException) + { + // screen may have exited before the push was successful. + } } protected override void Update() diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index d1fc8be005..0dc110951b 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -52,8 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts if (Beatmap.Value == null) return; - if (Beatmap.Value.Track.Length == double.PositiveInfinity) return; - float markerPos = MathHelper.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 07d9398d38..5628630d0e 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -47,8 +47,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; } - // Todo: This should be handled more gracefully - timeline.RelativeChildSize = Beatmap.Value.Track.Length == double.PositiveInfinity ? Vector2.One : new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); + timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); } protected void Add(Drawable visualisation) => timeline.Add(visualisation); 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..8cb0fdd908 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs @@ -52,17 +52,20 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); Beatmap.BindTo(beatmap); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Beatmap.BindValueChanged(b => waveform.Waveform = b.Waveform); - waveform.Waveform = Beatmap.Value.Waveform; + Beatmap.BindValueChanged(b => + { + waveform.Waveform = b.Waveform; + track = b.Track; + }, true); } /// - /// 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; @@ -76,6 +79,8 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline /// private bool trackWasPlaying; + private Track track; + protected override void Update() { base.Update(); @@ -83,49 +88,48 @@ 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() + { + if (!track.IsLoaded) + return; - void scrollToTrackTime() - { - if (!(Beatmap.Value.Track is TrackVirtual)) - ScrollTo((float)(adjustableClock.CurrentTime / Beatmap.Value.Track.Length) * Content.DrawWidth, false); - } + adjustableClock.Seek(Current / Content.DrawWidth * track.Length); + } + + private void scrollToTrackTime() + { + if (!track.IsLoaded) + return; + + ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 81abc4cd3d..374877673f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -148,8 +148,6 @@ namespace osu.Game.Screens.Menu case Key.Space: logo?.TriggerOnClick(state); return true; - case Key.Escape: - return goBack(); } return false; @@ -181,16 +179,7 @@ namespace osu.Game.Screens.Menu } } - public bool OnReleased(GlobalAction action) - { - switch (action) - { - case GlobalAction.Back: - return true; - default: - return false; - } - } + public bool OnReleased(GlobalAction action) => false; private void onPlay() { diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs index 62605da5a4..4ada46749c 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -1,34 +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.Input; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; using osu.Game.Overlays; -using OpenTK.Input; namespace osu.Game.Screens.Menu { - public class ExitConfirmOverlay : HoldToConfirmOverlay + public class ExitConfirmOverlay : HoldToConfirmOverlay, IKeyBindingHandler { - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public bool OnPressed(GlobalAction action) { - if (args.Key == Key.Escape && !args.Repeat) + if (action == GlobalAction.Back) { BeginConfirm(); return true; } - return base.OnKeyDown(state, args); + return false; } - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + public bool OnReleased(GlobalAction action) { - if (args.Key == Key.Escape) + if (action == GlobalAction.Back) { AbortConfirm(); return true; } - return base.OnKeyUp(state, args); + return false; } } } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 61018f9e08..2a7daff3d0 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -17,7 +16,6 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Screens.Menu; using OpenTK; -using OpenTK.Input; using osu.Game.Overlays; using osu.Framework.Graphics.Containers; @@ -82,22 +80,21 @@ namespace osu.Game.Screens private SampleChannel sampleExit; [BackgroundDependencyLoader(true)] - private void load(BindableBeatmap beatmap, OsuGame osuGame, AudioManager audio) + private void load(BindableBeatmap beatmap, OsuGame osu, AudioManager audio, Bindable ruleset) { - if (beatmap != null) - Beatmap.BindTo(beatmap); + Beatmap.BindTo(beatmap); + Ruleset.BindTo(ruleset); - if (osuGame != null) + if (osu != null) { - Ruleset.BindTo(osuGame.Ruleset); - OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); + OverlayActivationMode.BindTo(osu.OverlayActivationMode); updateOverlayStates = () => { if (HideOverlaysOnEnter) - osuGame.CloseAllOverlays(); + osu.CloseAllOverlays(); else - osuGame.Toolbar.State = Visibility.Visible; + osu.Toolbar.State = Visibility.Visible; }; } @@ -106,6 +103,8 @@ namespace osu.Game.Screens public bool OnPressed(GlobalAction action) { + if (!IsCurrentScreen) return false; + if (action == GlobalAction.Back && AllowBackButton) { Exit(); @@ -117,20 +116,6 @@ namespace osu.Game.Screens public bool OnReleased(GlobalAction action) => action == GlobalAction.Back && AllowBackButton; - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (args.Repeat || !IsCurrentScreen) return false; - - switch (args.Key) - { - case Key.Escape: - Exit(); - return true; - } - - return base.OnKeyDown(state, args); - } - protected override void OnResuming(Screen last) { sampleExit?.Play(); @@ -195,11 +180,10 @@ namespace osu.Game.Screens if (Background != null && !Background.Equals(nextOsu?.Background)) { - if (nextOsu != null) - //We need to use MakeCurrent in case we are jumping up multiple game screens. - nextOsu.Background?.MakeCurrent(); - else - Background.Exit(); + Background.Exit(); + + //We need to use MakeCurrent in case we are jumping up multiple game screens. + nextOsu?.Background?.MakeCurrent(); } if (base.OnExiting(next)) diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index 7b555776f7..bbed0c8843 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -1,12 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Input; -using OpenTK.Input; using osu.Game.Graphics; using OpenTK.Graphics; using osu.Framework.Allocation; -using System.Linq; namespace osu.Game.Screens.Play { @@ -21,16 +18,5 @@ namespace osu.Game.Screens.Play AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (!args.Repeat && args.Key == Key.Escape) - { - InternalButtons.Children.Last().TriggerOnClick(); - return true; - } - - return base.OnKeyDown(state, args); - } } } diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index db099cd16c..52c08bb351 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -15,10 +15,13 @@ using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Shapes; using OpenTK.Input; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Screens.Play { - public abstract class GameplayMenuOverlay : OverlayContainer + public abstract class GameplayMenuOverlay : OverlayContainer, IKeyBindingHandler { private const int transition_duration = 200; private const int button_height = 70; @@ -31,6 +34,11 @@ namespace osu.Game.Screens.Play public Action OnRetry; public Action OnQuit; + /// + /// Action that is invoked when is triggered. + /// + protected virtual Action BackAction => () => InternalButtons.Children.Last().TriggerOnClick(); + public abstract string Header { get; } public abstract string Description { get; } @@ -219,6 +227,19 @@ namespace osu.Game.Screens.Play return base.OnKeyDown(state, args); } + public bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Back) + { + BackAction.Invoke(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => action == GlobalAction.Back; + private void buttonSelectionChanged(DialogButton button, bool isSelected) { if (!isSelected) 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/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index 6262f71ddc..e2f133dde3 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -6,11 +6,9 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Timing; using osu.Game.Graphics; using OpenTK.Graphics; -using OpenTK.Input; namespace osu.Game.Screens.Play { @@ -138,16 +136,7 @@ namespace osu.Game.Screens.Play public override string Header => "paused"; public override string Description => "you're not going to do what i think you're going to do, are ya?"; - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (!args.Repeat && args.Key == Key.Escape) - { - InternalButtons.Children.First().TriggerOnClick(); - return true; - } - - return base.OnKeyDown(state, args); - } + protected override Action BackAction => () => InternalButtons.Children.First().TriggerOnClick(); [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a2ed01f5a7..b406bda411 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -49,8 +49,6 @@ namespace osu.Game.Screens.Play public bool AllowLeadIn { get; set; } = true; public bool AllowResults { get; set; } = true; - protected override bool AllowBackButton => false; - private Bindable mouseWheelDisabled; private Bindable userAudioOffset; @@ -158,7 +156,8 @@ namespace osu.Game.Screens.Play userAudioOffset.TriggerChange(); ScoreProcessor = RulesetContainer.CreateScoreProcessor(); - config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); + if (!ScoreProcessor.Mode.Disabled) + config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); Children = new Drawable[] { diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 7950018554..d26702fcf9 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -53,10 +53,9 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OsuGame osuGame) + private void load([CanBeNull] Bindable parentRuleset) { - if (osuGame != null) - ruleset.BindTo(osuGame.Ruleset); + ruleset.BindTo(parentRuleset); ruleset.ValueChanged += _ => updateDisplay(); } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index f9f3db3827..278d32b2d5 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -29,6 +29,7 @@ namespace osu.Game.Screens.Select private readonly TabControl groupTabs; private SortMode sort = SortMode.Title; + public SortMode Sort { get { return sort; } @@ -43,6 +44,7 @@ namespace osu.Game.Screens.Select } private GroupMode group = GroupMode.All; + public GroupMode Group { get { return group; } @@ -62,14 +64,15 @@ namespace osu.Game.Screens.Select Sort = sort, SearchText = searchTextBox.Text, AllowConvertedBeatmaps = showConverted, - Ruleset = ruleset + Ruleset = ruleset.Value }; public Action Exit; private readonly SearchTextBox searchTextBox; - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => base.ReceiveMouseInputAt(screenSpacePos) || groupTabs.ReceiveMouseInputAt(screenSpacePos) || sortTabs.ReceiveMouseInputAt(screenSpacePos); + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => + base.ReceiveMouseInputAt(screenSpacePos) || groupTabs.ReceiveMouseInputAt(screenSpacePos) || sortTabs.ReceiveMouseInputAt(screenSpacePos); public FilterControl() { @@ -163,24 +166,22 @@ namespace osu.Game.Screens.Select searchTextBox.HoldFocus = true; } - private readonly Bindable ruleset = new Bindable(); + private readonly IBindable ruleset = new Bindable(); private Bindable showConverted; public readonly Box Background; [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, OsuGame osu, OsuConfigManager config) + private void load(OsuColour colours, IBindable parentRuleset, OsuConfigManager config) { sortTabs.AccentColour = colours.GreenLight; showConverted = config.GetBindable(OsuSetting.ShowConvertedBeatmaps); showConverted.ValueChanged += val => updateCriteria(); - if (osu != null) - ruleset.BindTo(osu.Ruleset); - ruleset.ValueChanged += val => updateCriteria(); - ruleset.TriggerChange(); + ruleset.BindTo(parentRuleset); + ruleset.BindValueChanged(val => updateCriteria(), true); } private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); diff --git a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs index 9dae8fb273..c171e791d2 100644 --- a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs @@ -19,7 +19,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using System.Linq; using osu.Framework.Configuration; -using osu.Framework.Logging; using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Leaderboards @@ -33,7 +32,7 @@ namespace osu.Game.Screens.Select.Leaderboards private FillFlowContainer scrollFlow; - private readonly Bindable ruleset = new Bindable(); + private readonly IBindable ruleset = new Bindable(); public Action ScoreSelected; @@ -41,7 +40,10 @@ namespace osu.Game.Screens.Select.Leaderboards private ScheduledDelegate showScoresDelegate; + private bool scoresLoadedOnce; + private IEnumerable scores; + public IEnumerable Scores { get { return scores; } @@ -49,6 +51,8 @@ namespace osu.Game.Screens.Select.Leaderboards { scores = value; + scoresLoadedOnce = true; + scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; @@ -146,7 +150,7 @@ namespace osu.Game.Screens.Select.Leaderboards replacePlaceholder(new MessagePlaceholder(@"Please sign in to view online leaderboards!")); break; case PlaceholderState.NotSupporter: - replacePlaceholder(new MessagePlaceholder(@"Please invest in a supporter tag to view this leaderboard!")); + replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!")); break; default: replacePlaceholder(null); @@ -174,9 +178,8 @@ namespace osu.Game.Screens.Select.Leaderboards private APIAccess api; private BeatmapInfo beatmap; - private OsuGame osuGame; - private ScheduledDelegate pendingBeatmapSwitch; + private ScheduledDelegate pendingUpdateScores; public BeatmapInfo Beatmap { @@ -189,21 +192,17 @@ namespace osu.Game.Screens.Select.Leaderboards beatmap = value; Scores = null; - pendingBeatmapSwitch?.Cancel(); - pendingBeatmapSwitch = Schedule(updateScores); + updateScores(); } } [BackgroundDependencyLoader(permitNulls: true)] - private void load(APIAccess api, OsuGame osuGame) + private void load(APIAccess api, IBindable parentRuleset) { this.api = api; - this.osuGame = osuGame; - if (osuGame != null) - ruleset.BindTo(osuGame.Ruleset); - - ruleset.ValueChanged += r => updateScores(); + ruleset.BindTo(parentRuleset); + ruleset.ValueChanged += _ => updateScores(); if (api != null) api.OnStateChange += handleApiStateChange; @@ -231,51 +230,61 @@ namespace osu.Game.Screens.Select.Leaderboards private void updateScores() { - if (Scope == LeaderboardScope.Local) - { - // TODO: get local scores from wherever here. - PlaceholderState = PlaceholderState.NoScores; - return; - } + // don't display any scores or placeholder until the first Scores_Set has been called. + // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. + if (!scoresLoadedOnce) return; - if (Beatmap?.OnlineBeatmapID == null) - { - PlaceholderState = PlaceholderState.Unavailable; - return; - } + getScoresRequest?.Cancel(); + getScoresRequest = null; - if (api?.IsLoggedIn != true) + pendingUpdateScores?.Cancel(); + pendingUpdateScores = Schedule(() => { - PlaceholderState = PlaceholderState.NotLoggedIn; - return; - } - - if (Scope != LeaderboardScope.Global && !api.LocalUser.Value.IsSupporter) - { - PlaceholderState = PlaceholderState.NotSupporter; - return; - } - - PlaceholderState = PlaceholderState.Retrieving; - loading.Show(); - - getScoresRequest = new GetScoresRequest(Beatmap, osuGame?.Ruleset.Value ?? Beatmap.Ruleset, Scope); - getScoresRequest.Success += r => Schedule(() => - { - Scores = r.Scores; - PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; - }); - - getScoresRequest.Failure += e => Schedule(() => - { - if (e is OperationCanceledException) + if (Scope == LeaderboardScope.Local) + { + // TODO: get local scores from wherever here. + PlaceholderState = PlaceholderState.NoScores; return; + } - PlaceholderState = PlaceholderState.NetworkFailure; - Logger.Error(e, @"Couldn't fetch beatmap scores!"); + if (Beatmap?.OnlineBeatmapID == null) + { + PlaceholderState = PlaceholderState.Unavailable; + return; + } + + if (api?.IsLoggedIn != true) + { + PlaceholderState = PlaceholderState.NotLoggedIn; + return; + } + + if (Scope != LeaderboardScope.Global && !api.LocalUser.Value.IsSupporter) + { + PlaceholderState = PlaceholderState.NotSupporter; + return; + } + + PlaceholderState = PlaceholderState.Retrieving; + loading.Show(); + + getScoresRequest = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope); + getScoresRequest.Success += r => Schedule(() => + { + Scores = r.Scores; + PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; + }); + + getScoresRequest.Failure += e => Schedule(() => + { + if (e is OperationCanceledException) + return; + + PlaceholderState = PlaceholderState.NetworkFailure; + }); + + api.Queue(getScoresRequest); }); - - api.Queue(getScoresRequest); } private Placeholder currentPlaceholder; @@ -331,23 +340,4 @@ namespace osu.Game.Screens.Select.Leaderboards } } } - - public enum LeaderboardScope - { - Local, - Country, - Global, - Friend, - } - - public enum PlaceholderState - { - Successful, - Retrieving, - NetworkFailure, - Unavailable, - NoScores, - NotLoggedIn, - NotSupporter, - } } diff --git a/osu.Game/Screens/Select/Leaderboards/LeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/LeaderboardScope.cs new file mode 100644 index 0000000000..761f53a5e8 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/LeaderboardScope.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 + +namespace osu.Game.Screens.Select.Leaderboards +{ + public enum LeaderboardScope + { + Local, + Country, + Global, + Friend, + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.cs b/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.cs index ac2ac818d8..f01a55b662 100644 --- a/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.cs +++ b/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.cs @@ -2,10 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using OpenTK; namespace osu.Game.Screens.Select.Leaderboards { @@ -15,22 +12,13 @@ namespace osu.Game.Screens.Select.Leaderboards public MessagePlaceholder(string message) { - Direction = FillDirection.Horizontal; - AutoSizeAxes = Axes.Both; - Children = new Drawable[] + AddIcon(FontAwesome.fa_exclamation_circle, cp => { - new SpriteIcon - { - Icon = FontAwesome.fa_exclamation_circle, - Size = new Vector2(26), - Margin = new MarginPadding { Right = 10 }, - }, - new OsuSpriteText - { - Text = this.message = message, - TextSize = 22, - }, - }; + cp.TextSize = TEXT_SIZE; + cp.Padding = new MarginPadding { Right = 10 }; + }); + + AddText(this.message = message); } public override bool Equals(Placeholder other) => (other as MessagePlaceholder)?.message == message; diff --git a/osu.Game/Screens/Select/Leaderboards/Placeholder.cs b/osu.Game/Screens/Select/Leaderboards/Placeholder.cs index c56d279e6c..307986a299 100644 --- a/osu.Game/Screens/Select/Leaderboards/Placeholder.cs +++ b/osu.Game/Screens/Select/Leaderboards/Placeholder.cs @@ -3,16 +3,27 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Select.Leaderboards { - public abstract class Placeholder : FillFlowContainer, IEquatable + public abstract class Placeholder : OsuTextFlowContainer, IEquatable { + protected const float TEXT_SIZE = 22; + + public override bool HandleMouseInput => true; + protected Placeholder() + : base(cp => cp.TextSize = TEXT_SIZE) { Anchor = Anchor.Centre; Origin = Anchor.Centre; + TextAnchor = Anchor.TopCentre; + + Padding = new MarginPadding(20); + + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; } public virtual bool Equals(Placeholder other) => GetType() == other?.GetType(); diff --git a/osu.Game/Screens/Select/Leaderboards/PlaceholderState.cs b/osu.Game/Screens/Select/Leaderboards/PlaceholderState.cs new file mode 100644 index 0000000000..33a56540f3 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/PlaceholderState.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.Screens.Select.Leaderboards +{ + public enum PlaceholderState + { + Successful, + Retrieving, + NetworkFailure, + Unavailable, + NoScores, + NotLoggedIn, + NotSupporter, + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs b/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs index 174fbac120..99b0c53835 100644 --- a/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs +++ b/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs @@ -3,11 +3,9 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using OpenTK; namespace osu.Game.Screens.Select.Leaderboards @@ -18,22 +16,13 @@ namespace osu.Game.Screens.Select.Leaderboards public RetrievalFailurePlaceholder() { - Direction = FillDirection.Horizontal; - AutoSizeAxes = Axes.Both; - Children = new Drawable[] + AddArbitraryDrawable(new RetryButton { - new RetryButton - { - Action = () => OnRetry?.Invoke(), - Margin = new MarginPadding { Right = 10 }, - }, - new OsuSpriteText - { - Anchor = Anchor.TopLeft, - Text = @"Couldn't retrieve scores!", - TextSize = 22, - }, - }; + Action = () => OnRetry?.Invoke(), + Padding = new MarginPadding { Right = 10 } + }); + + AddText(@"Couldn't retrieve scores!"); } public class RetryButton : OsuHoverContainer @@ -44,18 +33,16 @@ namespace osu.Game.Screens.Select.Leaderboards public RetryButton() { - Height = 26; - Width = 26; + AutoSizeAxes = Axes.Both; + Child = new OsuClickableContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Action = () => Action?.Invoke(), Child = icon = new SpriteIcon { Icon = FontAwesome.fa_refresh, - Size = new Vector2(26), + Size = new Vector2(TEXT_SIZE), Shadow = true, }, }; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 70b473bcd9..94c16f1797 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -17,6 +18,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; @@ -62,6 +64,8 @@ namespace osu.Game.Screens.Select private SampleChannel sampleChangeDifficulty; private SampleChannel sampleChangeBeatmap; + protected new readonly Bindable Ruleset = new Bindable(); + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); @@ -123,7 +127,7 @@ namespace osu.Game.Screens.Select Size = new Vector2(carousel_width, 1), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - SelectionChanged = carouselSelectionChanged, + SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, }, FilterControl = new FilterControl @@ -177,9 +181,13 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader(true)] - private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours) + private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuColour colours) { dependencies.CacheAs(this); + dependencies.CacheAs(Ruleset); + dependencies.CacheAs>(Ruleset); + + base.Ruleset.ValueChanged += r => updateSelectedBeatmap(beatmapNoDebounce); if (Footer != null) { @@ -192,9 +200,6 @@ namespace osu.Game.Screens.Select if (this.beatmaps == null) this.beatmaps = beatmaps; - if (osu != null) - Ruleset.BindTo(osu.Ruleset); - this.beatmaps.ItemAdded += onBeatmapSetAdded; this.beatmaps.ItemRemoved += onBeatmapSetRemoved; this.beatmaps.BeatmapHidden += onBeatmapHidden; @@ -250,9 +255,6 @@ namespace osu.Game.Screens.Select private ScheduledDelegate selectionChangedDebounce; - // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. - private BeatmapInfo beatmapNoDebounce; - private void workingBeatmapChanged(WorkingBeatmap beatmap) { if (beatmap is DummyWorkingBeatmap) return; @@ -266,11 +268,17 @@ namespace osu.Game.Screens.Select } } + // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. + private BeatmapInfo beatmapNoDebounce; + private RulesetInfo rulesetNoDebounce; + /// - /// selection has been changed as the result of interaction with the carousel. + /// selection has been changed as the result of a user interaction. /// - private void carouselSelectionChanged(BeatmapInfo beatmap) + private void updateSelectedBeatmap(BeatmapInfo beatmap) { + var ruleset = base.Ruleset.Value; + void performLoad() { // We may be arriving here due to another component changing the bindable Beatmap. @@ -283,15 +291,18 @@ namespace osu.Game.Screens.Select ensurePlayingSelected(preview); } + Ruleset.Value = ruleset; + UpdateBeatmap(Beatmap.Value); } - if (beatmap?.Equals(beatmapNoDebounce) == true) + if (beatmap?.Equals(beatmapNoDebounce) == true && ruleset?.Equals(rulesetNoDebounce) == true) return; selectionChangedDebounce?.Cancel(); beatmapNoDebounce = beatmap; + rulesetNoDebounce = ruleset; if (beatmap == null) performLoad(); @@ -460,7 +471,7 @@ namespace osu.Game.Screens.Select { // in the case random selection failed, we want to trigger selectionChanged // to show the dummy beatmap (we have nothing else to display). - carouselSelectionChanged(null); + updateSelectedBeatmap(null); } } diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index 37693c99e8..2f5c887726 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -1,12 +1,10 @@ // 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.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Tests.Beatmaps { @@ -26,21 +24,6 @@ namespace osu.Game.Tests.Beatmaps private readonly IBeatmap beatmap; protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; - - protected override Track GetTrack() - { - var lastObject = beatmap.HitObjects.LastOrDefault(); - if (lastObject != null) - return new TestTrack(((lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime) + 1000); - return new TrackVirtual(); - } - - private class TestTrack : TrackVirtual - { - public TestTrack(double length) - { - Length = length; - } - } + protected override Track GetTrack() => null; } } diff --git a/osu.Game/Tests/OsuTestBrowser.cs b/osu.Game/Tests/OsuTestBrowser.cs index 738bb2d642..217af8eb77 100644 --- a/osu.Game/Tests/OsuTestBrowser.cs +++ b/osu.Game/Tests/OsuTestBrowser.cs @@ -3,6 +3,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Graphics; using osu.Game.Screens.Backgrounds; namespace osu.Game.Tests @@ -13,7 +14,11 @@ namespace osu.Game.Tests { base.LoadComplete(); - LoadComponentAsync(new BackgroundScreenDefault { Depth = 10 }, AddInternal); + LoadComponentAsync(new BackgroundScreenDefault + { + Colour = OsuColour.Gray(0.5f), + Depth = 10 + }, AddInternal); // Have to construct this here, rather than in the constructor, because // we depend on some dependencies to be loaded within OsuGameBase.load(). 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/Tests/Visual/OsuTestCase.cs b/osu.Game/Tests/Visual/OsuTestCase.cs index 1740658da6..5f70055021 100644 --- a/osu.Game/Tests/Visual/OsuTestCase.cs +++ b/osu.Game/Tests/Visual/OsuTestCase.cs @@ -1,10 +1,13 @@ // 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 osu.Framework.Audio; +using osu.Framework.Configuration; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Rulesets; namespace osu.Game.Tests.Visual { @@ -13,6 +16,8 @@ namespace osu.Game.Tests.Visual private readonly OsuTestBeatmap beatmap = new OsuTestBeatmap(new DummyWorkingBeatmap()); protected BindableBeatmap Beatmap => beatmap; + protected readonly Bindable Ruleset = new Bindable(); + protected DependencyContainer Dependencies { get; private set; } protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) @@ -22,13 +27,18 @@ namespace osu.Game.Tests.Visual Dependencies.CacheAs(beatmap); Dependencies.CacheAs(beatmap); + Dependencies.CacheAs(Ruleset); + Dependencies.CacheAs>(Ruleset); + return Dependencies; } [BackgroundDependencyLoader] - private void load(AudioManager audioManager) + private void load(AudioManager audioManager, RulesetStore rulesets) { beatmap.SetAudioManager(audioManager); + + Ruleset.Value = rulesets.AvailableRulesets.First(); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index 20c9646aa3..9afb1dd6cd 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -86,7 +86,11 @@ namespace osu.Game.Tests.Visual private readonly WeakList workingWeakReferences = new WeakList(); private readonly WeakList playerWeakReferences = new WeakList(); - private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance()); + private Player loadPlayerFor(RulesetInfo ri) + { + Ruleset.Value = ri; + return loadPlayerFor(ri.CreateInstance()); + } private Player loadPlayerFor(Ruleset r) { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f0bc330994..56c33c47af 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 @@ - +