1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 18:23:04 +08:00

Merge branch 'master' into Fabep/master

This commit is contained in:
Bartłomiej Dach 2024-09-02 14:35:44 +02:00
commit d99b2312cd
No known key found for this signature in database
55 changed files with 859 additions and 515 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.809.2" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.831.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
return actualDistance / expectedDistance;
}

View File

@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Catch.Replays
{
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
private readonly float halfCatcherWidth;
public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
}
protected override void GenerateFrames()
@ -47,10 +50,7 @@ namespace osu.Game.Rulesets.Catch.Replays
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
// todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
if (lastPosition - halfCatcherWidth < h.EffectiveX && lastPosition + halfCatcherWidth > h.EffectiveX)
{
// we are already in the correct range.
lastTime = h.StartTime;

View File

@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
protected void SetTexture(Texture? texture, Texture? overlayTexture)
{
colouredSprite.Texture = texture;
overlaySprite.Texture = overlayTexture;
hyperSprite.Texture = texture;
// Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture.
if (colouredSprite.Texture != texture)
{
colouredSprite.Size = Vector2.Zero;
colouredSprite.Texture = texture;
}
if (overlaySprite.Texture != overlayTexture)
{
overlaySprite.Size = Vector2.Zero;
overlaySprite.Texture = overlayTexture;
}
if (hyperSprite.Texture != texture)
{
hyperSprite.Size = Vector2.Zero;
hyperSprite.Texture = texture;
}
}
}
}

View File

@ -99,12 +99,6 @@ namespace osu.Game.Rulesets.Mania.UI
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Show();
return true;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
@ -172,17 +166,6 @@ namespace osu.Game.Rulesets.Mania.UI
updateButton(false);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)

View File

@ -0,0 +1,47 @@
// 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.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderChangeStates : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[TestCase(SplineType.Catmull)]
[TestCase(SplineType.BSpline)]
[TestCase(SplineType.Linear)]
[TestCase(SplineType.PerfectCurve)]
public void TestSliderRetainsCurveTypes(SplineType splineType)
{
Slider? slider = null;
PathType pathType = new PathType(splineType);
AddStep("add slider", () => EditorBeatmap.Add(slider = new Slider
{
StartTime = 500,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, pathType),
new PathControlPoint(new Vector2(200, 0), pathType),
})
}));
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
AddStep("remove object", () => EditorBeatmap.Remove(slider));
AddAssert("slider removed", () => EditorBeatmap.HitObjects.Count == 0);
addUndoSteps();
AddAssert("slider not removed", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
}
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint;
private readonly T hitObject;
private readonly Circle circle;
private readonly FastCircle circle;
private readonly Drawable markerRing;
[Resolved]
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new[]
{
circle = new Circle
circle = new FastCircle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -257,7 +257,7 @@ namespace osu.Game.Tests.Online
{
}
protected override string Target => null;
protected override string Target => string.Empty;
}
}
}

View File

@ -18,6 +18,7 @@ using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -48,13 +49,18 @@ namespace osu.Game.Tests.Visual.Background
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Add(detachedBeatmapStore);
Beatmap.SetDefault();
}

View File

@ -2,19 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Resources;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest;
namespace osu.Game.Tests.Visual.DailyChallenge
@ -27,63 +29,61 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay();
private Room room = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.Add(notificationOverlay);
base.Content.Add(metadataClient);
Add(notificationOverlay);
Add(metadataClient);
// add button to observe for daily challenge changes and perform its logic.
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
}
[Test]
[Solo]
public void TestDailyChallenge()
{
var room = new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First())
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallengeIntro(room)));
startChallenge(1234);
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
}
[Test]
public void TestNotifications()
public void TestPlayIntroOnceFlag()
{
var room = new Room
startChallenge(1234);
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
startChallenge(1235);
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
}
private void startChallenge(int roomId)
{
AddStep("add room", () =>
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
API.Perform(new CreateRoomRequest(room = new Room
{
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
RoomID = { Value = roomId },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo))
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
StartDate = { Value = DateTimeOffset.Now },
EndDate = { Value = DateTimeOffset.Now.AddHours(24) },
Category = { Value = RoomCategory.DailyChallenge }
}));
});
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
}
}
}

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded
&& songSelect.BeatmapSetsLoaded
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>

View File

@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -45,9 +46,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
Add(detachedBeatmapStore);
}
public override void SetUpSteps()

View File

@ -19,6 +19,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@ -65,9 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
Add(detachedBeatmapStore);
}
public override void SetUpSteps()

View File

@ -45,11 +45,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));
Add(detachedBeatmapStore);
}
public override void SetUpSteps()

View File

@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -33,13 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
var beatmapSet = TestResources.CreateTestBeatmapSetInfo();
manager.Import(beatmapSet);
Add(detachedBeatmapStore);
}
public override void SetUpSteps()

View File

@ -165,16 +165,19 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
[Solo]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{
prepareBeatmap();
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddStep("test gameplay", () => getEditor().TestGameplay());
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
AddStep("test gameplay", () => getEditor().TestGameplay());
AddUntilStep("wait for player", () =>
{
// notifications may fire at almost any inopportune time and cause annoying test failures.
@ -183,8 +186,7 @@ namespace osu.Game.Tests.Visual.Navigation
Game.CloseAllOverlays();
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
});
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
@ -352,7 +354,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded);
&& songSelect.BeatmapSetsLoaded);
}
private void openEditor()

View File

@ -176,6 +176,12 @@ namespace osu.Game.Tests.Visual.Navigation
private void confirmBeatmapInSongSelect(Func<BeatmapSetInfo> getImport)
{
AddUntilStep("wait for carousel loaded", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().SingleOrDefault()?.IsLoaded == true;
});
AddUntilStep("beatmap in song select", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
@ -187,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
@ -197,7 +203,7 @@ namespace osu.Game.Tests.Visual.Navigation
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 1024 + 2;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID));
}

View File

@ -1035,9 +1035,11 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestTouchScreenDetectionInGame()
{
BeatmapSetInfo beatmapSet = null;
PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet));
AddStep("select", () => InputManager.Key(Key.Enter));
Player player = null;

View File

