1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 20:47:28 +08:00

Merge branch 'master' into move-td-reduction

This commit is contained in:
Dan Balasescu 2022-09-13 16:12:26 +09:00 committed by GitHub
commit 4004d57448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 744 additions and 291 deletions

View File

@ -1,5 +1,8 @@
on: [push, pull_request]
name: Continuous Integration
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
inspect-code:

View File

@ -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;

View File

@ -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();

View File

@ -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)
{
}

View File

@ -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);

View File

@ -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();

View File

@ -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()
{

View File

@ -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();

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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

View File

@ -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
{

View File

@ -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)
}
};
});

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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()
{

View File

@ -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)

View File

@ -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();
}
}
}

View File

@ -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)
{

View File

@ -17,7 +17,7 @@ namespace osu.Game.Online.Leaderboards
set => Model = value;
}
public UpdateableRank(ScoreRank? rank)
public UpdateableRank(ScoreRank? rank = null)
{
Rank = rank;
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -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!;

View File

@ -226,6 +226,7 @@ namespace osu.Game.Overlays.Notifications
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
Depth = float.MaxValue,
},
loadingSpinner = new LoadingSpinner
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections
{
try
{
SettingsSubsection section = ruleset.CreateSettings();
SettingsSubsection? section = ruleset.CreateSettings();
if (section != null)
Add(section);

View File

@ -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;

View File

@ -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

View File

@ -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;
}
}

View File

@ -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");

View File

@ -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.

View File

@ -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();
}
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
};

View File

@ -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);
}
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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();

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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();
}
}
}
}