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:
commit
8c06d3873d
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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!;
|
||||
|
@ -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.");
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user