@ -446,7 +446,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Show overlay with channel 1", () =>
{
channelManager.JoinChannel(testChannel1);
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
chatOverlay.Show();
});
waitForChannel1Visible();
@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Show overlay with channel 1", () =>
{
channelManager.JoinChannel(testChannel1);
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
chatOverlay.Show();
});
waitForChannel1Visible();

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -52,11 +53,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createCarousel(new List<BeatmapSetInfo>());
AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria
AddStep("filter to ruleset 0", () => carousel.FilterImmediately(new FilterCriteria
{
Ruleset = rulesets.AvailableRulesets.ElementAt(0),
AllowConvertedBeatmaps = true,
}, false));
}));
AddStep("add mixed ruleset beatmapset", () =>
{
@ -78,11 +79,11 @@ namespace osu.Game.Tests.Visual.SongSelect
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1;
});
AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria
AddStep("filter to ruleset 1", () => carousel.FilterImmediately(new FilterCriteria
{
Ruleset = rulesets.AvailableRulesets.ElementAt(1),
AllowConvertedBeatmaps = true,
}, false));
}));
AddUntilStep("wait for filtered difficulties", () =>
{
@ -93,11 +94,11 @@ namespace osu.Game.Tests.Visual.SongSelect
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 1) == 1;
});
AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
AddStep("filter to ruleset 2", () => carousel.FilterImmediately(new FilterCriteria
{
Ruleset = rulesets.AvailableRulesets.ElementAt(2),
AllowConvertedBeatmaps = true,
}, false));
}));
AddUntilStep("wait for filtered difficulties", () =>
{
@ -344,7 +345,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// basic filtering
setSelected(1, 1);
AddStep("Filter", () => carousel.Filter(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title }, false));
AddStep("Filter", () => carousel.FilterImmediately(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title }));
checkVisibleItemCount(diff: false, count: 1);
checkVisibleItemCount(diff: true, count: 3);
waitForSelection(3, 1);
@ -360,13 +361,13 @@ namespace osu.Game.Tests.Visual.SongSelect
// test filtering some difficulties (and keeping current beatmap set selected).
setSelected(1, 2);
AddStep("Filter some difficulties", () => carousel.Filter(new FilterCriteria { SearchText = "Normal" }, false));
AddStep("Filter some difficulties", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Normal" }));
waitForSelection(1, 1);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria()));
waitForSelection(1, 1);
AddStep("Filter all", () => carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false));
AddStep("Filter all", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Dingo" }));
checkVisibleItemCount(false, 0);
checkVisibleItemCount(true, 0);
@ -378,7 +379,7 @@ namespace osu.Game.Tests.Visual.SongSelect
advanceSelection(false);
AddAssert("Selection is null", () => currentSelection == null);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria()));
AddAssert("Selection is non-null", () => currentSelection != null);
@ -399,7 +400,7 @@ namespace osu.Game.Tests.Visual.SongSelect
setSelected(1, 3);
AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
AddStep("Apply a range filter", () => carousel.FilterImmediately(new FilterCriteria
{
SearchText = searchText,
StarDifficulty = new FilterCriteria.OptionalRange<double>
@ -408,7 +409,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Max = 5.5,
IsLowerInclusive = true
}
}, false));
}));
// should reselect the buffered selection.
waitForSelection(3, 2);
@ -445,13 +446,13 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("ensure repeat", () => selectedSets.Contains(carousel.SelectedBeatmapSet));
AddStep("Add set with 100 difficulties", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo(100, rulesets.AvailableRulesets.ToArray())));
AddStep("Filter Extra", () => carousel.Filter(new FilterCriteria { SearchText = "Extra 10" }, false));
AddStep("Filter Extra", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Extra 10" }));
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria()));
}
[Test]
@ -527,7 +528,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count);
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
checkVisibleItemCount(false, local_set_count * local_diff_count);
@ -566,7 +567,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) });
AddStep("Set non-empty mode filter", () =>
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false));
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }));
AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null);
}
@ -601,7 +602,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
AddStep("Sort by date submitted", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.DateSubmitted }));
checkVisibleItemCount(diff: false, count: 10);
checkVisibleItemCount(diff: true, count: 5);
@ -610,11 +611,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(6));
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
AddStep("Sort by date submitted and string", () => carousel.FilterImmediately(new FilterCriteria
{
Sort = SortMode.DateSubmitted,
SearchText = zzz_string
}, false));
}));
checkVisibleItemCount(diff: false, count: 5);
checkVisibleItemCount(diff: true, count: 5);
@ -658,10 +659,10 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddStep("Sort by author", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Author }));
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_uppercase);
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Author.Username == zzz_lowercase);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_uppercase);
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase);
}
@ -703,7 +704,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
AddAssert("Check last item", () =>
{
var lastItem = carousel.BeatmapSets.Last();
@ -746,10 +747,10 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
}
@ -786,7 +787,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
@ -796,7 +797,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
@ -833,7 +834,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
@ -858,7 +859,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
@ -885,12 +886,12 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
checkVisibleItemCount(false, local_set_count * local_diff_count);
checkVisibleItemCount(true, 1);
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
AddStep("Filter to normal", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }));
checkVisibleItemCount(false, local_set_count);
checkVisibleItemCount(true, 1);
@ -901,7 +902,7 @@ namespace osu.Game.Tests.Visual.SongSelect
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count;
});
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
AddStep("Filter to insane", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }));
checkVisibleItemCount(false, local_set_count);
checkVisibleItemCount(true, 1);
@ -1022,7 +1023,7 @@ namespace osu.Game.Tests.Visual.SongSelect
carousel.UpdateBeatmapSet(testMixed);
});
AddStep("filter to ruleset 0", () =>
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }));
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 0);
@ -1068,12 +1069,12 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("Toggle non-matching filter", () =>
{
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
carousel.FilterImmediately(new FilterCriteria { SearchText = Guid.NewGuid().ToString() });
});
AddStep("Restore no filter", () =>
{
carousel.Filter(new FilterCriteria(), false);
carousel.FilterImmediately(new FilterCriteria());
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
});
}
@ -1097,7 +1098,7 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(manySets);
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
advanceSelection(direction: 1, diff: false);
@ -1105,12 +1106,12 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("Toggle non-matching filter", () =>
{
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
carousel.FilterImmediately(new FilterCriteria { SearchText = Guid.NewGuid().ToString() });
});
AddStep("Restore no filter", () =>
{
carousel.Filter(new FilterCriteria(), false);
carousel.FilterImmediately(new FilterCriteria());
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
});
}
@ -1185,7 +1186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title });
});
waitForSelection(i + 1, 1);
}
@ -1223,12 +1224,12 @@ namespace osu.Game.Tests.Visual.SongSelect
setSelected(i, 1);
AddStep("Set ruleset to taiko", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title });
});
waitForSelection(i - 1, 1);
AddStep("Remove ruleset filter", () =>
{
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title });
});
}
@ -1268,26 +1269,23 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
createCarousel(beatmapSets, c =>
createCarousel(beatmapSets, initialCriteria, c =>
{
carouselAdjust?.Invoke(c);
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
carouselAdjust?.Invoke(c);
});
AddUntilStep("Wait for load", () => changed);
}
private void createCarousel(List<BeatmapSetInfo> beatmapSets, Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
private void createCarousel(List<BeatmapSetInfo> beatmapSets, [CanBeNull] Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
{
AddStep("Create carousel", () =>
{
selectedSets.Clear();
eagerSelectedIDs.Clear();
carousel = new TestBeatmapCarousel
carousel = new TestBeatmapCarousel(initialCriteria?.Invoke() ?? new FilterCriteria())
{
RelativeSizeAxes = Axes.Both,
};
@ -1389,6 +1387,11 @@ namespace osu.Game.Tests.Visual.SongSelect
private partial class TestBeatmapCarousel : BeatmapCarousel
{
public TestBeatmapCarousel(FilterCriteria criteria)
: base(criteria)
{
}
public bool PendingFilterTask => PendingFilter != null;
public IEnumerable<DrawableCarouselItem> Items
@ -1410,6 +1413,12 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
}
public void FilterImmediately(FilterCriteria newCriteria)
{
Filter(newCriteria);
FlushPendingFilterOperations();
}
}
}
}

View File

@ -56,16 +56,20 @@ namespace osu.Game.Tests.Visual.SongSelect
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
DetachedBeatmapStore detachedBeatmapStore;
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
// At a point we have isolated interactive test runs enough, this can likely be removed.
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(Realm);
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(music = new MusicController());
// required to get bindables attached
Add(music);
Add(detachedBeatmapStore);
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
}
@ -242,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddAssert("filter count is 1", () => songSelect?.FilterCount == 1);
AddAssert("filter count is 0", () => songSelect?.FilterCount, () => Is.EqualTo(0));
}
[Test]
@ -362,7 +366,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("filter count is 1", () => songSelect!.FilterCount == 1);
AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0));
}
[Test]
@ -382,7 +386,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("filter count is 2", () => songSelect!.FilterCount == 2);
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
}
[Test]
@ -1270,11 +1274,11 @@ namespace osu.Game.Tests.Visual.SongSelect
// Mod that is guaranteed to never re-filter.
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0));
// Removing the mod should still not re-filter.
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0));
}
[Test]
@ -1286,35 +1290,35 @@ namespace osu.Game.Tests.Visual.SongSelect
// Change to mania ruleset.
AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2));
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(1));
// Apply a mod, but this should NOT re-filter because there's no search text.
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2));
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
// Set search text. Should re-filter.
AddStep("set search text to match mods", () => songSelect!.FilterControl.CurrentTextSearch.Value = "keys=3");
AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3));
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2));
// Change filterable mod. Should re-filter.
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3));
// Add non-filterable mod. Should NOT re-filter.
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3));
// Remove filterable mod. Should re-filter.
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5));
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
// Remove non-filterable mod. Should NOT re-filter.
AddStep("remove filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5));
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
// Add filterable mod. Should re-filter.
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
AddAssert("filter count is 6", () => songSelect!.FilterCount, () => Is.EqualTo(6));
AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5));
}
private void waitForInitialSelection()
@ -1397,8 +1401,6 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public Action? StartRequested;
public new Bindable<RulesetInfo> Ruleset => base.Ruleset;
public new FilterControl FilterControl => base.FilterControl;
public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
@ -1408,18 +1410,18 @@ namespace osu.Game.Tests.Visual.SongSelect
public new void PresentScore(ScoreInfo score) => base.PresentScore(score);
public int FilterCount;
protected override bool OnStart()
{
StartRequested?.Invoke();
return base.OnStart();
}
public int FilterCount;
protected override void ApplyFilterToCarousel(FilterCriteria criteria)
[BackgroundDependencyLoader]
private void load()
{
FilterCount++;
base.ApplyFilterToCarousel(criteria);
FilterControl.FilterChanged += _ => FilterCount++;
}
}
}

View File

@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private BeatmapCarousel createCarousel()
{
return carousel = new BeatmapCarousel
return carousel = new BeatmapCarousel(new FilterCriteria())
{
RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>

View File

@ -103,15 +103,79 @@ namespace osu.Game.Tests.Visual.UserInterface
foreach (var notification in notificationOverlay.AllNotifications)
notification.Close(runFlingAnimation: false);
});
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
AddStep("hide button's parent", () => buttonContainer.Hide());
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
}
[Test]
public void TestDailyChallengeButtonOldChallenge()
{
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case GetRoomRequest getRoomRequest:
if (getRoomRequest.RoomId != 1234)
return false;
var beatmap = CreateAPIBeatmap();
beatmap.OnlineID = 1001;
getRoomRequest.TriggerSuccess(new Room
{
RoomID = { Value = 1234 },
Playlist =
{
new PlaylistItem(beatmap)
},
StartDate = { Value = DateTimeOffset.Now.AddMinutes(-50) },
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
});
return true;
default:
return false;
}
});
NotificationOverlay notificationOverlay = null!;
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("add content", () =>
{
notificationOverlay = new NotificationOverlay();
Children = new Drawable[]
{
notificationOverlay,
new DependencyProvidingContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
},
},
};
});
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234
}));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
}
}
}

View File

@ -450,7 +450,7 @@ namespace osu.Game.Beatmaps.Formats
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE;
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1;
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.

View File

@ -80,5 +80,11 @@ namespace osu.Game.Configuration
/// Stores the local user's last score (can be completed or aborted).
/// </summary>
LastLocalUserScore,
/// <summary>
/// Whether the intro animation for the daily challenge screen has been played once.
/// This is reset when a new challenge is up.
/// </summary>
DailyChallengeIntroPlayed,
}
}

View File

