mirror of
https://github.com/ppy/osu
synced 2025-01-19 04:20:59 +00:00
Merge branch 'master' into carousel-maintain-selection-over-update
This commit is contained in:
commit
f2378d3fde
@ -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
|
||||
|
@ -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
|
||||
|
50
osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs
Normal file
50
osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs
Normal 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."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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"));
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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>>>
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 =>
|
||||
|
Loading…
Reference in New Issue
Block a user