diff --git a/.vscode/launch.json b/.vscode/launch.json index ed67fa92cc..e9b8d6f397 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,11 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Debug)", - "env": {}, + "linux": { + "env": { + "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.1:${env:LD_LIBRARY_PATH}" + } + }, "console": "internalConsole" }, { @@ -24,7 +28,11 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Release)", - "env": {}, + "linux": { + "env": { + "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.1:${env:LD_LIBRARY_PATH}" + } + }, "console": "internalConsole" }, { @@ -33,11 +41,15 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.1/osu!.dll", + "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.1/osu!.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Debug)", - "env": {}, + "linux": { + "env": { + "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.1:${env:LD_LIBRARY_PATH}" + } + }, "console": "internalConsole" }, { @@ -46,12 +58,16 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.1/osu!.dll", + "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.1/osu!.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Release)", - "env": {}, + "linux": { + "env": { + "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.1:${env:LD_LIBRARY_PATH}" + } + }, "console": "internalConsole" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 9d19f16ebd..fd0ba838cc 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,32 @@ # osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) -Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! +Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename "osu!lazer". Pew pew. # Status -This is still heavily under development and is not intended for end-user use. This repository is intended for developer collaboration. You're welcome to try and use it but please do not submit bug reports without a patch. Please do not ask for help building or using this software. +This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable osu! client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table. + +We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below. # Requirements -- A desktop platform that can compile .NET 4.7.1. We recommend using [Visual Studio Community Edition](https://www.visualstudio.com/) (Windows), [Visual Studio for Mac](https://www.visualstudio.com/vs/visual-studio-mac/) (macOS) or [MonoDevelop](http://www.monodevelop.com/download/) (Linux), all of which are free. [Visual Studio Code](https://code.visualstudio.com/) may also be used but requires further setup steps which are not covered here. +- A desktop platform with the [.NET Core SDK 2.1](https://www.microsoft.com/net/learn/get-started) or higher installed. +- When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio Community Edition](https://www.visualstudio.com/) (Windows), [Visual Studio Code](https://code.visualstudio.com/) (with the C# plugin installed) or [Jetbrains Rider](https://www.jetbrains.com/rider/) (commercial). -# Getting Started -- Clone the repository including submodules (`git clone --recurse-submodules https://github.com/ppy/osu`) -- Build in your IDE of choice (recommended IDEs automatically restore nuget packages; if you are using an alternative make sure to `nuget restore`) +# Building and running + +If you are not interested in developing the game, please head over to the [releases](https://github.com/ppy/osu/releases) to download a precompiled build with automatic updating enabled (download and run the install executable for your platform). + +Clone the repository including submodules + +`git clone --recurse-submodules https://github.com/ppy/osu` + +Build and run + +- Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included) +- From command line using `dotnet run --project osu.Desktop --framework netcoreapp2.1` + +The above methods should automatically do so, but if you run into issues building you may need to restore nuget packages (commonly via `dotnet restore`). # Contributing diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index 73555dcecb..58f2ab7747 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -13,11 +15,10 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] - public class ManiaBeatmapConversionTest : BeatmapConversionTest + public class ManiaBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; - [NonParallelizable] [TestCase("basic")] public new void Test(string name) { @@ -34,9 +35,35 @@ namespace osu.Game.Rulesets.Mania.Tests }; } + protected override ManiaConvertMapping CreateConvertMapping() => new ManiaConvertMapping(Converter); + protected override Ruleset CreateRuleset() => new ManiaRuleset(); } + public class ManiaConvertMapping : ConvertMapping, IEquatable + { + public uint RandomW; + public uint RandomX; + public uint RandomY; + public uint RandomZ; + + public ManiaConvertMapping() + { + } + + public ManiaConvertMapping(IBeatmapConverter converter) + { + var maniaConverter = (ManiaBeatmapConverter)converter; + RandomW = maniaConverter.Random.W; + RandomX = maniaConverter.Random.X; + RandomY = maniaConverter.Random.Y; + RandomZ = maniaConverter.Random.Z; + } + + public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ; + public override bool Equals(ConvertMapping other) => base.Equals(other) && Equals(other as ManiaConvertMapping); + } + public struct ConvertValue : IEquatable { /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs index 0f70ceece2..e827fa1fdc 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs @@ -153,9 +153,6 @@ namespace osu.Game.Rulesets.Mania.Tests if (!(obj.HitObject is IHasEndTime endTime)) continue; - if (!obj.HasNestedHitObjects) - continue; - foreach (var nested in obj.NestedHitObjects) { double finalPosition = (nested.HitObject.StartTime - obj.HitObject.StartTime) / endTime.Duration; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 19fef9eb54..c15b303048 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -5,6 +5,7 @@ using osu.Game.Rulesets.Mania.Objects; using System; using System.Linq; using System.Collections.Generic; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -28,8 +29,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps public int TargetColumns; public readonly bool IsForCurrentRuleset; + // Internal for testing purposes + internal FastRandom Random { get; private set; } + private Pattern lastPattern = new Pattern(); - private FastRandom random; private ManiaBeatmap beatmap; @@ -62,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate); - random = new FastRandom(seed); + Random = new FastRandom(seed); return base.ConvertBeatmap(original); } @@ -100,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private double lastTime; private Vector2 lastPosition; - private PatternType lastStair; + private PatternType lastStair = PatternType.Stair; private void recordNote(double time, Vector2 position) { lastTime = time; @@ -115,12 +118,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The hit objects generated. private IEnumerable generateSpecific(HitObject original, IBeatmap originalBeatmap) { - var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap); + var generator = new SpecificBeatmapPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); - Pattern newPattern = generator.Generate(); - lastPattern = newPattern; + foreach (var newPattern in generator.Generate()) + { + lastPattern = newPattern; - return newPattern.HitObjects; + foreach (var obj in newPattern.HitObjects) + yield return obj; + } } /// @@ -138,27 +144,44 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Patterns.PatternGenerator conversion = null; if (distanceData != null) - conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap); + { + var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); + conversion = generator; + + for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration) + { + recordNote(time, positionData?.Position ?? Vector2.Zero); + computeDensity(time); + } + } else if (endTimeData != null) - conversion = new EndTimeObjectPatternGenerator(random, original, beatmap, originalBeatmap); + { + conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap); + + recordNote(endTimeData.EndTime, new Vector2(256, 192)); + computeDensity(endTimeData.EndTime); + } else if (positionData != null) { computeDensity(original.StartTime); - conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap); + conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap); recordNote(original.StartTime, positionData.Position); } if (conversion == null) - return null; + yield break; - Pattern newPattern = conversion.Generate(); + foreach (var newPattern in conversion.Generate()) + { + lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern; + lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair; - lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern; - lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair; + foreach (var obj in newPattern.HitObjects) + yield return obj; - return newPattern.HitObjects; + } } /// @@ -171,7 +194,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { } - public override Pattern Generate() + public override IEnumerable Generate() + { + yield return generate(); + } + + private Pattern generate() { var endTimeData = HitObject as IHasEndTime; var positionData = HitObject as IHasXPosition; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index c6fa465a0f..280c2f45d4 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.MathUtils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; @@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// private const float osu_base_scoring_distance = 100; - private readonly double endTime; - private readonly double segmentDuration; + public readonly double EndTime; + public readonly double SegmentDuration; + private readonly int spanCount; private PatternType convertType; @@ -52,53 +54,81 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy // The duration of the osu! hit object double osuDuration = distance / osuVelocity; - endTime = hitObject.StartTime + osuDuration; - segmentDuration = (endTime - HitObject.StartTime) / spanCount; + EndTime = hitObject.StartTime + osuDuration; + SegmentDuration = (EndTime - HitObject.StartTime) / spanCount; } - public override Pattern Generate() + public override IEnumerable Generate() + { + var originalPattern = generate(); + + if (originalPattern.HitObjects.Count() == 1) + { + yield return originalPattern; + yield break; + } + + // We need to split the intermediate pattern into two new patterns: + // 1. A pattern containing all objects that do not end at our EndTime. + // 2. A pattern containing all objects that end at our EndTime. This will be used for further pattern generation. + var intermediatePattern = new Pattern(); + var endTimePattern = new Pattern(); + + foreach (var obj in originalPattern.HitObjects) + { + if (!Precision.AlmostEquals(EndTime, (obj as IHasEndTime)?.EndTime ?? obj.StartTime)) + intermediatePattern.Add(obj); + else + endTimePattern.Add(obj); + } + + yield return intermediatePattern; + yield return endTimePattern; + } + + private Pattern generate() { if (TotalColumns == 1) { var pattern = new Pattern(); - addToPattern(pattern, 0, HitObject.StartTime, endTime); + addToPattern(pattern, 0, HitObject.StartTime, EndTime); return pattern; } if (spanCount > 1) { - if (segmentDuration <= 90) + if (SegmentDuration <= 90) return generateRandomHoldNotes(HitObject.StartTime, 1); - if (segmentDuration <= 120) + if (SegmentDuration <= 120) { convertType |= PatternType.ForceNotStack; return generateRandomNotes(HitObject.StartTime, spanCount + 1); } - if (segmentDuration <= 160) + if (SegmentDuration <= 160) return generateStair(HitObject.StartTime); - if (segmentDuration <= 200 && ConversionDifficulty > 3) + if (SegmentDuration <= 200 && ConversionDifficulty > 3) return generateRandomMultipleNotes(HitObject.StartTime); - double duration = endTime - HitObject.StartTime; + double duration = EndTime - HitObject.StartTime; if (duration >= 4000) return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); - if (segmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart) + if (SegmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart) return generateTiledHoldNotes(HitObject.StartTime); return generateHoldAndNormalNotes(HitObject.StartTime); } - if (segmentDuration <= 110) + if (SegmentDuration <= 110) { if (PreviousPattern.ColumnWithObjects < TotalColumns) convertType |= PatternType.ForceNotStack; else convertType &= ~PatternType.ForceNotStack; - return generateRandomNotes(HitObject.StartTime, segmentDuration < 80 ? 1 : 2); + return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2); } if (ConversionDifficulty > 6.5) @@ -148,7 +178,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column nextColumn = Random.Next(RandomStart, TotalColumns); - addToPattern(pattern, nextColumn, startTime, endTime); + addToPattern(pattern, nextColumn, startTime, EndTime); } // This is can't be combined with the above loop due to RNG @@ -156,7 +186,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { while (pattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, TotalColumns); - addToPattern(pattern, nextColumn, startTime, endTime); + addToPattern(pattern, nextColumn, startTime, EndTime); } return pattern; @@ -193,7 +223,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy nextColumn = Random.Next(RandomStart, TotalColumns); lastColumn = nextColumn; - startTime += segmentDuration; + startTime += SegmentDuration; } return pattern; @@ -223,7 +253,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy for (int i = 0; i <= spanCount; i++) { addToPattern(pattern, column, startTime, startTime); - startTime += segmentDuration; + startTime += SegmentDuration; // Check if we're at the borders of the stage, and invert the pattern if so if (increasing) @@ -284,7 +314,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy addToPattern(pattern, nextColumn, startTime, startTime); nextColumn = Random.Next(RandomStart, TotalColumns); - startTime += segmentDuration; + startTime += SegmentDuration; } return pattern; @@ -372,8 +402,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy while (pattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, TotalColumns); - addToPattern(pattern, nextColumn, startTime, endTime); - startTime += segmentDuration; + addToPattern(pattern, nextColumn, startTime, EndTime); + startTime += SegmentDuration; } return pattern; @@ -402,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } // Create the hold note - addToPattern(pattern, holdColumn, startTime, endTime); + addToPattern(pattern, holdColumn, startTime, EndTime); int nextColumn = Random.Next(RandomStart, TotalColumns); int noteCount; @@ -434,7 +464,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy pattern.Add(rowPattern); rowPattern.Clear(); - startTime += segmentDuration; + startTime += SegmentDuration; } return pattern; @@ -452,7 +482,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (curveData == null) return HitObject.Samples; - double segmentTime = (endTime - HitObject.StartTime) / spanCount; + double segmentTime = (EndTime - HitObject.StartTime) / spanCount; int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); return curveData.RepeatSamples[index]; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 3f34afee85..0f170e8540 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -24,7 +24,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy endTime = endtimeData?.EndTime ?? 0; } - public override Pattern Generate() + public override IEnumerable Generate() + { + yield return generate(); + } + + private Pattern generate() { var pattern = new Pattern(); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index b4160dc98b..0e839d87a2 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; using OpenTK; using osu.Game.Audio; @@ -82,127 +83,133 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH) && TotalColumns != 8) convertType |= PatternType.Mirror; - else + else if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_CLAP)) convertType |= PatternType.Gathered; } } - public override Pattern Generate() + public override IEnumerable Generate() { - if (TotalColumns == 1) + yield return generate(); + } + + private Pattern generate() + { + var pattern = new Pattern(); + + try { - var pattern = new Pattern(); - addToPattern(pattern, 0); - return pattern; - } - - int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; - - if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any()) - { - // Generate a new pattern by copying the last hit objects in reverse-column order - var pattern = new Pattern(); - - for (int i = RandomStart; i < TotalColumns; i++) - if (PreviousPattern.ColumnHasObject(i)) - addToPattern(pattern, RandomStart + TotalColumns - i - 1); - - return pattern; - } - - if ((convertType & PatternType.Cycle) > 0 && PreviousPattern.HitObjects.Count() == 1 - // If we convert to 7K + 1, let's not overload the special key - && (TotalColumns != 8 || lastColumn != 0) - // Make sure the last column was not the centre column - && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) - { - // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) - var pattern = new Pattern(); - - int column = RandomStart + TotalColumns - lastColumn - 1; - addToPattern(pattern, column); - - return pattern; - } - - if ((convertType & PatternType.ForceStack) > 0 && PreviousPattern.HitObjects.Any()) - { - // Generate a new pattern by placing on the already filled columns - var pattern = new Pattern(); - - for (int i = RandomStart; i < TotalColumns; i++) - if (PreviousPattern.ColumnHasObject(i)) - addToPattern(pattern, i); - - return pattern; - } - - if ((convertType & PatternType.Stair) > 0 && PreviousPattern.HitObjects.Count() == 1) - { - // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" - var pattern = new Pattern(); - - int targetColumn = lastColumn + 1; - if (targetColumn == TotalColumns) + if (TotalColumns == 1) { - targetColumn = RandomStart; - StairType = PatternType.ReverseStair; + addToPattern(pattern, 0); + return pattern; } - addToPattern(pattern, targetColumn); - return pattern; - } + int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; - if ((convertType & PatternType.ReverseStair) > 0 && PreviousPattern.HitObjects.Count() == 1) - { - // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" - var pattern = new Pattern(); - - int targetColumn = lastColumn - 1; - if (targetColumn == RandomStart - 1) + if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any()) { - targetColumn = TotalColumns - 1; - StairType = PatternType.Stair; + // Generate a new pattern by copying the last hit objects in reverse-column order + for (int i = RandomStart; i < TotalColumns; i++) + if (PreviousPattern.ColumnHasObject(i)) + addToPattern(pattern, RandomStart + TotalColumns - i - 1); + + return pattern; } - addToPattern(pattern, targetColumn); - return pattern; - } + if ((convertType & PatternType.Cycle) > 0 && PreviousPattern.HitObjects.Count() == 1 + // If we convert to 7K + 1, let's not overload the special key + && (TotalColumns != 8 || lastColumn != 0) + // Make sure the last column was not the centre column + && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) + { + // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) + int column = RandomStart + TotalColumns - lastColumn - 1; + addToPattern(pattern, column); - if ((convertType & PatternType.KeepSingle) > 0) - return generateRandomNotes(1); + return pattern; + } + + if ((convertType & PatternType.ForceStack) > 0 && PreviousPattern.HitObjects.Any()) + { + // Generate a new pattern by placing on the already filled columns + for (int i = RandomStart; i < TotalColumns; i++) + if (PreviousPattern.ColumnHasObject(i)) + addToPattern(pattern, i); + + return pattern; + } + + if (PreviousPattern.HitObjects.Count() == 1) + { + if ((convertType & PatternType.Stair) > 0) + { + // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" + int targetColumn = lastColumn + 1; + if (targetColumn == TotalColumns) + targetColumn = RandomStart; + + addToPattern(pattern, targetColumn); + return pattern; + } + + if ((convertType & PatternType.ReverseStair) > 0) + { + // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" + int targetColumn = lastColumn - 1; + if (targetColumn == RandomStart - 1) + targetColumn = TotalColumns - 1; + + addToPattern(pattern, targetColumn); + return pattern; + } + } + + if ((convertType & PatternType.KeepSingle) > 0) + return pattern = generateRandomNotes(1); + + if ((convertType & PatternType.Mirror) > 0) + { + if (ConversionDifficulty > 6.5) + return pattern = generateRandomPatternWithMirrored(0.12, 0.38, 0.12); + if (ConversionDifficulty > 4) + return pattern = generateRandomPatternWithMirrored(0.12, 0.17, 0); + return pattern = generateRandomPatternWithMirrored(0.12, 0, 0); + } - if ((convertType & PatternType.Mirror) > 0) - { if (ConversionDifficulty > 6.5) - return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); + { + if ((convertType & PatternType.LowProbability) > 0) + return pattern = generateRandomPattern(0.78, 0.42, 0, 0); + return pattern = generateRandomPattern(1, 0.62, 0, 0); + } + if (ConversionDifficulty > 4) - return generateRandomPatternWithMirrored(0.12, 0.17, 0); - return generateRandomPatternWithMirrored(0.12, 0, 0); - } + { + if ((convertType & PatternType.LowProbability) > 0) + return pattern = generateRandomPattern(0.35, 0.08, 0, 0); + return pattern = generateRandomPattern(0.52, 0.15, 0, 0); + } - if (ConversionDifficulty > 6.5) + if (ConversionDifficulty > 2) + { + if ((convertType & PatternType.LowProbability) > 0) + return pattern = generateRandomPattern(0.18, 0, 0, 0); + return pattern = generateRandomPattern(0.45, 0, 0, 0); + } + + return pattern = generateRandomPattern(0, 0, 0, 0); + } + finally { - if ((convertType & PatternType.LowProbability) > 0) - return generateRandomPattern(0.78, 0.42, 0, 0); - return generateRandomPattern(1, 0.62, 0, 0); + foreach (var obj in pattern.HitObjects) + { + if ((convertType & PatternType.Stair) > 0 && obj.Column == TotalColumns - 1) + StairType = PatternType.ReverseStair; + if ((convertType & PatternType.ReverseStair) > 0 && obj.Column == RandomStart) + StairType = PatternType.Stair; + } } - - if (ConversionDifficulty > 4) - { - if ((convertType & PatternType.LowProbability) > 0) - return generateRandomPattern(0.35, 0.08, 0, 0); - return generateRandomPattern(0.52, 0.15, 0, 0); - } - - if (ConversionDifficulty > 2) - { - if ((convertType & PatternType.LowProbability) > 0) - return generateRandomPattern(0.18, 0, 0, 0); - return generateRandomPattern(0.45, 0, 0, 0); - } - - return generateRandomPattern(0, 0, 0, 0); } /// diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index 930597c1ad..55081e5822 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -103,17 +103,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy HitObject lastObject = OriginalBeatmap.HitObjects.LastOrDefault(); HitObject firstObject = OriginalBeatmap.HitObjects.FirstOrDefault(); - double drainTime = (lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0); - drainTime -= OriginalBeatmap.TotalBreakTime; + // Drain time in seconds + int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - OriginalBeatmap.TotalBreakTime) / 1000); if (drainTime == 0) - drainTime = 10000000; - - // We need this in seconds - drainTime /= 1000; + drainTime = 10000; BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; - conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15; + conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); return conversionDifficulty.Value; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index 2bfcd52b6a..a42d57cdd1 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns @@ -42,9 +43,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns } /// - /// Generates the pattern for , filled with hit objects. + /// Generates the patterns for , each filled with hit objects. /// - /// The containing the hit objects. - public abstract Pattern Generate(); + /// The s containing the hit objects. + public abstract IEnumerable Generate(); } } diff --git a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs index a3efd5c2bd..785cd5ab06 100644 --- a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs +++ b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs @@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Mania.MathUtils private const uint y = 842502087; private const uint z = 3579807591; private const uint w = 273326509; - private uint _x, _y = y, _z = z, _w = w; + + internal uint X { get; private set; } + internal uint Y { get; private set; } = y; + internal uint Z { get; private set; } = z; + internal uint W { get; private set; } = w; public FastRandom(int seed) { - _x = (uint)seed; + X = (uint)seed; } public FastRandom() @@ -33,11 +37,11 @@ namespace osu.Game.Rulesets.Mania.MathUtils /// 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; + uint t = X ^ X << 11; + X = Y; + Y = Z; + Z = W; + return W = W ^ W >> 19 ^ t ^ t >> 8; } /// diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json index d593b2b052..753db99856 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -1,103 +1,132 @@ { "Mappings": [{ - "StartTime": 500, - "Objects": [{ - "StartTime": 500, - "EndTime": 2500, - "Column": 0 - }, - { - "StartTime": 1500, - "EndTime": 2500, - "Column": 1 - } - ] - }, - { - "StartTime": 3000, - "Objects": [{ - "StartTime": 3000, - "EndTime": 4000, - "Column": 2 - }] - }, - { - "StartTime": 4500, - "Objects": [{ - "StartTime": 4500, - "EndTime": 5500, - "Column": 4 - }] - }, - { - "StartTime": 6000, - "Objects": [{ - "StartTime": 6000, - "EndTime": 6500, - "Column": 2 - }] - }, - { - "StartTime": 7000, - "Objects": [{ - "StartTime": 7000, - "EndTime": 8000, - "Column": 2 - }] - }, - { - "StartTime": 8500, - "Objects": [{ - "StartTime": 8500, - "EndTime": 11000, - "Column": 0 - }] - }, - { - "StartTime": 11500, - "Objects": [{ - "StartTime": 11500, - "EndTime": 12000, - "Column": 1 - }] - }, - { - "StartTime": 12500, - "Objects": [{ - "StartTime": 12500, - "EndTime": 16500, - "Column": 4 - }] - }, - { - "StartTime": 17000, - "Objects": [{ - "StartTime": 17000, - "EndTime": 18000, - "Column": 2 - }] - }, - { - "StartTime": 18500, - "Objects": [{ - "StartTime": 18500, - "EndTime": 19450, - "Column": 0 - }] - }, - { - "StartTime": 19875, - "Objects": [{ - "StartTime": 19875, - "EndTime": 23875, - "Column": 1 - }, - { - "StartTime": 19875, - "EndTime": 23875, - "Column": 0 - } - ] - } - ] + "RandomW": 2659373485, + "RandomX": 3579807591, + "RandomY": 273326509, + "RandomZ": 272969173, + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "EndTime": 2500.0, + "Column": 0 + }, { + "StartTime": 1500.0, + "EndTime": 2500.0, + "Column": 1 + }] + }, { + "RandomW": 3083803045, + "RandomX": 273326509, + "RandomY": 272969173, + "RandomZ": 2659373485, + "StartTime": 3000.0, + "Objects": [{ + "StartTime": 3000.0, + "EndTime": 4000.0, + "Column": 2 + }] + }, { + "RandomW": 4073554232, + "RandomX": 272969173, + "RandomY": 2659373485, + "RandomZ": 3083803045, + "StartTime": 4500.0, + "Objects": [{ + "StartTime": 4500.0, + "EndTime": 5500.0, + "Column": 4 + }] + }, { + "RandomW": 3420401969, + "RandomX": 2659373485, + "RandomY": 3083803045, + "RandomZ": 4073554232, + "StartTime": 6000.0, + "Objects": [{ + "StartTime": 6000.0, + "EndTime": 6500.0, + "Column": 2 + }] + }, { + "RandomW": 1129881182, + "RandomX": 3083803045, + "RandomY": 4073554232, + "RandomZ": 3420401969, + "StartTime": 7000.0, + "Objects": [{ + "StartTime": 7000.0, + "EndTime": 8000.0, + "Column": 2 + }] + }, { + "RandomW": 315568458, + "RandomX": 3420401969, + "RandomY": 1129881182, + "RandomZ": 2358617505, + "StartTime": 8500.0, + "Objects": [{ + "StartTime": 8500.0, + "EndTime": 11000.0, + "Column": 0 + }] + }, { + "RandomW": 548134043, + "RandomX": 1129881182, + "RandomY": 2358617505, + "RandomZ": 315568458, + "StartTime": 11500.0, + "Objects": [{ + "StartTime": 11500.0, + "EndTime": 12000.0, + "Column": 1 + }] + }, { + "RandomW": 3979422122, + "RandomX": 548134043, + "RandomY": 2810584254, + "RandomZ": 2250186050, + "StartTime": 12500.0, + "Objects": [{ + "StartTime": 12500.0, + "EndTime": 16500.0, + "Column": 4 + }] + }, { + "RandomW": 2466283411, + "RandomX": 2810584254, + "RandomY": 2250186050, + "RandomZ": 3979422122, + "StartTime": 17000.0, + "Objects": [{ + "StartTime": 17000.0, + "EndTime": 18000.0, + "Column": 2 + }] + }, { + "RandomW": 83157665, + "RandomX": 2250186050, + "RandomY": 3979422122, + "RandomZ": 2466283411, + "StartTime": 18500.0, + "Objects": [{ + "StartTime": 18500.0, + "EndTime": 19450.0, + "Column": 0 + }] + }, { + "RandomW": 2383087700, + "RandomX": 83157665, + "RandomY": 2055150192, + "RandomZ": 510071020, + "StartTime": 19875.0, + "Objects": [{ + "StartTime": 19875.0, + "EndTime": 23875.0, + "Column": 1 + }, { + "StartTime": 19875.0, + "EndTime": 23875.0, + "Column": 0 + }] + }] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index c8544d9cc1..f1907a92a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -93,9 +93,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.AccentColour = value; Body.AccentColour = AccentColour; Ball.AccentColour = AccentColour; - if (HasNestedHitObjects) - foreach (var drawableHitObject in NestedHitObjects) - drawableHitObject.AccentColour = AccentColour; + + foreach (var drawableHitObject in NestedHitObjects) + drawableHitObject.AccentColour = AccentColour; } } @@ -136,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.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 1628423fe8..ad203d2107 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; using osu.Game.Skinning; namespace osu.Game.Tests.Beatmaps.Formats @@ -211,5 +212,41 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == SampleInfo.HIT_CLAP)); } } + + [Test] + public void TestDecodeCustomSamples() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + using (var resStream = Resource.OpenResource("custom-samples.osu")) + using (var stream = new StreamReader(resStream)) + { + var hitObjects = decoder.Decode(stream).HitObjects; + + Assert.AreEqual("hitnormal", getTestableSampleInfo(hitObjects[0]).Name); + Assert.AreEqual("hitnormal", getTestableSampleInfo(hitObjects[1]).Name); + Assert.AreEqual("hitnormal2", getTestableSampleInfo(hitObjects[2]).Name); + Assert.AreEqual("hitnormal", getTestableSampleInfo(hitObjects[3]).Name); + } + + SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(new SampleInfo { Name = "hitnormal" }); + } + + [Test] + public void TestDecodeCustomHitObjectSamples() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + using (var resStream = Resource.OpenResource("custom-hitobject-samples.osu")) + using (var stream = new StreamReader(resStream)) + { + var hitObjects = decoder.Decode(stream).HitObjects; + + Assert.AreEqual("hit_1.wav", hitObjects[0].Samples[0].LookupNames.First()); + Assert.AreEqual("hit_2.wav", hitObjects[1].Samples[0].LookupNames.First()); + Assert.AreEqual("hitnormal2", getTestableSampleInfo(hitObjects[2]).Name); + Assert.AreEqual("hit_1.wav", hitObjects[3].Samples[0].LookupNames.First()); + } + + SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(new SampleInfo { Name = "hitnormal" }); + } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index b834be71f1..64bd563897 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -118,7 +118,11 @@ namespace osu.Game.Tests.Beatmaps.Formats public void TestParity(string beatmap) { var legacy = decode(beatmap, out Beatmap json); - json.WithDeepEqual(legacy).IgnoreProperty(r => r.DeclaringType == typeof(HitWindows)).Assert(); + json.WithDeepEqual(legacy) + .IgnoreProperty(r => r.DeclaringType == typeof(HitWindows) + // Todo: CustomSampleBank shouldn't exist going forward, we need a conversion mechanism + || r.Name == nameof(LegacyDecoder.LegacySampleControlPoint.CustomSampleBank)) + .Assert(); } /// diff --git a/osu.Game.Tests/Resources/custom-hitobject-samples.osu b/osu.Game.Tests/Resources/custom-hitobject-samples.osu new file mode 100644 index 0000000000..588672e2d9 --- /dev/null +++ b/osu.Game.Tests/Resources/custom-hitobject-samples.osu @@ -0,0 +1,16 @@ +osu file format v14 + +[General] +SampleSet: Normal + +[TimingPoints] +2170,468.75,4,1,0,40,1,0 +2638,-100,4,1,1,40,0,0 +3107,-100,4,1,2,40,0,0 +3576,-100,4,1,0,40,0,0 + +[HitObjects] +255,193,2170,1,0,0:0:0:0:hit_1.wav +256,191,2638,5,0,0:0:0:0:hit_2.wav +255,193,3107,1,0,0:0:0:0: +256,191,3576,1,0,0:0:0:0:hit_1.wav diff --git a/osu.Game.Tests/Resources/custom-samples.osu b/osu.Game.Tests/Resources/custom-samples.osu new file mode 100644 index 0000000000..1e0e6f558e --- /dev/null +++ b/osu.Game.Tests/Resources/custom-samples.osu @@ -0,0 +1,16 @@ +osu file format v14 + +[General] +SampleSet: Normal + +[TimingPoints] +2170,468.75,4,1,0,40,1,0 +2638,-100,4,1,1,40,0,0 +3107,-100,4,1,2,40,0,0 +3576,-100,4,1,0,40,0,0 + +[HitObjects] +255,193,2170,1,0,0:0:0:0: +256,191,2638,5,0,0:0:0:0: +255,193,3107,1,0,0:0:0:0: +256,191,3576,1,0,0:0:0:0: diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index f635b74030..7e329ac651 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; namespace osu.Game.Audio { @@ -32,5 +33,21 @@ namespace osu.Game.Audio /// The sample volume. /// public int Volume; + + /// + /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). + /// + public virtual IEnumerable LookupNames + { + get + { + if (!string.IsNullOrEmpty(Namespace)) + yield return $"{Namespace}/{Bank}-{Name}"; + + yield return $"{Bank}-{Name}"; // Without namespace as a fallback even when we have a namespace + } + } + + public SampleInfo Clone() => (SampleInfo)MemberwiseClone(); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index db9e712d86..9ed476d97c 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -14,6 +14,15 @@ namespace osu.Game.Beatmaps.ControlPoints public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); - public bool Equals(ControlPoint other) => Time.Equals(other?.Time); + /// + /// Whether this provides the same parametric changes as another . + /// Basically an equality check without considering the . + /// + /// The to compare to. + /// Whether this is equivalent to . + public virtual bool EquivalentTo(ControlPoint other) => true; + + public bool Equals(ControlPoint other) + => EquivalentTo(other) && Time.Equals(other?.Time); } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 9f717d21e3..526bddf51a 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -17,5 +17,10 @@ namespace osu.Game.Beatmaps.ControlPoints } private double speedMultiplier = 1; + + public override bool EquivalentTo(ControlPoint other) + => base.EquivalentTo(other) + && other is DifficultyControlPoint difficulty + && SpeedMultiplier.Equals(difficulty.SpeedMultiplier); } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 73d5232f44..dd9d568133 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -14,5 +14,11 @@ namespace osu.Game.Beatmaps.ControlPoints /// Whether the first bar line of this control point is ignored. /// public bool OmitFirstBarLine; + + public override bool EquivalentTo(ControlPoint other) + => base.EquivalentTo(other) + && other is EffectControlPoint effect + && KiaiMode.Equals(effect.KiaiMode) + && OmitFirstBarLine.Equals(effect.OmitFirstBarLine); } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 5d801a1163..acccbcde46 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -30,5 +30,25 @@ namespace osu.Game.Beatmaps.ControlPoints Name = sampleName, Volume = SampleVolume, }; + + /// + /// Applies and to a if necessary, returning the modified . + /// + /// The . This will not be modified. + /// The modified . This does not share a reference with . + public virtual SampleInfo ApplyTo(SampleInfo sampleInfo) + { + var newSampleInfo = sampleInfo.Clone(); + newSampleInfo.Bank = sampleInfo.Bank ?? SampleBank; + newSampleInfo.Name = sampleInfo.Name; + newSampleInfo.Volume = sampleInfo.Volume > 0 ? sampleInfo.Volume : SampleVolume; + return newSampleInfo; + } + + public override bool EquivalentTo(ControlPoint other) + => base.EquivalentTo(other) + && other is SampleControlPoint sample + && SampleBank.Equals(sample.SampleBank) + && SampleVolume.Equals(sample.SampleVolume); } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index d20b1b87a6..eb60133fed 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -23,5 +23,11 @@ namespace osu.Game.Beatmaps.ControlPoints } private double beatLength = 1000; + + public override bool EquivalentTo(ControlPoint other) + => base.EquivalentTo(other) + && other is TimingControlPoint timing + && TimeSignature.Equals(timing.TimeSignature) + && BeatLength.Equals(timing.BeatLength); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 581207607a..c79938e613 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -289,9 +289,9 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 4) sampleSet = (LegacySampleBank)int.Parse(split[3]); - //SampleBank sampleBank = SampleBank.Default; - //if (split.Length >= 5) - // sampleBank = (SampleBank)int.Parse(split[4]); + int customSampleBank = 0; + if (split.Length >= 5) + customSampleBank = int.Parse(split[4]); int sampleVolume = defaultSampleVolume; if (split.Length >= 6) @@ -314,13 +314,9 @@ namespace osu.Game.Beatmaps.Formats if (stringSampleSet == @"none") stringSampleSet = @"normal"; - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(time); - SampleControlPoint samplePoint = beatmap.ControlPointInfo.SamplePointAt(time); - EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); - if (timingChange) { - beatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint + handleTimingControlPoint(new TimingControlPoint { Time = time, BeatLength = beatLength, @@ -328,41 +324,68 @@ namespace osu.Game.Beatmaps.Formats }); } - if (speedMultiplier != difficultyPoint.SpeedMultiplier) + handleDifficultyControlPoint(new DifficultyControlPoint { - beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == time); - beatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint - { - Time = time, - SpeedMultiplier = speedMultiplier - }); - } + Time = time, + SpeedMultiplier = speedMultiplier + }); - if (stringSampleSet != samplePoint.SampleBank || sampleVolume != samplePoint.SampleVolume) + handleEffectControlPoint(new EffectControlPoint { - beatmap.ControlPointInfo.SamplePoints.Add(new SampleControlPoint - { - Time = time, - SampleBank = stringSampleSet, - SampleVolume = sampleVolume - }); - } + Time = time, + KiaiMode = kiaiMode, + OmitFirstBarLine = omitFirstBarSignature + }); - if (kiaiMode != effectPoint.KiaiMode || omitFirstBarSignature != effectPoint.OmitFirstBarLine) + handleSampleControlPoint(new LegacySampleControlPoint { - beatmap.ControlPointInfo.EffectPoints.Add(new EffectControlPoint - { - Time = time, - KiaiMode = kiaiMode, - OmitFirstBarLine = omitFirstBarSignature - }); - } + Time = time, + SampleBank = stringSampleSet, + SampleVolume = sampleVolume, + CustomSampleBank = customSampleBank + }); } catch (FormatException e) { } } + private void handleTimingControlPoint(TimingControlPoint newPoint) + { + beatmap.ControlPointInfo.TimingPoints.Add(newPoint); + } + + private void handleDifficultyControlPoint(DifficultyControlPoint newPoint) + { + var existing = beatmap.ControlPointInfo.DifficultyPointAt(newPoint.Time); + + if (newPoint.EquivalentTo(existing)) + return; + + beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == newPoint.Time); + beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint); + } + + private void handleEffectControlPoint(EffectControlPoint newPoint) + { + var existing = beatmap.ControlPointInfo.EffectPointAt(newPoint.Time); + + if (newPoint.EquivalentTo(existing)) + return; + + beatmap.ControlPointInfo.EffectPoints.Add(newPoint); + } + + private void handleSampleControlPoint(SampleControlPoint newPoint) + { + var existing = beatmap.ControlPointInfo.SamplePointAt(newPoint.Time); + + if (newPoint.EquivalentTo(existing)) + return; + + beatmap.ControlPointInfo.SamplePoints.Add(newPoint); + } + private void handleHitObject(string line) { // If the ruleset wasn't specified, assume the osu!standard ruleset. diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index e77efd8508..22a6acf459 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.IO; using osu.Framework.Logging; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; using OpenTK.Graphics; namespace osu.Game.Beatmaps.Formats @@ -167,5 +169,25 @@ namespace osu.Game.Beatmaps.Formats Pass = 2, Foreground = 3 } + + internal class LegacySampleControlPoint : SampleControlPoint + { + public int CustomSampleBank; + + public override SampleInfo ApplyTo(SampleInfo sampleInfo) + { + var baseInfo = base.ApplyTo(sampleInfo); + + if (CustomSampleBank > 1) + baseInfo.Name += CustomSampleBank; + + return baseInfo; + } + + public override bool EquivalentTo(ControlPoint other) + => base.EquivalentTo(other) + && other is LegacySampleControlPoint legacy + && CustomSampleBank == legacy.CustomSampleBank; + } } } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 39369350ef..d6bc4a2095 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -67,7 +67,7 @@ namespace osu.Game.Graphics.Containers return base.OnClick(state); } - public bool OnPressed(GlobalAction action) + public virtual bool OnPressed(GlobalAction action) { if (action == GlobalAction.Back) { diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 0bb33930b0..517be4c055 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -22,7 +22,9 @@ namespace osu.Game.Graphics.Cursor private readonly IBindable screenshotCursorVisibility = new Bindable(true); public override bool IsPresent => screenshotCursorVisibility.Value && base.IsPresent; - protected override Drawable CreateCursor() => new Cursor(); + protected override Drawable CreateCursor() => activeCursor = new Cursor(); + + private Cursor activeCursor; private Bindable cursorRotate; private DragRotationState dragRotationState; @@ -54,12 +56,12 @@ namespace osu.Game.Graphics.Cursor float degrees = (float)MathHelper.RadiansToDegrees(Math.Atan2(-offset.X, offset.Y)) + 24.3f; // Always rotate in the direction of least distance - float diff = (degrees - ActiveCursor.Rotation) % 360; + float diff = (degrees - activeCursor.Rotation) % 360; if (diff < -180) diff += 360; if (diff > 180) diff -= 360; - degrees = ActiveCursor.Rotation + diff; + degrees = activeCursor.Rotation + diff; - ActiveCursor.RotateTo(degrees, 600, Easing.OutQuint); + activeCursor.RotateTo(degrees, 600, Easing.OutQuint); } } @@ -68,11 +70,15 @@ namespace osu.Game.Graphics.Cursor protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - ActiveCursor.Scale = new Vector2(1); - ActiveCursor.ScaleTo(0.90f, 800, Easing.OutQuint); + // only trigger animation for main mouse buttons + if (args.Button <= MouseButton.Right) + { + activeCursor.Scale = new Vector2(1); + activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); - ((Cursor)ActiveCursor).AdditiveLayer.Alpha = 0; - ((Cursor)ActiveCursor).AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); + activeCursor.AdditiveLayer.Alpha = 0; + activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); + } if (args.Button == MouseButton.Left && cursorRotate) { @@ -86,36 +92,29 @@ namespace osu.Game.Graphics.Cursor { if (!state.Mouse.HasMainButtonPressed) { - ((Cursor)ActiveCursor).AdditiveLayer.FadeOut(500, Easing.OutQuint); - ActiveCursor.ScaleTo(1, 500, Easing.OutElastic); + activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); + 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); + activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); dragRotationState = DragRotationState.NotDragging; } return base.OnMouseUp(state, args); } - protected override bool OnClick(InputState state) - { - ((Cursor)ActiveCursor).AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); - - return base.OnClick(state); - } - protected override void PopIn() { - ActiveCursor.FadeTo(1, 250, Easing.OutQuint); - ActiveCursor.ScaleTo(1, 400, Easing.OutQuint); + activeCursor.FadeTo(1, 250, Easing.OutQuint); + activeCursor.ScaleTo(1, 400, Easing.OutQuint); } protected override void PopOut() { - ActiveCursor.FadeTo(0, 250, Easing.OutQuint); - ActiveCursor.ScaleTo(0.6f, 250, Easing.In); + activeCursor.FadeTo(0, 250, Easing.OutQuint); + activeCursor.ScaleTo(0.6f, 250, Easing.In); } public class Cursor : Container diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 83ffd415ae..b21deff509 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -39,7 +39,10 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.F4, GlobalAction.ToggleMute), new KeyBinding(InputKey.Escape, GlobalAction.Back), - new KeyBinding(InputKey.MouseButton1, GlobalAction.Back) + new KeyBinding(InputKey.MouseButton1, GlobalAction.Back), + + new KeyBinding(InputKey.Space, GlobalAction.Select), + new KeyBinding(InputKey.Enter, GlobalAction.Select), }; public IEnumerable InGameKeyBindings => new[] @@ -86,7 +89,7 @@ namespace osu.Game.Input.Bindings [Description("Toggle gameplay mouse buttons")] ToggleGameplayMouseButtons, - [Description("Go back")] + [Description("Back")] Back, [Description("Increase scroll speed")] @@ -94,5 +97,8 @@ namespace osu.Game.Input.Bindings [Description("Decrease scroll speed")] DecreaseScrollSpeed, + + [Description("Select")] + Select, } } 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 a633cf3f53..14fe670698 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -244,7 +244,8 @@ namespace osu.Game new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, - ActionRequested = action => volume.Adjust(action) + ActionRequested = action => volume.Adjust(action), + ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise), }, mainContent = new Container { RelativeSizeAxes = Axes.Both }, overlayContent = new Container { RelativeSizeAxes = Axes.Both, Depth = float.MinValue }, diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index d90a850f4a..e32d7fb036 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using OpenTK; using OpenTK.Graphics; using OpenTK.Input; @@ -69,10 +70,10 @@ namespace osu.Game.Overlays.Dialog { if (actionInvoked) return; - Hide(); - actionInvoked = true; action?.Invoke(); + + Hide(); }; } } @@ -192,16 +193,22 @@ namespace osu.Game.Overlays.Dialog }; } + public override bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Select: + Buttons.OfType().FirstOrDefault()?.TriggerOnClick(); + return true; + } + + return base.OnPressed(action); + } + 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) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 29eb1094cf..5d4d627995 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(new KeyCombination(KeyCombination.FromInputState(state).Keys.Append(state.Mouse.ScrollDelta.Y > 0 ? InputKey.MouseWheelUp : InputKey.MouseWheelDown))); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state, state.Mouse.ScrollDelta)); finalise(); return true; } diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 572b3f0c27..3a64e12b27 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -9,11 +9,13 @@ using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Volume { - public class VolumeControlReceptor : Container, IKeyBindingHandler, IHandleGlobalInput + public class VolumeControlReceptor : Container, IScrollBindingHandler, IHandleGlobalInput { public Func ActionRequested; + public Func ScrollActionRequested; public bool OnPressed(GlobalAction action) => ActionRequested?.Invoke(action) ?? false; + public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; public bool OnReleased(GlobalAction action) => false; } } diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 1d392e6ee8..9aeca1f35f 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -12,17 +12,15 @@ 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; using osu.Game.Graphics.Sprites; -using osu.Game.Input.Bindings; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Overlays.Volume { - public class VolumeMeter : Container, IKeyBindingHandler + public class VolumeMeter : Container { private CircularProgress volumeCircle; private CircularProgress volumeCircleGlow; @@ -226,59 +224,27 @@ namespace osu.Game.Overlays.Volume private const float adjust_step = 0.05f; - public void Increase() => adjust(1); - public void Decrease() => adjust(-1); - - private void adjust(int direction) - { - float amount = adjust_step * direction; - - // 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) - { - OnScroll(state); - return; - } - - Volume += amount; - } - - public bool OnPressed(GlobalAction action) - { - if (!IsHovered) return false; - - switch (action) - { - case GlobalAction.DecreaseVolume: - Decrease(); - return true; - case GlobalAction.IncreaseVolume: - Increase(); - return true; - } - - return false; - } + public void Increase(double amount = 1, bool isPrecise = false) => adjust(amount, isPrecise); + public void Decrease(double amount = 1, bool isPrecise = false) => adjust(-amount, isPrecise); // 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; + private double adjustAccumulator; + + private void adjust(double delta, bool isPrecise) + { + adjustAccumulator += delta * adjust_step * (isPrecise ? 0.1 : 1); + if (Math.Abs(adjustAccumulator) < Bindable.Precision) + return; + Volume += adjustAccumulator; + adjustAccumulator = 0; + } 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; + adjust(state.Mouse.ScrollDelta.Y, state.Mouse.HasPreciseScroll); return true; } - public bool OnReleased(GlobalAction action) => false; - private const float transition_length = 500; protected override bool OnHover(InputState state) diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 1c9c615bbb..e40597b2d4 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays muteButton.Current.ValueChanged += _ => Show(); } - public bool Adjust(GlobalAction action) + public bool Adjust(GlobalAction action, float amount = 1, bool isPrecise = false) { if (!IsLoaded) return false; @@ -103,13 +103,13 @@ namespace osu.Game.Overlays if (State == Visibility.Hidden) Show(); else - volumeMeterMaster.Decrease(); + volumeMeterMaster.Decrease(amount, isPrecise); return true; case GlobalAction.IncreaseVolume: if (State == Visibility.Hidden) Show(); else - volumeMeterMaster.Increase(); + volumeMeterMaster.Increase(amount, isPrecise); return true; case GlobalAction.ToggleMute: Show(); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 88990d435c..a22aaa784f 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. @@ -90,13 +89,12 @@ namespace osu.Game.Rulesets.Objects.Drawables if (HitObject.SampleControlPoint == null) throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo - { - Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, - Name = s.Name, - Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume, - Namespace = SampleNamespace - }).ToArray())); + + samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).ToArray(); + foreach (var s in samples) + s.Namespace = SampleNamespace; + + AddInternal(Samples = new SkinnableSound(samples)); } } @@ -206,9 +204,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/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 9edd0f1f34..95589d8953 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; @@ -196,9 +197,6 @@ namespace osu.Game.Rulesets.Objects.Legacy var bank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[0]); var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[1]); - // Let's not implement this for now, because this doesn't fit nicely into the bank structure - //string sampleFile = split2.Length > 4 ? split2[4] : string.Empty; - string stringBank = bank.ToString().ToLower(); if (stringBank == @"none") stringBank = null; @@ -211,6 +209,8 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 3) bankInfo.Volume = int.Parse(split[3]); + + bankInfo.Filename = split.Length > 4 ? split[4] : null; } /// @@ -252,6 +252,10 @@ namespace osu.Game.Rulesets.Objects.Legacy private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) { + // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario + if (!string.IsNullOrEmpty(bankInfo.Filename)) + return new List { new FileSampleInfo { Filename = bankInfo.Filename } }; + var soundTypes = new List { new SampleInfo @@ -297,14 +301,24 @@ namespace osu.Game.Rulesets.Objects.Legacy private class SampleBankInfo { + public string Filename; + public string Normal; public string Add; public int Volume; - public SampleBankInfo Clone() + public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); + } + + private class FileSampleInfo : SampleInfo + { + public string Filename; + + public override IEnumerable LookupNames => new[] { - return (SampleBankInfo)MemberwiseClone(); - } + Filename, + Path.ChangeExtension(Filename, null) + }; } [Flags] 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 745352a4d3..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); } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 374877673f..762f826425 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -11,7 +11,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Graphics; @@ -139,26 +138,15 @@ namespace osu.Game.Screens.Menu sampleBack = audio.Sample.Get(@"Menu/button-back-select"); } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (args.Repeat) return false; - - switch (args.Key) - { - case Key.Space: - logo?.TriggerOnClick(state); - return true; - } - - return false; - } - public bool OnPressed(GlobalAction action) { switch (action) { case GlobalAction.Back: return goBack(); + case GlobalAction.Select: + logo?.TriggerOnClick(); + return true; default: return false; } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 2a7daff3d0..4b893f0118 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens sampleExit = audio.Sample.Get(@"UI/screen-back"); } - public bool OnPressed(GlobalAction action) + public virtual bool OnPressed(GlobalAction action) { if (!IsCurrentScreen) return false; diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 602b03380c..3fa12283b5 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Select private readonly Box box; private readonly Box light; + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => box.ReceiveMouseInputAt(screenSpacePos); + public FooterButton() { Children = new Drawable[] diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 94c16f1797..9c62f92311 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -17,6 +17,7 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; @@ -67,6 +68,7 @@ namespace osu.Game.Screens.Select protected new readonly Bindable Ruleset = new Bindable(); private DependencyContainer dependencies; + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); @@ -464,7 +466,8 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { - if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false + && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) return; if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom()) @@ -481,16 +484,26 @@ namespace osu.Game.Screens.Select dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } + public override bool OnPressed(GlobalAction action) + { + if (!IsCurrentScreen) return false; + + switch (action) + { + case GlobalAction.Select: + FinaliseSelection(); + return true; + } + + return base.OnPressed(action); + } + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (args.Repeat) return false; switch (args.Key) { - case Key.KeypadEnter: - case Key.Enter: - FinaliseSelection(); - return true; case Key.Delete: if (state.Keyboard.ShiftPressed) { diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 1a11b0c03e..2fc9cff463 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -44,19 +44,17 @@ namespace osu.Game.Skinning private SampleChannel loadChannel(SampleInfo info, Func getSampleFunction) { - SampleChannel ch = null; + foreach (var lookup in info.LookupNames) + { + var ch = getSampleFunction($"Gameplay/{lookup}"); + if (ch == null) + continue; - if (info.Namespace != null) - ch = getSampleFunction($"Gameplay/{info.Namespace}/{info.Bank}-{info.Name}"); - - // try without namespace as a fallback. - if (ch == null) - ch = getSampleFunction($"Gameplay/{info.Bank}-{info.Name}"); - - if (ch != null) ch.Volume.Value = info.Volume / 100.0; + return ch; + } - return ch; + return null; } } } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index cf4dda52a8..ff57ef41c5 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -25,6 +25,8 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } + protected IBeatmapConverter Converter { get; private set; } + protected void Test(string name) { var ourResult = convert(name); @@ -41,14 +43,22 @@ namespace osu.Game.Tests.Beatmaps Assert.Fail($"A conversion did not generate any hitobjects, but should have, for hitobject at time: {expectedResult.Mappings[mappingCounter].StartTime}\n"); else if (mappingCounter >= expectedResult.Mappings.Count) Assert.Fail($"A conversion generated hitobjects, but should not have, for hitobject at time: {ourResult.Mappings[mappingCounter].StartTime}\n"); + else if (!expectedResult.Mappings[mappingCounter].Equals(ourResult.Mappings[mappingCounter])) + { + var expectedMapping = expectedResult.Mappings[mappingCounter]; + var ourMapping = ourResult.Mappings[mappingCounter]; + + Assert.Fail($"The conversion mapping differed for object at time {expectedMapping.StartTime}:\n" + + $"Expected {JsonConvert.SerializeObject(expectedMapping)}\n" + + $"Received: {JsonConvert.SerializeObject(ourMapping)}\n"); + } else { - var counter = mappingCounter; + var ourMapping = ourResult.Mappings[mappingCounter]; + var expectedMapping = expectedResult.Mappings[mappingCounter]; + Assert.Multiple(() => { - var ourMapping = ourResult.Mappings[counter]; - var expectedMapping = expectedResult.Mappings[counter]; - int objectCounter = 0; while (true) { @@ -60,10 +70,6 @@ namespace osu.Game.Tests.Beatmaps else if (objectCounter >= expectedMapping.Objects.Count) Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n" + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); - else if (!expectedMapping.Equals(ourMapping)) - Assert.Fail($"The conversion mapping differed for object at time {expectedMapping.StartTime}:\n" - + $"Expected {JsonConvert.SerializeObject(expectedMapping)}\n" - + $"Received: {JsonConvert.SerializeObject(ourMapping)}\n"); else if (!expectedMapping.Objects[objectCounter].Equals(ourMapping.Objects[objectCounter])) { Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}:\n" @@ -88,10 +94,11 @@ namespace osu.Game.Tests.Beatmaps var rulesetInstance = CreateRuleset(); beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); - var result = new ConvertResult(); - var converter = rulesetInstance.CreateBeatmapConverter(beatmap); + Converter = rulesetInstance.CreateBeatmapConverter(beatmap); - converter.ObjectConverted += (orig, converted) => + var result = new ConvertResult(); + + Converter.ObjectConverted += (orig, converted) => { converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); @@ -103,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps result.Mappings.Add(mapping); }; - IBeatmap convertedBeatmap = converter.Convert(); + IBeatmap convertedBeatmap = Converter.Convert(); rulesetInstance.CreateBeatmapProcessor(convertedBeatmap)?.PostProcess(); return result; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 56c33c47af..da55726447 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +