Merge branch 'master' into carousel-maintain-selection-over-update

This commit is contained in:
Salman Ahmed 2022-08-29 11:23:22 +03:00 committed by GitHub
commit f2378d3fde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 292 additions and 212 deletions

View File

@ -124,14 +124,19 @@ namespace osu.Game.Tests.Gameplay
Assert.That(score.Rank, Is.EqualTo(ScoreRank.F));
Assert.That(score.Passed, Is.False);
Assert.That(score.Statistics.Count(kvp => kvp.Value > 0), Is.EqualTo(7));
Assert.That(score.Statistics.Sum(kvp => kvp.Value), Is.EqualTo(4));
Assert.That(score.MaximumStatistics.Sum(kvp => kvp.Value), Is.EqualTo(8));
Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.Miss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickMiss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(2));
Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.IgnoreMiss], Is.EqualTo(1));
Assert.That(score.MaximumStatistics[HitResult.Perfect], Is.EqualTo(2));
Assert.That(score.MaximumStatistics[HitResult.LargeTickHit], Is.EqualTo(2));
Assert.That(score.MaximumStatistics[HitResult.SmallTickHit], Is.EqualTo(2));
Assert.That(score.MaximumStatistics[HitResult.SmallBonus], Is.EqualTo(1));
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
}
private class TestJudgement : Judgement

View File

@ -307,7 +307,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
HitObjects = { new TestHitObject(result) }
});
Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo
Assert.That(scoreProcessor.ComputeScore(ScoringMode.Standardised, new ScoreInfo
{
Ruleset = new TestRuleset().RulesetInfo,
MaxCombo = result.AffectsCombo() ? 1 : 0,
@ -350,7 +350,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
}
};
double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore);
double totalScore = new TestScoreProcessor().ComputeScore(ScoringMode.Standardised, testScore);
Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%).
}
#pragma warning restore CS0618

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Tests.Visual.Gameplay
{
[HeadlessTest]
public class TestSceneModValidity : TestSceneAllRulesetPlayers
{
protected override void AddCheckSteps()
{
AddStep("Check all mod acronyms are unique", () =>
{
var mods = Ruleset.Value.CreateInstance().AllMods;
IEnumerable<string> acronyms = mods.Select(m => m.Acronym);
Assert.That(acronyms, Is.Unique);
});
AddStep("Check all mods are two-way incompatible", () =>
{
var mods = Ruleset.Value.CreateInstance().AllMods;
IEnumerable<Mod> modInstances = mods.Select(mod => mod.CreateInstance());
foreach (var modToCheck in modInstances)
{
var incompatibleMods = modToCheck.IncompatibleMods;
foreach (var incompatible in incompatibleMods)
{
foreach (var incompatibleMod in modInstances.Where(m => incompatible.IsInstanceOfType(m)))
{
Assert.That(
incompatibleMod.IncompatibleMods.Any(m => m.IsInstanceOfType(modToCheck)),
$"{modToCheck} has {incompatibleMod} in it's incompatible mods, but {incompatibleMod} does not have {modToCheck} in it's incompatible mods."
);
}
}
}
});
}
}
}

View File