@ -0,0 +1,163 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using Realms;
namespace osu.Game.Database
{
public partial class DetachedBeatmapStore : Component
{
private readonly ManualResetEventSlim loaded = new ManualResetEventSlim();
private readonly BindableList<BeatmapSetInfo> detachedBeatmapSets = new BindableList<BeatmapSetInfo>();
private IDisposable? realmSubscription;
private readonly Queue<OperationArgs> pendingOperations = new Queue<OperationArgs>();
[Resolved]
private RealmAccess realm { get; set; } = null!;
public IBindableList<BeatmapSetInfo> GetDetachedBeatmaps(CancellationToken? cancellationToken)
{
loaded.Wait(cancellationToken ?? CancellationToken.None);
return detachedBeatmapSets.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load()
{
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected), beatmapSetsChanged);
}
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (changes == null)
{
if (detachedBeatmapSets.Count > 0 && sender.Count == 0)
{
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
//
// Note that due to the catch-up logic below, once operations are restored we will still be in a roughly
// correct state. The only things that this return will change is the carousel will not empty *during* the blocking
// operation.
return;
}
// Detaching beatmaps takes some time, so let's make sure it doesn't run on the update thread.
var frozenSets = sender.Freeze();
Task.Factory.StartNew(() =>
{
try
{
realm.Run(_ =>
{
var detached = frozenSets.Detach();
detachedBeatmapSets.Clear();
detachedBeatmapSets.AddRange(detached);
});
}
finally
{
loaded.Set();
}
}, TaskCreationOptions.LongRunning).FireAndForget();
return;
}
foreach (int i in changes.DeletedIndices.OrderDescending())
{
pendingOperations.Enqueue(new OperationArgs
{
Type = OperationType.Remove,
Index = i,
});
}
foreach (int i in changes.InsertedIndices)
{
pendingOperations.Enqueue(new OperationArgs
{
Type = OperationType.Insert,
BeatmapSet = sender[i].Detach(),
Index = i,
});
}
foreach (int i in changes.NewModifiedIndices)
{
pendingOperations.Enqueue(new OperationArgs
{
Type = OperationType.Update,
BeatmapSet = sender[i].Detach(),
Index = i,
});
}
}
protected override void Update()
{
base.Update();
// We can't start processing operations until we have finished detaching the initial list.
if (!loaded.IsSet)
return;
// If this ever leads to performance issues, we could dequeue a limited number of operations per update frame.
while (pendingOperations.TryDequeue(out var op))
{
switch (op.Type)
{
case OperationType.Insert:
detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!);
break;
case OperationType.Update:
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
break;
case OperationType.Remove:
detachedBeatmapSets.RemoveAt(op.Index);
break;
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
loaded.Set();
loaded.Dispose();
realmSubscription?.Dispose();
}
private record OperationArgs
{
public OperationType Type;
public BeatmapSetInfo? BeatmapSet;
public int Index;
}
private enum OperationType
{
Insert,
Update,
Remove
}
}
}

View File

@ -159,7 +159,7 @@ namespace osu.Game.Online.API
private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.SetValue(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
internal new void Schedule(Action action) => base.Schedule(action);
void IAPIProvider.Schedule(Action action) => base.Schedule(action);
public string AccessToken => authentication.RequestAccessToken();
@ -385,7 +385,8 @@ namespace osu.Game.Online.API
{
try
{
request.Perform(this);
request.AttachAPI(this);
request.Perform();
}
catch (Exception e)
{
@ -483,7 +484,8 @@ namespace osu.Game.Online.API
{
try
{
req.Perform(this);
req.AttachAPI(this);
req.Perform();
if (req.CompletionState != APIRequestCompletionState.Completed)
return false;
@ -568,6 +570,8 @@ namespace osu.Game.Online.API
{
lock (queue)
{
request.AttachAPI(this);
if (state.Value == APIState.Offline)
{
request.Fail(new WebException(@"User not logged in"));

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Diagnostics;
using System.IO;
using osu.Framework.IO.Network;
@ -34,7 +35,11 @@ namespace osu.Game.Online.API
return request;
}
private void request_Progress(long current, long total) => API.Schedule(() => Progressed?.Invoke(current, total));
private void request_Progress(long current, long total)
{
Debug.Assert(API != null);
API.Schedule(() => Progressed?.Invoke(current, total));
}
protected void TriggerSuccess(string filename)
{

View File

@ -1,11 +1,9 @@
// 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.Diagnostics;
using System.Globalization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.IO.Network;
@ -26,18 +24,17 @@ namespace osu.Game.Online.API
/// <summary>
/// The deserialised response object. May be null if the request or deserialisation failed.
/// </summary>
[CanBeNull]
public T Response { get; private set; }
public T? Response { get; private set; }
/// <summary>
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public new event APISuccessHandler<T> Success;
public new event APISuccessHandler<T>? Success;
protected APIRequest()
{
base.Success += () => Success?.Invoke(Response);
base.Success += () => Success?.Invoke(Response!);
}
protected override void PostProcess()
@ -71,27 +68,28 @@ namespace osu.Game.Online.API
protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri);
protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}";
protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}";
protected APIAccess API;
protected WebRequest WebRequest;
protected IAPIProvider? API;
protected WebRequest? WebRequest;
/// <summary>
/// The currently logged in user. Note that this will only be populated during <see cref="Perform"/>.
/// </summary>
protected APIUser User { get; private set; }
protected APIUser? User { get; private set; }
/// <summary>
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public event APISuccessHandler Success;
public event APISuccessHandler? Success;
/// <summary>
/// Invoked on failure to complete an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public event APIFailureHandler Failure;
public event APIFailureHandler? Failure;
private readonly object completionStateLock = new object();
@ -101,16 +99,29 @@ namespace osu.Game.Online.API
/// </summary>
public APIRequestCompletionState CompletionState { get; private set; }
public void Perform(IAPIProvider api)
/// <summary>
/// Should be called before <see cref="Perform"/> to give API context.
/// </summary>
/// <remarks>
/// This allows scheduling of operations back to the correct thread (which may be required before <see cref="Perform"/> is called).
/// </remarks>
public void AttachAPI(IAPIProvider apiAccess)
{
if (!(api is APIAccess apiAccess))
if (API != null && API != apiAccess)
throw new InvalidOperationException("Attached API cannot be changed after initial set.");
API = apiAccess;
}
public void Perform()
{
if (API == null)
{
Fail(new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests."));
return;
}
API = apiAccess;
User = apiAccess.LocalUser.Value;
User = API.LocalUser.Value;
if (isFailing) return;
@ -153,6 +164,8 @@ namespace osu.Game.Online.API
internal void TriggerSuccess()
{
Debug.Assert(API != null);
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
@ -161,14 +174,13 @@ namespace osu.Game.Online.API
CompletionState = APIRequestCompletionState.Completed;
}
if (API == null)
Success?.Invoke();
else
API.Schedule(() => Success?.Invoke());
API.Schedule(() => Success?.Invoke());
}
internal void TriggerFailure(Exception e)
{
Debug.Assert(API != null);
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
@ -177,10 +189,7 @@ namespace osu.Game.Online.API
CompletionState = APIRequestCompletionState.Failed;
}
if (API == null)
Failure?.Invoke(e);
else
API.Schedule(() => Failure?.Invoke(e));
API.Schedule(() => Failure?.Invoke(e));
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
@ -197,7 +206,7 @@ namespace osu.Game.Online.API
// in the case of a cancellation we don't care about whether there's an error in the response.
if (!(e is OperationCanceledException))
{
string responseString = WebRequest?.GetResponseString();
string? responseString = WebRequest?.GetResponseString();
// naive check whether there's an error in the response to avoid unnecessary JSON deserialisation.
if (!string.IsNullOrEmpty(responseString) && responseString.Contains(@"""error"""))
@ -235,7 +244,7 @@ namespace osu.Game.Online.API
private class DisplayableError
{
[JsonProperty("error")]
public string ErrorMessage { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
}
}

View File

@ -82,6 +82,8 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request)
{
request.AttachAPI(this);
Schedule(() =>
{
if (HandleRequest?.Invoke(request) != true)
@ -98,10 +100,17 @@ namespace osu.Game.Online.API
});
}
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
void IAPIProvider.Schedule(Action action) => base.Schedule(action);
public void Perform(APIRequest request)
{
request.AttachAPI(this);
HandleRequest?.Invoke(request);
}
public Task PerformAsync(APIRequest request)
{
request.AttachAPI(this);
HandleRequest?.Invoke(request);
return Task.CompletedTask;
}
@ -155,6 +164,8 @@ namespace osu.Game.Online.API
state.Value = APIState.Connecting;
LastLoginError = null;
request.AttachAPI(this);
// if no handler installed / handler can't handle verification, just assume that the server would verify for simplicity.
if (HandleRequest?.Invoke(request) != true)
onSuccessfulLogin();

View File

@ -134,6 +134,11 @@ namespace osu.Game.Online.API
/// </summary>
void UpdateStatistics(UserStatistics newStatistics);
/// <summary>
/// Schedule a callback to run on the update thread.
/// </summary>
internal void Schedule(Action action);
/// <summary>
/// Constructs a new <see cref="IHubClientConnector"/>. May be null if not supported.
/// </summary>

View File

@ -23,6 +23,6 @@ namespace osu.Game.Online.API.Requests
return req;
}
protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}";
protected override string Target => $@"chat/channels/{channel.Id}/users/{User!.Id}";
}
}

View File

@ -23,6 +23,6 @@ namespace osu.Game.Online.API.Requests
return req;
}
protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}";
protected override string Target => $@"chat/channels/{channel.Id}/users/{User!.Id}";
}
}

