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