diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index cc08e08653..257155478f 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -4,7 +4,11 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using osu.Framework;
+using osu.Framework.Development;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IPC;
@@ -20,6 +24,8 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
{
+ host.ExceptionThrown += handleException;
+
if (!host.IsPrimaryInstance)
{
var importer = new ArchiveImportIPCChannel(host);
@@ -45,5 +51,24 @@ namespace osu.Desktop
return 0;
}
}
+
+ private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
+
+ ///
+ /// Allow a maximum of one unhandled exception, per second of execution.
+ ///
+ ///
+ ///
+ private static bool handleException(Exception arg)
+ {
+ bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
+
+ Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
+
+ // restore the stock of allowable exceptions after a short delay.
+ Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
+
+ return continueExecution;
+ }
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 180abd7fec..e2fc4d14f6 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -28,8 +28,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 68a8dfb7d3..15d4edc411 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0
};
}
@@ -51,7 +52,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
- NewCombo = comboData?.NewCombo ?? false
+ NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
};
}
else
@@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
};
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index d55cdac115..621fc100c2 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects
public virtual bool NewCombo { get; set; }
+ public int ComboOffset { get; set; }
+
public int IndexInCurrentCombo { get; set; }
public int ComboIndex { get; set; }
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 8d0d78120a..37a8062d75 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -173,19 +173,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects;
- int nextColumn = Random.Next(RandomStart, TotalColumns);
+ int nextColumn = GetRandomColumn();
for (int i = 0; i < Math.Min(usableColumns, noteCount); i++)
{
- while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column
- nextColumn = Random.Next(RandomStart, TotalColumns);
+ // Find available column
+ nextColumn = FindAvailableColumn(nextColumn, pattern, PreviousPattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
}
// This is can't be combined with the above loop due to RNG
for (int i = 0; i < noteCount - usableColumns; i++)
{
- while (pattern.ColumnHasObject(nextColumn))
- nextColumn = Random.Next(RandomStart, TotalColumns);
+ nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
}
@@ -210,18 +209,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
- {
- while (PreviousPattern.ColumnHasObject(nextColumn))
- nextColumn = Random.Next(RandomStart, TotalColumns);
- }
+ nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn;
for (int i = 0; i < noteCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
- while (nextColumn == lastColumn)
- nextColumn = Random.Next(RandomStart, TotalColumns);
-
+ nextColumn = FindAvailableColumn(nextColumn, validation: c => c != lastColumn);
lastColumn = nextColumn;
startTime += SegmentDuration;
}
@@ -313,7 +307,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns > 2)
addToPattern(pattern, nextColumn, startTime, startTime);
- nextColumn = Random.Next(RandomStart, TotalColumns);
+ nextColumn = GetRandomColumn();
startTime += SegmentDuration;
}
@@ -392,16 +386,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
- {
- while (PreviousPattern.ColumnHasObject(nextColumn))
- nextColumn = Random.Next(RandomStart, TotalColumns);
- }
+ nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++)
{
- while (pattern.ColumnHasObject(nextColumn))
- nextColumn = Random.Next(RandomStart, TotalColumns);
-
+ nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
startTime += SegmentDuration;
}
@@ -426,15 +415,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
- {
- while (PreviousPattern.ColumnHasObject(holdColumn))
- holdColumn = Random.Next(RandomStart, TotalColumns);
- }
+ holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note
addToPattern(pattern, holdColumn, startTime, EndTime);
- int nextColumn = Random.Next(RandomStart, TotalColumns);
+ int nextColumn = GetRandomColumn();
int noteCount;
if (ConversionDifficulty > 6.5)
noteCount = GetRandomNoteCount(0.63, 0);
@@ -455,8 +441,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
for (int j = 0; j < noteCount; j++)
{
- while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn)
- nextColumn = Random.Next(RandomStart, TotalColumns);
+ nextColumn = FindAvailableColumn(nextColumn, validation: c => c != holdColumn, patterns: rowPattern);
addToPattern(rowPattern, nextColumn, startTime, startTime);
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index 06b4b8a27e..775a4145e6 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -39,32 +39,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
addToPattern(pattern, 0, generateHold);
break;
case 8:
- addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold);
+ addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
break;
default:
if (TotalColumns > 0)
- addToPattern(pattern, getNextRandomColumn(0), generateHold);
+ addToPattern(pattern, GetRandomColumn(), generateHold);
break;
}
return pattern;
}
- ///
- /// Picks a random column after a column.
- ///
- /// The starting column.
- /// A random column after .
- private int getNextRandomColumn(int start)
- {
- int nextColumn = Random.Next(start, TotalColumns);
-
- while (PreviousPattern.ColumnHasObject(nextColumn))
- nextColumn = Random.Next(start, TotalColumns);
-
- return nextColumn;
- }
-
///
/// Constructs and adds a note to a pattern.
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 84ebfdb839..da1dd62cf5 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
- if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime));
- if (density < 0) throw new ArgumentOutOfRangeException(nameof(density));
-
StairType = lastStair;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
@@ -234,22 +231,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i < noteCount; i++)
{
- while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking)
- {
- if (convertType.HasFlag(PatternType.Gathered))
- {
- nextColumn++;
- if (nextColumn == TotalColumns)
- nextColumn = RandomStart;
- }
- else
- nextColumn = Random.Next(RandomStart, TotalColumns);
- }
+ nextColumn = allowStacking
+ ? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern)
+ : FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern });
addToPattern(pattern, nextColumn);
}
return pattern;
+
+ int getNextColumn(int last)
+ {
+ if (convertType.HasFlag(PatternType.Gathered))
+ {
+ last++;
+ if (last == TotalColumns)
+ last = RandomStart;
+ }
+ else
+ last = GetRandomColumn();
+ return last;
+ }
}
///
@@ -286,17 +288,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The containing the hit objects.
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{
+ if (convertType.HasFlag(PatternType.ForceNotStack))
+ return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
+
var pattern = new Pattern();
bool addToCentre;
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
- int nextColumn = Random.Next(RandomStart, columnLimit);
+ int nextColumn = GetRandomColumn(upperBound: columnLimit);
for (int i = 0; i < noteCount; i++)
{
- while (pattern.ColumnHasObject(nextColumn))
- nextColumn = Random.Next(RandomStart, columnLimit);
+ nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern);
// Add normal note
addToPattern(pattern, nextColumn);
@@ -368,9 +372,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
addToCentre = false;
- if (convertType.HasFlag(PatternType.ForceNotStack))
- return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
-
switch (TotalColumns)
{
case 2:
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 55081e5822..05ca1d4365 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
@@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
private double? conversionDifficulty;
+
///
/// A difficulty factor used for various conversion methods from osu!stable.
///
@@ -116,5 +118,82 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return conversionDifficulty.Value;
}
}
+
+ ///
+ /// Finds a new column in which a can be placed.
+ /// This uses to pick the next candidate column.
+ ///
+ /// The initial column to test. This may be returned if it is already a valid column.
+ /// A list of patterns for which the validity of a column should be checked against.
+ /// A column is not a valid candidate if a occupies the same column in any of the patterns.
+ /// A column for which there are no s in any of occupying the same column.
+ /// If there are no valid candidate columns.
+ protected int FindAvailableColumn(int initialColumn, params Pattern[] patterns)
+ => FindAvailableColumn(initialColumn, null, patterns: patterns);
+
+ ///
+ /// Finds a new column in which a can be placed.
+ ///
+ /// The initial column to test. This may be returned if it is already a valid column.
+ /// A function to retrieve the next column. If null, a randomisation scheme will be used.
+ /// A function to perform additional validation checks to determine if a column is a valid candidate for a .
+ /// The minimum column index. If null, is used.
+ /// The maximum column index. If null, is used.
+ /// A list of patterns for which the validity of a column should be checked against.
+ /// A column is not a valid candidate if a occupies the same column in any of the patterns.
+ /// A column which has passed the check and for which there are no
+ /// s in any of occupying the same column.
+ /// If there are no valid candidate columns.
+ protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func nextColumn = null, [InstantHandle] Func validation = null,
+ params Pattern[] patterns)
+ {
+ lowerBound = lowerBound ?? RandomStart;
+ upperBound = upperBound ?? TotalColumns;
+ nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound));
+
+ // Check for the initial column
+ if (isValid(initialColumn))
+ return initialColumn;
+
+ // Ensure that we have at least one free column, so that an endless loop is avoided
+ bool hasValidColumns = false;
+ for (int i = lowerBound.Value; i < upperBound.Value; i++)
+ {
+ hasValidColumns = isValid(i);
+ if (hasValidColumns)
+ break;
+ }
+
+ if (!hasValidColumns)
+ throw new NotEnoughColumnsException();
+
+ // Iterate until a valid column is found. This is a random iteration in the default case.
+ do
+ {
+ initialColumn = nextColumn(initialColumn);
+ } while (!isValid(initialColumn));
+
+ return initialColumn;
+
+ bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
+ }
+
+ ///
+ /// Returns a random column index in the range [, ).
+ ///
+ /// The minimum column index. If null, is used.
+ /// The maximum column index. If null, is used.
+ protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
+
+ ///
+ /// Occurs when mania conversion is stuck in an infinite loop unable to find columns to place new hitobjects in.
+ ///
+ public class NotEnoughColumnsException : Exception
+ {
+ public NotEnoughColumnsException()
+ : base("There were not enough columns to complete conversion.")
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index e493956d6e..77562bb4c2 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -70,9 +70,6 @@ namespace osu.Game.Rulesets.Mania.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
-
- Head.ApplyDefaults(controlPointInfo, difficulty);
- Tail.ApplyDefaults(controlPointInfo, difficulty);
}
protected override void CreateNestedHitObjects()
@@ -80,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Objects
base.CreateNestedHitObjects();
createTicks();
+
+ AddNested(Head);
+ AddNested(Tail);
}
private void createTicks()
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 6bf63443b5..999f84ed8e 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -26,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.UI
throw new ArgumentException("Can't have zero or fewer stages.");
GridContainer playfieldGrid;
- InternalChild = playfieldGrid = new GridContainer
+ AddInternal(playfieldGrid = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] { new Drawable[stageDefinitions.Count] }
- };
+ });
var normalColumnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1;
diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs
new file mode 100644
index 0000000000..579cb77084
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs
@@ -0,0 +1,64 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.IO;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+using Decoder = osu.Game.Beatmaps.Formats.Decoder;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class StackingTest
+ {
+ [Test]
+ public void TestStacking()
+ {
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
+ using (var reader = new StreamReader(stream))
+ {
+ var beatmap = Decoder.GetDecoder(reader).Decode(reader);
+ var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
+
+ var objects = converted.HitObjects.ToList();
+
+ // The last hitobject triggers the stacking
+ for (int i = 0; i < objects.Count - 1; i++)
+ Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
+ }
+ }
+
+ private const string beatmap_data = @"
+osu file format v14
+
+[General]
+StackLeniency: 0.2
+
+[Difficulty]
+ApproachRate:9.2
+SliderMultiplier:1
+SliderTickRate:0.5
+
+[TimingPoints]
+217871,6400,4,2,1,20,1,0
+217871,-800,4,2,1,20,0,0
+218071,-787.5,4,2,1,20,0,0
+218271,-775,4,2,1,20,0,0
+218471,-762.5,4,2,1,20,0,0
+218671,-750,4,2,1,20,0,0
+240271,-10,4,2,0,5,0,0
+
+[HitObjects]
+311,185,217871,6,0,L|318:158,1,25
+311,185,218071,2,0,L|335:170,1,25
+311,185,218271,2,0,L|338:192,1,25
+311,185,218471,2,0,L|325:209,1,25
+311,185,218671,2,0,L|304:212,1,25
+311,185,240271,5,0,0:0:0:0:
+";
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs
index c2d3aab2ab..6b67188791 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
}
- private class TestDrawableHitCircle : DrawableHitCircle
+ protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
@@ -94,6 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto;
}
+ public void TriggerJudgement() => UpdateResult(true);
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (auto && !userTriggered && timeOffset > 0)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseShaking.cs
new file mode 100644
index 0000000000..97978cff1e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseShaking.cs
@@ -0,0 +1,23 @@
+// 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.MathUtils;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseShaking : TestCaseHitCircle
+ {
+ public override void Add(Drawable drawable)
+ {
+ base.Add(drawable);
+
+ if (drawable is TestDrawableHitCircle hitObject)
+ {
+ Scheduler.AddDelayed(() => hitObject.TriggerJudgement(),
+ hitObject.HitObject.StartTime - (hitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index 405493cde4..9e0e649eb2 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
- internal class OsuBeatmapConverter : BeatmapConverter
+ public class OsuBeatmapConverter : BeatmapConverter
{
public OsuBeatmapConverter(IBeatmap beatmap)
: base(beatmap)
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset
};
}
@@ -52,7 +53,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
EndTime = endTimeData.EndTime,
- Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2
+ Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
+ NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
};
}
else
@@ -62,7 +65,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
Position = positionData?.Position ?? Vector2.Zero,
- NewCombo = comboData?.NewCombo ?? false
+ NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
index bbe2d67baa..cfb1b0f050 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
@@ -8,16 +8,16 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
- internal class OsuBeatmapProcessor : BeatmapProcessor
+ public class OsuBeatmapProcessor : BeatmapProcessor
{
public OsuBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
}
- public override void PreProcess()
+ public override void PostProcess()
{
- base.PreProcess();
+ base.PostProcess();
applyStacking((Beatmap)Beatmap);
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 9f9c2a09b6..f3b7d60cf0 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -2,14 +2,89 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
+using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Input.States;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModRelax : ModRelax
+ public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToRulesetContainer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
+
+ public bool AllowFail => false;
+
+ public void Update(Playfield playfield)
+ {
+ bool requiresHold = false;
+ bool requiresHit = false;
+
+ const float relax_leniency = 3;
+
+ foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
+ {
+ if (!(drawable is DrawableOsuHitObject osuHit))
+ continue;
+
+ double time = osuHit.Clock.CurrentTime;
+ double relativetime = time - osuHit.HitObject.StartTime;
+
+ if (time < osuHit.HitObject.StartTime - relax_leniency) continue;
+
+ if (osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime || osuHit.IsHit)
+ continue;
+
+ requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime);
+ requiresHold |= osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered) || osuHit is DrawableSpinner;
+ }
+
+ if (requiresHit)
+ {
+ addAction(false);
+ addAction(true);
+ }
+
+ addAction(requiresHold);
+ }
+
+ private bool wasHit;
+ private bool wasLeft;
+
+ private OsuInputManager osuInputManager;
+
+ private void addAction(bool hitting)
+ {
+ if (wasHit == hitting)
+ return;
+
+ wasHit = hitting;
+
+ var state = new ReplayState
+ {
+ PressedActions = new List()
+ };
+
+ if (hitting)
+ {
+ state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
+ wasLeft = !wasLeft;
+ }
+
+ osuInputManager.HandleCustomInput(new InputState(), state);
+ }
+
+ public void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ {
+ // grab the input manager for future use.
+ osuInputManager = (OsuInputManager)rulesetContainer.KeyBindingInputManager;
+ osuInputManager.AllowUserPresses = false;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 6344fbb770..4bdddcef11 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -88,7 +88,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var result = HitObject.HitWindows.ResultFor(timeOffset);
if (result == HitResult.None)
+ {
+ Shake(Math.Abs(timeOffset) - HitObject.HitWindows.HalfWindowFor(HitResult.Miss));
return;
+ }
ApplyResult(r => r.Type = result);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 0501f8b7a0..e69f340184 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using OpenTK.Graphics;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -17,12 +18,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Time.Current >= HitObject.StartTime - HitObject.TimePreempt;
+ private readonly ShakeContainer shakeContainer;
+
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
+ base.AddInternal(shakeContainer = new ShakeContainer { RelativeSizeAxes = Axes.Both });
Alpha = 0;
}
+ // Forward all internal management to shakeContainer.
+ // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
+ protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);
+ protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren);
+ protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable);
+
protected sealed override void UpdateState(ArmedState state)
{
double transformTime = HitObject.StartTime - HitObject.TimePreempt;
@@ -68,6 +78,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager);
+ protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
+
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index f48f03f197..66f491532d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -44,14 +44,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
ticks = new Container { RelativeSizeAxes = Axes.Both },
repeatPoints = new Container { RelativeSizeAxes = Axes.Both },
- Ball = new SliderBall(s)
+ Ball = new SliderBall(s, this)
{
BypassAutoSizeAxes = Axes.Both,
Scale = new Vector2(s.Scale),
AlwaysPresent = true,
Alpha = 0
},
- HeadCircle = new DrawableSliderHead(s, s.HeadCircle),
+ HeadCircle = new DrawableSliderHead(s, s.HeadCircle)
+ {
+ OnShake = Shake
+ },
TailCircle = new DrawableSliderTail(s, s.TailCircle)
};
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index e823c870f9..6d6cba4936 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using System;
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
@@ -28,5 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!IsHit)
Position = slider.CurvePositionAt(completionProgress);
}
+
+ public Action OnShake;
+
+ protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index 182cf66df8..b79750a1b3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States;
using osu.Game.Rulesets.Objects.Types;
-using OpenTK;
using OpenTK.Graphics;
using osu.Game.Skinning;
@@ -37,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider;
public readonly Drawable FollowCircle;
private Drawable drawableBall;
+ private readonly DrawableSlider drawableSlider;
- public SliderBall(Slider slider)
+ public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{
+ this.drawableSlider = drawableSlider;
this.slider = slider;
Masking = true;
AutoSizeAxes = Axes.Both;
@@ -121,9 +122,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return base.OnMouseMove(state);
}
- // If the current time is between the start and end of the slider, we should track mouse input regardless of the cursor position.
- public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => canCurrentlyTrack || base.ReceiveMouseInputAt(screenSpacePos);
-
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
{
// Consider the case of rewinding - children's transforms are handled internally, so propagating down
@@ -158,8 +156,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position.
Tracking = canCurrentlyTrack
&& lastState != null
- && base.ReceiveMouseInputAt(lastState.Mouse.NativeState.Position)
- && ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+ && ReceiveMouseInputAt(lastState.Mouse.NativeState.Position)
+ && (drawableSlider?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 48a6365c00..fdf5aaffa8 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -54,6 +54,8 @@ namespace osu.Game.Rulesets.Osu.Objects
public virtual bool NewCombo { get; set; }
+ public int ComboOffset { get; set; }
+
public virtual int IndexInCurrentCombo { get; set; }
public virtual int ComboIndex { get; set; }
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index e1a7a7c6df..1c60fd4831 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public int SpinsRequired { get; protected set; } = 1;
- public override bool NewCombo => true;
-
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index d9ae836e0a..e7bbe755a0 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -4,6 +4,8 @@
using System.Collections.Generic;
using System.ComponentModel;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.EventArgs;
+using osu.Framework.Input.States;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@@ -12,8 +14,35 @@ namespace osu.Game.Rulesets.Osu
{
public IEnumerable PressedActions => KeyBindingContainer.PressedActions;
- public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ public bool AllowUserPresses
{
+ set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value;
+ }
+
+ protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
+ => new OsuKeyBindingContainer(ruleset, variant, unique);
+
+ public OsuInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+
+ private class OsuKeyBindingContainer : RulesetKeyBindingContainer
+ {
+ public bool AllowUserPresses = true;
+
+ public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
+ : base(ruleset, variant, unique)
+ {
+ }
+
+ protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => AllowUserPresses && base.OnKeyDown(state, args);
+ protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => AllowUserPresses && base.OnKeyUp(state, args);
+ protected override bool OnJoystickPress(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickPress(state, args);
+ protected override bool OnJoystickRelease(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickRelease(state, args);
+ protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => AllowUserPresses && base.OnMouseDown(state, args);
+ protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => AllowUserPresses && base.OnMouseUp(state, args);
+ protected override bool OnScroll(InputState state) => AllowUserPresses && base.OnScroll(state);
}
}
@@ -21,6 +50,7 @@ namespace osu.Game.Rulesets.Osu
{
[Description("Left Button")]
LeftButton,
+
[Description("Right Button")]
RightButton
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 0532fe0223..abcd1ddbda 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (Shared.VertexBuffer == null)
Shared.VertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw);
- Shader.GetUniform("g_FadeClock").Value = Time;
+ Shader.GetUniform("g_FadeClock").UpdateValue(ref Time);
int updateStart = -1, updateEnd = 0;
for (int i = 0; i < Parts.Length; ++i)
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 703d8764fc..61937a535c 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.UI
public override void PostProcess()
{
- connectionLayer.HitObjects = HitObjects.Objects.Select(d => d.HitObject).OfType();
+ connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType();
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 41972b5d20..c2cde332e8 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
- if (x.Skip(1).Any())
+ if (x.Skip(1).Any() && !(first is Swell))
first.IsStrong = true;
return first;
}).ToList();
@@ -168,7 +168,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
- IsStrong = strong,
Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
};
diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
index c3ea71af3f..702bf63bf5 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using System;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Taiko.Objects
@@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
public int RequiredHits = 10;
+ public override bool IsStrong { set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); }
+
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
index 6948c5bcde..9c86b60688 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
///
- public bool IsStrong;
+ public virtual bool IsStrong { get; set; }
protected override void CreateNestedHitObjects()
{
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 400380b407..d3351f86f8 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -11,7 +11,9 @@ using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning;
namespace osu.Game.Tests.Beatmaps.Formats
@@ -186,6 +188,50 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeBeatmapComboOffsetsOsu()
+ {
+ var decoder = new LegacyBeatmapDecoder();
+ using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu"))
+ using (var stream = new StreamReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+
+ var converted = new OsuBeatmapConverter(beatmap).Convert();
+ new OsuBeatmapProcessor(converted).PreProcess();
+ new OsuBeatmapProcessor(converted).PostProcess();
+
+ Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex);
+ Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex);
+ Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex);
+ Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex);
+ Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex);
+ Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex);
+ }
+ }
+
+ [Test]
+ public void TestDecodeBeatmapComboOffsetsCatch()
+ {
+ var decoder = new LegacyBeatmapDecoder();
+ using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu"))
+ using (var stream = new StreamReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+
+ var converted = new CatchBeatmapConverter(beatmap).Convert();
+ new CatchBeatmapProcessor(converted).PreProcess();
+ new CatchBeatmapProcessor(converted).PostProcess();
+
+ Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex);
+ Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex);
+ Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex);
+ Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex);
+ Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex);
+ Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex);
+ }
+ }
+
[Test]
public void TestDecodeBeatmapHitObjects()
{
diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu
new file mode 100644
index 0000000000..c1f0dab8e9
--- /dev/null
+++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu
@@ -0,0 +1,32 @@
+osu file format v14
+
+[HitObjects]
+// Circle with combo offset (3)
+255,193,1000,49,0,0:0:0:0:
+// Combo index = 4
+
+// Slider with new combo followed by circle with no new combo
+256,192,2000,12,0,2000,0:0:0:0:
+255,193,3000,1,0,0:0:0:0:
+// Combo index = 5
+
+// Slider without new combo followed by circle with no new combo
+256,192,4000,8,0,5000,0:0:0:0:
+255,193,6000,1,0,0:0:0:0:
+// Combo index = 5
+
+// Slider without new combo followed by circle with new combo
+256,192,7000,8,0,8000,0:0:0:0:
+255,193,9000,5,0,0:0:0:0:
+// Combo index = 6
+
+// Slider with new combo and offset (1) followed by circle with new combo and offset (3)
+256,192,10000,28,0,11000,0:0:0:0:
+255,193,12000,53,0,0:0:0:0:
+// Combo index = 11
+
+// Slider with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo
+256,192,13000,44,0,14000,0:0:0:0:
+256,192,15000,8,0,16000,0:0:0:0:
+255,193,17000,1,0,0:0:0:0:
+// Combo index = 14
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
index b232180eba..175db7d246 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using NUnit.Framework;
using OpenTK;
using osu.Framework.Allocation;
@@ -116,7 +117,7 @@ namespace osu.Game.Tests.Visual
private void testNullBeatmap()
{
- selectNullBeatmap();
+ selectBeatmap(null);
AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text));
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist);
@@ -124,28 +125,19 @@ namespace osu.Game.Tests.Visual
AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
}
- private void selectBeatmap(IBeatmap b)
+ private void selectBeatmap([CanBeNull] IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
- AddStep($"select {b.Metadata.Title} beatmap", () =>
+ AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
infoBefore = infoWedge.Info;
- infoWedge.Beatmap = Beatmap.Value = new TestWorkingBeatmap(b);
+ infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b);
});
AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load");
}
- private void selectNullBeatmap()
- {
- AddStep("select null beatmap", () =>
- {
- Beatmap.Value = Beatmap.Default;
- infoWedge.Beatmap = Beatmap;
- });
- }
-
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{
List objects = new List();
diff --git a/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs b/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs
index e4cb848d90..041fce6ce3 100644
--- a/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs
+++ b/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual
[BackgroundDependencyLoader]
private void load()
{
- AddInternal(trackManager);
+ Add(trackManager);
}
[Test]
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual
TestTrackOwner owner = null;
PreviewTrack track = null;
- AddStep("get track", () => AddInternal(owner = new TestTrackOwner(track = getTrack())));
+ AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
AddStep("start", () => track.Start());
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning);
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual
{
var track = getTrack();
- AddInternal(track);
+ Add(track);
return track;
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 67f02c8ac4..1adbb4a389 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -319,17 +319,17 @@ namespace osu.Game.Beatmaps
///
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
///
- public async Task ImportFromStable()
+ public Task ImportFromStable()
{
var stable = GetStableStorage?.Invoke();
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
- return;
+ return Task.CompletedTask;
}
- await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning);
+ return Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs").Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning);
}
///
@@ -350,7 +350,11 @@ namespace osu.Game.Beatmaps
{
// let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
- if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in this beatmap archive.");
+ if (string.IsNullOrEmpty(mapName))
+ {
+ Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database);
+ return null;
+ }
Beatmap beatmap;
using (var stream = new StreamReader(reader.GetStream(mapName)))
diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs
index 0173125e8b..9d7cd673dc 100644
--- a/osu.Game/Beatmaps/BeatmapProcessor.cs
+++ b/osu.Game/Beatmaps/BeatmapProcessor.cs
@@ -27,11 +27,10 @@ namespace osu.Game.Beatmaps
if (obj.NewCombo)
{
obj.IndexInCurrentCombo = 0;
+ obj.ComboIndex = (lastObj?.ComboIndex ?? 0) + obj.ComboOffset + 1;
+
if (lastObj != null)
- {
lastObj.LastInCombo = true;
- obj.ComboIndex = lastObj.ComboIndex + 1;
- }
}
else if (lastObj != null)
{
diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs
index 2927654f62..6f45718390 100644
--- a/osu.Game/Beatmaps/Formats/Decoder.cs
+++ b/osu.Game/Beatmaps/Formats/Decoder.cs
@@ -55,11 +55,11 @@ namespace osu.Game.Beatmaps.Formats
} while (line != null && line.Length == 0);
if (line == null)
- throw new IOException(@"Unknown file format");
+ throw new IOException(@"Unknown file format (null)");
- var decoder = typedDecoders.Select(d => line.StartsWith(d.Key) ? d.Value : null).FirstOrDefault();
+ var decoder = typedDecoders.Select(d => line.StartsWith(d.Key, StringComparison.InvariantCulture) ? d.Value : null).FirstOrDefault();
if (decoder == null)
- throw new IOException(@"Unknown file format");
+ throw new IOException($@"Unknown file format ({line})");
return (Decoder)decoder.Invoke(line);
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 29be751de2..181d17932d 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -126,16 +126,16 @@ namespace osu.Game.Beatmaps.Formats
switch (beatmap.BeatmapInfo.RulesetID)
{
case 0:
- parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser();
+ parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
case 1:
- parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser();
+ parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
case 2:
- parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser();
+ parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
case 3:
- parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser();
+ parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
}
@@ -405,14 +405,11 @@ namespace osu.Game.Beatmaps.Formats
{
// If the ruleset wasn't specified, assume the osu!standard ruleset.
if (parser == null)
- parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser();
-
- var obj = parser.Parse(line, getOffsetTime());
+ parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
+ var obj = parser.Parse(line);
if (obj != null)
- {
beatmap.HitObjects.Add(obj);
- }
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 76a3d75e36..e9f37e583b 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats
if (ShouldSkipLine(line))
continue;
- if (line.StartsWith(@"[") && line.EndsWith(@"]"))
+ if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal))
{
if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section))
{
@@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps.Formats
}
}
- protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//");
+ protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal);
protected virtual void ParseLine(T output, Section section, string line)
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index a8a62013b1..a73a32325a 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
private void handleEvents(string line)
{
var depth = 0;
- while (line.StartsWith(" ") || line.StartsWith("_"))
+ while (line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal))
{
++depth;
line = line.Substring(1);
@@ -269,9 +269,9 @@ namespace osu.Game.Beatmaps.Formats
return Anchor.BottomCentre;
case LegacyOrigins.BottomRight:
return Anchor.BottomRight;
+ default:
+ return Anchor.TopLeft;
}
-
- throw new InvalidDataException($@"Unknown origin: {value}");
}
private void handleVariables(string line)
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 6c906bb1e4..085c591fce 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Beatmaps
public bool BeatmapLoaded => beatmap.IsResultAvailable;
public IBeatmap Beatmap => beatmap.Value.Result;
- public async Task GetBeatmapAsync() => await beatmap.Value;
+ public Task GetBeatmapAsync() => beatmap.Value;
private readonly AsyncLazy beatmap;
private IBeatmap populateBeatmap()
@@ -138,14 +138,14 @@ namespace osu.Game.Beatmaps
public bool BackgroundLoaded => background.IsResultAvailable;
public Texture Background => background.Value.Result;
- public async Task GetBackgroundAsync() => await background.Value;
+ public Task GetBackgroundAsync() => background.Value;
private AsyncLazy background;
private Texture populateBackground() => GetBackground();
public bool TrackLoaded => track.IsResultAvailable;
public Track Track => track.Value.Result;
- public async Task
/// The archive to create the model for.
- /// A model populated with minimal information.
+ /// A model populated with minimal information. Returning a null will abort importing silently.
protected abstract TModel CreateModel(ArchiveReader archive);
///
@@ -412,7 +415,7 @@ namespace osu.Game.Database
private ArchiveReader getReaderFrom(string path)
{
if (ZipUtils.IsZipArchive(path))
- return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path));
+ return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), Path.GetFileName(path));
if (Directory.Exists(path))
return new LegacyFilesystemReader(path);
throw new InvalidFormatException($"{path} is not a valid archive");
diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs
index e70d753114..2037612a09 100644
--- a/osu.Game/Database/DatabaseContextFactory.cs
+++ b/osu.Game/Database/DatabaseContextFactory.cs
@@ -5,7 +5,6 @@ using System;
using System.Linq;
using System.Threading;
using Microsoft.EntityFrameworkCore.Storage;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform;
namespace osu.Game.Database
@@ -118,7 +117,9 @@ namespace osu.Game.Database
private void recycleThreadContexts()
{
- threadContexts?.Values.ForEach(c => c.Dispose());
+ // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed
+ // for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts
+ threadContexts?.Value.Dispose();
threadContexts = new ThreadLocal(CreateContext, true);
}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index bf57644caf..20e144c033 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -75,6 +75,13 @@ namespace osu.Game.Database
}
}
+ ~OsuDbContext()
+ {
+ // DbContext does not contain a finalizer (https://github.com/aspnet/EntityFrameworkCore/issues/8872)
+ // This is used to clean up previous contexts when fresh contexts are exposed via DatabaseContextFactory
+ Dispose();
+ }
+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
diff --git a/osu.Game/Graphics/Containers/ShakeContainer.cs b/osu.Game/Graphics/Containers/ShakeContainer.cs
new file mode 100644
index 0000000000..fde4d59f46
--- /dev/null
+++ b/osu.Game/Graphics/Containers/ShakeContainer.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace osu.Game.Graphics.Containers
+{
+ ///
+ /// A container that adds the ability to shake its contents.
+ ///
+ public class ShakeContainer : Container
+ {
+ ///
+ /// Shake the contents of this container.
+ ///
+ /// The maximum length the shake should last.
+ public void Shake(double maximumLength)
+ {
+ const float shake_amount = 8;
+ const float shake_duration = 30;
+
+ // if we don't have enough time, don't bother shaking.
+ if (maximumLength < shake_duration * 2)
+ return;
+
+ var sequence = this.MoveToX(shake_amount, shake_duration / 2, Easing.OutSine).Then()
+ .MoveToX(-shake_amount, shake_duration, Easing.InOutSine).Then();
+
+ // if we don't have enough time for the second shake, skip it.
+ if (maximumLength > shake_duration * 4)
+ sequence = sequence
+ .MoveToX(shake_amount, shake_duration, Easing.InOutSine).Then()
+ .MoveToX(-shake_amount, shake_duration, Easing.InOutSine).Then();
+
+ sequence.MoveToX(0, shake_duration / 2, Easing.InSine);
+ }
+ }
+}
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 7b3337cb23..be253f65c1 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
-using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -19,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
+using SixLabors.ImageSharp;
namespace osu.Game.Graphics
{
@@ -71,7 +71,7 @@ namespace osu.Game.Graphics
private volatile int screenShotTasks;
- public async Task TakeScreenshotAsync() => await Task.Run(async () =>
+ public Task TakeScreenshotAsync() => Task.Run(async () =>
{
Interlocked.Increment(ref screenShotTasks);
@@ -90,7 +90,7 @@ namespace osu.Game.Graphics
waitDelegate.Cancel();
}
- using (var bitmap = await host.TakeScreenshotAsync())
+ using (var image = await host.TakeScreenshotAsync())
{
Interlocked.Decrement(ref screenShotTasks);
@@ -102,10 +102,10 @@ namespace osu.Game.Graphics
switch (screenshotFormat.Value)
{
case ScreenshotFormat.Png:
- bitmap.Save(stream, ImageFormat.Png);
+ image.SaveAsPng(stream);
break;
case ScreenshotFormat.Jpg:
- bitmap.Save(stream, ImageFormat.Jpeg);
+ image.SaveAsJpeg(stream);
break;
default:
throw new ArgumentOutOfRangeException(nameof(screenshotFormat));
diff --git a/osu.Game/Graphics/SpriteIcon.cs b/osu.Game/Graphics/SpriteIcon.cs
index 6acd20719e..b72ba7e02f 100644
--- a/osu.Game/Graphics/SpriteIcon.cs
+++ b/osu.Game/Graphics/SpriteIcon.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Graphics
if (loadableIcon == loadedIcon) return;
- var texture = store?.Get(((char)loadableIcon).ToString());
+ var texture = store.Get(((char)loadableIcon).ToString());
spriteMain.Texture = texture;
spriteShadow.Texture = texture;
@@ -129,7 +129,7 @@ namespace osu.Game.Graphics
if (icon == value) return;
icon = value;
- if (IsLoaded)
+ if (LoadState == LoadState.Loaded)
updateTexture();
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index bf3805a44d..bb6a032a12 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -1,7 +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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.EventArgs;
+using osu.Framework.Input.States;
+using osu.Game.Graphics.Sprites;
+using OpenTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
@@ -10,9 +19,73 @@ namespace osu.Game.Graphics.UserInterface
///
public class OsuButton : Button
{
+ private Box hover;
+
public OsuButton()
{
- Add(new HoverClickSounds(HoverSampleSet.Loud));
+ Height = 40;
+
+ Content.Masking = true;
+ Content.CornerRadius = 5;
}
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ BackgroundColour = colours.BlueDark;
+
+ AddRange(new Drawable[]
+ {
+ hover = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingMode.Additive,
+ Colour = Color4.White.Opacity(0.1f),
+ Alpha = 0,
+ Depth = -1
+ },
+ new HoverClickSounds(HoverSampleSet.Loud),
+ });
+
+ Enabled.ValueChanged += enabled_ValueChanged;
+ Enabled.TriggerChange();
+ }
+
+ private void enabled_ValueChanged(bool enabled)
+ {
+ this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
+ }
+
+ protected override bool OnHover(InputState state)
+ {
+ hover.FadeIn(200);
+ return base.OnHover(state);
+ }
+
+ protected override void OnHoverLost(InputState state)
+ {
+ hover.FadeOut(200);
+ base.OnHoverLost(state);
+ }
+
+ protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
+ {
+ Content.ScaleTo(0.9f, 4000, Easing.OutQuint);
+ return base.OnMouseDown(state, args);
+ }
+
+ protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
+ {
+ Content.ScaleTo(1, 1000, Easing.OutElastic);
+ return base.OnMouseUp(state, args);
+ }
+
+ protected override SpriteText CreateText() => new OsuSpriteText
+ {
+ Depth = -1,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Font = @"Exo2.0-Bold",
+ };
}
}
diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs
index bfdc0c3bef..683b442d93 100644
--- a/osu.Game/Graphics/UserInterface/TriangleButton.cs
+++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs
@@ -2,17 +2,10 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
-using OpenTK.Graphics;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.EventArgs;
-using osu.Framework.Input.States;
using osu.Game.Graphics.Backgrounds;
-using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
{
@@ -21,79 +14,17 @@ namespace osu.Game.Graphics.UserInterface
///
public class TriangleButton : OsuButton, IFilterable
{
- private Box hover;
-
- protected Triangles Triangles;
-
- public TriangleButton()
- {
- Height = 40;
- }
-
- protected override SpriteText CreateText() => new OsuSpriteText
- {
- Depth = -1,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Font = @"Exo2.0-Bold",
- };
+ protected Triangles Triangles { get; private set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = colours.BlueDark;
-
- Content.Masking = true;
- Content.CornerRadius = 5;
-
- AddRange(new Drawable[]
+ Add(Triangles = new Triangles
{
- Triangles = new Triangles
- {
- RelativeSizeAxes = Axes.Both,
- ColourDark = colours.BlueDarker,
- ColourLight = colours.Blue,
- },
- hover = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Blending = BlendingMode.Additive,
- Colour = Color4.White.Opacity(0.1f),
- Alpha = 0,
- },
+ RelativeSizeAxes = Axes.Both,
+ ColourDark = colours.BlueDarker,
+ ColourLight = colours.Blue,
});
-
- Enabled.ValueChanged += enabled_ValueChanged;
- Enabled.TriggerChange();
- }
-
- private void enabled_ValueChanged(bool enabled)
- {
- this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
- }
-
- protected override bool OnHover(InputState state)
- {
- hover.FadeIn(200);
- return base.OnHover(state);
- }
-
- protected override void OnHoverLost(InputState state)
- {
- hover.FadeOut(200);
- base.OnHoverLost(state);
- }
-
- protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
- {
- Content.ScaleTo(0.9f, 4000, Easing.OutQuint);
- return base.OnMouseDown(state, args);
- }
-
- protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
- {
- Content.ScaleTo(1, 1000, Easing.OutElastic);
- return base.OnMouseUp(state, args);
}
public IEnumerable FilterTerms => new[] { Text };
diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs
index 808ce159bb..24a5094586 100644
--- a/osu.Game/IO/Archives/ArchiveReader.cs
+++ b/osu.Game/IO/Archives/ArchiveReader.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Threading.Tasks;
using osu.Framework.IO.Stores;
namespace osu.Game.IO.Archives
@@ -28,7 +29,9 @@ namespace osu.Game.IO.Archives
public abstract IEnumerable Filenames { get; }
- public virtual byte[] Get(string name)
+ public virtual byte[] Get(string name) => GetAsync(name).Result;
+
+ public async Task GetAsync(string name)
{
using (Stream input = GetStream(name))
{
@@ -36,7 +39,7 @@ namespace osu.Game.IO.Archives
return null;
byte[] buffer = new byte[input.Length];
- input.Read(buffer, 0, buffer.Length);
+ await input.ReadAsync(buffer, 0, buffer.Length);
return buffer;
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index a1e385921f..7f0576608d 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -36,6 +36,8 @@ using osu.Game.Skinning;
using OpenTK.Graphics;
using osu.Game.Overlays.Volume;
using osu.Game.Screens.Select;
+using osu.Game.Utils;
+using LogLevel = osu.Framework.Logging.LogLevel;
namespace osu.Game
{
@@ -65,16 +67,18 @@ namespace osu.Game
private ScreenshotManager screenshotManager;
+ protected RavenLogger RavenLogger;
+
public virtual Storage GetStorageForStableInstall() => null;
private Intro intro
{
get
{
- Screen s = screenStack;
- while (s != null && !(s is Intro))
- s = s.ChildScreen;
- return s as Intro;
+ Screen screen = screenStack;
+ while (screen != null && !(screen is Intro))
+ screen = screen.ChildScreen;
+ return screen as Intro;
}
}
@@ -108,6 +112,8 @@ namespace osu.Game
this.args = args;
forwardLoggedErrorsToNotifications();
+
+ RavenLogger = new RavenLogger(this);
}
public void ToggleSettings() => settings.ToggleVisibility();
@@ -120,8 +126,8 @@ namespace osu.Game
/// Whether the toolbar should also be hidden.
public void CloseAllOverlays(bool toolbar = true)
{
- foreach (var o in overlays)
- o.State = Visibility.Hidden;
+ foreach (var overlay in overlays)
+ overlay.State = Visibility.Hidden;
if (toolbar) Toolbar.State = Visibility.Hidden;
}
@@ -145,13 +151,15 @@ namespace osu.Game
if (args?.Length > 0)
{
- var paths = args.Where(a => !a.StartsWith(@"-"));
-
- Task.Run(() => Import(paths.ToArray()));
+ var paths = args.Where(a => !a.StartsWith(@"-")).ToArray();
+ if (paths.Length > 0)
+ Task.Run(() => Import(paths));
}
dependencies.CacheAs(this);
+ dependencies.Cache(RavenLogger);
+
dependencies.CacheAs(ruleset);
dependencies.CacheAs>(ruleset);
@@ -236,7 +244,7 @@ namespace osu.Game
/// The beatmap to show.
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
- protected void LoadScore(Score s)
+ protected void LoadScore(Score score)
{
scoreLoad?.Cancel();
@@ -244,18 +252,18 @@ namespace osu.Game
if (menu == null)
{
- scoreLoad = Schedule(() => LoadScore(s));
+ scoreLoad = Schedule(() => LoadScore(score));
return;
}
if (!menu.IsCurrentScreen)
{
menu.MakeCurrent();
- this.Delay(500).Schedule(() => LoadScore(s), out scoreLoad);
+ this.Delay(500).Schedule(() => LoadScore(score), out scoreLoad);
return;
}
- if (s.Beatmap == null)
+ if (score.Beatmap == null)
{
notifications.Post(new SimpleNotification
{
@@ -265,12 +273,18 @@ namespace osu.Game
return;
}
- ruleset.Value = s.Ruleset;
+ ruleset.Value = score.Ruleset;
- Beatmap.Value = BeatmapManager.GetWorkingBeatmap(s.Beatmap);
- Beatmap.Value.Mods.Value = s.Mods;
+ Beatmap.Value = BeatmapManager.GetWorkingBeatmap(score.Beatmap);
+ Beatmap.Value.Mods.Value = score.Mods;
- menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay)));
+ menu.Push(new PlayerLoader(new ReplayPlayer(score.Replay)));
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ RavenLogger.Dispose();
}
protected override void LoadComplete()
@@ -449,7 +463,7 @@ namespace osu.Game
Schedule(() => notifications.Post(new SimpleNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb,
- Text = entry.Message,
+ Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}
else if (recentLogCount == short_term_display_limit)
@@ -490,7 +504,27 @@ namespace osu.Game
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
// with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile,
// we could avoid the need for scheduling altogether.
- Schedule(() => { asyncLoadStream = asyncLoadStream?.ContinueWith(t => LoadComponentAsync(d, add).Wait()) ?? LoadComponentAsync(d, add); });
+ Schedule(() =>
+ {
+ var previousLoadStream = asyncLoadStream;
+
+ //chain with existing load stream
+ asyncLoadStream = Task.Run(async () =>
+ {
+ if (previousLoadStream != null)
+ await previousLoadStream;
+
+ try
+ {
+ Logger.Log($"Loading {d}...", LoggingTarget.Debug);
+ await LoadComponentAsync(d, add);
+ Logger.Log($"Loaded {d}!", LoggingTarget.Debug);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ });
+ });
}
public bool OnPressed(GlobalAction action)
@@ -601,6 +635,7 @@ namespace osu.Game
private void screenAdded(Screen newScreen)
{
currentScreen = (OsuScreen)newScreen;
+ Logger.Log($"Screen changed → {currentScreen}");
newScreen.ModePushed += screenAdded;
newScreen.Exited += screenRemoved;
@@ -609,6 +644,7 @@ namespace osu.Game
private void screenRemoved(Screen newScreen)
{
currentScreen = (OsuScreen)newScreen;
+ Logger.Log($"Screen changed ← {currentScreen}");
if (newScreen == null)
Exit();
diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs
index 423211659d..f63d314053 100644
--- a/osu.Game/Overlays/DirectOverlay.cs
+++ b/osu.Game/Overlays/DirectOverlay.cs
@@ -240,6 +240,15 @@ namespace osu.Game.Overlays
});
}
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ // Queries are allowed to be run only on the first pop-in
+ if (getSetsRequest == null)
+ Scheduler.AddOnce(updateSearch);
+ }
+
private SearchBeatmapSetsRequest getSetsRequest;
private readonly Bindable currentQuery = new Bindable();
@@ -251,16 +260,22 @@ namespace osu.Game.Overlays
{
queryChangedDebounce?.Cancel();
- if (!IsLoaded) return;
+ if (!IsLoaded)
+ return;
+
+ if (State == Visibility.Hidden)
+ return;
BeatmapSets = null;
ResultAmounts = null;
getSetsRequest?.Cancel();
- if (api == null) return;
+ if (api == null)
+ return;
- if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return;
+ if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty))
+ return;
previewTrackManager.StopAnyPlaying(this);
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 45703ac56d..e83dedaf35 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -27,6 +27,11 @@ namespace osu.Game.Overlays.Mods
{
public class ModSelectOverlay : WaveOverlayContainer
{
+ ///
+ /// How much this container should overflow the sides of the screen to account for parallax shifting.
+ ///
+ private const float overflow_padding = 50;
+
private const float content_width = 0.8f;
protected Color4 LowMultiplierColour, HighMultiplierColour;
@@ -199,6 +204,11 @@ namespace osu.Game.Overlays.Mods
Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e");
Height = 510;
+ Padding = new MarginPadding
+ {
+ Left = -overflow_padding,
+ Right = -overflow_padding
+ };
Children = new Drawable[]
{
@@ -258,6 +268,11 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Width = content_width,
+ Padding = new MarginPadding
+ {
+ Left = overflow_padding,
+ Right = overflow_padding
+ },
Children = new Drawable[]
{
new OsuSpriteText
@@ -295,7 +310,12 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Vertical = 10 },
+ Padding = new MarginPadding
+ {
+ Vertical = 10,
+ Left = overflow_padding,
+ Right = overflow_padding
+ },
Child = ModSectionsContainer = new FillFlowContainer
{
Origin = Anchor.TopCentre,
@@ -341,7 +361,9 @@ namespace osu.Game.Overlays.Mods
Direction = FillDirection.Horizontal,
Padding = new MarginPadding
{
- Vertical = 15
+ Vertical = 15,
+ Left = overflow_padding,
+ Right = overflow_padding
},
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index d891cd96e8..78f8f57343 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -96,8 +96,7 @@ namespace osu.Game.Overlays
base.LoadComplete();
StateChanged += _ => updateProcessingMode();
- OverlayActivationMode.ValueChanged += _ => updateProcessingMode();
- OverlayActivationMode.TriggerChange();
+ OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true);
}
private int totalCount => sections.Select(c => c.DisplayedCount).Sum();
diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs
index 9f550413f3..b14a4b8773 100644
--- a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs
@@ -13,15 +13,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
{
protected override string Header => "Garbage Collector";
+ private readonly Bindable latencyMode = new Bindable();
+ private Bindable configLatencyMode;
+
[BackgroundDependencyLoader]
private void load(FrameworkDebugConfigManager config)
{
Children = new Drawable[]
{
- new SettingsEnumDropdown
+ new SettingsEnumDropdown
{
LabelText = "Active mode",
- Bindable = config.GetBindable(DebugSetting.ActiveGCMode)
+ Bindable = latencyMode
},
new SettingsButton
{
@@ -29,6 +32,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
Action = GC.Collect
},
};
+
+ configLatencyMode = config.GetBindable(DebugSetting.ActiveGCMode);
+ configLatencyMode.BindValueChanged(v => latencyMode.Value = (LatencyMode)v, true);
+ latencyMode.BindValueChanged(v => configLatencyMode.Value = (GCLatencyMode)v);
+ }
+
+ private enum LatencyMode
+ {
+ Batch = GCLatencyMode.Batch,
+ Interactive = GCLatencyMode.Interactive,
+ LowLatency = GCLatencyMode.LowLatency,
+ SustainedLowLatency = GCLatencyMode.SustainedLowLatency
}
}
}
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index ac2c840c94..28e2b773ec 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.States;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -16,7 +17,7 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
- public class SidebarButton : OsuButton
+ public class SidebarButton : Button
{
private readonly SpriteIcon drawableIcon;
private readonly SpriteText headerText;
@@ -97,7 +98,8 @@ namespace osu.Game.Overlays.Settings
Width = 5,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- }
+ },
+ new HoverClickSounds(HoverSampleSet.Loud),
});
}
diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
new file mode 100644
index 0000000000..be879759bd
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
@@ -0,0 +1,12 @@
+// 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;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public interface IUpdatableByPlayfield : IApplicableMod
+ {
+ void Update(Playfield playfield);
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 2abb2eb289..7e3e955740 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -147,17 +147,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
public void PlaySamples() => Samples?.Play();
- private double lastUpdateTime;
-
protected override void Update()
{
base.Update();
- if (Result != null && lastUpdateTime > Time.Current)
+ if (Result != null && Result.HasResult)
{
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
- if (Result.TimeOffset + endTime < Time.Current)
+ if (Result.TimeOffset + endTime > Time.Current)
{
OnRevertResult?.Invoke(this, Result);
@@ -165,8 +163,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
State.Value = ArmedState.Idle;
}
}
-
- lastUpdateTime = Time.Current;
}
protected override void UpdateAfterChildren()
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs
index 50035ea116..0573a08361 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs
@@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
public float X { get; set; }
public bool NewCombo { get; set; }
+
+ public int ComboOffset { get; set; }
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
index c7451dc978..802080aedb 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
@@ -13,21 +13,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
///
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
- protected override HitObject CreateHit(Vector2 position, bool newCombo)
+ public ConvertHitObjectParser(double offset, int formatVersion)
+ : base(offset, formatVersion)
{
+ }
+
+ private bool forceNewCombo;
+ private int extraComboOffset;
+
+ protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
+ {
+ newCombo |= forceNewCombo;
+ comboOffset += extraComboOffset;
+
+ forceNewCombo = false;
+ extraComboOffset = 0;
+
return new ConvertHit
{
X = position.X,
NewCombo = newCombo,
+ ComboOffset = comboOffset
};
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
{
+ newCombo |= forceNewCombo;
+ comboOffset += extraComboOffset;
+
+ forceNewCombo = false;
+ extraComboOffset = 0;
+
return new ConvertSlider
{
X = position.X,
- NewCombo = newCombo,
+ NewCombo = FirstObject || newCombo,
+ ComboOffset = comboOffset,
ControlPoints = controlPoints,
Distance = length,
CurveType = curveType,
@@ -36,15 +58,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
};
}
- protected override HitObject CreateSpinner(Vector2 position, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
+ // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo
+ // Their combo offset is still added to that next hitobject's combo index
+ forceNewCombo |= FormatVersion <= 8 || newCombo;
+ extraComboOffset += comboOffset;
+
return new ConvertSpinner
{
EndTime = endTime
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
return null;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs
index 73e277a125..a187caaa26 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs
@@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
public float X { get; set; }
public bool NewCombo { get; set; }
+
+ public int ComboOffset { get; set; }
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
index 9dfe12f25e..db79ca60f1 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
@@ -8,10 +8,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
///
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : HitObject, IHasEndTime
+ internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasCombo
{
public double EndTime { get; set; }
public double Duration => EndTime - StartTime;
+
+ public bool NewCombo { get; set; }
+
+ public int ComboOffset { get; set; }
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index c48060bfa9..72168a4cd2 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -10,6 +10,8 @@ using System.IO;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Logging;
using osu.Framework.MathUtils;
namespace osu.Game.Rulesets.Objects.Legacy
@@ -19,12 +21,26 @@ namespace osu.Game.Rulesets.Objects.Legacy
///
public abstract class ConvertHitObjectParser : HitObjectParser
{
- public override HitObject Parse(string text)
+ ///
+ /// The offset to apply to all time values.
+ ///
+ protected readonly double Offset;
+
+ ///
+ /// The beatmap version.
+ ///
+ protected readonly int FormatVersion;
+
+ protected bool FirstObject { get; private set; } = true;
+
+ protected ConvertHitObjectParser(double offset, int formatVersion)
{
- return Parse(text, 0);
+ Offset = offset;
+ FormatVersion = formatVersion;
}
- public HitObject Parse(string text, double offset)
+ [CanBeNull]
+ public override HitObject Parse(string text)
{
try
{
@@ -32,7 +48,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture));
- ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]) & ~ConvertHitObjectType.ColourHax;
+ ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]);
+
+ int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4;
+ type &= ~ConvertHitObjectType.ComboOffset;
+
bool combo = type.HasFlag(ConvertHitObjectType.NewCombo);
type &= ~ConvertHitObjectType.NewCombo;
@@ -43,7 +63,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (type.HasFlag(ConvertHitObjectType.Circle))
{
- result = CreateHit(pos, combo);
+ result = CreateHit(pos, combo, comboOffset);
if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo);
@@ -148,11 +168,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
- result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples);
+ result = CreateSlider(pos, combo, comboOffset, points, length, curveType, repeatCount, nodeSamples);
}
else if (type.HasFlag(ConvertHitObjectType.Spinner))
{
- result = CreateSpinner(new Vector2(512, 384) / 2, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + offset);
+ result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + Offset);
if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo);
@@ -170,15 +190,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
}
- result = CreateHold(pos, combo, endTime + offset);
+ result = CreateHold(pos, combo, comboOffset, endTime + Offset);
}
if (result == null)
- throw new InvalidOperationException($@"Unknown hit object type {type}.");
+ {
+ Logger.Log($"Unknown hit object type: {type}. Skipped.", level: LogLevel.Error);
+ return null;
+ }
- result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + offset;
+ result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + Offset;
result.Samples = convertSoundType(soundType, bankInfo);
+ FirstObject = false;
+
return result;
}
catch (FormatException)
@@ -221,37 +246,42 @@ namespace osu.Game.Rulesets.Objects.Legacy
///
/// The position of the hit object.
/// Whether the hit object creates a new combo.
+ /// When starting a new combo, the offset of the new combo relative to the current one.
/// The hit object.
- protected abstract HitObject CreateHit(Vector2 position, bool newCombo);
+ protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset);
///
/// Creats a legacy Slider-type hit object.
///
/// The position of the hit object.
/// Whether the hit object creates a new combo.
+ /// When starting a new combo, the offset of the new combo relative to the current one.
/// The slider control points.
/// The slider length.
/// The slider curve type.
/// The slider repeat count.
/// The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.
/// The hit object.
- protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples);
+ protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples);
///
/// Creates a legacy Spinner-type hit object.
///
/// The position of the hit object.
+ /// Whether the hit object creates a new combo.
+ /// When starting a new combo, the offset of the new combo relative to the current one.
/// The spinner end time.
/// The hit object.
- protected abstract HitObject CreateSpinner(Vector2 position, double endTime);
+ protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime);
///
/// Creates a legacy Hold-type hit object.
///
/// The position of the hit object.
/// Whether the hit object creates a new combo.
+ /// When starting a new combo, the offset of the new combo relative to the current one.
/// The hold end time.
- protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime);
+ protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime);
private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
{
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs
index c0626c3e56..fa47e56de7 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
Slider = 1 << 1,
NewCombo = 1 << 2,
Spinner = 1 << 3,
- ColourHax = 112,
+ ComboOffset = 1 << 4 | 1 << 5 | 1 << 6,
Hold = 1 << 7
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
index ea4e7f6907..cbc8d2d4df 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
@@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
///
/// Legacy osu!mania Hit-type, used for parsing Beatmaps.
///
- internal sealed class ConvertHit : HitObject, IHasXPosition, IHasCombo
+ internal sealed class ConvertHit : HitObject, IHasXPosition
{
public float X { get; set; }
- public bool NewCombo { get; set; }
-
protected override HitWindows CreateHitWindows() => null;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
index 99ba1304e8..6f59965e18 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
@@ -13,21 +13,24 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
///
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
- protected override HitObject CreateHit(Vector2 position, bool newCombo)
+ public ConvertHitObjectParser(double offset, int formatVersion)
+ : base(offset, formatVersion)
+ {
+ }
+
+ protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
return new ConvertHit
{
- X = position.X,
- NewCombo = newCombo,
+ X = position.X
};
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
{
return new ConvertSlider
{
X = position.X,
- NewCombo = newCombo,
ControlPoints = controlPoints,
Distance = length,
CurveType = curveType,
@@ -36,7 +39,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
};
}
- protected override HitObject CreateSpinner(Vector2 position, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
return new ConvertSpinner
{
@@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
return new ConvertHold
{
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
index a8d7b23df1..e1572889a3 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
@@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
///
/// Legacy osu!mania Slider-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition, IHasCombo
+ internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition
{
public float X { get; set; }
- public bool NewCombo { get; set; }
-
protected override HitWindows CreateHitWindows() => null;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs
index f015272b2c..0062e83a28 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs
@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
public bool NewCombo { get; set; }
+ public int ComboOffset { get; set; }
+
protected override HitWindows CreateHitWindows() => null;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
index 801e4ea449..acd0de8688 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
@@ -14,21 +14,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
///
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
- protected override HitObject CreateHit(Vector2 position, bool newCombo)
+ public ConvertHitObjectParser(double offset, int formatVersion)
+ : base(offset, formatVersion)
{
+ }
+
+ private bool forceNewCombo;
+ private int extraComboOffset;
+
+ protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
+ {
+ newCombo |= forceNewCombo;
+ comboOffset += extraComboOffset;
+
+ forceNewCombo = false;
+ extraComboOffset = 0;
+
return new ConvertHit
{
Position = position,
- NewCombo = newCombo,
+ NewCombo = FirstObject || newCombo,
+ ComboOffset = comboOffset
};
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
{
+ newCombo |= forceNewCombo;
+ comboOffset += extraComboOffset;
+
+ forceNewCombo = false;
+ extraComboOffset = 0;
+
return new ConvertSlider
{
Position = position,
- NewCombo = newCombo,
+ NewCombo = FirstObject || newCombo,
+ ComboOffset = comboOffset,
ControlPoints = controlPoints,
Distance = Math.Max(0, length),
CurveType = curveType,
@@ -37,8 +59,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
};
}
- protected override HitObject CreateSpinner(Vector2 position, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
+ // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo
+ // Their combo offset is still added to that next hitobject's combo index
+ forceNewCombo |= FormatVersion <= 8 || newCombo;
+ extraComboOffset += comboOffset;
+
return new ConvertSpinner
{
Position = position,
@@ -46,7 +73,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
return null;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs
index ec5a002bbb..45f7bc9e67 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs
@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
public bool NewCombo { get; set; }
+ public int ComboOffset { get; set; }
+
protected override HitWindows CreateHitWindows() => null;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
index 0141785f31..3b349d9704 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
///
/// Legacy osu! Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition
+ internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition, IHasCombo
{
public double EndTime { get; set; }
@@ -22,5 +22,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
public float Y => Position.Y;
protected override HitWindows CreateHitWindows() => null;
+
+ public bool NewCombo { get; set; }
+
+ public int ComboOffset { get; set; }
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
index 5e9786c84a..66e504bf22 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
@@ -1,17 +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.Objects.Types;
-
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
///
/// Legacy osu!taiko Hit-type, used for parsing Beatmaps.
///
- internal sealed class ConvertHit : HitObject, IHasCombo
+ internal sealed class ConvertHit : HitObject
{
- public bool NewCombo { get; set; }
-
protected override HitWindows CreateHitWindows() => null;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
index 03b1a3187a..e5904825c2 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
@@ -13,19 +13,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
///
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
- protected override HitObject CreateHit(Vector2 position, bool newCombo)
+ public ConvertHitObjectParser(double offset, int formatVersion)
+ : base(offset, formatVersion)
{
- return new ConvertHit
- {
- NewCombo = newCombo,
- };
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
+ protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
+ {
+ return new ConvertHit();
+ }
+
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples)
{
return new ConvertSlider
{
- NewCombo = newCombo,
ControlPoints = controlPoints,
Distance = length,
CurveType = curveType,
@@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
};
}
- protected override HitObject CreateSpinner(Vector2 position, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
return new ConvertSpinner
{
@@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{
return null;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
index 8a9a0db0a7..11c0a2baae 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
@@ -1,17 +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.Objects.Types;
-
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
///
/// Legacy osu!taiko Slider-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasCombo
+ internal sealed class ConvertSlider : Legacy.ConvertSlider
{
- public bool NewCombo { get; set; }
-
protected override HitWindows CreateHitWindows() => null;
}
}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs
index cb8b6f495a..95f1a1cb3d 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs
@@ -12,5 +12,10 @@ namespace osu.Game.Rulesets.Objects.Types
/// Whether the HitObject starts a new combo.
///
bool NewCombo { get; }
+
+ ///
+ /// When starting a new combo, the offset of the new combo relative to the current one.
+ ///
+ int ComboOffset { get; }
}
}
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index dab8f7304e..e090a18eda 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -9,6 +9,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Configuration;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.UI
{
@@ -17,12 +19,12 @@ namespace osu.Game.Rulesets.UI
///
/// The contained in this Playfield.
///
- public HitObjectContainer HitObjects { get; private set; }
+ public HitObjectContainer HitObjectContainer { get; private set; }
///
/// All the s contained in this and all .
///
- public IEnumerable AllHitObjects => HitObjects?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty();
+ public IEnumerable AllHitObjects => HitObjectContainer?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty();
///
/// All s nested inside this .
@@ -51,13 +53,17 @@ namespace osu.Game.Rulesets.UI
RelativeSizeAxes = Axes.Both;
}
- [BackgroundDependencyLoader]
- private void load()
- {
- HitObjects = CreateHitObjectContainer();
- HitObjects.RelativeSizeAxes = Axes.Both;
+ private WorkingBeatmap beatmap;
- Add(HitObjects);
+ [BackgroundDependencyLoader]
+ private void load(IBindableBeatmap beatmap)
+ {
+ this.beatmap = beatmap.Value;
+
+ HitObjectContainer = CreateHitObjectContainer();
+ HitObjectContainer.RelativeSizeAxes = Axes.Both;
+
+ Add(HitObjectContainer);
}
///
@@ -69,13 +75,13 @@ namespace osu.Game.Rulesets.UI
/// Adds a DrawableHitObject to this Playfield.
///
/// The DrawableHitObject to add.
- public virtual void Add(DrawableHitObject h) => HitObjects.Add(h);
+ public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h);
///
/// Remove a DrawableHitObject from this Playfield.
///
/// The DrawableHitObject to remove.
- public virtual void Remove(DrawableHitObject h) => HitObjects.Remove(h);
+ public virtual void Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
///
/// Registers a as a nested .
@@ -92,5 +98,15 @@ namespace osu.Game.Rulesets.UI
/// Creates the container that will be used to contain the s.
///
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (beatmap != null)
+ foreach (var mod in beatmap.Mods.Value)
+ if (mod is IUpdatableByPlayfield updatable)
+ updatable.Update(this);
+ }
}
}
diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs
index 64ee680d45..a830803fb1 100644
--- a/osu.Game/Rulesets/UI/RulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/RulesetContainer.cs
@@ -306,7 +306,7 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess();
foreach (var mod in Mods.OfType())
- mod.ApplyToDrawableHitObjects(Playfield.HitObjects.Objects);
+ mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects);
}
protected override void Update()
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index 7146ad8064..ec73c0fb14 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
/// The container that contains the s.
///
- public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
+ public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)HitObjectContainer;
///
/// The direction in which s in this should scroll.
diff --git a/osu.Game/Screens/Edit/Components/CircularButton.cs b/osu.Game/Screens/Edit/Components/CircularButton.cs
new file mode 100644
index 0000000000..a8ad242772
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/CircularButton.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Graphics.UserInterface;
+using OpenTK;
+
+namespace osu.Game.Screens.Edit.Components
+{
+ public class CircularButton : OsuButton
+ {
+ private const float width = 125;
+ private const float height = 30;
+
+ public CircularButton()
+ {
+ Size = new Vector2(width, height);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ Content.CornerRadius = DrawHeight / 2f;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs
index c3b3e747fd..3cef20e510 100644
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shaders;
using osu.Game.Screens.Menu;
using OpenTK;
using osu.Framework.Screens;
+using osu.Game.Overlays;
namespace osu.Game.Screens
{
@@ -18,6 +19,8 @@ namespace osu.Game.Screens
protected override bool HideOverlaysOnEnter => true;
+ protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled;
+
protected override bool AllowBackButton => false;
public Loader()
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index ce00686c02..b9a799328e 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -174,6 +174,9 @@ namespace osu.Game.Screens.Menu
ButtonSystemState lastState = state;
state = value;
+ if (game != null)
+ game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All;
+
updateLogoState(lastState);
Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}");
@@ -205,11 +208,7 @@ namespace osu.Game.Screens.Menu
{
logoTracking = false;
- if (game != null)
- {
- game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All;
- game.Toolbar.Hide();
- }
+ game?.Toolbar.Hide();
logo.ClearTransforms(targetMember: nameof(Position));
logo.RelativePositionAxes = Axes.Both;
@@ -243,11 +242,7 @@ namespace osu.Game.Screens.Menu
if (impact)
logo.Impact();
- if (game != null)
- {
- game.OverlayActivationMode.Value = OverlayActivation.All;
- game.Toolbar.State = Visibility.Visible;
- }
+ game?.Toolbar.Show();
}, 200);
break;
default:
@@ -278,7 +273,7 @@ namespace osu.Game.Screens.Menu
if (logo != null)
{
- if (logoTracking && iconFacade.IsLoaded)
+ if (logoTracking && logo.RelativePositionAxes == Axes.None && iconFacade.IsLoaded)
logo.Position = logoTrackingPosition;
iconFacade.Width = logo.SizeForFlow * 0.5f;
diff --git a/osu.Game/Screens/Play/Break/BreakArrows.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs
index 4a4a7960fa..1382aa9d9d 100644
--- a/osu.Game/Screens/Play/Break/BreakArrows.cs
+++ b/osu.Game/Screens/Play/Break/BreakArrows.cs
@@ -31,23 +31,30 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
- leftGlowIcon = new GlowIcon
+ new ParallaxContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreRight,
- X = -glow_icon_offscreen_offset,
- Icon = Graphics.FontAwesome.fa_chevron_right,
- BlurSigma = new Vector2(glow_icon_blur_sigma),
- Size = new Vector2(glow_icon_size),
- },
- rightGlowIcon = new GlowIcon
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreLeft,
- X = glow_icon_offscreen_offset,
- Icon = Graphics.FontAwesome.fa_chevron_left,
- BlurSigma = new Vector2(glow_icon_blur_sigma),
- Size = new Vector2(glow_icon_size),
+ ParallaxAmount = -0.01f,
+ Children = new Drawable[]
+ {
+ leftGlowIcon = new GlowIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ X = -glow_icon_offscreen_offset,
+ Icon = Graphics.FontAwesome.fa_chevron_right,
+ BlurSigma = new Vector2(glow_icon_blur_sigma),
+ Size = new Vector2(glow_icon_size),
+ },
+ rightGlowIcon = new GlowIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreLeft,
+ X = glow_icon_offscreen_offset,
+ Icon = Graphics.FontAwesome.fa_chevron_left,
+ BlurSigma = new Vector2(glow_icon_blur_sigma),
+ Size = new Vector2(glow_icon_size),
+ },
+ }
},
new ParallaxContainer
{
diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs
index 894322dd41..1a164b473d 100644
--- a/osu.Game/Screens/Play/HUD/ModDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs
@@ -63,6 +63,12 @@ namespace osu.Game.Screens.Play.HUD
};
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ mods.UnbindAll();
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 2e23bb16f0..5ad0130fd7 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -124,7 +124,7 @@ namespace osu.Game.Screens.Play
if (!RulesetContainer.Objects.Any())
{
- Logger.Error(new InvalidOperationException("Beatmap contains no hit objects!"), "Beatmap contains no hit objects!");
+ Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
return;
}
}
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 6e4454a311..7bb0b95b9a 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -64,32 +64,36 @@ namespace osu.Game.Screens.Select
public IEnumerable BeatmapSets
{
- get { return beatmapSets.Select(g => g.BeatmapSet); }
- set
+ get => beatmapSets.Select(g => g.BeatmapSet);
+ set => loadBeatmapSets(() => value);
+ }
+
+ public void LoadBeatmapSetsFromManager(BeatmapManager manager) => loadBeatmapSets(manager.GetAllUsableBeatmapSetsEnumerable);
+
+ private void loadBeatmapSets(Func> beatmapSets)
+ {
+ CarouselRoot newRoot = new CarouselRoot(this);
+
+ Task.Run(() =>
{
- CarouselRoot newRoot = new CarouselRoot(this);
+ beatmapSets().Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild);
+ newRoot.Filter(activeCriteria);
- Task.Run(() =>
+ // preload drawables as the ctor overhead is quite high currently.
+ var _ = newRoot.Drawables;
+ }).ContinueWith(_ => Schedule(() =>
+ {
+ root = newRoot;
+ scrollableContent.Clear(false);
+ itemsCache.Invalidate();
+ scrollPositionCache.Invalidate();
+
+ Schedule(() =>
{
- value.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild);
- newRoot.Filter(activeCriteria);
-
- // preload drawables as the ctor overhead is quite high currently.
- var _ = newRoot.Drawables;
- }).ContinueWith(_ => Schedule(() =>
- {
- root = newRoot;
- scrollableContent.Clear(false);
- itemsCache.Invalidate();
- scrollPositionCache.Invalidate();
-
- Schedule(() =>
- {
- BeatmapSetsChanged?.Invoke();
- initialLoadComplete = true;
- });
- }));
- }
+ BeatmapSetsChanged?.Invoke();
+ initialLoadComplete = true;
+ });
+ }));
}
private readonly List yPositions = new List();
diff --git a/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs b/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs
index 0c4b369f36..0cf1e60304 100644
--- a/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs
+++ b/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs
@@ -41,7 +41,10 @@ namespace osu.Game.Screens.Select.Leaderboards
updateTexture();
}
- private void updateTexture() => rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}");
+ private void updateTexture()
+ {
+ rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}");
+ }
public void UpdateRank(ScoreRank newRank)
{
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 54143bef8a..efdf55e477 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -221,7 +221,7 @@ namespace osu.Game.Screens.Select
sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty");
sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand");
- Carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSetsEnumerable();
+ Carousel.LoadBeatmapSetsFromManager(this.beatmaps);
}
public void Edit(BeatmapInfo beatmap)
@@ -460,6 +460,8 @@ namespace osu.Game.Screens.Select
{
base.Dispose(isDisposing);
+ Ruleset.UnbindAll();
+
if (beatmaps != null)
{
beatmaps.ItemAdded -= onBeatmapSetAdded;
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 45c6ec80aa..9c881c6abb 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
@@ -108,10 +109,12 @@ namespace osu.Game.Skinning
return path == null ? null : underlyingStore.GetStream(path);
}
- byte[] IResourceStore.Get(string name)
+ byte[] IResourceStore.Get(string name) => GetAsync(name).Result;
+
+ public Task GetAsync(string name)
{
string path = getPathForFile(name);
- return path == null ? null : underlyingStore.Get(path);
+ return path == null ? Task.FromResult(null) : underlyingStore.GetAsync(path);
}
#region IDisposable Support
diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/LocalSkinOverrideContainer.cs
index 3adf287cf7..25d9442e6f 100644
--- a/osu.Game/Skinning/LocalSkinOverrideContainer.cs
+++ b/osu.Game/Skinning/LocalSkinOverrideContainer.cs
@@ -85,12 +85,10 @@ namespace osu.Game.Skinning
private void load(OsuConfigManager config)
{
beatmapSkins = config.GetBindable(OsuSetting.BeatmapSkins);
- beatmapSkins.ValueChanged += val => onSourceChanged();
- beatmapSkins.TriggerChange();
+ beatmapSkins.BindValueChanged(_ => onSourceChanged());
beatmapHitsounds = config.GetBindable(OsuSetting.BeatmapHitsounds);
- beatmapHitsounds.ValueChanged += val => onSourceChanged();
- beatmapHitsounds.TriggerChange();
+ beatmapHitsounds.BindValueChanged(_ => onSourceChanged(), true);
}
protected override void LoadComplete()
diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs
index f1ee0db6ce..0b94697405 100644
--- a/osu.Game/Skinning/SkinReloadableDrawable.cs
+++ b/osu.Game/Skinning/SkinReloadableDrawable.cs
@@ -52,5 +52,13 @@ namespace osu.Game.Skinning
protected virtual void SkinChanged(ISkinSource skin, bool allowFallback)
{
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (skin != null)
+ skin.SourceChanged -= onChange;
+ }
}
}
diff --git a/osu.Game/Utils/RavenLogger.cs b/osu.Game/Utils/RavenLogger.cs
new file mode 100644
index 0000000000..b28dd1fb73
--- /dev/null
+++ b/osu.Game/Utils/RavenLogger.cs
@@ -0,0 +1,89 @@
+// 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.Threading.Tasks;
+using osu.Framework.Logging;
+using SharpRaven;
+using SharpRaven.Data;
+
+namespace osu.Game.Utils
+{
+ ///
+ /// Report errors to sentry.
+ ///
+ public class RavenLogger : IDisposable
+ {
+ private readonly RavenClient raven = new RavenClient("https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255");
+
+ private readonly List tasks = new List();
+
+ private Exception lastException;
+
+ public RavenLogger(OsuGame game)
+ {
+ raven.Release = game.Version;
+
+ if (!game.IsDeployedBuild) return;
+
+ Logger.NewEntry += entry =>
+ {
+ if (entry.Level < LogLevel.Verbose) return;
+
+ var exception = entry.Exception;
+
+ if (exception != null)
+ {
+ // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports.
+ if (lastException != null &&
+ lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace))
+ {
+ return;
+ }
+
+ lastException = exception;
+ queuePendingTask(raven.CaptureAsync(new SentryEvent(exception)));
+ }
+ else
+ raven.AddTrail(new Breadcrumb(entry.Target.ToString(), BreadcrumbType.Navigation) { Message = entry.Message });
+ };
+ }
+
+ private void queuePendingTask(Task task)
+ {
+ lock (tasks) tasks.Add(task);
+ task.ContinueWith(_ =>
+ {
+ lock (tasks)
+ tasks.Remove(task);
+ });
+ }
+
+ #region Disposal
+
+ ~RavenLogger()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private bool isDisposed;
+
+ protected virtual void Dispose(bool isDisposing)
+ {
+ if (isDisposed)
+ return;
+
+ isDisposed = true;
+ lock (tasks) Task.WaitAll(tasks.ToArray(), 5000);
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index e9fc51ee9b..669b775674 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -15,12 +15,13 @@
-
-
+
+
-
+
+
\ No newline at end of file
diff --git a/osu.TestProject.props b/osu.TestProject.props
index a73a4f8ce2..58de6ec030 100644
--- a/osu.TestProject.props
+++ b/osu.TestProject.props
@@ -11,7 +11,7 @@
-
+