1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 01:43:20 +08:00

Merge branch 'master' into skinnable-score-display

This commit is contained in:
Dan Balasescu 2020-10-15 19:16:03 +09:00 committed by GitHub
commit 1ce0e83e59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 648 additions and 258 deletions

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3683365342338796d, "diffcalc-test")]
[TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);

View File

@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
public int TotalColumns => Stages.Sum(g => g.Columns);
/// <summary>
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
/// </summary>
public readonly int OriginalTotalColumns;
/// <summary>
/// Creates a new <see cref="ManiaBeatmap"/>.
/// </summary>
/// <param name="defaultStage">The initial stages.</param>
public ManiaBeatmap(StageDefinition defaultStage)
/// <param name="originalTotalColumns">The total number of columns present before any user adjustments. Defaults to the total columns in <paramref name="defaultStage"/>.</param>
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{
Stages.Add(defaultStage);
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
}
public override IEnumerable<BeatmapStatistic> GetStatistics()

View File

@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public bool Dual;
public readonly bool IsForCurrentRuleset;
private readonly int originalTargetColumns;
// Internal for testing purposes
internal FastRandom Random { get; private set; }
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
else
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
originalTargetColumns = TargetColumns;
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> CreateBeatmap()
{
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });

View File

@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
public class ManiaDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
public double ScoreMultiplier;
}
}

View File

@ -1,6 +1,7 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private const double star_scaling_factor = 0.018;
private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes
{
StarRating = difficultyValue(skills) * star_scaling_factor,
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}
private double difficultyValue(Skill[] skills)
{
// Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
var overall = skills.OfType<Overall>().Single();
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
foreach (var individual in skills.OfType<Individual>())
{
for (int i = 0; i < individual.StrainPeaks.Count; i++)
{
double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
if (aggregate > aggregatePeaks[i])
aggregatePeaks[i] = aggregate;
}
}
aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in aggregatePeaks)
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
for (int i = 1; i < beatmap.HitObjects.Count; i++)
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
var sortedObjects = beatmap.HitObjects.ToArray();
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
for (int i = 1; i < sortedObjects.Length; i++)
yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
}
protected override Skill[] CreateSkills(IBeatmap beatmap)
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
var skills = new List<Skill> { new Overall(columnCount) };
for (int i = 0; i < columnCount; i++)
skills.Add(new Individual(i, columnCount));
return skills.ToArray();
}
new Strain(((ManiaBeatmap)beatmap).TotalColumns)
};
protected override Mod[] DifficultyAdjustmentMods
{
@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
new ManiaModKey6(),
new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
new ManiaModKey7(),
new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
new ManiaModKey8(),
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
new ManiaModKey9(),
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
}).ToArray();
}
}
private int getHitWindow300(Mod[] mods)
{
if (isForCurrentRuleset)
{
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
return applyModAdjustments(34 + 3 * od, mods);
}
if (Math.Round(originalOverallDifficulty) > 4)
return applyModAdjustments(34, mods);
return applyModAdjustments(47, mods);
static int applyModAdjustments(double value, Mod[] mods)
{
if (mods.Any(m => m is ManiaModHardRock))
value /= 1.4;
else if (mods.Any(m => m is ManiaModEasy))
value *= 1.4;
if (mods.Any(m => m is ManiaModDoubleTime))
value *= 1.5;
else if (mods.Any(m => m is ManiaModHalfTime))
value *= 0.75;
return (int)value;
}
}
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
{
double scoreMultiplier = 1;
foreach (var m in mods)
{
switch (m)
{
case ManiaModNoFail _:
case ManiaModEasy _:
case ManiaModHalfTime _:
scoreMultiplier *= 0.5;
break;
}
}
var maniaBeatmap = (ManiaBeatmap)beatmap;
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
if (diff > 0)
scoreMultiplier *= 0.9;
else if (diff < 0)
scoreMultiplier *= 0.9 + 0.04 * diff;
return scoreMultiplier;
}
}
}

View File

@ -1,47 +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.
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Individual : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.125;
private readonly double[] holdEndTimes;
private readonly int column;
public Individual(int column, int columnCount)
{
this.column = column;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
try
{
if (maniaCurrent.BaseObject.Column != column)
return 0;
// We give a slight bonus if something is held meanwhile
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
}
finally
{
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
}
}
}
}

View File