View File

@ -80,7 +80,7 @@ namespace osu.Game.Online.Chat
fetchReq.Success += updates =>
{
if (updates?.Presence != null)
if (updates.Presence != null)
{
foreach (var channel in updates.Presence)
joinChannel(channel);

View File

@ -27,6 +27,6 @@ namespace osu.Game.Online.Rooms
return req;
}
protected override string Target => $@"rooms/{Room.RoomID.Value}/users/{User.Id}";
protected override string Target => $@"rooms/{Room.RoomID.Value}/users/{User!.Id}";
}
}

View File

@ -23,6 +23,6 @@ namespace osu.Game.Online.Rooms
return req;
}
protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}";
protected override string Target => $"rooms/{room.RoomID.Value}/users/{User!.Id}";
}
}

View File

@ -1141,6 +1141,7 @@ namespace osu.Game
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());

View File

@ -692,7 +692,7 @@ namespace osu.Game
if (Interlocked.Decrement(ref allowableExceptions) < 0)
{
Logger.Log("Too many unhandled exceptions, crashing out.");
RulesetStore.TryDisableCustomRulesetsCausing(ex);
RulesetStore?.TryDisableCustomRulesetsCausing(ex);
return false;
}

View File

@ -351,13 +351,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
int endPointLength = endPoint == null ? 0 : 1;
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;
else if (isLinear(points[0], points[1], endPoint ?? points[2]))
if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.LINEAR;
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;
else if (isLinear(points[0], points[1], endPoint ?? points[2]))
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.LINEAR;
}
}
else if (vertices.Length + endPointLength > 3)
// Lazer supports perfect curves with less than 3 points and colinear points
type = PathType.BEZIER;
}
// The first control point must have a definite type.

View File

@ -1,132 +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.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public partial class EffectPointVisualisation : CompositeDrawable, IControlPointVisualisation
{
private readonly EffectControlPoint effect;
private Bindable<bool> kiai = null!;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public EffectPointVisualisation(EffectControlPoint point)
{
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Y;
effect = point;
}
[BackgroundDependencyLoader]
private void load()
{
kiai = effect.KiaiModeBindable.GetBoundCopy();
kiai.BindValueChanged(_ => refreshDisplay(), true);
}
private EffectControlPoint? nextControlPoint;
protected override void LoadComplete()
{
base.LoadComplete();
// Due to the limitations of ControlPointInfo, it's impossible to know via event flow when the next kiai point has changed.
// This is due to the fact that an EffectPoint can be added to an existing group. We would need to bind to ItemAdded on *every*
// future group to track this.
//
// I foresee this being a potential performance issue on beatmaps with many control points, so let's limit how often we check
// for changes. ControlPointInfo needs a refactor to make this flow better, but it should do for now.
Scheduler.AddDelayed(() =>
{
EffectControlPoint? next = null;
for (int i = 0; i < beatmap.ControlPointInfo.EffectPoints.Count; i++)
{
var point = beatmap.ControlPointInfo.EffectPoints[i];
if (point.Time > effect.Time)
{
next = point;
break;
}
}
if (!ReferenceEquals(nextControlPoint, next))
{
nextControlPoint = next;
refreshDisplay();
}
}, 100, true);
}
private void refreshDisplay()
{
ClearInternal();
if (beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed)
{
AddInternal(new ControlPointVisualisation(effect)
{
// importantly, override the x position being set since we do that in the GroupVisualisation parent drawable.
X = 0,
});
}
if (!kiai.Value)
return;
// handle kiai duration
// eventually this will be simpler when we have control points with durations.
if (nextControlPoint != null)
{
RelativeSizeAxes = Axes.Both;
Origin = Anchor.TopLeft;
Width = (float)(nextControlPoint.Time - effect.Time);
AddInternal(new KiaiVisualisation(effect.Time, nextControlPoint.Time)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.CentreLeft,
Height = 0.4f,
Depth = float.MaxValue,
Colour = colours.Purple1,
});
}
}
private partial class KiaiVisualisation : Circle, IHasTooltip
{
private readonly double startTime;
private readonly double endTime;
public KiaiVisualisation(double startTime, double endTime)
{
this.startTime = startTime;
this.endTime = endTime;
}
public LocalisableString TooltipText => $"{startTime.ToEditorFormattedString()} - {endTime.ToEditorFormattedString()} kiai time";
}
// kiai sections display duration, so are required to be visualised.
public bool IsVisuallyRedundant(ControlPoint other) => other is EffectControlPoint otherEffect && effect.KiaiMode == otherEffect.KiaiMode;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
private bool showScrollSpeed;
public GroupVisualisation(ControlPointGroup group)
{
RelativePositionAxes = Axes.X;
@ -24,8 +27,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
Group = group;
X = (float)group.Time;
}
[BackgroundDependencyLoader]
private void load(EditorBeatmap beatmap)
{
showScrollSpeed = beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed;
// Run in constructor so IsRedundant calls can work correctly.
controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, _) =>
{
@ -47,8 +55,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
});
break;
case EffectControlPoint effect:
AddInternal(new EffectPointVisualisation(effect));
case EffectControlPoint:
if (!showScrollSpeed)
return;
AddInternal(new ControlPointVisualisation(point)
{
// importantly, override the x position being set since we do that above.
X = 0,
});
break;
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Extensions;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
/// <summary>
/// The part of the timeline that displays kiai sections in the song.
/// </summary>
public partial class KiaiPart : TimelinePart
{
private DrawablePool<KiaiVisualisation> pool = null!;
[BackgroundDependencyLoader]
private void load()
{
AddInternal(pool = new DrawablePool<KiaiVisualisation>(10));
}
protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
EditorBeatmap.ControlPointInfo.ControlPointsChanged += updateParts;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateParts();
}
private void updateParts() => Scheduler.AddOnce(() =>
{
Clear(disposeChildren: false);
double? startTime = null;
foreach (var effectPoint in EditorBeatmap.ControlPointInfo.EffectPoints)
{
if (startTime.HasValue)
{
if (effectPoint.KiaiMode)
continue;
var section = new KiaiSection
{
StartTime = startTime.Value,
EndTime = effectPoint.Time
};
Add(pool.Get(v => v.Section = section));
startTime = null;
}
else
{
if (!effectPoint.KiaiMode)
continue;
startTime = effectPoint.Time;
}
}
// last effect point has kiai enabled, kiai should last until the end of the map
if (startTime.HasValue)
{
Add(pool.Get(v => v.Section = new KiaiSection
{
StartTime = startTime.Value,
EndTime = Content.RelativeChildSize.X
}));
}
});
private partial class KiaiVisualisation : PoolableDrawable, IHasTooltip
{
private KiaiSection section;
public KiaiSection Section
{
set
{
section = value;
X = (float)value.StartTime;
Width = (float)value.Duration;
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Height = 0.2f;
AddInternal(new FastCircle
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Purple1
});
}
public LocalisableString TooltipText => $"{section.StartTime.ToEditorFormattedString()} - {section.EndTime.ToEditorFormattedString()} kiai time";
}
private readonly struct KiaiSection
{
public double StartTime { get; init; }
public double EndTime { get; init; }
public double Duration => EndTime - StartTime;
}
}
}

