mirror of
https://github.com/ppy/osu.git
synced 2025-02-12 00:42:55 +08:00
Merge remote-tracking branch 'upstream/master' into taiko-classic
This commit is contained in:
commit
02c9022e59
@ -92,8 +92,8 @@ namespace osu.Desktop
|
||||
[SupportedOSPlatform("windows")]
|
||||
private string? getStableInstallPathFromRegistry()
|
||||
{
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
|
||||
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
@ -173,13 +174,16 @@ namespace osu.Desktop
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.InstallAssociations();
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UpdateAssociations();
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UninstallAssociations();
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
|
17
osu.Desktop/Windows/Icons.cs
Normal file
17
osu.Desktop/Windows/Icons.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
public static class Icons
|
||||
{
|
||||
/// <summary>
|
||||
/// Fully qualified path to the directory that contains icons (in the installation folder).
|
||||
/// </summary>
|
||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||
|
||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||
}
|
||||
}
|
292
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
292
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
@ -0,0 +1,292 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static class WindowsAssociationManager
|
||||
{
|
||||
private const string software_classes = @"Software\Classes";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the icon.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
|
||||
/// </summary>
|
||||
private const string default_icon = @"DefaultIcon";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the command line that the shell invokes.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
|
||||
/// </summary>
|
||||
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
|
||||
|
||||
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
|
||||
|
||||
/// <summary>
|
||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||
/// </summary>
|
||||
private const string program_id_prefix = "osu.File";
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||
};
|
||||
|
||||
private static readonly UriAssociation[] uri_associations =
|
||||
{
|
||||
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
||||
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Installs file and URI associations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void InstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates associations with latest definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void UpdateAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
|
||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
updateDescriptions(localisationManager);
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI association descriptions.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UninstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Uninstall();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Uninstall();
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to uninstall file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Installs or updates associations.
|
||||
/// </summary>
|
||||
private static void updateAssociations()
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Install();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Install();
|
||||
}
|
||||
|
||||
private static void updateDescriptions(LocalisationManager? localisation)
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
string getLocalisedString(LocalisableString s)
|
||||
{
|
||||
if (localisation == null)
|
||||
return s.ToString();
|
||||
|
||||
var b = localisation.GetLocalisedBindableString(s);
|
||||
b.UnbindAll();
|
||||
return b.Value;
|
||||
}
|
||||
}
|
||||
|
||||
#region Native interop
|
||||
|
||||
[DllImport("Shell32.dll")]
|
||||
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
private enum EventId
|
||||
{
|
||||
/// <summary>
|
||||
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
|
||||
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
|
||||
/// </summary>
|
||||
SHCNE_ASSOCCHANGED = 0x08000000
|
||||
}
|
||||
|
||||
private enum Flags : uint
|
||||
{
|
||||
SHCNF_IDLIST = 0x0000
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||
{
|
||||
private string programId => $@"{program_id_prefix}{Extension}";
|
||||
|
||||
/// <summary>
|
||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
// register a program id for the given extension
|
||||
using (var programKey = classes.CreateSubKey(programId))
|
||||
{
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
|
||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||
{
|
||||
// set ourselves as the default program
|
||||
extensionKey.SetValue(null, programId);
|
||||
|
||||
// add to the open with dialog
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey.SetValue(programId, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var programKey = classes.OpenSubKey(programId, true))
|
||||
programKey?.SetValue(null, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
|
||||
/// </summary>
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||
{
|
||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||
}
|
||||
|
||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||
{
|
||||
/// <summary>
|
||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public const string URL_PROTOCOL = @"URL Protocol";
|
||||
|
||||
/// <summary>
|
||||
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||
{
|
||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||
|
||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -31,4 +31,7 @@
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Windows Icons">
|
||||
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -20,6 +20,7 @@
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
<file src="**.ico" target="lib\net45\"/>
|
||||
<file src="icon.png" target=""/>
|
||||
</files>
|
||||
</package>
|
||||
|
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Fruit(), 0.01, true],
|
||||
[new Droplet(), 0.01, true],
|
||||
[new TinyDroplet(), 0, false],
|
||||
[new Banana(), 0, false],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
// matches stable.
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
|
||||
// the above early-return skips the failure check at the end of the same method:
|
||||
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
|
||||
// making it impossible to fail on a tiny droplet regardless of result.
|
||||
if (result.Type == HitResult.SmallTickMiss)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
// No matter what, mania doesn't have passive HP drain.
|
||||
Assert.That(processor.DrainRate, Is.Zero);
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Note(), 0.01, true],
|
||||
[new HeadNote(), 0.01, true],
|
||||
[new TailNote(), 0.01, true],
|
||||
[new HoldNoteBody(), 0, true], // hold note break
|
||||
[new HoldNote(), 0, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup hierarchy", () => Child = new Container
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
Child = new Container
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new HitCircle(), 0.01, true],
|
||||
[new SliderHeadCircle(), 0.01, true],
|
||||
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SliderTick(), 0.01, true],
|
||||
[new SliderRepeat(new Slider()), 0.01, true],
|
||||
[new SliderTailCircle(new Slider()), 0, true],
|
||||
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new Slider(), 0, true],
|
||||
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SpinnerTick(), 0, false],
|
||||
[new SpinnerBonusTick(), 0, false],
|
||||
[new Spinner(), 0.01, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissHitAndHitSwell()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[1].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, fail expected after miss
|
||||
[new Hit(), true],
|
||||
[new Hit.StrongNestedHit(new Hit()), false],
|
||||
[new DrumRollTick(new DrumRoll()), false],
|
||||
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
|
||||
[new DrumRoll(), false],
|
||||
[new SwellTick(), false],
|
||||
[new Swell(), false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public partial class TestSceneTaikoPlayerScroller : LegacySkinPlayerTestScene
|
||||
{
|
||||
private Storyboard? currentStoryboard;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[Test]
|
||||
public void TestForegroundSpritesHidesScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller not present", () => !this.ChildrenOfType<LegacyTaikoScroller>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlaySpritesKeepsScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller present", () => this.ChildrenOfType<LegacyTaikoScroller>().Single().IsPresent);
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> base.CreateWorkingBeatmap(beatmap, currentStoryboard ?? storyboard);
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
@ -37,6 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
[CanBeNull]
|
||||
private SkinnableDrawable scroller;
|
||||
|
||||
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
@ -46,16 +51,24 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] GameplayState gameplayState)
|
||||
{
|
||||
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar));
|
||||
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
var spriteElements = gameplayState?.Storyboard.Layers.Where(l => l.Name != @"Overlay")
|
||||
.SelectMany(l => l.Elements)
|
||||
.OfType<StoryboardSprite>()
|
||||
.DistinctBy(e => e.Path) ?? Enumerable.Empty<StoryboardSprite>();
|
||||
|
||||
if (spriteElements.Count() < 10)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
}
|
||||
|
||||
KeyBindingInputManager.Add(new DrumTouchInputArea());
|
||||
}
|
||||
@ -74,7 +87,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
|
||||
if (scroller != null)
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
}
|
||||
|
||||
public MultiplierControlPoint ControlPointAt(double time)
|
||||
|
@ -100,6 +100,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override bool AllowFail => false;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
|
@ -130,8 +130,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
|
||||
{
|
||||
mainContainer.Child = new FrameStabilityContainer(gameplayStartTime)
|
||||
.WithChild(consumer = new ClockConsumingChild()));
|
||||
{
|
||||
AllowBackwardsSeeks = true,
|
||||
}.WithChild(consumer = new ClockConsumingChild());
|
||||
});
|
||||
|
||||
private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
|
||||
|
||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[Test]
|
||||
public void TestAllSamplesStopDuringSeek()
|
||||
{
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
|
@ -288,6 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private bool gameplayClockAlwaysGoingForward = true;
|
||||
private double lastForwardCheckTime;
|
||||
|
||||
public TestScenePause()
|
||||
{
|
||||
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
|
||||
@ -67,12 +71,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
confirmPausedWithNoOverlay();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestForwardPlaybackGuarantee()
|
||||
{
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddUntilStep("wait for forward playback", () => Player.GameplayClockContainer.CurrentTime > 1000);
|
||||
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
||||
|
||||
checkForwardPlayback();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseWithLargeOffset()
|
||||
{
|
||||
double lastStopTime;
|
||||
bool alwaysGoingForward = true;
|
||||
|
||||
AddStep("force large offset", () =>
|
||||
{
|
||||
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
@ -82,25 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
offset.Value = -5000;
|
||||
});
|
||||
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastStopTime = double.MinValue;
|
||||
alwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
|
||||
|
||||
double currentTime = masterClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastStopTime;
|
||||
|
||||
alwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||
|
||||
@ -108,11 +102,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
resumeAndConfirm();
|
||||
|
||||
AddAssert("time didn't go too far backwards", () => alwaysGoingForward);
|
||||
checkForwardPlayback();
|
||||
|
||||
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||
}
|
||||
|
||||
private void checkForwardPlayback() => AddAssert("time didn't go too far backwards", () => gameplayClockAlwaysGoingForward);
|
||||
|
||||
private void hookForwardPlaybackCheck()
|
||||
{
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastForwardCheckTime = double.MinValue;
|
||||
gameplayClockAlwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var frameStableClock = Player.ChildrenOfType<FrameStabilityContainer>().Single().Clock;
|
||||
|
||||
double currentTime = frameStableClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastForwardCheckTime;
|
||||
lastForwardCheckTime = currentTime;
|
||||
|
||||
gameplayClockAlwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastForwardCheckTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseResume()
|
||||
{
|
||||
|
@ -269,6 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
|
||||
drawableRuleset.FrameStablePlayback = true;
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
drawableRuleset.PoolSize = poolSize;
|
||||
|
||||
Child = new Container
|
||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
protected new OutroPlayer Player => (OutroPlayer)base.Player;
|
||||
|
||||
private double currentBeatmapDuration;
|
||||
|
@ -14,7 +14,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Users;
|
||||
@ -142,7 +141,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
|
||||
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
|
||||
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo()));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo(), new OsuRuleset().RulesetInfo));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
private readonly DecouplingFramedClock decoupledTrack;
|
||||
private readonly InterpolatingFramedClock interpolatedTrack;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
|
||||
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
|
||||
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
|
||||
if (applyOffsets)
|
||||
{
|
||||
@ -190,5 +191,28 @@ namespace osu.Game.Beatmaps
|
||||
base.Dispose(isDisposing);
|
||||
beatmapOffsetSubscription?.Dispose();
|
||||
}
|
||||
|
||||
public string GetSnapshot()
|
||||
{
|
||||
return
|
||||
$"originalSource: {output(Source)}\n" +
|
||||
$"userGlobalOffsetClock: {output(userGlobalOffsetClock)}\n" +
|
||||
$"platformOffsetClock: {output(platformOffsetClock)}\n" +
|
||||
$"userBeatmapOffsetClock: {output(userBeatmapOffsetClock)}\n" +
|
||||
$"interpolatedTrack: {output(interpolatedTrack)}\n" +
|
||||
$"decoupledTrack: {output(decoupledTrack)}\n" +
|
||||
$"finalClockSource: {output(finalClockSource)}\n";
|
||||
|
||||
string output(IClock? clock)
|
||||
{
|
||||
if (clock == null)
|
||||
return "null";
|
||||
|
||||
if (clock is IFrameBasedClock framed)
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate} elapsed: {framed.ElapsedFrameTime:N2}";
|
||||
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Performance points will not be granted due to active mods."
|
||||
/// </summary>
|
||||
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"ranked_explanation"), @"Performance points will not be granted due to active mods.");
|
||||
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"unranked_explanation"), @"Performance points will not be granted due to active mods.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
39
osu.Game/Localisation/WindowsAssociationManagerStrings.cs
Normal file
39
osu.Game/Localisation/WindowsAssociationManagerStrings.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class WindowsAssociationManagerStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.WindowsAssociationManager";
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Beatmap"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuBeatmap => new TranslatableString(getKey(@"osu_beatmap"), @"osu! Beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Replay"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuReplay => new TranslatableString(getKey(@"osu_replay"), @"osu! Replay");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Skin"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuSkin => new TranslatableString(getKey(@"osu_skin"), @"osu! Skin");
|
||||
|
||||
/// <summary>
|
||||
/// "osu!"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuProtocol => new TranslatableString(getKey(@"osu_protocol"), @"osu!");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Multiplayer"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuMultiplayer => new TranslatableString(getKey(@"osu_multiplayer"), @"osu! Multiplayer");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -1190,6 +1190,9 @@ namespace osu.Game
|
||||
{
|
||||
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
|
||||
|
||||
if (entry.Exception is SentryOnlyDiagnosticsException)
|
||||
return;
|
||||
|
||||
const int short_term_display_limit = 3;
|
||||
|
||||
if (recentLogCount < short_term_display_limit)
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
/// <summary>
|
||||
@ -9,7 +11,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public partial class AccumulatingHealthProcessor : HealthProcessor
|
||||
{
|
||||
protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth;
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult _) => JudgedHits == MaxHits && Health.Value < requiredHealth;
|
||||
|
||||
private readonly double requiredHealth;
|
||||
|
||||
|
@ -142,6 +142,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
if (result.Judgement.MaxResult.IsBonus() || result.Type == HitResult.IgnoreHit)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public event Func<bool>? Failed;
|
||||
|
||||
/// <summary>
|
||||
/// Additional conditions on top of <see cref="DefaultFailCondition"/> that cause a failing state.
|
||||
/// Additional conditions on top of <see cref="CheckDefaultFailCondition"/> that cause a failing state.
|
||||
/// </summary>
|
||||
public event Func<HealthProcessor, JudgementResult, bool>? FailConditions;
|
||||
|
||||
@ -69,9 +69,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease;
|
||||
|
||||
/// <summary>
|
||||
/// The default conditions for failing.
|
||||
/// Checks whether the default conditions for failing are met.
|
||||
/// </summary>
|
||||
protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
|
||||
/// <returns><see langword="true"/> if failure should be invoked.</returns>
|
||||
protected virtual bool CheckDefaultFailCondition(JudgementResult result) => Precision.AlmostBigger(Health.MinValue, Health.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current state of <see cref="HealthProcessor"/> or the provided <paramref name="result"/> meets any fail condition.
|
||||
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="result">The judgement result.</param>
|
||||
private bool meetsAnyFailCondition(JudgementResult result)
|
||||
{
|
||||
if (DefaultFailCondition)
|
||||
if (CheckDefaultFailCondition(result))
|
||||
return true;
|
||||
|
||||
if (FailConditions != null)
|
||||
|
@ -81,6 +81,19 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public override IFrameStableClock FrameStableClock => frameStabilityContainer;
|
||||
|
||||
private bool allowBackwardsSeeks;
|
||||
|
||||
public override bool AllowBackwardsSeeks
|
||||
{
|
||||
get => allowBackwardsSeeks;
|
||||
set
|
||||
{
|
||||
allowBackwardsSeeks = value;
|
||||
if (frameStabilityContainer != null)
|
||||
frameStabilityContainer.AllowBackwardsSeeks = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool frameStablePlayback = true;
|
||||
|
||||
internal override bool FrameStablePlayback
|
||||
@ -178,6 +191,7 @@ namespace osu.Game.Rulesets.UI
|
||||
InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
|
||||
{
|
||||
FrameStablePlayback = FrameStablePlayback,
|
||||
AllowBackwardsSeeks = AllowBackwardsSeeks,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
FrameStableComponents,
|
||||
@ -463,6 +477,12 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
internal abstract bool FrameStablePlayback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When a replay is not attached, we usually block any backwards seeks.
|
||||
/// This will bypass the check. Should only be used for tests.
|
||||
/// </summary>
|
||||
public abstract bool AllowBackwardsSeeks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mods which are to be applied.
|
||||
/// </summary>
|
||||
|
@ -3,14 +3,19 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -24,6 +29,9 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public ReplayInputHandler? ReplayInputHandler { get; set; }
|
||||
|
||||
public bool AllowBackwardsSeeks { get; set; }
|
||||
private double? lastBackwardsSeekLogTime;
|
||||
|
||||
/// <summary>
|
||||
/// The number of CPU milliseconds to spend at most during seek catch-up.
|
||||
/// </summary>
|
||||
@ -150,6 +158,29 @@ namespace osu.Game.Rulesets.UI
|
||||
state = PlaybackState.NotValid;
|
||||
}
|
||||
|
||||
// This is a hotfix for https://github.com/ppy/osu/issues/26879 while we figure how the hell time is seeking
|
||||
// backwards by 11,850 ms for some users during gameplay.
|
||||
//
|
||||
// It basically says that "while we're running in frame stable mode, and don't have a replay attached,
|
||||
// time should never go backwards". If it does, we stop running gameplay until it returns to normal.
|
||||
if (!hasReplayAttached && FrameStablePlayback && proposedTime > referenceClock.CurrentTime && !AllowBackwardsSeeks)
|
||||
{
|
||||
if (lastBackwardsSeekLogTime == null || Math.Abs(Clock.CurrentTime - lastBackwardsSeekLogTime.Value) > 1000)
|
||||
{
|
||||
lastBackwardsSeekLogTime = Clock.CurrentTime;
|
||||
|
||||
string loggableContent = $"Denying backwards seek during gameplay (reference: {referenceClock.CurrentTime:N2} stable: {proposedTime:N2})";
|
||||
|
||||
if (parentGameplayClock is GameplayClockContainer gcc)
|
||||
loggableContent += $"\n{gcc.ChildrenOfType<FramedBeatmapClock>().Single().GetSnapshot()}";
|
||||
|
||||
Logger.Error(new SentryOnlyDiagnosticsException("backwards seek"), loggableContent);
|
||||
}
|
||||
|
||||
state = PlaybackState.NotValid;
|
||||
return;
|
||||
}
|
||||
|
||||
// if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously.
|
||||
// this avoids spurious flips in direction from -1 to 1 during rewinds.
|
||||
if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -53,7 +52,18 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
|
||||
// for changes. ControlPointInfo needs a refactor to make this flow better, but it should do for now.
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
var next = beatmap.ControlPointInfo.EffectPoints.FirstOrDefault(c => c.Time > effect.Time);
|
||||
EffectControlPoint? next = null;
|
||||
|
||||
for (int i = 0; i < beatmap.ControlPointInfo.EffectPoints.Count; i++)
|
||||
{
|
||||
var point = beatmap.ControlPointInfo.EffectPoints[i];
|
||||
|
||||
if (point.Time > effect.Time)
|
||||
{
|
||||
next = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(nextControlPoint, next))
|
||||
{
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
||||
private readonly Editor editor;
|
||||
private readonly EditorState editorState;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo);
|
||||
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; } = null!;
|
||||
|
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Storyboards;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -40,6 +41,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public readonly ScoreProcessor ScoreProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// The storyboard associated with the beatmap.
|
||||
/// </summary>
|
||||
public readonly Storyboard Storyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Whether gameplay completed without the user failing.
|
||||
/// </summary>
|
||||
@ -62,7 +68,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
||||
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null)
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null, Storyboard? storyboard = null)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
Ruleset = ruleset;
|
||||
@ -76,6 +82,7 @@ namespace osu.Game.Screens.Play
|
||||
};
|
||||
Mods = mods ?? Array.Empty<Mod>();
|
||||
ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
|
||||
Storyboard = storyboard ?? new Storyboard();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -107,8 +107,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
JudgementSpacing.BindValueChanged(_ => updateMetrics(), true);
|
||||
}
|
||||
|
||||
private readonly DrawablePool<HitErrorShape> judgementLinePool = new DrawablePool<HitErrorShape>(50);
|
||||
|
||||
public void Push(HitErrorShape shape)
|
||||
{
|
||||
Add(shape);
|
||||
|
@ -255,7 +255,7 @@ namespace osu.Game.Screens.Play
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = gameplayMods;
|
||||
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor));
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, Beatmap.Value.Storyboard));
|
||||
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
|
||||
|
||||
@ -397,7 +397,7 @@ namespace osu.Game.Screens.Play
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
|
||||
private Drawable createUnderlayComponents() =>
|
||||
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
@ -456,7 +456,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
RequestSkip = performUserRequestedSkip
|
||||
},
|
||||
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
|
||||
skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0)
|
||||
{
|
||||
RequestSkip = () => progressToResults(false),
|
||||
Alpha = 0
|
||||
@ -1088,7 +1088,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
|
||||
|
||||
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
|
||||
storyboardReplacesBackground.Value = GameplayState.Storyboard.ReplacesBackground && GameplayState.Storyboard.HasDrawable;
|
||||
|
||||
foreach (var mod in GameplayState.Mods.OfType<IApplicableToPlayer>())
|
||||
mod.ApplyToPlayer(this);
|
||||
|
@ -70,10 +70,20 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
AddStep($"Load player for {CreatePlayerRuleset().Description}", LoadPlayer);
|
||||
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||
|
||||
if (AllowBackwardsSeeks)
|
||||
{
|
||||
AddStep("allow backwards seeking", () =>
|
||||
{
|
||||
Player.DrawableRuleset.AllowBackwardsSeeks = AllowBackwardsSeeks;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool AllowFail => false;
|
||||
|
||||
protected virtual bool AllowBackwardsSeeks => false;
|
||||
|
||||
protected virtual bool Autoplay => false;
|
||||
|
||||
protected void LoadPlayer() => LoadPlayer(Array.Empty<Mod>());
|
||||
@ -126,6 +136,6 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset();
|
||||
|
||||
protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false);
|
||||
protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false, AllowBackwardsSeeks);
|
||||
}
|
||||
}
|
||||
|
@ -119,10 +119,10 @@ namespace osu.Game.Users
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class TestingBeatmap : InGame
|
||||
public class TestingBeatmap : EditingBeatmap
|
||||
{
|
||||
public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset)
|
||||
: base(beatmapInfo, ruleset)
|
||||
public TestingBeatmap(IBeatmapInfo beatmapInfo)
|
||||
: base(beatmapInfo)
|
||||
{
|
||||
}
|
||||
|
||||
@ -151,7 +151,11 @@ namespace osu.Game.Users
|
||||
public EditingBeatmap() { }
|
||||
|
||||
public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap";
|
||||
public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle;
|
||||
|
||||
public override string GetDetails(bool hideIdentifiableInformation = false) => hideIdentifiableInformation
|
||||
// For now let's assume that showing the beatmap a user is editing could reveal unwanted information.
|
||||
? string.Empty
|
||||
: BeatmapDisplayTitle;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
21
osu.Game/Utils/SentryOnlyDiagnosticsException.cs
Normal file
21
osu.Game/Utils/SentryOnlyDiagnosticsException.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Log to sentry without showing an error notification to the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be used to convey important diagnostics to us developers without
|
||||
/// getting in the user's way. Should be used sparingly.</remarks>
|
||||
internal class SentryOnlyDiagnosticsException : Exception
|
||||
{
|
||||
public SentryOnlyDiagnosticsException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1005,6 +1005,7 @@ private void load()
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Migratable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nightcore/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Omni/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=osump/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Overlined/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pausable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pippidon/@EntryIndexedValue">True</s:Boolean>
|
||||
|
Loading…
Reference in New Issue
Block a user