@ -1,27 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
namespace osu.Game.Tests.Visual.Gameplay
{
[HeadlessTest]
public class TestSceneNoConflictingModAcronyms : TestSceneAllRulesetPlayers
{
protected override void AddCheckSteps()
{
AddStep("Check all mod acronyms are unique", () =>
{
var mods = Ruleset.Value.CreateInstance().AllMods;
IEnumerable<string> acronyms = mods.Select(m => m.Acronym);
Assert.That(acronyms, Is.Unique);
});
}
}
}

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
@ -39,8 +38,6 @@ namespace osu.Game.Tests.Visual.UserInterface
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp]
@ -66,6 +63,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}
beatmapSets.First().ToLive(Realm);
// Ensure all the initial imports are present before running any tests.
Realm.Run(r => r.Refresh());
});
[Test]
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("Add collection", () =>
{
Dependencies.Get<RealmAccess>().Write(r =>
Realm.Write(r =>
{
r.RemoveAll<BeatmapCollection>();
r.Add(new BeatmapCollection("wang"));

View File

@ -146,6 +146,7 @@ namespace osu.Game.Beatmaps
/// Get the loaded audio track instance. <see cref="LoadTrack"/> must have first been called.
/// This generally happens via MusicController when changing the global beatmap.
/// </summary>
[NotNull]
public Track Track
{
get

View File

@ -75,7 +75,14 @@ namespace osu.Game.Collections
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
// a warning that it's going to be a frustrating journey.
Current.Value = allBeatmaps;
Schedule(() => Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]);
Schedule(() =>
{
// current may have changed before the scheduled call is run.
if (Current.Value != allBeatmaps)
return;
Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0];
});
// Trigger a re-filter if the current item was in the change set.
if (selectedItem != null && changes != null)

View File

@ -4,10 +4,8 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
@ -31,6 +29,12 @@ namespace osu.Game.Configuration
[ExcludeFromDynamicCompile]
public class OsuConfigManager : IniConfigManager<OsuSetting>
{
public OsuConfigManager(Storage storage)
: base(storage)
{
Migrate();
}
protected override void InitialiseDefaults()
{
// UI/selection defaults
@ -172,12 +176,9 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
}
public IDictionary<OsuSetting, string> GetLoggableState() =>
new Dictionary<OsuSetting, string>(ConfigStore.Where(kvp => !keyContainsPrivateInformation(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString()));
private static bool keyContainsPrivateInformation(OsuSetting argKey)
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
{
switch (argKey)
switch (lookup)
{
case OsuSetting.Token:
return true;
@ -186,12 +187,6 @@ namespace osu.Game.Configuration
return false;
}
public OsuConfigManager(Storage storage)
: base(storage)
{
Migrate();
}
public void Migrate()
{
// arrives as 2020.123.0

View File

@ -20,6 +20,10 @@ namespace osu.Game.Database
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
{
// make sure the directory exists
if (!storage.ExistsDirectory(string.Empty))
yield break;
foreach (string directory in storage.GetDirectories(string.Empty))
{
var directoryStorage = storage.GetStorageForDirectory(directory);

View File

@ -46,8 +46,24 @@ namespace osu.Game.Graphics.UserInterface
private Sample? textCommittedSample;
private Sample? caretMovedSample;
private Sample? selectCharSample;
private Sample? selectWordSample;
private Sample? selectAllSample;
private Sample? deselectSample;
private OsuCaret? caret;
private bool selectionStarted;
private double sampleLastPlaybackTime;
private enum SelectionSampleType
{
Character,
Word,
All,
Deselect
}
public OsuTextBox()
{
Height = 40;
@ -78,6 +94,11 @@ namespace osu.Game.Graphics.UserInterface
textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete");
textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm");
caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement");
selectCharSample = audio.Samples.Get(@"Keyboard/select-char");
selectWordSample = audio.Samples.Get(@"Keyboard/select-word");
selectAllSample = audio.Samples.Get(@"Keyboard/select-all");
deselectSample = audio.Samples.Get(@"Keyboard/deselect");
}
private Color4 selectionColour;
@ -112,7 +133,41 @@ namespace osu.Game.Graphics.UserInterface
{
base.OnCaretMoved(selecting);
caretMovedSample?.Play();
if (!selecting)
caretMovedSample?.Play();
}
protected override void OnTextSelectionChanged(TextSelectionType selectionType)
{
base.OnTextSelectionChanged(selectionType);
switch (selectionType)
{
case TextSelectionType.Character:
playSelectSample(SelectionSampleType.Character);
break;
case TextSelectionType.Word:
playSelectSample(selectionStarted ? SelectionSampleType.Character : SelectionSampleType.Word);
break;
case TextSelectionType.All:
playSelectSample(SelectionSampleType.All);
break;
}
selectionStarted = true;
}
protected override void OnTextDeselected()
{
base.OnTextDeselected();
if (!selectionStarted) return;
playSelectSample(SelectionSampleType.Deselect);
selectionStarted = false;
}
protected override void OnImeComposition(string newComposition, int removedTextLength, int addedTextLength, bool caretMoved)
@ -204,6 +259,41 @@ namespace osu.Game.Graphics.UserInterface
SelectionColour = SelectionColour,
};
private void playSelectSample(SelectionSampleType selectionType)
{
if (Time.Current < sampleLastPlaybackTime + 15) return;
SampleChannel? channel;
double pitch = 0.98 + RNG.NextDouble(0.04);
switch (selectionType)
{
case SelectionSampleType.All:
channel = selectAllSample?.GetChannel();
break;
case SelectionSampleType.Word:
channel = selectWordSample?.GetChannel();
break;
case SelectionSampleType.Deselect:
channel = deselectSample?.GetChannel();
break;
default:
channel = selectCharSample?.GetChannel();
pitch += (SelectedText.Length / (double)Text.Length) * 0.15f;
break;
}
if (channel == null) return;
channel.Frequency.Value = pitch;
channel.Play();
sampleLastPlaybackTime = Time.Current;
}
private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play();
private class OsuCaret : Caret

View File

@ -65,11 +65,12 @@ namespace osu.Game.Online.Spectator
public virtual event Action<int, SpectatorState>? OnUserFinishedPlaying;
/// <summary>
/// All users currently being watched.
/// A dictionary containing all users currently being watched, with the number of watching components for each user.
/// </summary>
private readonly List<int> watchedUsers = new List<int>();
private readonly Dictionary<int, int> watchedUsersRefCounts = new Dictionary<int, int>();
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly SpectatorState currentState = new SpectatorState();
@ -94,12 +95,15 @@ namespace osu.Game.Online.Spectator
if (connected.NewValue)
{
// get all the users that were previously being watched
int[] users = watchedUsers.ToArray();
watchedUsers.Clear();
var users = new Dictionary<int, int>(watchedUsersRefCounts);
watchedUsersRefCounts.Clear();
// resubscribe to watched users.
foreach (int userId in users)
WatchUser(userId);
foreach ((int user, int watchers) in users)
{
for (int i = 0; i < watchers; i++)
WatchUser(user);
}
// re-send state in case it wasn't received
if (IsPlaying)
@ -121,7 +125,7 @@ namespace osu.Game.Online.Spectator
if (!playingUsers.Contains(userId))
playingUsers.Add(userId);
if (watchedUsers.Contains(userId))
if (watchedUsersRefCounts.ContainsKey(userId))
watchedUserStates[userId] = state;
OnUserBeganPlaying?.Invoke(userId, state);
@ -136,7 +140,7 @@ namespace osu.Game.Online.Spectator
{
playingUsers.Remove(userId);
if (watchedUsers.Contains(userId))
if (watchedUsersRefCounts.ContainsKey(userId))
watchedUserStates[userId] = state;
OnUserFinishedPlaying?.Invoke(userId, state);
@ -232,11 +236,13 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(ThreadSafety.IsUpdateThread);
if (watchedUsers.Contains(userId))
if (watchedUsersRefCounts.ContainsKey(userId))
{
watchedUsersRefCounts[userId]++;
return;
}
watchedUsers.Add(userId);
watchedUsersRefCounts.Add(userId, 1);
WatchUserInternal(userId);
}
@ -246,7 +252,13 @@ namespace osu.Game.Online.Spectator
// Todo: This should not be a thing, but requires framework changes.
Schedule(() =>
{
watchedUsers.Remove(userId);
if (watchedUsersRefCounts.TryGetValue(userId, out int watchers) && watchers > 1)
{
watchedUsersRefCounts[userId]--;
return;
}
watchedUsersRefCounts.Remove(userId);
watchedUserStates.Remove(userId);
StopWatchingUserInternal(userId);
});

View File

@ -27,6 +27,12 @@ namespace osu.Game.Overlays.Music
set => base.Padding = value;
}
protected override void OnItemsChanged()
{
base.OnItemsChanged();
Filter(currentCriteria);
}
public void Filter(FilterCriteria criteria)
{
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
@ -44,12 +50,12 @@ namespace osu.Game.Overlays.Music
public Live<BeatmapSetInfo>? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
protected override OsuRearrangeableListItem<Live<BeatmapSetInfo>> CreateOsuDrawable(Live<BeatmapSetInfo> item) => new PlaylistItem(item)
{
InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false,
SelectedSet = { BindTarget = SelectedSet },
RequestSelection = set => RequestSelection?.Invoke(set)
};
protected override OsuRearrangeableListItem<Live<BeatmapSetInfo>> CreateOsuDrawable(Live<BeatmapSetInfo> item) =>
new PlaylistItem(item)
{
SelectedSet = { BindTarget = SelectedSet },
RequestSelection = set => RequestSelection?.Invoke(set)
};
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapSetInfo>>> CreateListFillFlowContainer() => new SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>
{

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods;
perfectPlay.TotalScore = (long)scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, perfectPlay);
perfectPlay.TotalScore = (long)scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
// compute rank achieved
// default to SS, then adjust the rank with mods

View File

@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
/// Only populated via <see cref="ComputeFinalScore"/> or <see cref="ResetFromReplayFrame"/>.
/// Only populated via <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoreInfo)"/> or <see cref="ResetFromReplayFrame"/>.
/// </summary>
private HitResult? maxBasicResult;
@ -281,7 +281,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
public double ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
@ -291,60 +291,6 @@ namespace osu.Game.Rulesets.Scoring
return ComputeScore(mode, current, maximum);
}
/// <summary>
/// Computes the total score of a partially-completed <see cref="ScoreInfo"/>. This should be used when it is unknown whether a score is complete.
/// </summary>
/// <remarks>
/// Requires <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
if (!beatmapApplied)
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
ExtractScoringValues(scoreInfo, out var current, out _);
return ComputeScore(mode, current, MaximumScoringValues);
}
/// <summary>
/// Computes the total score of a given <see cref="ScoreInfo"/> with a given custom max achievable combo.
/// </summary>
/// <remarks>
/// This is useful for processing legacy scores in which the maximum achievable combo can be more accurately determined via external means (e.g. database values or difficulty calculation).
/// <p>Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.</p>
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
double accuracyRatio = scoreInfo.Accuracy;
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
ExtractScoringValues(scoreInfo, out var current, out var maximum);
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
accuracyRatio = current.BaseScore / maximum.BaseScore;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
}
/// <summary>
/// Computes the total score from scoring values.
/// </summary>
@ -454,11 +400,11 @@ namespace osu.Game.Rulesets.Scoring
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else.
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
score.TotalScore = (long)Math.Round(ComputeScore(ScoringMode.Standardised, score));
}
/// <summary>
/// Populates the given score with remaining statistics as "missed" and marks it with <see cref="ScoreRank.F"/> rank.
/// Populates a failed score, marking it with the <see cref="ScoreRank.F"/> rank.
/// </summary>
public void FailScore(ScoreInfo score)
{
@ -468,22 +414,6 @@ namespace osu.Game.Rulesets.Scoring
score.Passed = false;
Rank.Value = ScoreRank.F;
Debug.Assert(maximumResultCounts != null);
if (maximumResultCounts.TryGetValue(HitResult.LargeTickHit, out int maximumLargeTick))
scoreResultCounts[HitResult.LargeTickMiss] = maximumLargeTick - scoreResultCounts.GetValueOrDefault(HitResult.LargeTickHit);
if (maximumResultCounts.TryGetValue(HitResult.SmallTickHit, out int maximumSmallTick))
scoreResultCounts[HitResult.SmallTickMiss] = maximumSmallTick - scoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
int maximumBonusOrIgnore = maximumResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
int currentBonusOrIgnore = scoreResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.IgnoreMiss] = maximumBonusOrIgnore - currentBonusOrIgnore;
int maximumBasic = maximumResultCounts.SingleOrDefault(kvp => kvp.Key.IsBasic()).Value;
int currentBasic = scoreResultCounts.Where(kvp => kvp.Key.IsBasic() && kvp.Key != HitResult.Miss).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.Miss] = maximumBasic - currentBasic;
PopulateScore(score);
}

View File

@ -148,7 +148,7 @@ namespace osu.Game.Scoring
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo.Value));
return (long)Math.Round(scoreProcessor.ComputeScore(mode, score));
}
/// <summary>

View File

@ -197,19 +197,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
=> instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score);
protected override void EndGameplay(int userId, SpectatorState state)
protected override void QuitGameplay(int userId)
{
// Allowed passed/failed users to complete their remaining replay frames.
// The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used.
if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed)
return;
// we could also potentially receive EndGameplay with "Playing" state, at which point we can only early-return and hope it's a passing player.
// todo: this shouldn't exist, but it's here as a hotfix for an issue with multi-spectator screen not proceeding to results screen.
// see: https://github.com/ppy/osu/issues/19593
if (state.State == SpectatedUserState.Playing)
return;
RemoveUser(userId);
var instance = instances.Single(i => i.UserId == userId);