View File

@ -65,6 +65,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
new KiaiPart
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
new ControlPointPart
{
Anchor = Anchor.Centre,

View File

@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// <summary>
/// Represents a singular point on a timeline part.
/// </summary>
public partial class PointVisualisation : Circle
public partial class PointVisualisation : FastCircle
{
public readonly double StartTime;

View File

@ -122,6 +122,7 @@ namespace osu.Game.Screens
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle"));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
@ -46,6 +47,9 @@ namespace osu.Game.Screens.Menu
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
[Resolved]
private SessionStatics statics { get; set; } = null!;
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
{
@ -128,7 +132,7 @@ namespace osu.Game.Screens.Menu
}
}
private long? lastNotifiedDailyChallengeRoomId;
private long? lastDailyChallengeRoomID;
private void dailyChallengeChanged(ValueChangedEvent<DailyChallengeInfo?> _)
{
@ -151,13 +155,16 @@ namespace osu.Game.Screens.Menu
Room = room;
cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet;
// We only want to notify the user if a new challenge recently went live.
if (room.StartDate.Value != null
&& Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 1800
&& room.RoomID.Value != lastNotifiedDailyChallengeRoomId)
if (room.StartDate.Value != null && room.RoomID.Value != lastDailyChallengeRoomID)
{
lastNotifiedDailyChallengeRoomId = room.RoomID.Value;
notificationOverlay?.Post(new NewDailyChallengeNotification(room));
lastDailyChallengeRoomID = room.RoomID.Value;
// new challenge is live, reset intro played static.
statics.SetValue(Static.DailyChallengeIntroPlayed, false);
// we only want to notify the user if the new challenge just went live.
if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 1800)
notificationOverlay?.Post(new NewDailyChallengeNotification(room));
}
updateCountdown();

View File

@ -54,8 +54,6 @@ namespace osu.Game.Screens.Menu
public override bool? AllowGlobalTrackControl => true;
private Screen songSelect;
private MenuSideFlashes sideFlashes;
protected ButtonSystem Buttons;
@ -150,7 +148,10 @@ namespace osu.Game.Screens.Menu
OnPlaylists = () => this.Push(new Playlists()),
OnDailyChallenge = room =>
{
this.Push(new DailyChallengeIntro(room));
if (statics.Get<bool>(Static.DailyChallengeIntroPlayed))
this.Push(new DailyChallenge(room));
else
this.Push(new DailyChallengeIntro(room));
},
OnExit = () =>
{
@ -220,26 +221,11 @@ namespace osu.Game.Screens.Menu
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh");
preloadSongSelect();
}
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
private void preloadSongSelect()
{
if (songSelect == null)
LoadComponentAsync(songSelect = new PlaySongSelect());
}
private void loadSoloSongSelect() => this.Push(consumeSongSelect());
private Screen consumeSongSelect()
{
var s = songSelect;
songSelect = null;
return s;
}
private void loadSoloSongSelect() => this.Push(new PlaySongSelect());
public override void OnEntering(ScreenTransitionEvent e)
{
@ -373,9 +359,6 @@ namespace osu.Game.Screens.Menu
ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next());
// we may have consumed our preloaded instance, so let's make another.
preloadSongSelect();
musicController.EnsurePlayingSomething();
// Cycle tip on resuming

View File

@ -70,6 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
[Resolved]
private MusicController musicController { get; set; } = null!;
[Resolved]
private SessionStatics statics { get; set; } = null!;
private Sample? dateWindupSample;
private Sample? dateImpactSample;
private Sample? beatmapWindupSample;
@ -462,6 +465,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
Schedule(() =>
{
statics.SetValue(Static.DailyChallengeIntroPlayed, true);
if (this.IsCurrentScreen())
this.Push(new DailyChallenge(room));
});

View File

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -21,6 +23,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Select.Carousel;
@ -76,8 +79,6 @@ namespace osu.Game.Screens.Select
private CarouselBeatmapSet? selectedBeatmapSet;
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>();
/// <summary>
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
/// </summary>
@ -109,28 +110,38 @@ namespace osu.Game.Screens.Select
[Cached]
protected readonly CarouselScrollContainer Scroll;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private DetachedBeatmapStore? detachedBeatmapStore { get; set; }
private IBindableList<BeatmapSetInfo>? detachedBeatmapSets;
private readonly NoResultsPlaceholder noResultsPlaceholder;
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Items.OfType<CarouselBeatmapSet>();
// todo: only used for testing, maybe remove.
private bool loadedTestBeatmaps;
public IEnumerable<BeatmapSetInfo> BeatmapSets
internal IEnumerable<BeatmapSetInfo> BeatmapSets
{
get => beatmapSets.Select(g => g.BeatmapSet);
set
{
loadedTestBeatmaps = true;
Schedule(() => loadBeatmapSets(value));
if (LoadState != LoadState.NotLoaded)
throw new InvalidOperationException("If not using a realm source, beatmap sets must be set before load.");
detachedBeatmapSets = new BindableList<BeatmapSetInfo>(value);
Schedule(loadNewRoot);
}
}
private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
private void loadNewRoot()
{
originalBeatmapSetsDetached = beatmapSets.Detach();
// Ensure no changes are made to the list while we are initialising items.
// We'll catch up on changes via subscriptions anyway.
BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray();
if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet))
if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null;
var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo;
@ -139,7 +150,7 @@ namespace osu.Game.Screens.Select
if (beatmapsSplitOut)
{
var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b =>
var carouselBeatmapSets = loadableSets.SelectMany(s => s.Beatmaps).Select(b =>
{
return createCarouselSet(new BeatmapSetInfo(new[] { b })
{
@ -153,25 +164,18 @@ namespace osu.Game.Screens.Select
}
else
{
var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType<CarouselBeatmapSet>();
var carouselBeatmapSets = loadableSets.Select(createCarouselSet).OfType<CarouselBeatmapSet>();
newRoot.AddItems(carouselBeatmapSets);
}
root = newRoot;
root.Filter(activeCriteria);
Scroll.Clear(false);
itemsCache.Invalidate();
ScrollToSelected();
applyActiveCriteria(false);
if (loadedTestBeatmaps)
{
invalidateAfterChange();
BeatmapSetsLoaded = true;
}
// Restore selection
if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates))
{
@ -180,6 +184,12 @@ namespace osu.Game.Screens.Select
if (found != null)
found.State.Value = CarouselItemState.Selected;
}
Schedule(() =>
{
invalidateAfterChange();
BeatmapSetsLoaded = true;
});
}
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
@ -195,7 +205,6 @@ namespace osu.Game.Screens.Select
private CarouselRoot root;
private IDisposable? subscriptionSets;
private IDisposable? subscriptionBeatmaps;
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
@ -205,7 +214,7 @@ namespace osu.Game.Screens.Select
private int visibleSetsCount;
public BeatmapCarousel()
public BeatmapCarousel(FilterCriteria initialCriterial)
{
root = new CarouselRoot(this);
InternalChild = new Container
@ -227,10 +236,12 @@ namespace osu.Game.Screens.Select
noResultsPlaceholder = new NoResultsPlaceholder()
}
};
activeCriteria = initialCriterial;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, AudioManager audio)
private void load(OsuConfigManager config, AudioManager audio, CancellationToken? cancellationToken)
{
spinSample = audio.Samples.Get("SongSelect/random-spin");
randomSelectSample = audio.Samples.Get(@"SongSelect/select-random");
@ -241,97 +252,61 @@ namespace osu.Game.Screens.Select
RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
RightClickScrollingEnabled.TriggerChange();
if (!loadedTestBeatmaps)
if (detachedBeatmapStore != null && detachedBeatmapSets == null)
{
// This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons
// we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update
// thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time).
realm.Run(r => loadBeatmapSets(getBeatmapSets(r)));
detachedBeatmapSets = detachedBeatmapStore.GetDetachedBeatmaps(cancellationToken);
detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged);
loadNewRoot();
}
}
[Resolved]
private RealmAccess realm { get; set; } = null!;
/// <summary>
/// Track GUIDs of all sets in realm to allow handling deletions.
/// </summary>
private readonly List<Guid> realmBeatmapSets = new List<Guid>();
protected override void LoadComplete()
{
base.LoadComplete();
subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged);
subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged);
}
private readonly HashSet<Guid> setsRequiringUpdate = new HashSet<Guid>();
private readonly HashSet<Guid> setsRequiringRemoval = new HashSet<Guid>();
private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
private readonly HashSet<BeatmapSetInfo> setsRequiringRemoval = new HashSet<BeatmapSetInfo>();
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
{
// If loading test beatmaps, avoid overwriting with realm subscription callbacks.
if (loadedTestBeatmaps)
return;
IEnumerable<BeatmapSetInfo>? newBeatmapSets = changed.NewItems?.Cast<BeatmapSetInfo>();
if (changes == null)
switch (changed.Action)
{
realmBeatmapSets.Clear();
realmBeatmapSets.AddRange(sender.Select(r => r.ID));
case NotifyCollectionChangedAction.Add:
HashSet<Guid> newBeatmapSetIDs = newBeatmapSets!.Select(s => s.ID).ToHashSet();
if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0)
{
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
//
// Note that due to the catch-up logic below, once operations are restored we will still be in a roughly
// correct state. The only things that this return will change is the carousel will not empty *during* the blocking
// operation.
return;
}
setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID));
setsRequiringUpdate.AddRange(newBeatmapSets!);
break;
// Do a full two-way check for missing (or incorrectly present) beatmaps.
// Let's assume that the worst that can happen is deletions or additions.
setsRequiringRemoval.Clear();
setsRequiringUpdate.Clear();
case NotifyCollectionChangedAction.Remove:
IEnumerable<BeatmapSetInfo> oldBeatmapSets = changed.OldItems!.Cast<BeatmapSetInfo>();
HashSet<Guid> oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet();
foreach (Guid id in realmBeatmapSets)
{
if (!root.BeatmapSetsByID.ContainsKey(id))
setsRequiringUpdate.Add(id);
}
setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID));
setsRequiringRemoval.AddRange(oldBeatmapSets);
break;
foreach (Guid id in root.BeatmapSetsByID.Keys)
{
if (!realmBeatmapSets.Contains(id))
setsRequiringRemoval.Add(id);
}
}
else
{
foreach (int i in changes.DeletedIndices.OrderDescending())
{
Guid id = realmBeatmapSets[i];
case NotifyCollectionChangedAction.Replace:
setsRequiringUpdate.AddRange(newBeatmapSets!);
break;
setsRequiringRemoval.Add(id);
setsRequiringUpdate.Remove(id);
case NotifyCollectionChangedAction.Move:
setsRequiringUpdate.AddRange(newBeatmapSets!);
break;
realmBeatmapSets.RemoveAt(i);
}
foreach (int i in changes.InsertedIndices)
{
Guid id = sender[i].ID;
setsRequiringRemoval.Remove(id);
setsRequiringUpdate.Add(id);
realmBeatmapSets.Insert(i, id);
}
foreach (int i in changes.NewModifiedIndices)
setsRequiringUpdate.Add(sender[i].ID);
case NotifyCollectionChangedAction.Reset:
setsRequiringRemoval.Clear();
setsRequiringUpdate.Clear();
loadNewRoot();
break;
}
Scheduler.AddOnce(processBeatmapChanges);
@ -345,9 +320,9 @@ namespace osu.Game.Screens.Select
{
try
{
foreach (var set in setsRequiringRemoval) removeBeatmapSet(set);
foreach (var set in setsRequiringRemoval) removeBeatmapSet(set.ID);
foreach (var set in setsRequiringUpdate) updateBeatmapSet(fetchFromID(set)!);
foreach (var set in setsRequiringUpdate) updateBeatmapSet(set);
if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null)
{
@ -365,7 +340,7 @@ namespace osu.Game.Screens.Select
// This relies on the full update operation being in a single transaction, so please don't change that.
foreach (var set in setsRequiringUpdate)
{
foreach (var beatmapInfo in fetchFromID(set)!.Beatmaps)
foreach (var beatmapInfo in set.Beatmaps)
{
if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue;
@ -380,7 +355,7 @@ namespace osu.Game.Screens.Select
// If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed.
// Let's attempt to follow set-level selection anyway.
SelectBeatmap(fetchFromID(setsRequiringUpdate.First())!.Beatmaps.First());
SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First());
}
}
}
@ -425,8 +400,6 @@ namespace osu.Game.Screens.Select
invalidateAfterChange();
}
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
removeBeatmapSet(beatmapSet.ID);
@ -438,8 +411,6 @@ namespace osu.Game.Screens.Select
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets))
return;
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID);
foreach (var set in existingSets)
{
foreach (var beatmap in set.Beatmaps)
@ -450,24 +421,14 @@ namespace osu.Game.Screens.Select
}
}
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet)
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
beatmapSet = beatmapSet.Detach();
Schedule(() =>
{
updateBeatmapSet(beatmapSet);
invalidateAfterChange();
});
}
updateBeatmapSet(beatmapSet);
invalidateAfterChange();
});
private void updateBeatmapSet(BeatmapSetInfo beatmapSet)
{
beatmapSet = beatmapSet.Detach();
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID);
originalBeatmapSetsDetached.Add(beatmapSet);
var newSets = new List<CarouselBeatmapSet>();
if (beatmapsSplitOut)
@ -696,7 +657,7 @@ namespace osu.Game.Screens.Select
item.State.Value = CarouselItemState.Selected;
}
private FilterCriteria activeCriteria = new FilterCriteria();
private FilterCriteria activeCriteria;
protected ScheduledDelegate? PendingFilter;
@ -733,12 +694,12 @@ namespace osu.Game.Screens.Select
}
}
public void Filter(FilterCriteria? newCriteria, bool debounce = true)
public void Filter(FilterCriteria? newCriteria)
{
if (newCriteria != null)
activeCriteria = newCriteria;
applyActiveCriteria(debounce);
applyActiveCriteria(true);
}
private bool beatmapsSplitOut;
@ -766,7 +727,7 @@ namespace osu.Game.Screens.Select
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
{
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
loadBeatmapSets(originalBeatmapSetsDetached);
loadNewRoot();
return;
}
@ -1315,7 +1276,6 @@ namespace osu.Game.Screens.Select
{
base.Dispose(isDisposing);
subscriptionSets?.Dispose();
subscriptionBeatmaps?.Dispose();
}
}

