mirror of
https://github.com/ppy/osu.git
synced 2026-05-20 06:39:54 +08:00
Compare commits
65 Commits
@@ -46,22 +46,16 @@ body:
|
||||
value: |
|
||||
## Logs
|
||||
|
||||
Attaching log files is required for every reported bug. See instructions below on how to find them.
|
||||
|
||||
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
|
||||
Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them.
|
||||
|
||||
### Desktop platforms
|
||||
|
||||
If the game has not yet been closed since you found the bug:
|
||||
1. Head on to game settings and click on "Open osu! folder"
|
||||
2. Then open the `logs` folder located there
|
||||
1. Head on to game settings and click on "Export logs"
|
||||
2. Click the notification to locate the file
|
||||
3. Drag the generated `.zip` files into the github issue window
|
||||
|
||||
The default places to find the logs on desktop platforms are as follows:
|
||||
- `%AppData%/osu/logs` *on Windows*
|
||||
- `~/.local/share/osu/logs` *on Linux*
|
||||
- `~/Library/Application Support/osu/logs` *on macOS*
|
||||
|
||||
If you have selected a custom location for the game files, you can find the `logs` folder there.
|
||||

|
||||
|
||||
### Mobile platforms
|
||||
|
||||
@@ -69,10 +63,6 @@ body:
|
||||
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
|
||||
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
|
||||
|
||||
---
|
||||
|
||||
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1213.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1219.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -59,23 +59,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
|
||||
{
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
}
|
||||
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@@ -160,6 +161,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Position = new Vector2(256 - slider_path_length / 2, 192),
|
||||
TickDistanceMultiplier = 3,
|
||||
ClassicSliderBehaviour = classic,
|
||||
Samples = new[]
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
},
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
||||
@@ -128,8 +128,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
foreach (var drawableHitObject in NestedHitObjects)
|
||||
drawableHitObject.AccentColour.Value = colour.NewValue;
|
||||
}, true);
|
||||
|
||||
Tracking.BindValueChanged(updateSlidingSample);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
@@ -166,14 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
slidingSample?.Stop();
|
||||
}
|
||||
|
||||
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
slidingSample?.Play();
|
||||
else
|
||||
slidingSample?.Stop();
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
@@ -238,9 +228,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
Tracking.Value = SliderInputManager.Tracking;
|
||||
|
||||
if (Tracking.Value && slidingSample != null)
|
||||
// keep the sliding sample playing at the current tracking position
|
||||
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
||||
if (slidingSample != null)
|
||||
{
|
||||
if (Tracking.Value && Time.Current >= HitObject.StartTime)
|
||||
{
|
||||
// keep the sliding sample playing at the current tracking position
|
||||
if (!slidingSample.IsPlaying)
|
||||
slidingSample.Play();
|
||||
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
||||
}
|
||||
else if (slidingSample.IsPlaying)
|
||||
slidingSample.Stop();
|
||||
}
|
||||
|
||||
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
||||
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
@@ -115,23 +115,10 @@ namespace osu.Game.Rulesets.Taiko
|
||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||
yield return new TaikoModRelax();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Random))
|
||||
yield return new TaikoModRandom();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
{
|
||||
var value = base.ConvertToLegacyMods(mods);
|
||||
|
||||
if (mods.OfType<TaikoModRandom>().Any())
|
||||
value |= LegacyMods.Random;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
{
|
||||
switch (type)
|
||||
|
||||
@@ -127,8 +127,11 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(30000001)]
|
||||
[TestCase(30000002)]
|
||||
[TestCase(30000003)]
|
||||
[TestCase(30000004)]
|
||||
[TestCase(30000005)]
|
||||
public void TestScoreUpgradeSuccess(int scoreVersion)
|
||||
{
|
||||
ScoreInfo scoreInfo = null!;
|
||||
|
||||
@@ -166,6 +166,29 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionNameCollisionsWithBuiltInItems()
|
||||
{
|
||||
AddStep("add dropdown", () =>
|
||||
{
|
||||
Add(new CollectionDropdown
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f,
|
||||
});
|
||||
});
|
||||
AddStep("add two collections which collide with default items", () => Realm.Write(r => r.Add(new[]
|
||||
{
|
||||
new BeatmapCollection(name: "All beatmaps"),
|
||||
new BeatmapCollection(name: "Manage collections...")
|
||||
{
|
||||
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||
},
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveCollectionViaButton()
|
||||
{
|
||||
|
||||
@@ -464,7 +464,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
manager.Import(testBeatmapSetInfo);
|
||||
}, 10);
|
||||
|
||||
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
|
||||
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID));
|
||||
|
||||
Task<Live<BeatmapSetInfo>?> updateTask = null!;
|
||||
|
||||
@@ -476,7 +476,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
AddUntilStep("wait for update completion", () => updateTask.IsCompleted);
|
||||
|
||||
AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
|
||||
AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -572,7 +572,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestTextSearchActiveByDefault()
|
||||
{
|
||||
configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true);
|
||||
AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
|
||||
createScreen();
|
||||
|
||||
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
@@ -587,7 +587,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestTextSearchNotActiveByDefault()
|
||||
{
|
||||
configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false);
|
||||
AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false));
|
||||
createScreen();
|
||||
|
||||
AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus);
|
||||
@@ -599,6 +599,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions()
|
||||
{
|
||||
AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
|
||||
createScreen();
|
||||
|
||||
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
|
||||
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
|
||||
AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value), () => Is.EqualTo(1));
|
||||
|
||||
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
|
||||
assertCustomisationToggleState(false, true);
|
||||
AddStep("hover over mod settings slider", () =>
|
||||
{
|
||||
var slider = modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<OsuSliderBar<double>>().First();
|
||||
InputManager.MoveMouseTo(slider);
|
||||
});
|
||||
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
|
||||
AddAssert("DT speed changed", () => !SelectedMods.Value.OfType<OsuModDoubleTime>().Single().SpeedChange.IsDefault);
|
||||
|
||||
AddStep("close customisation area", () => InputManager.PressKey(Key.Escape));
|
||||
AddUntilStep("search text box reacquired focus", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeselectAllViaKey()
|
||||
{
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace osu.Game
|
||||
|
||||
checkForOutdatedStarRatings();
|
||||
processBeatmapSetsWithMissingMetrics();
|
||||
// Note that the previous method will also update these on a fresh run.
|
||||
processBeatmapsWithMissingObjectCounts();
|
||||
processScoresWithMissingStatistics();
|
||||
convertLegacyTotalScoreToStandardised();
|
||||
@@ -144,12 +145,24 @@ namespace osu.Game
|
||||
}
|
||||
});
|
||||
|
||||
if (beatmapSetIds.Count == 0)
|
||||
return;
|
||||
|
||||
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing.");
|
||||
|
||||
int i = 0;
|
||||
// Technically this is doing more than just star ratings, but easier for the end user to understand.
|
||||
var notification = showProgressNotification(beatmapSetIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated");
|
||||
|
||||
int processedCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
foreach (var id in beatmapSetIds)
|
||||
{
|
||||
if (notification?.State == ProgressNotificationState.Cancelled)
|
||||
break;
|
||||
|
||||
updateNotificationProgress(notification, processedCount, beatmapSetIds.Count);
|
||||
|
||||
sleepIfRequired();
|
||||
|
||||
realmAccess.Run(r =>
|
||||
@@ -160,16 +173,19 @@ namespace osu.Game
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})");
|
||||
beatmapUpdater.Process(set);
|
||||
++processedCount;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Background processing failed on {set}: {e}");
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount);
|
||||
}
|
||||
|
||||
private void processBeatmapsWithMissingObjectCounts()
|
||||
@@ -180,16 +196,27 @@ namespace osu.Game
|
||||
|
||||
realmAccess.Run(r =>
|
||||
{
|
||||
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount == 0))
|
||||
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount < 0 || b.EndTimeObjectCount < 0))
|
||||
beatmapIds.Add(b.ID);
|
||||
});
|
||||
|
||||
Logger.Log($"Found {beatmapIds.Count} beatmaps which require reprocessing.");
|
||||
if (beatmapIds.Count == 0)
|
||||
return;
|
||||
|
||||
int i = 0;
|
||||
Logger.Log($"Found {beatmapIds.Count} beatmaps which require statistics population.");
|
||||
|
||||
var notification = showProgressNotification(beatmapIds.Count, "Populating missing statistics for beatmaps", "beatmaps have been populated with missing statistics");
|
||||
|
||||
int processedCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
foreach (var id in beatmapIds)
|
||||
{
|
||||
if (notification?.State == ProgressNotificationState.Cancelled)
|
||||
break;
|
||||
|
||||
updateNotificationProgress(notification, processedCount, beatmapIds.Count);
|
||||
|
||||
sleepIfRequired();
|
||||
|
||||
realmAccess.Run(r =>
|
||||
@@ -200,16 +227,19 @@ namespace osu.Game
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Log($"Background processing {beatmap} ({++i} / {beatmapIds.Count})");
|
||||
beatmapUpdater.ProcessObjectCounts(beatmap);
|
||||
++processedCount;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Background processing failed on {beatmap}: {e}");
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
completeNotification(notification, processedCount, beatmapIds.Count, failedCount);
|
||||
}
|
||||
|
||||
private void processScoresWithMissingStatistics()
|
||||
@@ -231,10 +261,23 @@ namespace osu.Game
|
||||
}
|
||||
});
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require reprocessing.");
|
||||
if (scoreIds.Count == 0)
|
||||
return;
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require statistics population.");
|
||||
|
||||
var notification = showProgressNotification(scoreIds.Count, "Populating missing statistics for scores", "scores have been populated with missing statistics");
|
||||
|
||||
int processedCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
foreach (var id in scoreIds)
|
||||
{
|
||||
if (notification?.State == ProgressNotificationState.Cancelled)
|
||||
break;
|
||||
|
||||
updateNotificationProgress(notification, processedCount, scoreIds.Count);
|
||||
|
||||
sleepIfRequired();
|
||||
|
||||
try
|
||||
@@ -250,7 +293,7 @@ namespace osu.Game
|
||||
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
|
||||
});
|
||||
|
||||
Logger.Log($"Populated maximum statistics for score {id}");
|
||||
++processedCount;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
@@ -260,8 +303,11 @@ namespace osu.Game
|
||||
{
|
||||
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
||||
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
|
||||
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
|
||||
}
|
||||
|
||||
private void convertLegacyTotalScoreToStandardised()
|
||||
@@ -270,8 +316,7 @@ namespace osu.Game
|
||||
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
||||
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
|
||||
&& (s.TotalScoreVersion == 30000002
|
||||
|| s.TotalScoreVersion == 30000003))
|
||||
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||
.AsEnumerable().Select(s => s.ID)));
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||
@@ -279,20 +324,17 @@ namespace osu.Game
|
||||
if (scoreIds.Count == 0)
|
||||
return;
|
||||
|
||||
ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
||||
|
||||
notificationOverlay?.Post(notification);
|
||||
var notification = showProgressNotification(scoreIds.Count, "Upgrading scores to new scoring algorithm", "scores have been upgraded to the new scoring algorithm");
|
||||
|
||||
int processedCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
foreach (var id in scoreIds)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
if (notification?.State == ProgressNotificationState.Cancelled)
|
||||
break;
|
||||
|
||||
notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})";
|
||||
notification.Progress = (float)processedCount / scoreIds.Count;
|
||||
updateNotificationProgress(notification, processedCount, scoreIds.Count);
|
||||
|
||||
sleepIfRequired();
|
||||
|
||||
@@ -310,7 +352,6 @@ namespace osu.Game
|
||||
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||
});
|
||||
|
||||
Logger.Log($"Converted total score for score {id}");
|
||||
++processedCount;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
@@ -325,24 +366,64 @@ namespace osu.Game
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount == scoreIds.Count)
|
||||
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
|
||||
}
|
||||
|
||||
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
|
||||
{
|
||||
if (notification == null)
|
||||
return;
|
||||
|
||||
notification.Text = notification.Text.ToString().Split('(').First().TrimEnd() + $" ({processedCount} of {totalCount})";
|
||||
notification.Progress = (float)processedCount / totalCount;
|
||||
|
||||
if (processedCount % 100 == 0)
|
||||
Logger.Log(notification.Text.ToString());
|
||||
}
|
||||
|
||||
private void completeNotification(ProgressNotification? notification, int processedCount, int totalCount, int? failedCount = null)
|
||||
{
|
||||
if (notification == null)
|
||||
return;
|
||||
|
||||
if (processedCount == totalCount)
|
||||
{
|
||||
notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm";
|
||||
notification.CompletionText = $"{processedCount} {notification.CompletionText}";
|
||||
notification.Progress = 1;
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
else
|
||||
{
|
||||
notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm.";
|
||||
notification.Text = $"{processedCount} of {totalCount} {notification.CompletionText}";
|
||||
|
||||
// We may have arrived here due to user cancellation or completion with failures.
|
||||
if (failedCount > 0)
|
||||
notification.Text += $" Check logs for issues with {failedCount} failed upgrades.";
|
||||
notification.Text += $" Check logs for issues with {failedCount} failed items.";
|
||||
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressNotification? showProgressNotification(int totalCount, string running, string completed)
|
||||
{
|
||||
if (notificationOverlay == null)
|
||||
return null;
|
||||
|
||||
if (totalCount < 10)
|
||||
return null;
|
||||
|
||||
ProgressNotification notification = new ProgressNotification
|
||||
{
|
||||
Text = running,
|
||||
CompletionText = completed,
|
||||
State = ProgressNotificationState.Active
|
||||
};
|
||||
|
||||
notificationOverlay?.Post(notification);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private void sleepIfRequired()
|
||||
{
|
||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
||||
|
||||
@@ -120,9 +120,9 @@ namespace osu.Game.Beatmaps
|
||||
[JsonIgnore]
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
public int EndTimeObjectCount { get; set; }
|
||||
public int EndTimeObjectCount { get; set; } = -1;
|
||||
|
||||
public int TotalObjectCount { get; set; }
|
||||
public int TotalObjectCount { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Reset any fetched online linking information (and history).
|
||||
|
||||
@@ -77,6 +77,8 @@ namespace osu.Game.Beatmaps
|
||||
beatmap.StarRating = calculator.Calculate().StarRating;
|
||||
beatmap.Length = working.Beatmap.CalculatePlayableLength();
|
||||
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
|
||||
beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||
beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
|
||||
}
|
||||
|
||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||
|
||||
@@ -59,11 +59,13 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
/// <summary>
|
||||
/// The basic star rating for this beatmap (with no mods applied).
|
||||
/// Defaults to -1 (meaning not-yet-calculated).
|
||||
/// </summary>
|
||||
double StarRating { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of hitobjects in the beatmap with a distinct end time.
|
||||
/// Defaults to -1 (meaning not-yet-calculated).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Canonically, these are hitobjects are either sliders or spinners.
|
||||
@@ -72,6 +74,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
/// <summary>
|
||||
/// The total number of hitobjects in the beatmap.
|
||||
/// Defaults to -1 (meaning not-yet-calculated).
|
||||
/// </summary>
|
||||
int TotalObjectCount { get; }
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace osu.Game.Collections
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
filters.Clear();
|
||||
filters.Add(allBeatmapsItem);
|
||||
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
|
||||
if (ShowManageCollectionsItem)
|
||||
|
||||
@@ -37,22 +37,17 @@ namespace osu.Game.Collections
|
||||
CollectionName = name;
|
||||
}
|
||||
|
||||
public bool Equals(CollectionFilterMenuItem? other)
|
||||
public virtual bool Equals(CollectionFilterMenuItem? other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
// collections may have the same name, so compare first on reference equality.
|
||||
// this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager.
|
||||
if (Collection != null)
|
||||
return Collection.ID == other.Collection?.ID;
|
||||
if (Collection == null) return false;
|
||||
|
||||
// fallback to name-based comparison.
|
||||
// this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below).
|
||||
return CollectionName == other.CollectionName;
|
||||
return Collection.ID == other.Collection?.ID;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => CollectionName.GetHashCode();
|
||||
public override int GetHashCode() => Collection?.ID.GetHashCode() ?? 0;
|
||||
}
|
||||
|
||||
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
|
||||
@@ -61,6 +56,10 @@ namespace osu.Game.Collections
|
||||
: base("All beatmaps")
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Equals(CollectionFilterMenuItem? other) => other is AllBeatmapsCollectionFilterMenuItem;
|
||||
|
||||
public override int GetHashCode() => 1;
|
||||
}
|
||||
|
||||
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
|
||||
@@ -69,5 +68,9 @@ namespace osu.Game.Collections
|
||||
: base("Manage collections...")
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Equals(CollectionFilterMenuItem? other) => other is ManageCollectionsFilterMenuItem;
|
||||
|
||||
public override int GetHashCode() => 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,9 @@ namespace osu.Game.Database
|
||||
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
|
||||
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
|
||||
/// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo.
|
||||
/// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values.
|
||||
/// </summary>
|
||||
private const int schema_version = 38;
|
||||
private const int schema_version = 39;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@@ -1095,6 +1096,20 @@ namespace osu.Game.Database
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 39:
|
||||
foreach (var b in migration.NewRealm.All<BeatmapInfo>())
|
||||
{
|
||||
// Either actually no objects, or processing ran and failed.
|
||||
// Reset to -1 so the next time they become zero we know that processing was attempted.
|
||||
if (b.TotalObjectCount == 0 && b.EndTimeObjectCount == 0)
|
||||
{
|
||||
b.TotalObjectCount = -1;
|
||||
b.EndTimeObjectCount = -1;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Database
|
||||
if (score.IsLegacyScore)
|
||||
return false;
|
||||
|
||||
if (score.TotalScoreVersion > 30000004)
|
||||
if (score.TotalScoreVersion > 30000002)
|
||||
return false;
|
||||
|
||||
// Recalculate the old-style standardised score to see if this was an old lazer score.
|
||||
@@ -293,13 +293,23 @@ namespace osu.Game.Database
|
||||
// Roughly corresponds to integrating f(combo) = combo ^ COMBO_EXPONENT (omitting constants)
|
||||
double maximumAchievableComboPortionInStandardisedScore = Math.Pow(maximumLegacyCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double comboPortionInScoreV1 = maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy;
|
||||
|
||||
// This is - roughly - how much score, in the combo portion, the longest combo on this particular play would gain in score V1.
|
||||
double comboPortionFromLongestComboInScoreV1 = Math.Pow(score.MaxCombo, 2);
|
||||
// Same for standardised score.
|
||||
double comboPortionFromLongestComboInStandardisedScore = Math.Pow(score.MaxCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
// We estimate the combo portion of the score in score V1 terms.
|
||||
// The division by accuracy is supposed to lessen the impact of accuracy on the combo portion,
|
||||
// but in some edge cases it cannot sanely undo it.
|
||||
// Therefore the resultant value is clamped from both sides for sanity.
|
||||
// The clamp from below to `comboPortionFromLongestComboInScoreV1` targets near-FC scores wherein
|
||||
// the player had bad accuracy at the end of their longest combo, which causes the division by accuracy
|
||||
// to underestimate the combo portion.
|
||||
// Ideally, this would be clamped from above to `maximumAchievableComboPortionInScoreV1` too,
|
||||
// but in practice this appears to fail for some scores (https://github.com/ppy/osu/pull/25876#issuecomment-1862248413).
|
||||
// TODO: investigate the above more closely
|
||||
double comboPortionInScoreV1 = Math.Max(maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy, comboPortionFromLongestComboInScoreV1);
|
||||
|
||||
// Calculate how many times the longest combo the user has achieved in the play can repeat
|
||||
// without exceeding the combo portion in score V1 as achieved by the player.
|
||||
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
|
||||
|
||||
@@ -44,9 +44,6 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private bool shouldPerformRightMouseScroll(MouseButtonEvent e) => RightMouseScrollbar && e.Button == MouseButton.Right;
|
||||
|
||||
private void scrollFromMouseEvent(MouseEvent e) =>
|
||||
ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar);
|
||||
|
||||
private bool rightMouseDragging;
|
||||
|
||||
protected override bool IsDragging => base.IsDragging || rightMouseDragging;
|
||||
@@ -80,7 +77,7 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
if (shouldPerformRightMouseScroll(e))
|
||||
{
|
||||
scrollFromMouseEvent(e);
|
||||
ScrollFromMouseEvent(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -91,7 +88,7 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
if (rightMouseDragging)
|
||||
{
|
||||
scrollFromMouseEvent(e);
|
||||
ScrollFromMouseEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,6 +126,9 @@ namespace osu.Game.Graphics.Containers
|
||||
return base.OnScroll(e);
|
||||
}
|
||||
|
||||
protected virtual void ScrollFromMouseEvent(MouseEvent e) =>
|
||||
ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar);
|
||||
|
||||
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction);
|
||||
|
||||
protected partial class OsuScrollbar : ScrollbarContainer
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@@ -46,6 +47,12 @@ namespace osu.Game.Graphics.Containers
|
||||
base.ScrollIntoView(target, animated);
|
||||
}
|
||||
|
||||
protected override void ScrollFromMouseEvent(MouseEvent e)
|
||||
{
|
||||
UserScrolling = true;
|
||||
base.ScrollFromMouseEvent(e);
|
||||
}
|
||||
|
||||
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
|
||||
{
|
||||
UserScrolling = false;
|
||||
|
||||
@@ -397,7 +397,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar
|
||||
{
|
||||
Padding = new MarginPadding { Right = 36 },
|
||||
Padding = new MarginPadding { Right = 26 },
|
||||
};
|
||||
|
||||
private partial class OsuDropdownSearchBar : DropdownSearchBar
|
||||
|
||||
+2
-2
@@ -1190,7 +1190,7 @@ namespace osu.Game
|
||||
}
|
||||
else if (recentLogCount == short_term_display_limit)
|
||||
{
|
||||
string logFile = $@"{entry.Target.Value.ToString().ToLowerInvariant()}.log";
|
||||
string logFile = Logger.GetLogger(entry.Target.Value).Filename;
|
||||
|
||||
Schedule(() => Notifications.Post(new SimpleNotification
|
||||
{
|
||||
@@ -1198,7 +1198,7 @@ namespace osu.Game
|
||||
Text = NotificationsStrings.SubsequentMessagesLogged,
|
||||
Activated = () =>
|
||||
{
|
||||
Storage.GetStorageForDirectory(@"logs").PresentFileExternally(logFile);
|
||||
Logger.Storage.PresentFileExternally(logFile);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -132,6 +132,8 @@ namespace osu.Game.Overlays.Mods
|
||||
protected ShearedToggleButton? CustomisationButton { get; private set; }
|
||||
protected SelectAllModsButton? SelectAllModsButton { get; set; }
|
||||
|
||||
private bool textBoxShouldFocus;
|
||||
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
private WorkingBeatmap? beatmap;
|
||||
@@ -508,6 +510,11 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
|
||||
TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic);
|
||||
|
||||
if (customisationVisible.Value)
|
||||
SearchTextBox.KillFocus();
|
||||
else
|
||||
setTextBoxFocus(textBoxShouldFocus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -621,8 +628,7 @@ namespace osu.Game.Overlays.Mods
|
||||
nonFilteredColumnCount += 1;
|
||||
}
|
||||
|
||||
if (textSearchStartsActive.Value)
|
||||
SearchTextBox.TakeFocus();
|
||||
setTextBoxFocus(textSearchStartsActive.Value);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
@@ -761,14 +767,20 @@ namespace osu.Game.Overlays.Mods
|
||||
return false;
|
||||
|
||||
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
|
||||
if (SearchTextBox.HasFocus)
|
||||
SearchTextBox.KillFocus();
|
||||
else
|
||||
SearchTextBox.TakeFocus();
|
||||
|
||||
setTextBoxFocus(!textBoxShouldFocus);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setTextBoxFocus(bool keepFocus)
|
||||
{
|
||||
textBoxShouldFocus = keepFocus;
|
||||
|
||||
if (textBoxShouldFocus)
|
||||
SearchTextBox.TakeFocus();
|
||||
else
|
||||
SearchTextBox.KillFocus();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample playback control
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace osu.Game.Overlays.Mods
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
public ModSettingsArea()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
set
|
||||
{
|
||||
text = value;
|
||||
Schedule(() => textDrawable.Text = text);
|
||||
Scheduler.AddOnce(t => textDrawable.Text = t, text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override LocalisableString Description => "Whoaaaaa...";
|
||||
|
||||
[SettingSource("Speed decrease", "The actual decrease to apply")]
|
||||
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
|
||||
{
|
||||
MinValue = 0.5,
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
@@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override LocalisableString Description => "Uguuuuuuuu...";
|
||||
|
||||
[SettingSource("Speed increase", "The actual increase to apply")]
|
||||
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
|
||||
{
|
||||
MinValue = 1.01,
|
||||
|
||||
@@ -32,9 +32,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
|
||||
/// <item><description>30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000005: Introduce combo exponent in the osu! gamemode. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000006: Fix edge cases in conversion after combo exponent introduction that lead to NaNs. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000005;
|
||||
public const int LATEST_VERSION = 30000006;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace osu.Game.Screens.Select
|
||||
/// <summary>
|
||||
/// The total count of non-filtered beatmaps displayed.
|
||||
/// </summary>
|
||||
public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.Beatmaps.Count(b => !b.Filtered.Value));
|
||||
public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.TotalItemsNotFiltered);
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected beatmap set.
|
||||
@@ -168,7 +168,10 @@ namespace osu.Game.Screens.Select
|
||||
applyActiveCriteria(false);
|
||||
|
||||
if (loadedTestBeatmaps)
|
||||
signalBeatmapsLoaded();
|
||||
{
|
||||
invalidateAfterChange();
|
||||
BeatmapSetsLoaded = true;
|
||||
}
|
||||
|
||||
// Restore selection
|
||||
if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates))
|
||||
@@ -266,8 +269,30 @@ namespace osu.Game.Screens.Select
|
||||
if (changes == null)
|
||||
return;
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
removeBeatmapSet(sender[i].ID);
|
||||
var removeableSets = changes.InsertedIndices.Select(i => sender[i].ID).ToHashSet();
|
||||
|
||||
// This schedule is required to retain selection of beatmaps over an ImportAsUpdate operation.
|
||||
// This is covered by TestPlaySongSelect.TestSelectionRetainedOnBeatmapUpdate.
|
||||
//
|
||||
// In short, we have specialised logic in `beatmapSetsChanged` (directly below) to infer that an
|
||||
// update operation has occurred. For this to work, we need to confirm the `DeletePending` flag
|
||||
// of the current selection.
|
||||
//
|
||||
// If we don't schedule the following code, it is possible for the `deleteBeatmapSetsChanged` handler
|
||||
// to be invoked before the `beatmapSetsChanged` handler (realm call order seems non-deterministic)
|
||||
// which will lead to the currently selected beatmap changing via `CarouselGroupEagerSelect`.
|
||||
//
|
||||
// We need a better path forward here. A few ideas:
|
||||
// - Avoid the necessity of having realm subscriptions on deleted/hidden items, maybe by storing all guids in realm
|
||||
// to a local list so we can better look them up on receiving `DeletedIndices`.
|
||||
// - Add a new property on `BeatmapSetInfo` to link to the pre-update set, and use that to handle the update case.
|
||||
Schedule(() =>
|
||||
{
|
||||
foreach (var set in removeableSets)
|
||||
removeBeatmapSet(set);
|
||||
|
||||
invalidateAfterChange();
|
||||
});
|
||||
}
|
||||
|
||||
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||
@@ -289,7 +314,7 @@ namespace osu.Game.Screens.Select
|
||||
foreach (var id in realmSets)
|
||||
{
|
||||
if (!root.BeatmapSetsByID.ContainsKey(id))
|
||||
UpdateBeatmapSet(realm.Realm.Find<BeatmapSetInfo>(id)!.Detach());
|
||||
updateBeatmapSet(realm.Realm.Find<BeatmapSetInfo>(id)!.Detach());
|
||||
}
|
||||
|
||||
foreach (var id in root.BeatmapSetsByID.Keys)
|
||||
@@ -298,15 +323,16 @@ namespace osu.Game.Screens.Select
|
||||
removeBeatmapSet(id);
|
||||
}
|
||||
|
||||
signalBeatmapsLoaded();
|
||||
invalidateAfterChange();
|
||||
BeatmapSetsLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
UpdateBeatmapSet(sender[i].Detach());
|
||||
updateBeatmapSet(sender[i].Detach());
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
UpdateBeatmapSet(sender[i].Detach());
|
||||
updateBeatmapSet(sender[i].Detach());
|
||||
|
||||
if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null)
|
||||
{
|
||||
@@ -316,7 +342,7 @@ namespace osu.Game.Screens.Select
|
||||
// To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions.
|
||||
// When an update occurs, the previous beatmap set is either soft or hard deleted.
|
||||
// Check if the current selection was potentially deleted by re-querying its validity.
|
||||
bool selectedSetMarkedDeleted = realm.Run(r => r.Find<BeatmapSetInfo>(SelectedBeatmapSet.ID))?.DeletePending != false;
|
||||
bool selectedSetMarkedDeleted = sender.Realm.Find<BeatmapSetInfo>(SelectedBeatmapSet.ID)?.DeletePending != false;
|
||||
|
||||
int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray();
|
||||
|
||||
@@ -347,6 +373,8 @@ namespace osu.Game.Screens.Select
|
||||
SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First());
|
||||
}
|
||||
}
|
||||
|
||||
invalidateAfterChange();
|
||||
}
|
||||
|
||||
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
|
||||
@@ -355,6 +383,8 @@ namespace osu.Game.Screens.Select
|
||||
if (changes == null)
|
||||
return;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
{
|
||||
var beatmapInfo = sender[i];
|
||||
@@ -367,17 +397,24 @@ namespace osu.Game.Screens.Select
|
||||
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
|
||||
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
|
||||
{
|
||||
UpdateBeatmapSet(beatmapSet.Detach());
|
||||
updateBeatmapSet(beatmapSet.Detach());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
invalidateAfterChange();
|
||||
}
|
||||
|
||||
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
|
||||
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) =>
|
||||
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
|
||||
{
|
||||
removeBeatmapSet(beatmapSet.ID);
|
||||
invalidateAfterChange();
|
||||
});
|
||||
|
||||
private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
|
||||
private void removeBeatmapSet(Guid beatmapSetID)
|
||||
{
|
||||
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets))
|
||||
return;
|
||||
@@ -392,16 +429,15 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
root.RemoveItem(set);
|
||||
}
|
||||
|
||||
itemsCache.Invalidate();
|
||||
|
||||
if (!Scroll.UserScrolling)
|
||||
ScrollToSelected(true);
|
||||
|
||||
BeatmapSetsChanged?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
|
||||
{
|
||||
updateBeatmapSet(beatmapSet);
|
||||
invalidateAfterChange();
|
||||
});
|
||||
|
||||
private void updateBeatmapSet(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
Guid? previouslySelectedID = null;
|
||||
|
||||
@@ -464,14 +500,7 @@ namespace osu.Game.Screens.Select
|
||||
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
||||
}
|
||||
}
|
||||
|
||||
itemsCache.Invalidate();
|
||||
|
||||
if (!Scroll.UserScrolling)
|
||||
ScrollToSelected(true);
|
||||
|
||||
BeatmapSetsChanged?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a given beatmap on the carousel.
|
||||
@@ -748,15 +777,14 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private void signalBeatmapsLoaded()
|
||||
private void invalidateAfterChange()
|
||||
{
|
||||
if (!BeatmapSetsLoaded)
|
||||
{
|
||||
BeatmapSetsChanged?.Invoke();
|
||||
BeatmapSetsLoaded = true;
|
||||
}
|
||||
|
||||
itemsCache.Invalidate();
|
||||
|
||||
if (!Scroll.UserScrolling)
|
||||
ScrollToSelected(true);
|
||||
|
||||
BeatmapSetsChanged?.Invoke();
|
||||
}
|
||||
|
||||
private float? scrollTarget;
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
public IReadOnlyList<CarouselItem> Items => items;
|
||||
|
||||
public int TotalItemsNotFiltered { get; private set; }
|
||||
|
||||
private readonly List<CarouselItem> items = new List<CarouselItem>();
|
||||
|
||||
/// <summary>
|
||||
@@ -31,6 +33,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
items.Remove(i);
|
||||
|
||||
if (!i.Filtered.Value)
|
||||
TotalItemsNotFiltered--;
|
||||
|
||||
// it's important we do the deselection after removing, so any further actions based on
|
||||
// State.ValueChanged make decisions post-removal.
|
||||
i.State.Value = CarouselItemState.Collapsed;
|
||||
@@ -55,6 +60,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
// criteria may be null for initial population. the filtering will be applied post-add.
|
||||
items.Add(i);
|
||||
}
|
||||
|
||||
if (!i.Filtered.Value)
|
||||
TotalItemsNotFiltered++;
|
||||
}
|
||||
|
||||
public CarouselGroup(List<CarouselItem>? items = null)
|
||||
@@ -84,10 +92,17 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
||||
items.ForEach(c => c.Filter(criteria));
|
||||
TotalItemsNotFiltered = 0;
|
||||
|
||||
foreach (var c in items)
|
||||
{
|
||||
c.Filter(criteria);
|
||||
if (!c.Filtered.Value)
|
||||
TotalItemsNotFiltered++;
|
||||
}
|
||||
|
||||
// Sorting is expensive, so only perform if it's actually changed.
|
||||
if (lastCriteria?.Sort != criteria.Sort)
|
||||
if (lastCriteria?.RequiresSorting(criteria) != false)
|
||||
{
|
||||
criteriaComparer = Comparer<CarouselItem>.Create((x, y) =>
|
||||
{
|
||||
|
||||
@@ -219,6 +219,44 @@ namespace osu.Game.Screens.Select
|
||||
public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a new filter criteria, decide whether a full sort needs to be performed.
|
||||
/// </summary>
|
||||
/// <param name="newCriteria"></param>
|
||||
/// <returns></returns>
|
||||
public bool RequiresSorting(FilterCriteria newCriteria)
|
||||
{
|
||||
if (Sort != newCriteria.Sort)
|
||||
return true;
|
||||
|
||||
switch (Sort)
|
||||
{
|
||||
// Some sorts are stable across all other changes.
|
||||
// Running these sorts will sort all items, including currently hidden items.
|
||||
case SortMode.Artist:
|
||||
case SortMode.Author:
|
||||
case SortMode.DateSubmitted:
|
||||
case SortMode.DateAdded:
|
||||
case SortMode.DateRanked:
|
||||
case SortMode.Source:
|
||||
case SortMode.Title:
|
||||
return false;
|
||||
|
||||
// Some sorts use aggregate max comparisons, which will change based on filtered items.
|
||||
// These sorts generally ignore items hidden by filtered state, so we must force a sort under all circumstances here.
|
||||
//
|
||||
// This makes things very slow when typing a text search, and we probably want to consider a way to optimise things going forward.
|
||||
case SortMode.LastPlayed:
|
||||
case SortMode.BPM:
|
||||
case SortMode.Length:
|
||||
case SortMode.Difficulty:
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(Sort), Sort, "Unknown sort mode");
|
||||
}
|
||||
}
|
||||
|
||||
public enum MatchMode
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace osu.Game.Screens.Select
|
||||
BleedBottom = Footer.HEIGHT,
|
||||
SelectionChanged = updateSelectedBeatmap,
|
||||
BeatmapSetsChanged = carouselBeatmapsLoaded,
|
||||
FilterApplied = updateVisibleBeatmapCount,
|
||||
FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount),
|
||||
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
|
||||
}, c => carouselContainer.Child = c);
|
||||
|
||||
@@ -843,7 +843,7 @@ namespace osu.Game.Screens.Select
|
||||
private void carouselBeatmapsLoaded()
|
||||
{
|
||||
bindBindables();
|
||||
updateVisibleBeatmapCount();
|
||||
Scheduler.AddOnce(updateVisibleBeatmapCount);
|
||||
|
||||
Carousel.AllowSelection = true;
|
||||
|
||||
@@ -877,7 +877,8 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
// Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
|
||||
// but also in this case we want support for formatting a number within a string).
|
||||
FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed:#,0} matches" : $"{Carousel.CountDisplayed:#,0} match";
|
||||
int carouselCountDisplayed = Carousel.CountDisplayed;
|
||||
FilterControl.InformationalText = carouselCountDisplayed != 1 ? $"{carouselCountDisplayed:#,0} matches" : $"{carouselCountDisplayed:#,0} match";
|
||||
}
|
||||
|
||||
private bool boundLocalBindables;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1213.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1219.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1215.0" />
|
||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
||||
+1
-1
@@ -23,6 +23,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1213.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1219.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user