View File

@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo));
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo));
}
protected override void Dispose(bool isDisposing)

View File

@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@ -34,27 +31,27 @@ namespace osu.Game.Screens.Play
/// </summary>
public class FailAnimation : Container
{
public Action OnComplete;
public Action? OnComplete;
private readonly DrawableRuleset drawableRuleset;
private readonly BindableDouble trackFreq = new BindableDouble(1);
private readonly BindableDouble volumeAdjustment = new BindableDouble(0.5);
private Container filters;
private Container filters = null!;
private Box redFlashLayer;
private Box redFlashLayer = null!;
private Track track;
private Track track = null!;
private AudioFilter failLowPassFilter;
private AudioFilter failHighPassFilter;
private AudioFilter failLowPassFilter = null!;
private AudioFilter failHighPassFilter = null!;
private const float duration = 2500;
private Sample failSample;
private Sample? failSample;
[Resolved]
private OsuConfigManager config { get; set; }
private OsuConfigManager config { get; set; } = null!;
protected override Container<Drawable> Content { get; } = new Container
{
@ -66,8 +63,7 @@ namespace osu.Game.Screens.Play
/// <summary>
/// The player screen background, used to adjust appearance on failing.
/// </summary>
[CanBeNull]
public BackgroundScreen Background { private get; set; }
public BackgroundScreen? Background { private get; set; }
public FailAnimation(DrawableRuleset drawableRuleset)
{
@ -105,6 +101,7 @@ namespace osu.Game.Screens.Play
}
private bool started;
private bool filtersRemoved;
/// <summary>
/// Start the fail animation playing.
@ -113,6 +110,7 @@ namespace osu.Game.Screens.Play
public void Start()
{
if (started) throw new InvalidOperationException("Animation cannot be started more than once.");
if (filtersRemoved) throw new InvalidOperationException("Animation cannot be started after filters have been removed.");
started = true;
@ -125,7 +123,7 @@ namespace osu.Game.Screens.Play
failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
failSample.Play();
failSample?.Play();
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
@ -155,10 +153,15 @@ namespace osu.Game.Screens.Play
public void RemoveFilters(bool resetTrackFrequency = true)
{
if (resetTrackFrequency)
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
filtersRemoved = true;
track?.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
if (!started)
return;
if (resetTrackFrequency)
track.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
if (filters.Parent == null)
return;

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
@ -827,9 +828,17 @@ namespace osu.Game.Screens.Play
private bool onFail()
{
// Failing after the quit sequence has started may cause weird side effects with the fail animation / effects.
if (GameplayState.HasQuit)
return false;
if (!CheckModsAllowFailure())
return false;
Debug.Assert(!GameplayState.HasFailed);
Debug.Assert(!GameplayState.HasPassed);
Debug.Assert(!GameplayState.HasQuit);
GameplayState.HasFailed = true;
updateGameplayState();

View File

@ -182,7 +182,7 @@ namespace osu.Game.Screens.Play
scheduleStart(spectatorGameplayState);
}
protected override void EndGameplay(int userId, SpectatorState state)
protected override void QuitGameplay(int userId)
{
scheduledStart?.Cancel();
immediateSpectatorGameplayState = null;

View File

@ -115,14 +115,9 @@ namespace osu.Game.Screens.Spectate
{
case NotifyDictionaryChangedAction.Add:
case NotifyDictionaryChangedAction.Replace:
foreach ((int userId, var state) in e.NewItems.AsNonNull())
foreach ((int userId, SpectatorState state) in e.NewItems.AsNonNull())
onUserStateChanged(userId, state);
break;
case NotifyDictionaryChangedAction.Remove:
foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull())
onUserStateRemoved(userId, state);
break;
}
}
@ -136,33 +131,21 @@ namespace osu.Game.Screens.Spectate
switch (newState.State)
{
case SpectatedUserState.Passed:
// Make sure that gameplay completes to the end.
if (gameplayStates.TryGetValue(userId, out var gameplayState))
gameplayState.Score.Replay.HasReceivedAllFrames = true;
break;
case SpectatedUserState.Playing:
Schedule(() => OnNewPlayingUserState(userId, newState));
startGameplay(userId);
break;
case SpectatedUserState.Passed:
markReceivedAllFrames(userId);
break;
case SpectatedUserState.Quit:
quitGameplay(userId);
break;
}
}
private void onUserStateRemoved(int userId, SpectatorState state)
{
if (!userMap.ContainsKey(userId))
return;
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
return;
gameplayState.Score.Replay.HasReceivedAllFrames = true;
gameplayStates.Remove(userId);
Schedule(() => EndGameplay(userId, state));
}
private void startGameplay(int userId)
{
Debug.Assert(userMap.ContainsKey(userId));
@ -196,6 +179,29 @@ namespace osu.Game.Screens.Spectate
Schedule(() => StartGameplay(userId, gameplayState));
}
/// <summary>
/// Marks an existing gameplay session as received all frames.
/// </summary>
private void markReceivedAllFrames(int userId)
{
if (gameplayStates.TryGetValue(userId, out var gameplayState))
gameplayState.Score.Replay.HasReceivedAllFrames = true;
}
private void quitGameplay(int userId)
{
if (!userMap.ContainsKey(userId))
return;
if (!gameplayStates.ContainsKey(userId))
return;
markReceivedAllFrames(userId);
gameplayStates.Remove(userId);
Schedule(() => QuitGameplay(userId));
}
/// <summary>
/// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing.
/// </summary>
@ -211,11 +217,10 @@ namespace osu.Game.Screens.Spectate
protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState);
/// <summary>
/// Ends gameplay for a user.
/// Quits gameplay for a user.
/// </summary>
/// <param name="userId">The user to end gameplay for.</param>
/// <param name="state">The final user state.</param>
protected abstract void EndGameplay(int userId, SpectatorState state);
/// <param name="userId">The user to quit gameplay for.</param>
protected abstract void QuitGameplay(int userId);
/// <summary>
/// Stops spectating a user.
@ -223,10 +228,10 @@ namespace osu.Game.Screens.Spectate
/// <param name="userId">The user to stop spectating.</param>
protected void RemoveUser(int userId)
{
if (!userStates.TryGetValue(userId, out var state))
if (!userStates.ContainsKey(userId))
return;
onUserStateRemoved(userId, state);
quitGameplay(userId);
users.Remove(userId);
userMap.Remove(userId);

View File

@ -9,6 +9,7 @@ using System.Net;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Logging;
using osu.Framework.Statistics;
using osu.Game.Beatmaps;
@ -112,8 +113,8 @@ namespace osu.Game.Utils
scope.Contexts[@"config"] = new
{
Game = game.Dependencies.Get<OsuConfigManager>().GetLoggableState()
// TODO: add framework config here. needs some consideration on how to expose.
Game = game.Dependencies.Get<OsuConfigManager>().GetCurrentConfigurationForLogging(),
Framework = game.Dependencies.Get<FrameworkConfigManager>().GetCurrentConfigurationForLogging(),
};
game.Dependencies.Get<RealmAccess>().Run(realm =>