1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

Merge branch 'master' into mania-scorev2-values

This commit is contained in:
Bartłomiej Dach 2023-12-20 15:54:31 +01:00
commit 8c06d3873d
No known key found for this signature in database
9 changed files with 151 additions and 95 deletions

View File

@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
public override double ScoreMultiplier => 1;
}
}

View File

@ -11,5 +11,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map any harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
public override double ScoreMultiplier => 1;
}
}

View File

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

View File

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

View File

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

View File

@ -97,8 +97,11 @@ namespace osu.Game.Online.Metadata
{
if (!connected.NewValue)
{
isWatchingUserPresence.Value = false;
userStates.Clear();
Schedule(() =>
{
isWatchingUserPresence.Value = false;
userStates.Clear();
});
return;
}
@ -187,13 +190,13 @@ namespace osu.Game.Online.Metadata
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
{
lock (userStates)
Schedule(() =>
{
if (presence != null)
userStates[userId] = presence.Value;
else
userStates.Remove(userId);
}
});
return Task.CompletedTask;
}
@ -205,7 +208,7 @@ namespace osu.Game.Online.Metadata
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
isWatchingUserPresence.Value = true;
Schedule(() => isWatchingUserPresence.Value = true);
}
public override async Task EndWatchingUserPresence()
@ -215,14 +218,14 @@ namespace osu.Game.Online.Metadata
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
// must happen synchronously before any remote calls to avoid misordering.
userStates.Clear();
// must be scheduled before any remote calls to avoid mis-ordering.
Schedule(() => userStates.Clear());
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
}
finally
{
isWatchingUserPresence.Value = false;
Schedule(() => isWatchingUserPresence.Value = false);
}
}

View File

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

View File