@ -1,56 +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.
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Overall : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.3;
private readonly double[] holdEndTimes;
private readonly int columnCount;
public Overall(int columnCount)
{
this.columnCount = columnCount;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
double holdFactor = 1.0; // Factor in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
for (int i = 0; i < columnCount; i++)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending.
// Releasing multiple notes at the same time is just as easy as releasing one
if (endTime == holdEndTimes[i])
holdAddition = 0;
// We give a slight bonus if something is held meanwhile
if (holdEndTimes[i] > endTime)
holdFactor = 1.25;
}
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
return (1 + holdAddition) * holdFactor;
}
}
}

View File

@ -0,0 +1,80 @@
// 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;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Strain : Skill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
private readonly double[] holdEndTimes;
private readonly double[] individualStrains;
private double individualStrain;
private double overallStrain;
public Strain(int totalColumns)
{
holdEndTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
overallStrain = 1;
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
var column = maniaCurrent.BaseObject.Column;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
// Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
holdAddition = 0;
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25;
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
}
holdEndTimes[column] = endTime;
// Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
return individualStrain + overallStrain - CurrentStrain;
}
protected override double GetPeakStrain(double offset)
=> applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
private double applyDecay(double value, double deltaTime, double decayBase)
=> value * Math.Pow(decayBase, deltaTime / 1000);
}
}

View File

@ -0,0 +1,165 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace osu.Game.Rulesets.Mania.MathUtils
{
/// <summary>
/// Provides access to .NET4.0 unstable sorting methods.
/// </summary>
/// <remarks>
/// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
/// Copyright (c) Microsoft Corporation. All rights reserved.
/// </remarks>
internal static class LegacySortHelper<T>
{
private const int quick_sort_depth_threshold = 32;
public static void Sort(T[] keys, IComparer<T> comparer)
{
if (keys == null)
throw new ArgumentNullException(nameof(keys));
if (keys.Length == 0)
return;
comparer ??= Comparer<T>.Default;
depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
}
private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer<T> comparer, int depthLimit)
{
do
{
if (depthLimit == 0)
{
heapsort(keys, left, right, comparer);
return;
}
int i = left;
int j = right;
// pre-sort the low, middle (pivot), and high values in place.
// this improves performance in the face of already sorted data, or
// data that is made up of multiple sorted runs appended together.
int middle = i + ((j - i) >> 1);
swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
swapIfGreater(keys, comparer, i, j); // swap the low with the high
swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
T x = keys[middle];
do
{
while (comparer.Compare(keys[i], x) < 0) i++;
while (comparer.Compare(x, keys[j]) < 0) j--;
Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
if (i > j) break;
if (i < j)
{
T key = keys[i];
keys[i] = keys[j];
keys[j] = key;
}
i++;
j--;
} while (i <= j);
// The next iteration of the while loop is to "recursively" sort the larger half of the array and the
// following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
// both sorts see the new value.
depthLimit--;
if (j - left <= right - i)
{
if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
left = i;
}
else
{
if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
right = j;
}
} while (left < right);
}
private static void heapsort(T[] keys, int lo, int hi, IComparer<T> comparer)
{
Contract.Requires(keys != null);
Contract.Requires(comparer != null);
Contract.Requires(lo >= 0);
Contract.Requires(hi > lo);
Contract.Requires(hi < keys.Length);
int n = hi - lo + 1;
for (int i = n / 2; i >= 1; i = i - 1)
{
downHeap(keys, i, n, lo, comparer);
}
for (int i = n; i > 1; i = i - 1)
{
swap(keys, lo, lo + i - 1);
downHeap(keys, 1, i - 1, lo, comparer);
}
}
private static void downHeap(T[] keys, int i, int n, int lo, IComparer<T> comparer)
{
Contract.Requires(keys != null);
Contract.Requires(comparer != null);
Contract.Requires(lo >= 0);
Contract.Requires(lo < keys.Length);
T d = keys[lo + i - 1];
while (i <= n / 2)
{
var child = 2 * i;
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
{
child++;
}
if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
break;
keys[lo + i - 1] = keys[lo + child - 1];
i = child;
}
keys[lo + i - 1] = d;
}
private static void swap(T[] a, int i, int j)
{
if (i != j)
{
T t = a[i];
a[i] = a[j];
a[j] = t;
}
}
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
{
if (a != b)
{
if (comparer.Compare(keys[a], keys[b]) > 0)
{
T key = keys[a];
keys[a] = keys[b];
keys[b] = key;
}
}
}
}
}

View File

@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModKey7),
typeof(ManiaModKey8),
typeof(ManiaModKey9),
typeof(ManiaModKey10),
}.Except(new[] { GetType() }).ToArray();
}
}

