mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 02:02:53 +08:00
Merge branch 'master' into move-td-reduction
This commit is contained in:
commit
4004d57448
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -1,5 +1,8 @@
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
inspect-code:
|
||||
|
@ -5,26 +5,24 @@ using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osuTK;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||
|
||||
namespace osu.Desktop.Updater
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
|
||||
public class SquirrelUpdateManager : UpdateManager
|
||||
{
|
||||
private UpdateManager? updateManager;
|
||||
private Squirrel.UpdateManager? updateManager;
|
||||
private INotificationOverlay notificationOverlay = null!;
|
||||
|
||||
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
|
||||
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
|
||||
|
||||
private static readonly Logger logger = Logger.GetLogger("updater");
|
||||
|
||||
@ -35,6 +33,9 @@ namespace osu.Desktop.Updater
|
||||
|
||||
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
@ -63,7 +64,14 @@ namespace osu.Desktop.Updater
|
||||
if (updatePending)
|
||||
{
|
||||
// the user may have dismissed the completion notice, so show it again.
|
||||
notificationOverlay.Post(new UpdateCompleteNotification(this));
|
||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
restartToApplyUpdate();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -75,19 +83,21 @@ namespace osu.Desktop.Updater
|
||||
|
||||
if (notification == null)
|
||||
{
|
||||
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
|
||||
notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = restartToApplyUpdate,
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
}
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Downloading update...";
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Installing update...";
|
||||
notification.StartInstall();
|
||||
|
||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
@ -107,9 +117,7 @@ namespace osu.Desktop.Updater
|
||||
else
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
notification.Close();
|
||||
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
@ -131,78 +139,24 @@ namespace osu.Desktop.Updater
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool restartToApplyUpdate()
|
||||
{
|
||||
PrepareUpdateAsync()
|
||||
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
updateManager?.Dispose();
|
||||
}
|
||||
|
||||
private class UpdateCompleteNotification : ProgressCompletionNotification
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
|
||||
{
|
||||
Text = @"Update ready to install. Click to restart!";
|
||||
|
||||
Activated = () =>
|
||||
{
|
||||
updateManager.PrepareUpdateAsync()
|
||||
.ContinueWith(_ => updateManager.Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateProgressNotification : ProgressNotification
|
||||
{
|
||||
private readonly SquirrelUpdateManager updateManager;
|
||||
|
||||
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
|
||||
{
|
||||
this.updateManager = updateManager;
|
||||
}
|
||||
|
||||
protected override Notification CreateCompletionNotification()
|
||||
{
|
||||
return new UpdateCompleteNotification(updateManager);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
IconContent.AddRange(new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Upload,
|
||||
Size = new Vector2(20),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
// cancelling updates is not currently supported by the underlying updater.
|
||||
// only allow dismissing for now.
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case ProgressNotificationState.Cancelled:
|
||||
base.Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SquirrelLogger : ILogger, IDisposable
|
||||
{
|
||||
public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
|
||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
||||
|
||||
public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
|
||||
public void Write(string message, LogLevel logLevel)
|
||||
{
|
||||
if (logLevel < Level)
|
||||
return;
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
public class CatchRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
{
|
||||
public class ManiaRulesetConfigManager : RulesetConfigManager<ManiaRulesetSetting>
|
||||
{
|
||||
public ManiaRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
|
||||
public ManiaRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
|
||||
: base(settings, ruleset, variant)
|
||||
{
|
||||
}
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -48,7 +46,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
/// </summary>
|
||||
public const int MAX_STAGE_KEYS = 10;
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
|
||||
@ -285,7 +283,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
|
||||
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -53,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<float> sliderScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> sliderVersion;
|
||||
|
||||
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
|
||||
{
|
||||
this.slider = slider;
|
||||
@ -61,7 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the slider.
|
||||
cachePoints(slider);
|
||||
|
||||
slider.Path.Version.BindValueChanged(_ =>
|
||||
sliderVersion = slider.Path.Version.GetBoundCopy();
|
||||
sliderVersion.BindValueChanged(_ =>
|
||||
{
|
||||
cachePoints(slider);
|
||||
updatePathType();
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -44,7 +42,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
public class OsuRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
|
||||
|
||||
@ -239,7 +237,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -37,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
public class TaikoRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
|
||||
|
||||
|
@ -97,6 +97,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectAnimationStartTime()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("animation-starts-before-alpha.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
|
||||
Assert.AreEqual(1, background.Elements.Count);
|
||||
|
||||
Assert.AreEqual(2000, background.Elements[0].StartTime);
|
||||
// This property should be used in DrawableStoryboardAnimation as a starting point for animation playback.
|
||||
Assert.AreEqual(1000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfOrderStartTimes()
|
||||
{
|
||||
|
@ -118,17 +118,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.IsNull(filterCriteria.BPM.Max);
|
||||
}
|
||||
|
||||
private static readonly object[] length_query_examples =
|
||||
private static readonly object[] correct_length_query_examples =
|
||||
{
|
||||
new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) },
|
||||
new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) },
|
||||
new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) },
|
||||
new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "7m27s", TimeSpan.FromSeconds(447), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "7:27", TimeSpan.FromSeconds(447), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "1h2m3s", TimeSpan.FromSeconds(3723), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "1h2m3.5s", TimeSpan.FromSeconds(3723.5), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "1:2:3", TimeSpan.FromSeconds(3723), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "1:02:03", TimeSpan.FromSeconds(3723), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "6", TimeSpan.FromSeconds(6), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "6.5", TimeSpan.FromSeconds(6.5), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "6.5s", TimeSpan.FromSeconds(6.5), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "6.5m", TimeSpan.FromMinutes(6.5), TimeSpan.FromMinutes(1) },
|
||||
new object[] { "6h5m", TimeSpan.FromMinutes(365), TimeSpan.FromMinutes(1) },
|
||||
new object[] { "65m", TimeSpan.FromMinutes(65), TimeSpan.FromMinutes(1) },
|
||||
new object[] { "90s", TimeSpan.FromSeconds(90), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "80m20s", TimeSpan.FromSeconds(4820), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "1h20s", TimeSpan.FromSeconds(3620), TimeSpan.FromSeconds(1) },
|
||||
};
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(length_query_examples))]
|
||||
[TestCaseSource(nameof(correct_length_query_examples))]
|
||||
public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale)
|
||||
{
|
||||
string query = $"length={lengthQuery} time";
|
||||
@ -140,6 +154,29 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
|
||||
}
|
||||
|
||||
private static readonly object[] incorrect_length_query_examples =
|
||||
{
|
||||
new object[] { "7.5m27s" },
|
||||
new object[] { "7m27" },
|
||||
new object[] { "7m7m7m" },
|
||||
new object[] { "7m70s" },
|
||||
new object[] { "5s6m" },
|
||||
new object[] { "0:" },
|
||||
new object[] { ":0" },
|
||||
new object[] { "0:3:" },
|
||||
new object[] { "3:15.5" },
|
||||
};
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(incorrect_length_query_examples))]
|
||||
public void TestInvalidLengthQueries(string lengthQuery)
|
||||
{
|
||||
string query = $"length={lengthQuery} time";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(false, filterCriteria.Length.HasFilter);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyDivisorQueries()
|
||||
{
|
||||
|
@ -0,0 +1,5 @@
|
||||
[Events]
|
||||
//Storyboard Layer 0 (Background)
|
||||
Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
|
||||
S,0,1000,1500,0.08 // animation should start playing from this point in time..
|
||||
F,0,2000,,0,1 // .. not this point in time
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
@ -45,7 +46,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
Dependencies.Cache(EditorBeatmap);
|
||||
Dependencies.CacheAs<IBeatSnapProvider>(EditorBeatmap);
|
||||
|
||||
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
|
||||
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer();
|
||||
Debug.Assert(Composer != null);
|
||||
|
||||
Composer.Alpha = 0;
|
||||
|
||||
Add(new OsuContextMenuContainer
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -19,14 +20,24 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
|
||||
{
|
||||
private HitEventTimingDistributionGraph graph = null!;
|
||||
private readonly BindableFloat width = new BindableFloat(600);
|
||||
private readonly BindableFloat height = new BindableFloat(130);
|
||||
|
||||
private static readonly HitObject placeholder_object = new HitCircle();
|
||||
|
||||
public TestSceneHitEventTimingDistributionGraph()
|
||||
{
|
||||
width.BindValueChanged(e => graph.Width = e.NewValue);
|
||||
height.BindValueChanged(e => graph.Height = e.NewValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManyDistributedEvents()
|
||||
{
|
||||
createTest(CreateDistributedHitEvents());
|
||||
AddStep("add adjustment", () => graph.UpdateOffset(10));
|
||||
AddSliderStep("width", 0.0f, 1000.0f, width.Value, width.Set);
|
||||
AddSliderStep("height", 0.0f, 1000.0f, height.Value, height.Set);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -137,7 +148,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(600, 130)
|
||||
Size = new Vector2(width.Value, height.Value)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("select unchanged Difficulty Adjust mod", () =>
|
||||
{
|
||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>();
|
||||
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
||||
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
||||
});
|
||||
@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("select changed Difficulty Adjust mod", () =>
|
||||
{
|
||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||
var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>();
|
||||
var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>().AsNonNull();
|
||||
var originalDifficulty = advancedStats.BeatmapInfo.Difficulty;
|
||||
|
||||
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
||||
|
@ -1,8 +1,7 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,6 +12,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Tests.Resources;
|
||||
@ -22,10 +22,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public class TestSceneTopLocalRank : OsuTestScene
|
||||
{
|
||||
private RulesetStore rulesets;
|
||||
private BeatmapManager beatmapManager;
|
||||
private ScoreManager scoreManager;
|
||||
private TopLocalRank topLocalRank;
|
||||
private RulesetStore rulesets = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private ScoreManager scoreManager = null!;
|
||||
private TopLocalRank topLocalRank = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@ -47,21 +47,21 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("Create local rank", () =>
|
||||
{
|
||||
Add(topLocalRank = new TopLocalRank(importedBeatmap)
|
||||
Child = topLocalRank = new TopLocalRank(importedBeatmap)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(10),
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("No rank displayed initially", () => topLocalRank.DisplayedRank == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicImportDelete()
|
||||
{
|
||||
ScoreInfo testScoreInfo = null;
|
||||
|
||||
AddAssert("Initially not present", () => !topLocalRank.IsPresent);
|
||||
ScoreInfo testScoreInfo = null!;
|
||||
|
||||
AddStep("Add score for current user", () =>
|
||||
{
|
||||
@ -73,25 +73,19 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
||||
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Delete score", () =>
|
||||
{
|
||||
scoreManager.Delete(testScoreInfo);
|
||||
});
|
||||
AddStep("Delete score", () => scoreManager.Delete(testScoreInfo));
|
||||
|
||||
AddUntilStep("Became not present", () => !topLocalRank.IsPresent);
|
||||
AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetChange()
|
||||
{
|
||||
ScoreInfo testScoreInfo;
|
||||
|
||||
AddStep("Add score for current user", () =>
|
||||
{
|
||||
testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
@ -99,25 +93,21 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("Wait for initial presence", () => topLocalRank.IsPresent);
|
||||
AddUntilStep("Wait for initial display", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Change ruleset", () => Ruleset.Value = rulesets.GetRuleset("fruits"));
|
||||
AddUntilStep("Became not present", () => !topLocalRank.IsPresent);
|
||||
AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank == null);
|
||||
|
||||
AddStep("Change ruleset back", () => Ruleset.Value = rulesets.GetRuleset("osu"));
|
||||
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHigherScoreSet()
|
||||
{
|
||||
ScoreInfo testScoreInfo = null;
|
||||
|
||||
AddAssert("Initially not present", () => !topLocalRank.IsPresent);
|
||||
|
||||
AddStep("Add score for current user", () =>
|
||||
{
|
||||
testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
@ -125,21 +115,58 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
||||
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Add higher score for current user", () =>
|
||||
{
|
||||
var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo2.User = API.LocalUser.Value;
|
||||
testScoreInfo2.Rank = ScoreRank.S;
|
||||
testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
|
||||
testScoreInfo2.Rank = ScoreRank.X;
|
||||
testScoreInfo2.TotalScore = 1000000;
|
||||
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
|
||||
|
||||
scoreManager.Import(testScoreInfo2);
|
||||
});
|
||||
|
||||
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
|
||||
AddUntilStep("SS rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.X);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyScore()
|
||||
{
|
||||
ScoreInfo testScoreInfo = null!;
|
||||
|
||||
AddStep("Add legacy score for current user", () =>
|
||||
{
|
||||
testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
testScoreInfo.TotalScore = scoreManager.GetTotalScore(testScoreInfo, ScoringMode.Classic);
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Add higher score for current user", () =>
|
||||
{
|
||||
var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo2.User = API.LocalUser.Value;
|
||||
testScoreInfo2.Rank = ScoreRank.X;
|
||||
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
|
||||
testScoreInfo2.TotalScore = scoreManager.GetTotalScore(testScoreInfo2);
|
||||
|
||||
// ensure second score has a total score (standardised) less than first one (classic)
|
||||
// despite having better statistics, otherwise this test is pointless.
|
||||
Debug.Assert(testScoreInfo2.TotalScore < testScoreInfo.TotalScore);
|
||||
|
||||
scoreManager.Import(testScoreInfo2);
|
||||
});
|
||||
|
||||
AddUntilStep("SS rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.X);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,14 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using osu.Game.Updater;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneNotificationOverlay : OsuTestScene
|
||||
public class TestSceneNotificationOverlay : OsuManualInputManagerTestScene
|
||||
{
|
||||
private NotificationOverlay notificationOverlay = null!;
|
||||
|
||||
@ -32,7 +35,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
TimeToCompleteProgress = 2000;
|
||||
progressingNotifications.Clear();
|
||||
|
||||
Content.Children = new Drawable[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
notificationOverlay = new NotificationOverlay
|
||||
{
|
||||
@ -45,6 +48,89 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestDismissWithoutActivationCloseButton()
|
||||
{
|
||||
bool activated = false;
|
||||
SimpleNotification notification = null!;
|
||||
|
||||
AddStep("post", () =>
|
||||
{
|
||||
activated = false;
|
||||
notificationOverlay.Post(notification = new SimpleNotification
|
||||
{
|
||||
Text = @"Welcome to osu!. Enjoy your stay!",
|
||||
Activated = () => activated = true,
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("click to activate", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(notificationOverlay
|
||||
.ChildrenOfType<Notification>().Single()
|
||||
.ChildrenOfType<Notification.CloseButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for closed", () => notification.WasClosed);
|
||||
AddAssert("was not activated", () => !activated);
|
||||
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDismissWithoutActivationRightClick()
|
||||
{
|
||||
bool activated = false;
|
||||
SimpleNotification notification = null!;
|
||||
|
||||
AddStep("post", () =>
|
||||
{
|
||||
activated = false;
|
||||
notificationOverlay.Post(notification = new SimpleNotification
|
||||
{
|
||||
Text = @"Welcome to osu!. Enjoy your stay!",
|
||||
Activated = () => activated = true,
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("click to activate", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single());
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for closed", () => notification.WasClosed);
|
||||
AddAssert("was not activated", () => !activated);
|
||||
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestActivate()
|
||||
{
|
||||
bool activated = false;
|
||||
SimpleNotification notification = null!;
|
||||
|
||||
AddStep("post", () =>
|
||||
{
|
||||
activated = false;
|
||||
notificationOverlay.Post(notification = new SimpleNotification
|
||||
{
|
||||
Text = @"Welcome to osu!. Enjoy your stay!",
|
||||
Activated = () => activated = true,
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("click to activate", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for closed", () => notification.WasClosed);
|
||||
AddAssert("was activated", () => activated);
|
||||
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPresence()
|
||||
{
|
||||
@ -134,6 +220,35 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("cancel notification", () => notification.State = ProgressNotificationState.Cancelled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUpdateNotificationFlow()
|
||||
{
|
||||
bool applyUpdate = false;
|
||||
|
||||
AddStep(@"post update", () =>
|
||||
{
|
||||
applyUpdate = false;
|
||||
|
||||
var updateNotification = new UpdateManager.UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = () => applyUpdate = true
|
||||
};
|
||||
|
||||
notificationOverlay.Post(updateNotification);
|
||||
progressingNotifications.Add(updateNotification);
|
||||
});
|
||||
|
||||
checkProgressingCount(1);
|
||||
waitForCompletion();
|
||||
|
||||
UpdateManager.UpdateApplicationCompleteNotification? completionNotification = null;
|
||||
AddUntilStep("wait for completion notification",
|
||||
() => (completionNotification = notificationOverlay.ChildrenOfType<UpdateManager.UpdateApplicationCompleteNotification>().SingleOrDefault()) != null);
|
||||
AddStep("click notification", () => completionNotification?.TriggerClick());
|
||||
|
||||
AddUntilStep("wait for update applied", () => applyUpdate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFlow()
|
||||
{
|
||||
|
@ -280,12 +280,15 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
|
||||
var processor = rulesetInstance.CreateBeatmapProcessor(converted);
|
||||
|
||||
foreach (var mod in mods.OfType<IApplicableToBeatmapProcessor>())
|
||||
mod.ApplyToBeatmapProcessor(processor);
|
||||
if (processor != null)
|
||||
{
|
||||
foreach (var mod in mods.OfType<IApplicableToBeatmapProcessor>())
|
||||
mod.ApplyToBeatmapProcessor(processor);
|
||||
|
||||
processor?.PreProcess();
|
||||
processor.PreProcess();
|
||||
}
|
||||
|
||||
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
|
||||
foreach (var obj in converted.HitObjects)
|
||||
|
@ -4,7 +4,9 @@
|
||||
#nullable disable
|
||||
|
||||
using Markdig;
|
||||
using Markdig.Extensions.AutoIdentifiers;
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Extensions.Yaml;
|
||||
using Markdig.Syntax;
|
||||
@ -18,6 +20,18 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
{
|
||||
public class OsuMarkdownContainer : MarkdownContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows this markdown container to parse and link footnotes.
|
||||
/// </summary>
|
||||
/// <seealso cref="FootnoteExtension"/>
|
||||
protected virtual bool Footnotes => false;
|
||||
|
||||
/// <summary>
|
||||
/// Allows this markdown container to make URL text clickable.
|
||||
/// </summary>
|
||||
/// <seealso cref="AutoLinkExtension"/>
|
||||
protected virtual bool Autolinks => false;
|
||||
|
||||
public OsuMarkdownContainer()
|
||||
{
|
||||
LineSpacing = 21;
|
||||
@ -78,10 +92,22 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
return new OsuMarkdownUnorderedListItem(level);
|
||||
}
|
||||
|
||||
// reference: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
|
||||
protected override MarkdownPipeline CreateBuilder()
|
||||
=> new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
|
||||
.UseEmojiAndSmiley()
|
||||
.UseYamlFrontMatter()
|
||||
.UseAdvancedExtensions().Build();
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoIdentifiers()
|
||||
.UsePipeTables()
|
||||
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
|
||||
.UseYamlFrontMatter();
|
||||
|
||||
if (Footnotes)
|
||||
pipeline = pipeline.UseFootnotes();
|
||||
|
||||
if (Autolinks)
|
||||
pipeline = pipeline.UseAutoLinks();
|
||||
|
||||
return pipeline.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
public Mod ToMod(Ruleset ruleset)
|
||||
{
|
||||
Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
|
||||
Mod? resultMod = ruleset.CreateModFromAcronym(Acronym);
|
||||
|
||||
if (resultMod == null)
|
||||
{
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
set => Model = value;
|
||||
}
|
||||
|
||||
public UpdateableRank(ScoreRank? rank)
|
||||
public UpdateableRank(ScoreRank? rank = null)
|
||||
{
|
||||
Rank = rank;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// Whether only a single instance of this <see cref="MultiplayerCountdown"/> type may be active at any one time.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
public virtual bool IsExclusive => true;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
@ -93,7 +94,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.Repository))
|
||||
if (!string.IsNullOrEmpty(entry.Repository) && !string.IsNullOrEmpty(entry.GithubUrl))
|
||||
addRepositoryReference(title, entryColour);
|
||||
|
||||
if (entry.GithubUser != null)
|
||||
@ -104,17 +105,22 @@ namespace osu.Game.Overlays.Changelog
|
||||
|
||||
private void addRepositoryReference(LinkFlowContainer title, Color4 entryColour)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(entry.Repository));
|
||||
Debug.Assert(!string.IsNullOrEmpty(entry.GithubUrl));
|
||||
|
||||
title.AddText(" (", t =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
|
||||
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
|
||||
t =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
|
||||
title.AddText(")", t =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
public class CommentMarkdownContainer : OsuMarkdownContainer
|
||||
{
|
||||
protected override bool Autolinks => true;
|
||||
|
||||
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
||||
|
||||
private class CommentMarkdownHeading : OsuMarkdownHeading
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Overlays.Notifications
|
||||
{
|
||||
@ -170,11 +171,25 @@ namespace osu.Game.Overlays.Notifications
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
// right click doesn't trigger OnClick so we need to handle here until that changes.
|
||||
if (e.Button != MouseButton.Left)
|
||||
{
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (Activated?.Invoke() ?? true)
|
||||
Close();
|
||||
// Clicking with anything but left button should dismiss but not perform the activation action.
|
||||
if (e.Button == MouseButton.Left)
|
||||
Activated?.Invoke();
|
||||
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -203,7 +218,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
Expire();
|
||||
}
|
||||
|
||||
private class CloseButton : OsuClickableContainer
|
||||
internal class CloseButton : OsuClickableContainer
|
||||
{
|
||||
private SpriteIcon icon = null!;
|
||||
private Box background = null!;
|
||||
|
@ -226,6 +226,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
Depth = float.MaxValue,
|
||||
},
|
||||
loadingSpinner = new LoadingSpinner
|
||||
{
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsSubsection section = ruleset.CreateSettings();
|
||||
SettingsSubsection? section = ruleset.CreateSettings();
|
||||
|
||||
if (section != null)
|
||||
Add(section);
|
||||
|
@ -15,6 +15,8 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
public class WikiMarkdownContainer : OsuMarkdownContainer
|
||||
{
|
||||
protected override bool Footnotes => true;
|
||||
|
||||
public string CurrentPath
|
||||
{
|
||||
set => DocumentUrl = value;
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
public interface ILegacyRuleset
|
||||
|
@ -1,13 +1,10 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -100,7 +97,7 @@ namespace osu.Game.Rulesets
|
||||
/// Returns a fresh instance of the mod matching the specified acronym.
|
||||
/// </summary>
|
||||
/// <param name="acronym">The acronym to query for .</param>
|
||||
public Mod CreateModFromAcronym(string acronym)
|
||||
public Mod? CreateModFromAcronym(string acronym)
|
||||
{
|
||||
return AllMods.FirstOrDefault(m => m.Acronym == acronym)?.CreateInstance();
|
||||
}
|
||||
@ -108,7 +105,7 @@ namespace osu.Game.Rulesets
|
||||
/// <summary>
|
||||
/// Returns a fresh instance of the mod matching the specified type.
|
||||
/// </summary>
|
||||
public T CreateMod<T>()
|
||||
public T? CreateMod<T>()
|
||||
where T : Mod
|
||||
{
|
||||
return AllMods.FirstOrDefault(m => m is T)?.CreateInstance() as T;
|
||||
@ -122,7 +119,6 @@ namespace osu.Game.Rulesets
|
||||
/// then the proper behaviour is to return an empty enumerable.
|
||||
/// <see langword="null"/> mods should not be present in the returned enumerable.
|
||||
/// </remarks>
|
||||
[ItemNotNull]
|
||||
public abstract IEnumerable<Mod> GetModsFor(ModType type);
|
||||
|
||||
/// <summary>
|
||||
@ -202,10 +198,9 @@ namespace osu.Game.Rulesets
|
||||
return value;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public ModAutoplay GetAutoplayMod() => CreateMod<ModAutoplay>();
|
||||
public ModAutoplay? GetAutoplayMod() => CreateMod<ModAutoplay>();
|
||||
|
||||
public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;
|
||||
public virtual ISkin? CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => null;
|
||||
|
||||
protected Ruleset()
|
||||
{
|
||||
@ -225,7 +220,7 @@ namespace osu.Game.Rulesets
|
||||
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to apply.</param>
|
||||
/// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception>
|
||||
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null);
|
||||
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ScoreProcessor"/> for this <see cref="Ruleset"/>.
|
||||
@ -251,7 +246,7 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> to be processed.</param>
|
||||
/// <returns>The <see cref="IBeatmapProcessor"/>.</returns>
|
||||
public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null;
|
||||
public virtual IBeatmapProcessor? CreateBeatmapProcessor(IBeatmap beatmap) => null;
|
||||
|
||||
public abstract DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap);
|
||||
|
||||
@ -259,12 +254,11 @@ namespace osu.Game.Rulesets
|
||||
/// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score.
|
||||
/// </summary>
|
||||
/// <returns>A performance calculator instance for the provided score.</returns>
|
||||
[CanBeNull]
|
||||
public virtual PerformanceCalculator CreatePerformanceCalculator() => null;
|
||||
public virtual PerformanceCalculator? CreatePerformanceCalculator() => null;
|
||||
|
||||
public virtual HitObjectComposer CreateHitObjectComposer() => null;
|
||||
public virtual HitObjectComposer? CreateHitObjectComposer() => null;
|
||||
|
||||
public virtual IBeatmapVerifier CreateBeatmapVerifier() => null;
|
||||
public virtual IBeatmapVerifier? CreateBeatmapVerifier() => null;
|
||||
|
||||
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle };
|
||||
|
||||
@ -272,13 +266,13 @@ namespace osu.Game.Rulesets
|
||||
|
||||
public abstract string Description { get; }
|
||||
|
||||
public virtual RulesetSettingsSubsection CreateSettings() => null;
|
||||
public virtual RulesetSettingsSubsection? CreateSettings() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="IRulesetConfigManager"/> for this <see cref="Ruleset"/>.
|
||||
/// </summary>
|
||||
/// <param name="settings">The <see cref="SettingsStore"/> to store the settings.</param>
|
||||
public virtual IRulesetConfigManager CreateConfig(SettingsStore settings) => null;
|
||||
public virtual IRulesetConfigManager? CreateConfig(SettingsStore? settings) => null;
|
||||
|
||||
/// <summary>
|
||||
/// A unique short name to reference this ruleset in online requests.
|
||||
@ -314,7 +308,7 @@ namespace osu.Game.Rulesets
|
||||
/// for conversion use.
|
||||
/// </summary>
|
||||
/// <returns>An empty frame for the current ruleset, or null if unsupported.</returns>
|
||||
public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null;
|
||||
public virtual IConvertibleReplayFrame? CreateConvertibleReplayFrame() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the statistics for a <see cref="ScoreInfo"/> to be displayed in the results screen.
|
||||
@ -322,7 +316,6 @@ namespace osu.Game.Rulesets
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to create the statistics for. The score is guaranteed to have <see cref="ScoreInfo.HitEvents"/> populated.</param>
|
||||
/// <param name="playableBeatmap">The <see cref="IBeatmap"/>, converted for this <see cref="Ruleset"/> with all relevant <see cref="Mod"/>s applied.</param>
|
||||
/// <returns>The <see cref="StatisticRow"/>s to display. Each <see cref="StatisticRow"/> may contain 0 or more <see cref="StatisticItem"/>.</returns>
|
||||
[NotNull]
|
||||
public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticRow>();
|
||||
|
||||
/// <summary>
|
||||
@ -375,13 +368,11 @@ namespace osu.Game.Rulesets
|
||||
/// <summary>
|
||||
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null;
|
||||
public virtual IRulesetFilterCriteria? CreateRulesetFilterCriteria() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public virtual RulesetSetupSection CreateEditorSetupSection() => null;
|
||||
public virtual RulesetSetupSection? CreateEditorSetupSection() => null;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
@ -18,7 +16,7 @@ namespace osu.Game.Rulesets
|
||||
private readonly RealmAccess realm;
|
||||
private readonly RulesetStore rulesets;
|
||||
|
||||
private readonly Dictionary<string, IRulesetConfigManager> configCache = new Dictionary<string, IRulesetConfigManager>();
|
||||
private readonly Dictionary<string, IRulesetConfigManager?> configCache = new Dictionary<string, IRulesetConfigManager?>();
|
||||
|
||||
public RulesetConfigCache(RealmAccess realm, RulesetStore rulesets)
|
||||
{
|
||||
@ -42,7 +40,7 @@ namespace osu.Game.Rulesets
|
||||
}
|
||||
}
|
||||
|
||||
public IRulesetConfigManager GetConfigFor(Ruleset ruleset)
|
||||
public IRulesetConfigManager? GetConfigFor(Ruleset ruleset)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
throw new InvalidOperationException($@"Cannot retrieve {nameof(IRulesetConfigManager)} before {nameof(RulesetConfigCache)} has loaded");
|
||||
|
@ -272,7 +272,24 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score of a given finalised <see cref="ScoreInfo"/>. This should be used when a score is known to be complete.
|
||||
/// Computes the accuracy of a given <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The score's accuracy.</returns>
|
||||
[Pure]
|
||||
public double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
|
||||
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
|
||||
|
||||
return maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score of a given <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -12,6 +10,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
@ -44,29 +43,26 @@ namespace osu.Game.Rulesets.UI
|
||||
public ShaderManager ShaderManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ruleset config manager.
|
||||
/// The ruleset config manager. May be null if ruleset does not expose a configuration manager.
|
||||
/// </summary>
|
||||
public IRulesetConfigManager RulesetConfigManager { get; private set; }
|
||||
public IRulesetConfigManager? RulesetConfigManager { get; }
|
||||
|
||||
public DrawableRulesetDependencies(Ruleset ruleset, IReadOnlyDependencyContainer parent)
|
||||
: base(parent)
|
||||
{
|
||||
var resources = ruleset.CreateResourceStore();
|
||||
|
||||
if (resources != null)
|
||||
{
|
||||
var host = parent.Get<GameHost>();
|
||||
var host = parent.Get<GameHost>();
|
||||
|
||||
TextureStore = new TextureStore(host.Renderer, parent.Get<GameHost>().CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
|
||||
CacheAs(TextureStore = new FallbackTextureStore(host.Renderer, TextureStore, parent.Get<TextureStore>()));
|
||||
TextureStore = new TextureStore(host.Renderer, parent.Get<GameHost>().CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
|
||||
CacheAs(TextureStore = new FallbackTextureStore(host.Renderer, TextureStore, parent.Get<TextureStore>()));
|
||||
|
||||
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
||||
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
|
||||
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
||||
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
|
||||
|
||||
ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore<byte[]>(resources, @"Shaders"));
|
||||
CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get<ShaderManager>()));
|
||||
}
|
||||
ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore<byte[]>(resources, @"Shaders"));
|
||||
CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get<ShaderManager>()));
|
||||
|
||||
RulesetConfigManager = parent.Get<IRulesetConfigCache>().GetConfigFor(ruleset);
|
||||
if (RulesetConfigManager != null)
|
||||
@ -96,10 +92,9 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
isDisposed = true;
|
||||
|
||||
SampleStore?.Dispose();
|
||||
TextureStore?.Dispose();
|
||||
ShaderManager?.Dispose();
|
||||
RulesetConfigManager = null;
|
||||
if (ShaderManager.IsNotNull()) SampleStore.Dispose();
|
||||
if (TextureStore.IsNotNull()) TextureStore.Dispose();
|
||||
if (ShaderManager.IsNotNull()) ShaderManager.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -160,7 +155,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
primary?.Dispose();
|
||||
if (primary.IsNotNull()) primary.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,7 +180,7 @@ namespace osu.Game.Rulesets.UI
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
primary?.Dispose();
|
||||
if (primary.IsNotNull()) primary.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,12 +196,12 @@ namespace osu.Game.Rulesets.UI
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
public override byte[] LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
|
||||
public override byte[]? LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
primary?.Dispose();
|
||||
if (primary.IsNotNull()) primary.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -34,8 +35,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(CancellationToken cancellationToken)
|
||||
{
|
||||
// HUD overlay may not be loaded if load has been cancelled early.
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
HUDOverlay.PlayerSettingsOverlay.Expire();
|
||||
HUDOverlay.HoldToQuit.Expire();
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
@ -84,6 +85,7 @@ namespace osu.Game.Screens.Play
|
||||
foreach (var frame in bundle.Frames)
|
||||
{
|
||||
IConvertibleReplayFrame convertibleFrame = GameplayState.Ruleset.CreateConvertibleReplayFrame();
|
||||
Debug.Assert(convertibleFrame != null);
|
||||
convertibleFrame.FromLegacy(frame, GameplayState.Beatmap);
|
||||
|
||||
var convertedFrame = (ReplayFrame)convertibleFrame;
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -47,6 +45,12 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
/// </summary>
|
||||
private readonly IReadOnlyList<HitEvent> hitEvents;
|
||||
|
||||
private readonly IDictionary<HitResult, int>[] bins;
|
||||
private double binSize;
|
||||
private double hitOffset;
|
||||
|
||||
private Bar[]? barDrawables;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HitEventTimingDistributionGraph"/>.
|
||||
/// </summary>
|
||||
@ -54,22 +58,15 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
public HitEventTimingDistributionGraph(IReadOnlyList<HitEvent> hitEvents)
|
||||
{
|
||||
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
|
||||
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
|
||||
}
|
||||
|
||||
private IDictionary<HitResult, int>[] bins;
|
||||
private double binSize;
|
||||
private double hitOffset;
|
||||
|
||||
private Bar[] barDrawables;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (hitEvents == null || hitEvents.Count == 0)
|
||||
if (hitEvents.Count == 0)
|
||||
return;
|
||||
|
||||
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
|
||||
|
||||
binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins);
|
||||
|
||||
// Prevent div-by-0 by enforcing a minimum bin size
|
||||
@ -209,25 +206,30 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
|
||||
private class Bar : CompositeDrawable
|
||||
{
|
||||
private float totalValue => values.Sum(v => v.Value);
|
||||
private float basalHeight => BoundingBox.Width / BoundingBox.Height;
|
||||
private float availableHeight => 1 - basalHeight;
|
||||
|
||||
private readonly IReadOnlyList<KeyValuePair<HitResult, int>> values;
|
||||
private readonly float maxValue;
|
||||
private readonly bool isCentre;
|
||||
private readonly float totalValue;
|
||||
|
||||
private Circle[] boxOriginals;
|
||||
private Circle boxAdjustment;
|
||||
private float basalHeight;
|
||||
private float offsetAdjustment;
|
||||
|
||||
private Circle[] boxOriginals = null!;
|
||||
|
||||
private Circle? boxAdjustment;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private const double duration = 300;
|
||||
|
||||
public Bar(IDictionary<HitResult, int> values, float maxValue, bool isCentre)
|
||||
{
|
||||
this.values = values.OrderBy(v => v.Key.GetIndexForOrderedDisplay()).ToList();
|
||||
this.maxValue = maxValue;
|
||||
this.isCentre = isCentre;
|
||||
totalValue = values.Sum(v => v.Value);
|
||||
offsetAdjustment = totalValue;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
@ -254,38 +256,32 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
else
|
||||
{
|
||||
// A bin with no value draws a grey dot instead.
|
||||
InternalChildren = boxOriginals = new[]
|
||||
Circle dot = new Circle
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = isCentre ? Color4.White : Color4.Gray,
|
||||
Height = 0,
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = isCentre ? Color4.White : Color4.Gray,
|
||||
Height = 0,
|
||||
};
|
||||
InternalChildren = boxOriginals = new[] { dot };
|
||||
}
|
||||
}
|
||||
|
||||
private const double duration = 300;
|
||||
|
||||
private float offsetForValue(float value)
|
||||
{
|
||||
return availableHeight * value / maxValue;
|
||||
}
|
||||
|
||||
private float heightForValue(float value)
|
||||
{
|
||||
return basalHeight + offsetForValue(value);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (!values.Any())
|
||||
return;
|
||||
|
||||
updateBasalHeight();
|
||||
|
||||
foreach (var boxOriginal in boxOriginals)
|
||||
{
|
||||
boxOriginal.Y = 0;
|
||||
boxOriginal.Height = basalHeight;
|
||||
}
|
||||
|
||||
float offsetValue = 0;
|
||||
|
||||
@ -297,6 +293,12 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updateBasalHeight();
|
||||
}
|
||||
|
||||
public void UpdateOffset(float adjustment)
|
||||
{
|
||||
bool hasAdjustment = adjustment != totalValue;
|
||||
@ -318,7 +320,53 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
});
|
||||
}
|
||||
|
||||
boxAdjustment.ResizeHeightTo(heightForValue(adjustment), duration, Easing.OutQuint);
|
||||
offsetAdjustment = adjustment;
|
||||
drawAdjustmentBar();
|
||||
}
|
||||
|
||||
private void updateBasalHeight()
|
||||
{
|
||||
float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1;
|
||||
|
||||
if (newBasalHeight == basalHeight)
|
||||
return;
|
||||
|
||||
basalHeight = newBasalHeight;
|
||||
foreach (var dot in boxOriginals)
|
||||
dot.Height = basalHeight;
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue;
|
||||
|
||||
private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0);
|
||||
|
||||
private void draw()
|
||||
{
|
||||
resizeBars();
|
||||
|
||||
if (boxAdjustment != null)
|
||||
drawAdjustmentBar();
|
||||
}
|
||||
|
||||
private void resizeBars()
|
||||
{
|
||||
float offsetValue = 0;
|
||||
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight;
|
||||
boxOriginals[i].Height = heightForValue(values[i].Value);
|
||||
offsetValue -= values[i].Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAdjustmentBar()
|
||||
{
|
||||
bool hasAdjustment = offsetAdjustment != totalValue;
|
||||
|
||||
boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint);
|
||||
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
@ -20,27 +19,39 @@ using Realms;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public class TopLocalRank : UpdateableRank
|
||||
public class TopLocalRank : CompositeDrawable
|
||||
{
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
private IDisposable scoreSubscription;
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private IDisposable? scoreSubscription;
|
||||
|
||||
private readonly UpdateableRank updateable;
|
||||
|
||||
public ScoreRank? DisplayedRank => updateable.Rank;
|
||||
|
||||
public TopLocalRank(BeatmapInfo beatmapInfo)
|
||||
: base(null)
|
||||
{
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
|
||||
Size = new Vector2(40, 20);
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = updateable = new UpdateableRank
|
||||
{
|
||||
Size = new Vector2(40, 20),
|
||||
Alpha = 0,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -55,23 +66,27 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
|
||||
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
|
||||
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
|
||||
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName)
|
||||
.OrderByDescending(s => s.TotalScore),
|
||||
(items, _, _) =>
|
||||
{
|
||||
Rank = items.FirstOrDefault()?.Rank;
|
||||
// Required since presence is changed via IsPresent override
|
||||
Invalidate(Invalidation.Presence);
|
||||
});
|
||||
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName),
|
||||
localScoresChanged);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public override bool IsPresent => base.IsPresent && Rank != null;
|
||||
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes, Exception _)
|
||||
{
|
||||
// This subscription may fire from changes to linked beatmaps, which we don't care about.
|
||||
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
|
||||
if (changes?.HasCollectionChanges() == false)
|
||||
return;
|
||||
|
||||
ScoreInfo? topScore = scoreManager.OrderByTotalScore(sender.Detach()).FirstOrDefault();
|
||||
|
||||
updateable.Rank = topScore?.Rank;
|
||||
updateable.Alpha = topScore != null ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
scoreSubscription?.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -22,6 +24,15 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
private SpriteIcon icon = null!;
|
||||
private Box progressFill = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private LoginOverlay? loginOverlay { get; set; }
|
||||
|
||||
public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo)
|
||||
{
|
||||
this.beatmapSetInfo = beatmapSetInfo;
|
||||
@ -32,9 +43,6 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
Origin = Anchor.CentreLeft;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -90,6 +98,12 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
Action = () =>
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
loginOverlay?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo);
|
||||
attachExistingDownload();
|
||||
};
|
||||
|
@ -1,9 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@ -125,10 +124,24 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
if (Enum.TryParse(value, true, out result)) return true;
|
||||
|
||||
value = Enum.GetNames(typeof(TEnum)).FirstOrDefault(name => name.StartsWith(value, true, CultureInfo.InvariantCulture));
|
||||
string? prefixMatch = Enum.GetNames(typeof(TEnum)).FirstOrDefault(name => name.StartsWith(value, true, CultureInfo.InvariantCulture));
|
||||
|
||||
if (prefixMatch == null)
|
||||
return false;
|
||||
|
||||
return Enum.TryParse(value, true, out result);
|
||||
}
|
||||
|
||||
private static GroupCollection? tryMatchRegex(string value, string regex)
|
||||
{
|
||||
Match matches = Regex.Match(value, regex);
|
||||
|
||||
if (matches.Success)
|
||||
return matches.Groups;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter with the specified <paramref name="op"/> and textual <paramref name="value"/>.
|
||||
/// If the value indicates a valid textual filter, the function returns <c>true</c> and the resulting data is stored into
|
||||
@ -312,11 +325,45 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val)
|
||||
{
|
||||
if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out double length))
|
||||
List<string> parts = new List<string>();
|
||||
|
||||
GroupCollection? match = null;
|
||||
|
||||
match ??= tryMatchRegex(val, @"^((?<hours>\d+):)?(?<minutes>\d+):(?<seconds>\d+)$");
|
||||
match ??= tryMatchRegex(val, @"^((?<hours>\d+(\.\d+)?)h)?((?<minutes>\d+(\.\d+)?)m)?((?<seconds>\d+(\.\d+)?)s)?$");
|
||||
match ??= tryMatchRegex(val, @"^(?<seconds>\d+(\.\d+)?)$");
|
||||
|
||||
if (match == null)
|
||||
return false;
|
||||
|
||||
int scale = getLengthScale(val);
|
||||
return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
if (match["seconds"].Success)
|
||||
parts.Add(match["seconds"].Value + "s");
|
||||
if (match["minutes"].Success)
|
||||
parts.Add(match["minutes"].Value + "m");
|
||||
if (match["hours"].Success)
|
||||
parts.Add(match["hours"].Value + "h");
|
||||
|
||||
double totalLength = 0;
|
||||
int minScale = 3600000;
|
||||
|
||||
for (int i = 0; i < parts.Count; i++)
|
||||
{
|
||||
string part = parts[i];
|
||||
string partNoUnit = part.TrimEnd('m', 's', 'h');
|
||||
if (!tryParseDoubleWithPoint(partNoUnit, out double length))
|
||||
return false;
|
||||
|
||||
if (i != parts.Count - 1 && length >= 60)
|
||||
return false;
|
||||
if (i != 0 && partNoUnit.Contains('.'))
|
||||
return false;
|
||||
|
||||
int scale = getLengthScale(part);
|
||||
totalLength += length * scale;
|
||||
minScale = Math.Min(minScale, scale);
|
||||
}
|
||||
|
||||
return tryUpdateCriteriaRange(ref criteria.Length, op, totalLength, minScale / 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Skinning
|
||||
if (result != HitResult.Miss)
|
||||
{
|
||||
//new judgement shows old as a temporary effect
|
||||
AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f)
|
||||
AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f, true)
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -18,11 +18,13 @@ namespace osu.Game.Skinning
|
||||
private readonly HitResult result;
|
||||
|
||||
private readonly float finalScale;
|
||||
private readonly bool forceTransforms;
|
||||
|
||||
public LegacyJudgementPieceOld(HitResult result, Func<Drawable> createMainDrawable, float finalScale = 1f)
|
||||
public LegacyJudgementPieceOld(HitResult result, Func<Drawable> createMainDrawable, float finalScale = 1f, bool forceTransforms = false)
|
||||
{
|
||||
this.result = result;
|
||||
this.finalScale = finalScale;
|
||||
this.forceTransforms = forceTransforms;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
@ -43,8 +45,8 @@ namespace osu.Game.Skinning
|
||||
this.FadeInFromZero(fade_in_length);
|
||||
this.Delay(fade_out_delay).FadeOut(fade_out_length);
|
||||
|
||||
// legacy judgements don't play any transforms if they are an animation.
|
||||
if (animation?.FrameCount > 1)
|
||||
// legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece.
|
||||
if (animation?.FrameCount > 1 && !forceTransforms)
|
||||
return;
|
||||
|
||||
switch (result)
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -115,6 +116,21 @@ namespace osu.Game.Storyboards.Drawables
|
||||
Animation.ApplyTransforms(this);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IGameplayClock gameplayClock { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// Framework animation class tries its best to synchronise the animation at LoadComplete,
|
||||
// but in some cases (such as fast forward) this results in an incorrect start offset.
|
||||
//
|
||||
// In the case of storyboard animations, we want to synchronise with game time perfectly
|
||||
// so let's get a correct time based on gameplay clock and earliest transform.
|
||||
PlaybackPosition = gameplayClock.CurrentTime - Animation.EarliestTransformTime;
|
||||
}
|
||||
|
||||
private void skinSourceChanged()
|
||||
{
|
||||
ClearFrames();
|
||||
|
@ -54,6 +54,14 @@ namespace osu.Game.Storyboards
|
||||
return firstAlpha.startTime;
|
||||
}
|
||||
|
||||
return EarliestTransformTime;
|
||||
}
|
||||
}
|
||||
|
||||
public double EarliestTransformTime
|
||||
{
|
||||
get
|
||||
{
|
||||
// If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value.
|
||||
// The sprite's StartTime will be determined by the earliest command, regardless of type.
|
||||
double earliestStartTime = TimelineGroup.StartTime;
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
@ -14,8 +12,8 @@ namespace osu.Game.Tests.Rulesets
|
||||
/// </summary>
|
||||
public class TestRulesetConfigCache : IRulesetConfigCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IRulesetConfigManager> configCache = new ConcurrentDictionary<string, IRulesetConfigManager>();
|
||||
private readonly ConcurrentDictionary<string, IRulesetConfigManager?> configCache = new ConcurrentDictionary<string, IRulesetConfigManager?>();
|
||||
|
||||
public IRulesetConfigManager GetConfigFor(Ruleset ruleset) => configCache.GetOrAdd(ruleset.ShortName, _ => ruleset.CreateConfig(null));
|
||||
public IRulesetConfigManager? GetConfigFor(Ruleset ruleset) => configCache.GetOrAdd(ruleset.ShortName, _ => ruleset.CreateConfig(null));
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Updater
|
||||
{
|
||||
@ -27,13 +27,13 @@ namespace osu.Game.Updater
|
||||
GetType() != typeof(UpdateManager);
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
protected INotificationOverlay Notifications { get; private set; }
|
||||
protected INotificationOverlay Notifications { get; private set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@ -59,7 +59,7 @@ namespace osu.Game.Updater
|
||||
|
||||
private readonly object updateTaskLock = new object();
|
||||
|
||||
private Task<bool> updateCheckTask;
|
||||
private Task<bool>? updateCheckTask;
|
||||
|
||||
public async Task<bool> CheckForUpdateAsync()
|
||||
{
|
||||
@ -109,5 +109,76 @@ namespace osu.Game.Updater
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateApplicationCompleteNotification : ProgressCompletionNotification
|
||||
{
|
||||
public UpdateApplicationCompleteNotification()
|
||||
{
|
||||
Text = @"Update ready to install. Click to restart!";
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateProgressNotification : ProgressNotification
|
||||
{
|
||||
protected override Notification CreateCompletionNotification() => new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = CompletionClickAction
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
IconContent.AddRange(new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Upload,
|
||||
Size = new Vector2(34),
|
||||
Colour = OsuColour.Gray(0.2f),
|
||||
Depth = float.MaxValue,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
StartDownload();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
// cancelling updates is not currently supported by the underlying updater.
|
||||
// only allow dismissing for now.
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case ProgressNotificationState.Cancelled:
|
||||
base.Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void StartDownload()
|
||||
{
|
||||
State = ProgressNotificationState.Active;
|
||||
Progress = 0;
|
||||
Text = @"Downloading update...";
|
||||
}
|
||||
|
||||
public void StartInstall()
|
||||
{
|
||||
Progress = 0;
|
||||
Text = @"Installing update...";
|
||||
}
|
||||
|
||||
public void FailDownload()
|
||||
{
|
||||
State = ProgressNotificationState.Cancelled;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user