1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 04:11:29 +08:00

Compare commits

...

144 Commits

64 changed files with 1349 additions and 358 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2022.1.1",
"version": "2022.2.3",
"commands": [
"jb"
]
+3
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:
+33 -79
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;
+1 -3
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();
@@ -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)
{
}
+2 -4
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);
@@ -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();
+2 -4
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()
{
+1 -3
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();
@@ -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()
{
@@ -154,6 +191,16 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive);
}
[Test]
public void TestPartialStatusMatch()
{
const string query = "status=r";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
}
[Test]
public void TestApplyStatusQueries()
{
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Online
{
AddStep("download beatmap", () => beatmaps.Download(test_db_model));
AddStep("cancel download from notification", () => recentNotification.Close());
AddStep("cancel download from notification", () => recentNotification.Close(true));
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
@@ -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
@@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneSelectionBlueprintDeselection : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestSingleDeleteAtSameTime()
{
HitCircle? circle1 = null;
AddStep("add two circles at the same time", () =>
{
EditorClock.Seek(0);
circle1 = new HitCircle();
var circle2 = new HitCircle();
EditorBeatmap.Add(circle1);
EditorBeatmap.Add(circle2);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(circle2);
});
AddStep("delete the first circle", () => EditorBeatmap.Remove(circle1));
AddAssert("one hitobject remains", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("one hitobject selected", () => EditorBeatmap.SelectedHitObjects.Count == 1);
}
[Test]
public void TestBigStackDeleteAtSameTime()
{
AddStep("add 20 circles at the same time", () =>
{
EditorClock.Seek(0);
for (int i = 0; i < 20; i++)
{
EditorBeatmap.Add(new HitCircle());
}
});
AddStep("select half of the circles", () =>
{
foreach (var hitObject in EditorBeatmap.HitObjects.SkipLast(10).Reverse())
{
EditorBeatmap.SelectedHitObjects.Add(hitObject);
}
});
AddStep("delete all selected circles", () =>
{
InputManager.PressKey(Key.Delete);
InputManager.ReleaseKey(Key.Delete);
});
AddAssert("10 hitobjects remain", () => EditorBeatmap.HitObjects.Count == 10);
AddAssert("no hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
}
}
}
@@ -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
{
@@ -35,6 +35,8 @@ namespace osu.Game.Tests.Visual.Online
private OsuConfigManager localConfig;
private bool returnCursorOnResponse;
[BackgroundDependencyLoader]
private void load()
{
@@ -61,6 +63,7 @@ namespace osu.Game.Tests.Visual.Online
searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
{
BeatmapSets = setsForResponse,
Cursor = returnCursorOnResponse ? new Cursor() : null,
});
return true;
@@ -106,7 +109,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
AddStep("show many results", () => fetchFor(getManyBeatmaps(100).ToArray()));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
@@ -127,10 +130,10 @@ namespace osu.Game.Tests.Visual.Online
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
AddStep("show many results", () => fetchFor(getManyBeatmaps(100).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(100);
AddStep("show more results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 30).ToArray()));
AddStep("show more results", () => fetchFor(getManyBeatmaps(30).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(30);
}
@@ -139,7 +142,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
AddStep("show many results", () => fetchFor(getManyBeatmaps(100).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(100);
setCardSize(BeatmapCardSize.Extra, viaConfig);
@@ -161,7 +164,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("fetch for 0 beatmaps", () => fetchFor());
placeholderShown();
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
AddStep("show many results", () => fetchFor(getManyBeatmaps(100).ToArray()));
AddUntilStep("wait for loaded", () => this.ChildrenOfType<BeatmapCard>().Count() == 100);
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
@@ -180,6 +183,32 @@ namespace osu.Game.Tests.Visual.Online
});
}
/// <summary>
/// During pagination, the first beatmap of the second page may be a duplicate of the last beatmap from the previous page.
/// This is currently the case with osu!web API due to ES relevance score's presence in the response cursor.
/// See: https://github.com/ppy/osu-web/issues/9270
/// </summary>
[Test]
public void TestDuplicatedBeatmapOnlyShowsOnce()
{
APIBeatmapSet beatmapSet = null;
AddStep("show many results", () =>
{
beatmapSet = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSet.Title = "last beatmap of first page";
fetchFor(getManyBeatmaps(49).Append(beatmapSet).ToArray(), true);
});
AddUntilStep("wait for loaded", () => this.ChildrenOfType<BeatmapCard>().Count() == 50);
AddStep("set next page", () => setSearchResponse(getManyBeatmaps(49).Prepend(beatmapSet).ToArray(), false));
AddStep("scroll to end", () => overlay.ChildrenOfType<OverlayScrollContainer>().Single().ScrollToEnd());
AddUntilStep("wait for loaded", () => this.ChildrenOfType<BeatmapCard>().Count() == 99);
AddAssert("beatmap not duplicated", () => overlay.ChildrenOfType<BeatmapCard>().Count(c => c.BeatmapSet.Equals(beatmapSet)) == 1);
}
[Test]
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
{
@@ -336,15 +365,25 @@ namespace osu.Game.Tests.Visual.Online
private static int searchCount;
private void fetchFor(params APIBeatmapSet[] beatmaps)
private APIBeatmapSet[] getManyBeatmaps(int count) => Enumerable.Range(0, count).Select(_ => CreateAPIBeatmapSet(Ruleset.Value)).ToArray();
private void fetchFor(params APIBeatmapSet[] beatmaps) => fetchFor(beatmaps, false);
private void fetchFor(APIBeatmapSet[] beatmaps, bool hasNextPage)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps);
setSearchResponse(beatmaps, hasNextPage);
// trigger arbitrary change for fetching.
searchControl.Query.Value = $"search {searchCount++}";
}
private void setSearchResponse(APIBeatmapSet[] beatmaps, bool hasNextPage)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps);
returnCursorOnResponse = hasNextPage;
}
private void setRankAchievedFilter(ScoreRank[] ranks)
{
AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () =>
@@ -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);
}
}
}
@@ -3,9 +3,13 @@
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
@@ -15,12 +19,13 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneBeatmapListingSortTabControl : OsuTestScene
{
private readonly BeatmapListingSortTabControl control;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
public TestSceneBeatmapListingSortTabControl()
{
BeatmapListingSortTabControl control;
OsuSpriteText current;
OsuSpriteText direction;
@@ -45,5 +50,83 @@ namespace osu.Game.Tests.Visual.UserInterface
control.SortDirection.BindValueChanged(sortDirection => direction.Text = $"Sort direction: {sortDirection.NewValue}", true);
control.Current.BindValueChanged(criteria => current.Text = $"Criteria: {criteria.NewValue}", true);
}
[Test]
public void TestRankedSort()
{
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Any);
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Leaderboard);
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Ranked);
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Qualified);
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Loved);
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Favourites);
criteriaShowsOnCategory(false, SortCriteria.Ranked, SearchCategory.Pending);
criteriaShowsOnCategory(false, SortCriteria.Ranked, SearchCategory.Wip);
criteriaShowsOnCategory(false, SortCriteria.Ranked, SearchCategory.Graveyard);
criteriaShowsOnCategory(true, SortCriteria.Ranked, SearchCategory.Mine);
}
[Test]
public void TestUpdatedSort()
{
criteriaShowsOnCategory(true, SortCriteria.Updated, SearchCategory.Any);
criteriaShowsOnCategory(false, SortCriteria.Updated, SearchCategory.Leaderboard);
criteriaShowsOnCategory(false, SortCriteria.Updated, SearchCategory.Ranked);
criteriaShowsOnCategory(false, SortCriteria.Updated, SearchCategory.Qualified);
criteriaShowsOnCategory(false, SortCriteria.Updated, SearchCategory.Loved);
criteriaShowsOnCategory(true, SortCriteria.Updated, SearchCategory.Favourites);
criteriaShowsOnCategory(true, SortCriteria.Updated, SearchCategory.Pending);
criteriaShowsOnCategory(true, SortCriteria.Updated, SearchCategory.Wip);
criteriaShowsOnCategory(true, SortCriteria.Updated, SearchCategory.Graveyard);
criteriaShowsOnCategory(true, SortCriteria.Updated, SearchCategory.Mine);
}
[Test]
public void TestNominationsSort()
{
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Any);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Leaderboard);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Ranked);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Qualified);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Loved);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Favourites);
criteriaShowsOnCategory(true, SortCriteria.Nominations, SearchCategory.Pending);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Wip);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Graveyard);
criteriaShowsOnCategory(false, SortCriteria.Nominations, SearchCategory.Mine);
}
[Test]
public void TestResetNoQuery()
{
resetUsesCriteriaOnCategory(SortCriteria.Ranked, SearchCategory.Any);
resetUsesCriteriaOnCategory(SortCriteria.Ranked, SearchCategory.Leaderboard);
resetUsesCriteriaOnCategory(SortCriteria.Ranked, SearchCategory.Ranked);
resetUsesCriteriaOnCategory(SortCriteria.Ranked, SearchCategory.Qualified);
resetUsesCriteriaOnCategory(SortCriteria.Ranked, SearchCategory.Loved);
resetUsesCriteriaOnCategory(SortCriteria.Ranked, SearchCategory.Favourites);
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Pending);
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Wip);
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Graveyard);
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Mine);
}
private void criteriaShowsOnCategory(bool expected, SortCriteria criteria, SearchCategory category)
{
AddAssert($"{criteria.ToString().ToLowerInvariant()} {(expected ? "shown" : "not shown")} on {category.ToString().ToLowerInvariant()}", () =>
{
control.Reset(category, false);
return control.ChildrenOfType<TabControl<SortCriteria>>().Single().Items.Contains(criteria) == expected;
});
}
private void resetUsesCriteriaOnCategory(SortCriteria criteria, SearchCategory category)
{
AddAssert($"reset uses {criteria.ToString().ToLowerInvariant()} on {category.ToString().ToLowerInvariant()}", () =>
{
control.Reset(category, false);
return control.Current.Value == criteria;
});
}
}
}
@@ -12,11 +12,14 @@ using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Updater;
using osuTK;
using osuTK.Input;
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,161 @@ namespace osu.Game.Tests.Visual.UserInterface
notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };
});
[Test]
public void TestForwardWithFlingRight()
{
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("start drag", () =>
{
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single());
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(500, 0));
});
AddStep("fling away", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("was not closed", () => !notification.WasClosed);
AddAssert("was not activated", () => !activated);
AddAssert("is not read", () => !notification.Read);
AddAssert("is not toast", () => !notification.IsInToastTray);
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("unread count one", () => notificationOverlay.UnreadCount.Value == 1);
}
[Test]
public void TestDismissWithoutActivationFling()
{
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("start drag", () =>
{
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single());
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
});
AddStep("fling away", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
});
AddUntilStep("wait for closed", () => notification.WasClosed);
AddAssert("was not activated", () => !activated);
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("unread count zero", () => notificationOverlay.UnreadCount.Value == 0);
}
[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));
AddAssert("unread count zero", () => notificationOverlay.UnreadCount.Value == 0);
}
[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 +292,55 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("cancel notification", () => notification.State = ProgressNotificationState.Cancelled);
}
[Test]
public void TestReadState()
{
SimpleNotification notification = null!;
AddStep(@"post", () => notificationOverlay.Post(notification = new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" }));
AddUntilStep("check is toast", () => !notification.IsInToastTray);
AddAssert("light is not visible", () => notification.ChildrenOfType<Notification.NotificationLight>().Single().Alpha == 0);
AddUntilStep("wait for forward to overlay", () => !notification.IsInToastTray);
setState(Visibility.Visible);
AddAssert("state is not read", () => !notification.Read);
AddUntilStep("light is visible", () => notification.ChildrenOfType<Notification.NotificationLight>().Single().Alpha == 1);
setState(Visibility.Hidden);
setState(Visibility.Visible);
AddAssert("state is read", () => notification.Read);
AddUntilStep("light is not visible", () => notification.ChildrenOfType<Notification.NotificationLight>().Single().Alpha == 0);
}
[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()
{
@@ -14,7 +14,7 @@ using osu.Game.Overlays;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public abstract class BeatmapCard : OsuClickableContainer
public abstract class BeatmapCard : OsuClickableContainer, IEquatable<BeatmapCard>
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
@@ -96,5 +96,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");
}
}
public bool Equals(BeatmapCard? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return BeatmapSet.Equals(other.BeatmapSet);
}
public override bool Equals(object obj) => obj is BeatmapCard other && Equals(other);
public override int GetHashCode() => BeatmapSet.GetHashCode();
}
}
+7 -4
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)
+1 -1
View File
@@ -111,7 +111,7 @@ namespace osu.Game.Database
{
if (error is WebException webException && webException.Message == @"TooManyRequests")
{
notification.Close();
notification.Close(false);
PostNotification?.Invoke(new TooManyDownloadsNotification());
}
else
@@ -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();
}
}
}
+1 -1
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)
{
@@ -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;
}
}
@@ -151,19 +151,20 @@ namespace osu.Game.Overlays.BeatmapListing
config.BindWith(OsuSetting.BeatmapListingCardSize, cardSize);
var sortCriteria = sortControl.Current;
var sortDirection = sortControl.SortDirection;
searchControl.Query.BindValueChanged(query =>
searchControl.Query.BindValueChanged(_ =>
{
sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? SortCriteria.Ranked : SortCriteria.Relevance;
sortDirection.Value = SortDirection.Descending;
resetSortControl();
queueUpdateSearch(true);
});
searchControl.Category.BindValueChanged(_ =>
{
resetSortControl();
queueUpdateSearch();
});
searchControl.General.CollectionChanged += (_, _) => queueUpdateSearch();
searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch());
searchControl.Category.BindValueChanged(_ => queueUpdateSearch());
searchControl.Genre.BindValueChanged(_ => queueUpdateSearch());
searchControl.Language.BindValueChanged(_ => queueUpdateSearch());
searchControl.Extra.CollectionChanged += (_, _) => queueUpdateSearch();
@@ -171,8 +172,8 @@ namespace osu.Game.Overlays.BeatmapListing
searchControl.Played.BindValueChanged(_ => queueUpdateSearch());
searchControl.ExplicitContent.BindValueChanged(_ => queueUpdateSearch());
sortCriteria.BindValueChanged(_ => queueUpdateSearch());
sortDirection.BindValueChanged(_ => queueUpdateSearch());
sortControl.Current.BindValueChanged(_ => queueUpdateSearch());
sortControl.SortDirection.BindValueChanged(_ => queueUpdateSearch());
apiUser = api.LocalUser.GetBoundCopy();
apiUser.BindValueChanged(_ => queueUpdateSearch());
@@ -199,6 +200,8 @@ namespace osu.Game.Overlays.BeatmapListing
performRequest();
}
private void resetSortControl() => sortControl.Reset(searchControl.Category.Value, !string.IsNullOrEmpty(searchControl.Query.Value));
private void queueUpdateSearch(bool queryTextChanged = false)
{
SearchStarted?.Invoke();
@@ -17,18 +17,65 @@ namespace osu.Game.Overlays.BeatmapListing
{
public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>(Overlays.SortDirection.Descending);
public BeatmapListingSortTabControl()
private SearchCategory? lastCategory;
private bool? lastHasQuery;
protected override void LoadComplete()
{
Current.Value = SortCriteria.Ranked;
base.LoadComplete();
Reset(SearchCategory.Leaderboard, false);
}
public void Reset(SearchCategory category, bool hasQuery)
{
if (category != lastCategory || hasQuery != lastHasQuery)
{
TabControl.Clear();
TabControl.AddItem(SortCriteria.Title);
TabControl.AddItem(SortCriteria.Artist);
TabControl.AddItem(SortCriteria.Difficulty);
if (category == SearchCategory.Any || category > SearchCategory.Loved)
TabControl.AddItem(SortCriteria.Updated);
if (category < SearchCategory.Pending || category == SearchCategory.Mine)
TabControl.AddItem(SortCriteria.Ranked);
TabControl.AddItem(SortCriteria.Rating);
TabControl.AddItem(SortCriteria.Plays);
TabControl.AddItem(SortCriteria.Favourites);
if (hasQuery)
TabControl.AddItem(SortCriteria.Relevance);
if (category == SearchCategory.Pending)
TabControl.AddItem(SortCriteria.Nominations);
}
var nonQueryCriteria = category >= SearchCategory.Pending ? SortCriteria.Updated : SortCriteria.Ranked;
Current.Value = hasQuery ? SortCriteria.Relevance : nonQueryCriteria;
SortDirection.Value = Overlays.SortDirection.Descending;
// if the new criteria isn't different from the previous one,
// then re-adding tab items will not mark the current tab as selected.
// see: https://github.com/ppy/osu-framework/issues/5412
TabControl.Current.TriggerChange();
lastCategory = category;
lastHasQuery = hasQuery;
}
protected override SortTabControl CreateControl() => new BeatmapSortTabControl
{
SortDirection = { BindTarget = SortDirection }
SortDirection = { BindTarget = SortDirection },
};
private class BeatmapSortTabControl : SortTabControl
{
protected override bool AddEnumEntriesAutomatically => false;
public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>();
protected override TabItem<SortCriteria> CreateTabItem(SortCriteria value) => new BeatmapSortTabItem(value)
@@ -19,6 +19,9 @@ namespace osu.Game.Overlays.BeatmapListing
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingDifficulty))]
Difficulty,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingUpdated))]
Updated,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingRanked))]
Ranked,
@@ -32,6 +35,9 @@ namespace osu.Game.Overlays.BeatmapListing
Favourites,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingRelevance))]
Relevance
Relevance,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingNominations))]
Nominations,
}
}
+6 -1
View File
@@ -176,6 +176,11 @@ namespace osu.Game.Overlays
}
else
{
// new results may contain beatmaps from a previous page,
// this is dodgy but matches web behaviour for now.
// see: https://github.com/ppy/osu-web/issues/9270
newCards = newCards.Except(foundContent);
panelLoadTask = LoadComponentsAsync(newCards, loaded =>
{
lastFetchDisplayedTime = Time.Current;
@@ -185,7 +190,7 @@ namespace osu.Game.Overlays
}
}
private BeatmapCard[] createCardsFor(IEnumerable<APIBeatmapSet> beatmapSets) => beatmapSets.Select(set => BeatmapCard.Create(set, filterControl.CardSize.Value).With(c =>
private IEnumerable<BeatmapCard> createCardsFor(IEnumerable<APIBeatmapSet> beatmapSets) => beatmapSets.Select(set => BeatmapCard.Create(set, filterControl.CardSize.Value).With(c =>
{
c.Anchor = Anchor.TopCentre;
c.Origin = Anchor.TopCentre;
@@ -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
+5
View File
@@ -158,7 +158,10 @@ namespace osu.Game.Overlays
playDebouncedSample(notification.PopInSampleName);
if (State.Value == Visibility.Hidden)
{
notification.IsInToastTray = true;
toastTray.Post(notification);
}
else
addPermanently(notification);
@@ -167,6 +170,8 @@ namespace osu.Game.Overlays
private void addPermanently(Notification notification)
{
notification.IsInToastTray = false;
var ourType = notification.GetType();
int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
@@ -26,6 +26,8 @@ namespace osu.Game.Overlays
{
public override bool IsPresent => toastContentBackground.Height > 0 || toastFlow.Count > 0;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => toastFlow.ReceivePositionalInputAt(screenSpacePos);
public bool IsDisplayingToasts => toastFlow.Count > 0;
private FillFlowContainer<Notification> toastFlow = null!;
@@ -78,7 +80,6 @@ namespace osu.Game.Overlays
{
LayoutDuration = 150,
LayoutEasing = Easing.OutQuart,
Spacing = new Vector2(3),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
@@ -101,6 +102,8 @@ namespace osu.Game.Overlays
{
++runningDepth;
notification.ForwardToOverlay = () => forwardNotification(notification);
int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
toastFlow.Insert(depth, notification);
@@ -118,7 +121,7 @@ namespace osu.Game.Overlays
return;
// Notification hovered; delay dismissal.
if (notification.IsHovered)
if (notification.IsHovered || notification.IsDragged)
{
scheduleDismissal();
return;
@@ -131,6 +134,9 @@ namespace osu.Game.Overlays
private void forwardNotification(Notification notification)
{
if (!notification.IsInToastTray)
return;
Debug.Assert(notification.Parent == toastFlow);
// Temporarily remove from flow so we can animate the position off to the right.
+179 -8
View File
@@ -2,20 +2,24 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays.Notifications
{
@@ -38,6 +42,8 @@ namespace osu.Game.Overlays.Notifications
/// </summary>
public Func<bool>? Activated;
public Action? ForwardToOverlay { get; set; }
/// <summary>
/// Should we show at the top of our section on display?
/// </summary>
@@ -55,13 +61,40 @@ namespace osu.Game.Overlays.Notifications
protected Container MainContent;
private readonly DragContainer dragContainer;
public virtual bool Read { get; set; }
public new bool IsDragged => dragContainer.IsDragged;
protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && !WasClosed;
private bool isInToastTray;
/// <summary>
/// Whether this notification is in the <see cref="NotificationOverlayToastTray"/>.
/// </summary>
public bool IsInToastTray
{
get => isInToastTray;
set
{
isInToastTray = value;
if (!isInToastTray)
{
dragContainer.ResetPosition();
if (!Read)
Light.FadeIn(100);
}
}
}
private readonly Box initialFlash;
private Box background = null!;
@@ -75,11 +108,19 @@ namespace osu.Game.Overlays.Notifications
{
Light = new NotificationLight
{
Alpha = 0,
Margin = new MarginPadding { Right = 5 },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
},
MainContent = new Container
dragContainer = new DragContainer(this)
{
// Use margin instead of FillFlow spacing to fix extra padding appearing when notification shrinks
// in height.
Padding = new MarginPadding { Vertical = 3f },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}.WithChild(MainContent = new Container
{
CornerRadius = 6,
Masking = true,
@@ -129,7 +170,7 @@ namespace osu.Game.Overlays.Notifications
},
new CloseButton(CloseButtonIcon)
{
Action = Close,
Action = () => Close(true),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
@@ -143,7 +184,7 @@ namespace osu.Game.Overlays.Notifications
Blending = BlendingParameters.Additive,
},
}
}
})
};
}
@@ -170,11 +211,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(true);
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(false);
return true;
}
@@ -192,18 +247,134 @@ namespace osu.Game.Overlays.Notifications
public bool WasClosed;
public virtual void Close()
public virtual void Close(bool runFlingAnimation)
{
if (WasClosed) return;
WasClosed = true;
if (runFlingAnimation && dragContainer.FlingLeft())
this.FadeOut(600, Easing.In);
else
this.FadeOut(100);
Closed?.Invoke();
this.FadeOut(100);
Expire();
}
private class CloseButton : OsuClickableContainer
private class DragContainer : Container
{
private Vector2 velocity;
private Vector2 lastPosition;
private readonly Notification notification;
public DragContainer(Notification notification)
{
this.notification = notification;
}
public override RectangleF BoundingBox
{
get
{
var childBounding = Children.First().BoundingBox;
if (X < 0) childBounding *= new Vector2(1, Math.Max(0, 1 + (X / 300)));
if (Y > 0) childBounding *= new Vector2(1, Math.Max(0, 1 - (Y / 200)));
return childBounding;
}
}
protected override bool OnDragStart(DragStartEvent e) => notification.IsInToastTray;
protected override void OnDrag(DragEvent e)
{
if (!notification.IsInToastTray)
return;
Vector2 change = e.MousePosition - e.MouseDownPosition;
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.8f) / change.Length;
// Only apply Y change if dragging to the left.
if (change.X >= 0)
change.Y = 0;
else
change.Y *= (float)Interpolation.ApplyEasing(Easing.InOutQuart, Math.Min(1, -change.X / 200));
this.MoveTo(change);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (Rotation < -10 || velocity.X < -0.3f)
notification.Close(true);
else if (X > 30 || velocity.X > 0.3f)
notification.ForwardToOverlay?.Invoke();
else
ResetPosition();
base.OnDragEnd(e);
}
private bool flinging;
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Rotation = Math.Min(0, X * 0.1f);
if (flinging)
{
velocity.Y += (float)Clock.ElapsedFrameTime * 0.005f;
Position += (float)Clock.ElapsedFrameTime * velocity;
}
else if (Clock.ElapsedFrameTime > 0)
{
Vector2 change = (Position - lastPosition) / (float)Clock.ElapsedFrameTime;
if (velocity.X == 0)
velocity = change;
else
{
velocity = new Vector2(
(float)Interpolation.DampContinuously(velocity.X, change.X, 40, Clock.ElapsedFrameTime),
(float)Interpolation.DampContinuously(velocity.Y, change.Y, 40, Clock.ElapsedFrameTime)
);
}
lastPosition = Position;
}
}
public bool FlingLeft()
{
if (!notification.IsInToastTray)
return false;
if (flinging)
return true;
if (velocity.X > -0.3f)
velocity.X = -0.3f - 0.5f * RNG.NextSingle();
flinging = true;
ClearTransforms();
return true;
}
public void ResetPosition()
{
this.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
this.RotateTo(0, 800, Easing.OutElastic);
}
}
internal class CloseButton : OsuClickableContainer
{
private SpriteIcon icon = null!;
private Box background = null!;
@@ -106,14 +106,13 @@ namespace osu.Game.Overlays.Notifications
RelativeSizeAxes = Axes.X,
LayoutDuration = 150,
LayoutEasing = Easing.OutQuart,
Spacing = new Vector2(3),
}
});
}
private void clearAll()
{
notifications.Children.ForEach(c => c.Close());
notifications.Children.ForEach(c => c.Close(true));
}
protected override void Update()
@@ -142,7 +142,7 @@ namespace osu.Game.Overlays.Notifications
case ProgressNotificationState.Completed:
loadingSpinner.Hide();
attemptPostCompletion();
base.Close();
base.Close(false);
break;
}
}
@@ -226,6 +226,7 @@ namespace osu.Game.Overlays.Notifications
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
Depth = float.MaxValue,
},
loadingSpinner = new LoadingSpinner
{
@@ -234,12 +235,12 @@ namespace osu.Game.Overlays.Notifications
});
}
public override void Close()
public override void Close(bool runFlingAnimation)
{
switch (State)
{
case ProgressNotificationState.Cancelled:
base.Close();
base.Close(runFlingAnimation);
break;
case ProgressNotificationState.Active:
+3 -1
View File
@@ -26,6 +26,8 @@ namespace osu.Game.Overlays
{
public class OverlaySortTabControl<T> : CompositeDrawable, IHasCurrentValue<T>
{
public TabControl<T> TabControl { get; }
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
public Bindable<T> Current
@@ -59,7 +61,7 @@ namespace osu.Game.Overlays
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = SortStrings.Default
},
CreateControl().With(c =>
TabControl = CreateControl().With(c =>
{
c.Anchor = Anchor.CentreLeft;
c.Origin = Anchor.CentreLeft;
@@ -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;
-2
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
+14 -23
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;
}
}
+2 -4
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");
+18 -1
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.
@@ -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();
}
}
}
+4 -1
View File
@@ -58,7 +58,10 @@ namespace osu.Game.Scoring
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> scores)
=> scores.OrderByDescending(s => GetTotalScore(s)).ThenBy(s => s.OnlineID);
=> scores.OrderByDescending(s => GetTotalScore(s))
.ThenBy(s => s.OnlineID)
// Local scores may not have an online ID. Fall back to date in these cases.
.ThenBy(s => s.Date);
/// <summary>
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
@@ -24,11 +24,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
editorBeatmap.BeatmapReprocessed += SortInternal;
}
private void hitObjectUpdated(HitObject _) => SortInternal();
public override void Add(SelectionBlueprint<HitObject> drawable)
{
SortInternal();
@@ -72,7 +70,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.Dispose(isDisposing);
if (editorBeatmap != null)
editorBeatmap.HitObjectUpdated -= hitObjectUpdated;
editorBeatmap.BeatmapReprocessed -= SortInternal;
}
}
}
+11
View File
@@ -48,6 +48,15 @@ namespace osu.Game.Screens.Edit
/// </summary>
public event Action<HitObject> HitObjectUpdated;
/// <summary>
/// Invoked after any state changes occurred which triggered a beatmap reprocess via an <see cref="IBeatmapProcessor"/>.
/// </summary>
/// <remarks>
/// Beatmap processing may change the order of hitobjects. This event gives external components a chance to handle any changes
/// not covered by the <see cref="HitObjectAdded"/> / <see cref="HitObjectUpdated"/> / <see cref="HitObjectRemoved"/> events.
/// </remarks>
public event Action BeatmapReprocessed;
/// <summary>
/// All currently selected <see cref="HitObject"/>s.
/// </summary>
@@ -331,6 +340,8 @@ namespace osu.Game.Screens.Edit
beatmapProcessor?.PostProcess();
BeatmapReprocessed?.Invoke();
// callbacks may modify the lists so let's be safe about it
var deletes = batchPendingDeletes.ToArray();
batchPendingDeletes.Clear();
@@ -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();
}
@@ -47,7 +47,10 @@ namespace osu.Game.Screens.Play.HUD
if (clock != null)
gameplayClock = clock;
AutoSizeAxes = Axes.Y;
// Lock height so changes in text autosize (if character height changes)
// don't cause parent invalidation.
Height = 14;
Children = new Drawable[]
{
new Container
-19
View File
@@ -1,19 +0,0 @@
// 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.Screens.Play
{
public class KeyCounterState
{
public KeyCounterState(double time, int count)
{
Time = time;
Count = count;
}
public readonly double Time;
public readonly int Count;
}
}
+2
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;
@@ -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();
};
+58 -8
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;
@@ -123,10 +122,27 @@ namespace osu.Game.Screens.Select
private static bool tryParseEnum<TEnum>(string value, out TEnum result) where TEnum : struct
{
if (Enum.TryParse(value, true, out result)) return true;
// First try an exact match.
if (Enum.TryParse(value, true, out result))
return true;
value = Enum.GetNames(typeof(TEnum)).FirstOrDefault(name => name.StartsWith(value, true, CultureInfo.InvariantCulture));
return Enum.TryParse(value, true, out result);
// Then try a prefix match.
string? prefixMatch = Enum.GetNames(typeof(TEnum)).FirstOrDefault(name => name.StartsWith(value, true, CultureInfo.InvariantCulture));
if (prefixMatch == null)
return false;
return Enum.TryParse(prefixMatch, 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>
@@ -312,11 +328,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);
}
}
}
+1 -1
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,
+5 -3
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)
@@ -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();
+8
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;
@@ -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));
}
}
+77 -6
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(bool runFlingAnimation)
{
// cancelling updates is not currently supported by the underlying updater.
// only allow dismissing for now.
switch (State)
{
case ProgressNotificationState.Cancelled:
base.Close(runFlingAnimation);
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(false);
}
}
}
}