View File

@ -162,20 +162,6 @@ namespace osu.Game.Screens.Select
ApplyToBackground(applyBlurToBackground);
});
LoadComponentAsync(Carousel = new BeatmapCarousel
{
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
BleedTop = FilterControl.HEIGHT,
BleedBottom = Select.Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount),
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
}, c => carouselContainer.Child = c);
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
@ -227,7 +213,6 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
FilterChanged = ApplyFilterToCarousel,
},
new GridContainer // used for max width implementation
{
@ -328,6 +313,23 @@ namespace osu.Game.Screens.Select
modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(),
});
// Important to load this after the filter control is loaded (so we have initial filter criteria prepared).
LoadComponentAsync(Carousel = new BeatmapCarousel(FilterControl.CreateCriteria())
{
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
BleedTop = FilterControl.HEIGHT,
BleedBottom = Select.Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount),
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
}, c => carouselContainer.Child = c);
FilterControl.FilterChanged = Carousel.Filter;
if (ShowSongSelectFooter)
{
AddRangeInternal(new Drawable[]
@ -401,14 +403,6 @@ namespace osu.Game.Screens.Select
protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay();
protected virtual void ApplyFilterToCarousel(FilterCriteria criteria)
{
// if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter).
bool shouldDebounce = this.IsCurrentScreen();
Carousel.Filter(criteria, shouldDebounce);
}
private DependencyContainer dependencies = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -434,7 +428,8 @@ namespace osu.Game.Screens.Select
// Forced refetch is important here to guarantee correct invalidation across all difficulties.
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce, true);
this.Push(new EditorLoader());
FinaliseSelection(customStartAction: () => this.Push(new EditorLoader()));
}
/// <summary>
@ -992,7 +987,8 @@ namespace osu.Game.Screens.Select
// if we have a pending filter operation, we want to run it now.
// it could change selection (ie. if the ruleset has been changed).
Carousel.FlushPendingFilterOperations();
if (IsLoaded)
Carousel.FlushPendingFilterOperations();
return true;
}

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests
fetchReq.Success += updates =>
{
if (updates?.Presence != null)
if (updates.Presence != null)
{
foreach (var channel in updates.Presence)
handleChannelJoined(channel);

View File

@ -19,6 +19,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.OnlinePlay
{
@ -278,11 +279,18 @@ namespace osu.Game.Tests.Visual.OnlinePlay
var result = JsonConvert.DeserializeObject<Room>(JsonConvert.SerializeObject(source));
Debug.Assert(result != null);
// Playlist item IDs aren't serialised.
// Playlist item IDs and beatmaps aren't serialised.
if (source.CurrentPlaylistItem.Value != null)
{
result.CurrentPlaylistItem.Value = result.CurrentPlaylistItem.Value.With(new Optional<IBeatmapInfo>(source.CurrentPlaylistItem.Value.Beatmap));
result.CurrentPlaylistItem.Value.ID = source.CurrentPlaylistItem.Value.ID;
}
for (int i = 0; i < source.Playlist.Count; i++)
{
result.Playlist[i] = result.Playlist[i].With(new Optional<IBeatmapInfo>(source.Playlist[i].Beatmap));
result.Playlist[i].ID = source.Playlist[i].ID;
}
return result;
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Utils
/// </remarks>
public readonly bool HasValue;
private Optional(T value)
public Optional(T value)
{
Value = value;
HasValue = true;

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.809.2" />
<PackageReference Include="ppy.osu.Framework" Version="2024.831.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.810.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.809.2" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.831.0" />
</ItemGroup>
</Project>