mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 22:22:54 +08:00
Merge branch 'master' into skinnable-score-display
This commit is contained in:
commit
1ce0e83e59
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 });
|
||||
|
@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
public double GreatHitWindow;
|
||||
public double ScoreMultiplier;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
80
osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
Normal file
80
osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
Normal 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);
|
||||
}
|
||||
}
|
165
osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
Normal file
165
osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
typeof(ManiaModKey7),
|
||||
typeof(ManiaModKey8),
|
||||
typeof(ManiaModKey9),
|
||||
typeof(ManiaModKey10),
|
||||
}.Except(new[] { GetType() }).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -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)}";
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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()
|
||||
{
|
||||
gameplayClock.IsPaused.Value = false;
|
||||
|
||||
Children = new Drawable[]
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
skinSource = new TestSkinSourceContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Clock = gameplayClock,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
|
||||
},
|
||||
};
|
||||
});
|
||||
skinSource = new TestSkinSourceContainer
|
||||
{
|
||||
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);
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
84
osu.Game/Overlays/Mods/ModSettingsContainer.cs
Normal file
84
osu.Game/Overlays/Mods/ModSettingsContainer.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user