View File

@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
}
[Test]
public void TestMultiModFlattening()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(combinations[3] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
}
[Test]
public void TestIncompatibleThroughMultiMod()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
}
[Test]
public void TestIncompatibleWithSameInstanceViaMultiMod()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
}
private class ModA : Mod
{
public override string Name => nameof(ModA);
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
}
private class ModC : Mod
{
public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC);
public override double ScoreMultiplier => 1;
}
private class ModIncompatibleWithA : Mod
{
public override string Name => $"Incompatible With {nameof(ModA)}";

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("get variables", () =>
{
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock;
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First();
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
});

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@ -22,27 +21,24 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableSound : OsuTestScene
{
[Cached(typeof(ISamplePlaybackDisabler))]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource;
private PausableSkinnableSound skinnableSound;
[SetUp]
public void SetUp() => Schedule(() =>
public void SetUpSteps()
{
AddStep("setup hierarchy", () =>
{
gameplayClock.IsPaused.Value = false;
Children = new Drawable[]
{
skinSource = new TestSkinSourceContainer
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
},
};
});
}
[Test]
public void TestStoppedSoundDoesntResumeAfterPause()
@ -62,8 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5);
AddAssert("sample not playing", () => !sample.Playing);
@ -82,8 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for sample to start playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddUntilStep("wait for sample to start playing", () => sample.Playing);
}
[Test]
@ -98,10 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddUntilStep("sample not playing", () => !sample.Playing);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing);
@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
@ -133,20 +134,25 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddAssert("new sample stopped", () => !sample.Playing);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5);
AddAssert("new sample not played", () => !sample.Playing);
}
[Cached(typeof(ISkinSource))]
private class TestSkinSourceContainer : Container, ISkinSource
[Cached(typeof(ISamplePlaybackDisabler))]
private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{
[Resolved]
private ISkinSource source { get; set; }
public event Action SourceChanged;
public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>();
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -18,10 +19,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModSettings : OsuTestScene
public class TestSceneModSettings : OsuManualInputManagerTestScene
{
private TestModSelectOverlay modSelect;
@ -95,6 +97,41 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
}
[Test]
public void TestMultiModSettingsUnboundWhenCopied()
{
MultiMod original = null;
MultiMod copy = null;
AddStep("create mods", () =>
{
original = new MultiMod(new OsuModDoubleTime());
copy = (MultiMod)original.CreateCopy();
});
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
}
[Test]
public void TestCustomisationMenuNoClickthrough()
{
createModSelect();
openModSelect();
AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1);
AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
}
private void createModSelect()
{
AddStep("create mod select", () =>
@ -121,9 +158,16 @@ namespace osu.Game.Tests.Visual.UserInterface
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
public ModButton GetModButton(Mod mod)
{
return ModSectionsContainer.ChildrenOfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
}
public void SelectMod(Mod mod) =>
ModSectionsContainer.Children.Single(s => s.ModType == mod.Type)
.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
GetModButton(mod).SelectNext(1);
public void SetModSettingsWidth(float newWidth) =>
ModSettingsContainer.Width = newWidth;
}
public class TestRulesetInfo : RulesetInfo

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
@ -45,9 +44,7 @@ namespace osu.Game.Overlays.Mods
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
protected readonly FillFlowContainer<ModControlSection> ModSettingsContent;
protected readonly Container ModSettingsContainer;
protected readonly ModSettingsContainer ModSettingsContainer;
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
@ -284,7 +281,7 @@ namespace osu.Game.Overlays.Mods
},
},
},
ModSettingsContainer = new Container
ModSettingsContainer = new ModSettingsContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
@ -292,29 +289,11 @@ namespace osu.Game.Overlays.Mods
Width = 0.25f,
Alpha = 0,
X = -100,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(0, 0, 0, 192)
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = ModSettingsContent = new FillFlowContainer<ModControlSection>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Padding = new MarginPadding(20),
}
}
}
SelectedMods = { BindTarget = SelectedMods },
}
};
((IBindable<bool>)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection);
}
[BackgroundDependencyLoader(true)]
@ -423,8 +402,6 @@ namespace osu.Game.Overlays.Mods
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
updateMods();
updateModSettings(mods);
}
private void updateMods()
@ -445,25 +422,6 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(Color4.White, 200);
}
private void updateModSettings(ValueChangedEvent<IReadOnlyList<Mod>> selectedMods)
{
ModSettingsContent.Clear();
foreach (var mod in selectedMods.NewValue)
{
var settings = mod.CreateSettingsControls().ToList();
if (settings.Count > 0)
ModSettingsContent.Add(new ModControlSection(mod, settings));
}
bool hasSettings = ModSettingsContent.Count > 0;
CustomiseButton.Enabled.Value = hasSettings;
if (!hasSettings)
ModSettingsContainer.Hide();
}
private void modButtonPressed(Mod selectedMod)
{
if (selectedMod != null)

View File

@ -0,0 +1,84 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public class ModSettingsContainer : Container
{
public readonly IBindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public IBindable<bool> HasSettingsForSelection => hasSettingsForSelection;
private readonly Bindable<bool> hasSettingsForSelection = new Bindable<bool>();
private readonly FillFlowContainer<ModControlSection> modSettingsContent;
public ModSettingsContainer()
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(0, 0, 0, 192)
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = modSettingsContent = new FillFlowContainer<ModControlSection>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Padding = new MarginPadding(20),
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedMods.BindValueChanged(modsChanged, true);
}
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
modSettingsContent.Clear();
foreach (var mod in mods.NewValue)
{
var settings = mod.CreateSettingsControls().ToList();
if (settings.Count > 0)
modSettingsContent.Add(new ModControlSection(mod, settings));
}
bool hasSettings = modSettingsContent.Count > 0;
if (!hasSettings)
Hide();
hasSettingsForSelection.Value = hasSettings;
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
}
}

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Difficulty
if (!beatmap.HitObjects.Any())
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
double sectionLength = SectionLength * clockRate;
@ -100,15 +100,24 @@ namespace osu.Game.Rulesets.Difficulty
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
}
/// <summary>
/// Sorts a given set of <see cref="DifficultyHitObject"/>s.
/// </summary>
/// <param name="input">The <see cref="DifficultyHitObject"/>s to sort.</param>
/// <returns>The sorted <see cref="DifficultyHitObject"/>s.</returns>
protected virtual IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input)
=> input.OrderBy(h => h.BaseObject.StartTime);
/// <summary>
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
/// </summary>
public Mod[] CreateDifficultyAdjustmentModCombinations()
{
return createDifficultyAdjustmentModCombinations(Array.Empty<Mod>(), DifficultyAdjustmentMods).ToArray();
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
IEnumerable<Mod> createDifficultyAdjustmentModCombinations(IEnumerable<Mod> currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0)
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
{
// Return the current set.
switch (currentSetCount)
{
case 0:
@ -128,18 +137,43 @@ namespace osu.Game.Rulesets.Difficulty
break;
}
// Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod
// combinations in further recursions, so a moving subset is used to eliminate this effect
for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++)
// Apply the rest of the remaining mods recursively.
for (int i = 0; i < remainingMods.Length; i++)
{
var adjustmentMod = adjustmentSet[i];
if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod))))
var (nextSet, nextCount) = flatten(remainingMods.Span[i]);
// Check if any mods in the next set are incompatible with any of the current set.
if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType)))
continue;
foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1))
// Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType())))
continue;
// If all's good, attach the next set to the current set and recurse further.
foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount))
yield return combo;
}
}
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
{
if (!(mod is MultiMod multi))
return (mod.Yield(), 1);
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
int count = 0;
foreach (var nested in multi.Mods)
{
var (nestedSet, nestedCount) = flatten(nested);
set = set.Concat(nestedSet);
count += nestedCount;
}
return (set, count);
}
}
/// <summary>

