1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 16:32:54 +08:00

Merge branch 'master' into fix-standardised-score-conversion

This commit is contained in:
Bartłomiej Dach 2023-12-19 17:41:38 +01:00
commit 322cfaae1d
No known key found for this signature in database
12 changed files with 196 additions and 81 deletions

View File

@ -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.
![export logs button](https://github.com/ppy/osu/assets/191335/cbfa5550-b7ed-4c5c-8dd0-8b87cc90ad9b)
### 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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