diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index cc08e08653..71613753bc 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.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 930ca26660..5860480a91 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -286,6 +286,9 @@ 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;
@@ -370,9 +373,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/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
index 3434c9f01e..e51cbcdc60 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
@@ -18,8 +18,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
///
/// An arbitrary maximum amount of iterations to perform in .
/// The specific value is not super important - enough such that no false-positives occur.
+ ///
+ /// /b/933228 requires at least 23 iterations.
///
- private const int max_rng_iterations = 20;
+ private const int max_rng_iterations = 30;
///
/// The last pattern.
@@ -55,15 +57,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
{
int iterations = 0;
- while (condition() && iterations++ < max_rng_iterations)
+ while (condition())
+ {
+ if (iterations++ >= max_rng_iterations)
+ {
+ // log an error but don't throw. we want to continue execution.
+ Logger.Error(new ExceededAllowedIterationsException(new StackTrace(0)),
+ "Conversion encountered errors. The beatmap may not be correctly converted.");
+ return;
+ }
+
action();
-
- if (iterations < max_rng_iterations)
- return;
-
- // Generate + log an error/stacktrace
-
- Logger.Log($"Allowable iterations ({max_rng_iterations}) exceeded:\n{new StackTrace(0)}", level: LogLevel.Error);
+ }
}
///
@@ -71,5 +76,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
///
/// The s containing the hit objects.
public abstract IEnumerable Generate();
+
+ ///
+ /// Denotes when a single conversion operation is in an infinitely looping state.
+ ///
+ public class ExceededAllowedIterationsException : Exception
+ {
+ private readonly string stackTrace;
+
+ public ExceededAllowedIterationsException(StackTrace stackTrace)
+ {
+ this.stackTrace = stackTrace.ToString();
+ }
+
+ public override string StackTrace => stackTrace;
+ public override string ToString() => $"{GetType().Name}: {Message}\r\n{StackTrace}";
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index 93f3f06dc2..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)
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
index bbe2d67baa..5fe2457645 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
@@ -8,7 +8,7 @@ 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)
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.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 0a5df0e093..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
@@ -187,14 +189,46 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
[Test]
- public void TestDecodeBeatmapComboOffsets()
+ 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);
- Assert.AreEqual(3, ((IHasCombo)beatmap.HitObjects[0]).ComboOffset);
+
+ 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);
}
}
diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu
index 4a44d31e22..c1f0dab8e9 100644
--- a/osu.Game.Tests/Resources/hitobject-combo-offset.osu
+++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu
@@ -1,4 +1,32 @@
osu file format v14
[HitObjects]
-255,193,2170,49,0,0:0:0:0:
\ No newline at end of file
+// 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/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index c00df59e3e..ac79a8f565 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -198,6 +198,8 @@ namespace osu.Game.Database
try
{
+ Logger.Log($"Importing {item}...", LoggingTarget.Database);
+
using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{
try
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index a1e385921f..d54bdee1b2 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,6 +67,8 @@ namespace osu.Game
private ScreenshotManager screenshotManager;
+ protected RavenLogger RavenLogger;
+
public virtual Storage GetStorageForStableInstall() => null;
private Intro intro
@@ -108,6 +112,8 @@ namespace osu.Game
this.args = args;
forwardLoggedErrorsToNotifications();
+
+ RavenLogger = new RavenLogger(this);
}
public void ToggleSettings() => settings.ToggleVisibility();
@@ -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);
@@ -273,6 +281,12 @@ namespace osu.Game
menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay)));
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ RavenLogger.Dispose();
+ }
+
protected override void LoadComplete()
{
// this needs to be cached before base.LoadComplete as it is used by MenuCursorContainer.
@@ -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)
@@ -601,6 +615,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 +624,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/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
index fb4cde479b..802080aedb 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
@@ -18,8 +18,17 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
{
}
+ 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,
@@ -30,6 +39,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
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,
@@ -45,11 +60,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
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,
- NewCombo = FirstObject || newCombo,
- ComboOffset = comboOffset
+ EndTime = endTime
};
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
index 0823653830..acd0de8688 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
@@ -19,8 +19,17 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
{
}
+ 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,
@@ -31,6 +40,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
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,
@@ -46,12 +61,15 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
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,
- EndTime = endTime,
- NewCombo = FormatVersion <= 8 || FirstObject || newCombo,
- ComboOffset = comboOffset
+ EndTime = endTime
};
}
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 eef586fd4c..da17500128 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -21,6 +21,7 @@
+
\ No newline at end of file