View File

@ -41,7 +41,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// </summary>
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
/// <summary>
/// The current strain level.
/// </summary>
protected double CurrentStrain { get; private set; } = 1;
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List<double> strainPeaks = new List<double>();
@ -51,10 +55,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// </summary>
public void Process(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += StrainValueOf(current) * SkillMultiplier;
CurrentStrain *= strainDecay(current.DeltaTime);
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
Previous.Push(current);
}
@ -71,15 +75,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// <summary>
/// Sets the initial strain level for a new section.
/// </summary>
/// <param name="offset">The beginning of the new section in milliseconds.</param>
public void StartNewSectionFrom(double offset)
/// <param name="time">The beginning of the new section in milliseconds.</param>
public void StartNewSectionFrom(double time)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
if (Previous.Count > 0)
currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime);
currentSectionPeak = GetPeakStrain(time);
}
/// <summary>
/// Retrieves the peak strain at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the peak strain at.</param>
/// <returns>The peak strain.</returns>
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime);
/// <summary>
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
/// </summary>

View File

@ -6,7 +6,7 @@ using System.Linq;
namespace osu.Game.Rulesets.Mods
{
public class MultiMod : Mod
public sealed class MultiMod : Mod
{
public override string Name => string.Empty;
public override string Acronym => string.Empty;
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods
Mods = mods;
}
public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
}
}