@ -301,6 +301,9 @@ namespace osu.Game.Screens.Select
if (loadedTestBeatmaps)
return;
var setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
var setsRequiringRemoval = new HashSet<Guid>();
if (changes == null)
{
// During initial population, we must manually account for the fact that our original query was done on an async thread.
@ -314,67 +317,80 @@ namespace osu.Game.Screens.Select
foreach (var id in realmSets)
{
if (!root.BeatmapSetsByID.ContainsKey(id))
updateBeatmapSet(realm.Realm.Find<BeatmapSetInfo>(id)!.Detach());
setsRequiringUpdate.Add(realm.Realm.Find<BeatmapSetInfo>(id)!.Detach());
}
foreach (var id in root.BeatmapSetsByID.Keys)
{
if (!realmSets.Contains(id))
removeBeatmapSet(id);
setsRequiringRemoval.Add(id);
}
}
else
{
foreach (int i in changes.NewModifiedIndices)
setsRequiringUpdate.Add(sender[i].Detach());
invalidateAfterChange();
BeatmapSetsLoaded = true;
return;
foreach (int i in changes.InsertedIndices)
setsRequiringUpdate.Add(sender[i].Detach());
}
foreach (int i in changes.NewModifiedIndices)
updateBeatmapSet(sender[i].Detach());
foreach (int i in changes.InsertedIndices)
updateBeatmapSet(sender[i].Detach());
if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null)
// All local operations must be scheduled.
//
// If we don't schedule, beatmaps getting changed while song select is suspended (ie. last played being updated)
// will cause unexpected sounds and operations to occur in the background.
Schedule(() =>
{
// If SelectedBeatmapInfo is non-null, the set should also be non-null.
Debug.Assert(SelectedBeatmapSet != null);
// 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 = sender.Realm.Find<BeatmapSetInfo>(SelectedBeatmapSet.ID)?.DeletePending != false;
int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray();
if (selectedSetMarkedDeleted && modifiedAndInserted.Any())
try
{
// If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices.
// This relies on the full update operation being in a single transaction, so please don't change that.
foreach (int i in modifiedAndInserted)
foreach (var set in setsRequiringRemoval)
removeBeatmapSet(set);
foreach (var set in setsRequiringUpdate)
updateBeatmapSet(set);
if (changes?.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null)
{
var beatmapSetInfo = sender[i];
// If SelectedBeatmapInfo is non-null, the set should also be non-null.
Debug.Assert(SelectedBeatmapSet != null);
foreach (var beatmapInfo in beatmapSetInfo.Beatmaps)
// 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);
if (selectedSetMarkedDeleted && setsRequiringUpdate.Any())
{
if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata))
continue;
// Best effort matching. We can't use ID because in the update flow a new version will get its own GUID.
if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName)
// If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices.
// This relies on the full update operation being in a single transaction, so please don't change that.
foreach (var set in setsRequiringUpdate)
{
SelectBeatmap(beatmapInfo);
return;
foreach (var beatmapInfo in set.Beatmaps)
{
if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata))
continue;
// Best effort matching. We can't use ID because in the update flow a new version will get its own GUID.
if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName)
{
SelectBeatmap(beatmapInfo);
return;
}
}
}
// 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(setsRequiringUpdate.First().Beatmaps.First());
}
}
// 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(sender[modifiedAndInserted.First()].Beatmaps.First());
}
}
invalidateAfterChange();
finally
{
BeatmapSetsLoaded = true;
invalidateAfterChange();
}
});
}
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
@ -439,30 +455,13 @@ namespace osu.Game.Screens.Select
private void updateBeatmapSet(BeatmapSetInfo beatmapSet)
{
Guid? previouslySelectedID = null;
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID);
originalBeatmapSetsDetached.Add(beatmapSet.Detach());
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
var removedSets = root.RemoveItemsByID(beatmapSet.ID);
foreach (var removedSet in removedSets)
{
// If we don't remove this here, it may remain in a hidden state until scrolled off screen.
// Doesn't really affect anything during actual user interaction, but makes testing annoying.
var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet);
if (removedDrawable != null)
expirePanelImmediately(removedDrawable);
}
var newSets = new List<CarouselBeatmapSet>();
if (beatmapsSplitOut)
{
var newSets = new List<CarouselBeatmapSet>();
foreach (var beatmap in beatmapSet.Beatmaps)
{
var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap })
@ -473,18 +472,7 @@ namespace osu.Game.Screens.Select
});
if (newSet != null)
{
newSets.Add(newSet);
root.AddItem(newSet);
}
}
// check if we can/need to maintain our current selection.
if (previouslySelectedID != null)
{
var toSelect = newSets.FirstOrDefault(s => s.Beatmaps.Any(b => b.BeatmapInfo.ID == previouslySelectedID))
?? newSets.FirstOrDefault();
select(toSelect);
}
}
else
@ -492,13 +480,18 @@ namespace osu.Game.Screens.Select
var newSet = createCarouselSet(beatmapSet);
if (newSet != null)
{
root.AddItem(newSet);
newSets.Add(newSet);
}
// check if we can/need to maintain our current selection.
if (previouslySelectedID != null)
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
}
var removedSets = root.ReplaceItem(beatmapSet, newSets);
// If we don't remove these here, it may remain in a hidden state until scrolled off screen.
// Doesn't really affect anything during actual user interaction, but makes testing annoying.
foreach (var removedSet in removedSets)
{
var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet);
if (removedDrawable != null)
expirePanelImmediately(removedDrawable);
}
}
@ -1191,6 +1184,43 @@ namespace osu.Game.Screens.Select
base.AddItem(i);
}
/// <summary>
/// A special method to handle replace operations (general for updating a beatmap).
/// Avoids event-driven selection flip-flopping during the remove/add process.
/// </summary>
/// <param name="oldItem">The beatmap set to be replaced.</param>
/// <param name="newItems">All new items to replace the removed beatmap set.</param>
/// <returns>All removed items, for any further processing.</returns>
public IEnumerable<CarouselBeatmapSet> ReplaceItem(BeatmapSetInfo oldItem, List<CarouselBeatmapSet> newItems)
{
var previousSelection = (LastSelected as CarouselBeatmapSet)?.Beatmaps
.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected)
?.BeatmapInfo;
bool wasSelected = previousSelection?.BeatmapSet?.ID == oldItem.ID;
// Without doing this, the removal of the old beatmap will cause carousel's eager selection
// logic to invoke, causing one unnecessary selection.
DisableSelection = true;
var removedSets = RemoveItemsByID(oldItem.ID);
DisableSelection = false;
foreach (var set in newItems)
AddItem(set);
// Check if we can/need to maintain our current selection.
if (wasSelected)
{
CarouselBeatmap? matchingBeatmap = newItems.SelectMany(s => s.Beatmaps)
.FirstOrDefault(b => b.BeatmapInfo.ID == previousSelection?.ID);
if (matchingBeatmap != null)
matchingBeatmap.State.Value = CarouselItemState.Selected;
}
return removedSets;
}
public IEnumerable<CarouselBeatmapSet> RemoveItemsByID(Guid beatmapSetID)
{
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets))

View File

@ -36,13 +36,13 @@ namespace osu.Game.Screens.Select.Carousel
/// items have been filtered. This bool will be true during the base <see cref="Filter(FilterCriteria)"/>
/// operation.
/// </summary>
private bool filteringItems;
protected bool DisableSelection;
public override void Filter(FilterCriteria criteria)
{
filteringItems = true;
DisableSelection = true;
base.Filter(criteria);
filteringItems = false;
DisableSelection = false;
attemptSelection();
}
@ -95,7 +95,7 @@ namespace osu.Game.Screens.Select.Carousel
private void attemptSelection()
{
if (filteringItems) return;
if (DisableSelection) return;
// we only perform eager selection if we are a currently selected group.
if (State.Value != CarouselItemState.Selected) return;