View File

@ -18,8 +18,11 @@ namespace osu.Game.Rulesets.UI
/// A container which consumes a parent gameplay clock and standardises frame counts for children.
/// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks.
/// </summary>
public class FrameStabilityContainer : Container, IHasReplayHandler
[Cached(typeof(ISamplePlaybackDisabler))]
public class FrameStabilityContainer : Container, IHasReplayHandler, ISamplePlaybackDisabler
{
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
private readonly double gameplayStartTime;
/// <summary>
@ -35,7 +38,6 @@ namespace osu.Game.Rulesets.UI
public GameplayClock GameplayClock => stabilityGameplayClock;
[Cached(typeof(GameplayClock))]
[Cached(typeof(ISamplePlaybackDisabler))]
private readonly StabilityGameplayClock stabilityGameplayClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
@ -102,6 +104,8 @@ namespace osu.Game.Rulesets.UI
requireMoreUpdateLoops = true;
validState = !GameplayClock.IsPaused.Value;
samplePlaybackDisabled.Value = stabilityGameplayClock.ShouldDisableSamplePlayback;
int loops = 0;
while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames)
@ -224,6 +228,8 @@ namespace osu.Game.Rulesets.UI
public ReplayInputHandler ReplayInputHandler { get; set; }
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
private class StabilityGameplayClock : GameplayClock
{
public GameplayClock ParentGameplayClock;
@ -237,7 +243,7 @@ namespace osu.Game.Rulesets.UI
{
}
protected override bool ShouldDisableSamplePlayback =>
public override bool ShouldDisableSamplePlayback =>
// handle the case where playback is catching up to real-time.
base.ShouldDisableSamplePlayback
|| ParentSampleDisabler?.SamplePlaybackDisabled.Value == true

View File

@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play
/// <see cref="IFrameBasedClock"/>, as this should only be done once to ensure accuracy.
/// </remarks>
/// </summary>
public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler
public class GameplayClock : IFrameBasedClock
{
private readonly IFrameBasedClock underlyingClock;
@ -28,8 +28,6 @@ namespace osu.Game.Screens.Play
/// </summary>
public virtual IEnumerable<Bindable<double>> NonGameplayAdjustments => Enumerable.Empty<Bindable<double>>();
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
public GameplayClock(IFrameBasedClock underlyingClock)
{
this.underlyingClock = underlyingClock;
@ -66,13 +64,11 @@ namespace osu.Game.Screens.Play
/// <summary>
/// Whether nested samples supporting the <see cref="ISamplePlaybackDisabler"/> interface should be paused.
/// </summary>
protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
public virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
public void ProcessFrame()
{
// intentionally not updating the underlying clock (handled externally).
samplePlaybackDisabled.Value = ShouldDisableSamplePlayback;
}
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
@ -82,7 +78,5 @@ namespace osu.Game.Screens.Play
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public IClock Source => underlyingClock;
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
}
}

View File

@ -54,7 +54,6 @@ namespace osu.Game.Screens.Play
public GameplayClock GameplayClock => localGameplayClock;
[Cached(typeof(GameplayClock))]
[Cached(typeof(ISamplePlaybackDisabler))]
private readonly LocalGameplayClock localGameplayClock;
private Bindable<double> userAudioOffset;

View File

@ -35,7 +35,8 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play
{
[Cached]
public class Player : ScreenWithBeatmapBackground
[Cached(typeof(ISamplePlaybackDisabler))]
public class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
{
/// <summary>
/// The delay upon completion of the beatmap before displaying the results screen.
@ -55,6 +56,8 @@ namespace osu.Game.Screens.Play
// We are managing our own adjustments (see OnEntering/OnExiting).
public override bool AllowRateAdjustments => false;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
/// <summary>
/// Whether gameplay should pause when the game window focus is lost.
/// </summary>
@ -229,7 +232,11 @@ namespace osu.Game.Screens.Play
skipOverlay.Hide();
}
DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.IsPaused.BindValueChanged(paused =>
{
updateGameplayState();
samplePlaybackDisabled.Value = paused.NewValue;
});
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
@ -752,5 +759,7 @@ namespace osu.Game.Screens.Play
}
#endregion
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
}
}