Merge branch 'master' into beatmap-carousel-refactor

This commit is contained in:
Dean Herbert
2020-10-19 13:17:11 +09:00
committed by GitHub
125 changed files with 2238 additions and 884 deletions

View File

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1013.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1013.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
{ {
public class CatchLegacySkinTransformer : LegacySkinTransformer public class CatchLegacySkinTransformer : LegacySkinTransformer
{ {
/// <summary>
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
/// </summary>
private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score");
public CatchLegacySkinTransformer(ISkinSource source) public CatchLegacySkinTransformer(ISkinSource source)
: base(source) : base(source)
{ {
@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (component is HUDSkinComponent hudComponent)
{
switch (hudComponent.Component)
{
case HUDSkinComponents.ComboCounter:
// catch may provide its own combo counter; hide the default.
return providesComboCounter ? Drawable.Empty() : null;
}
}
if (!(component is CatchSkinComponent catchSkinComponent)) if (!(component is CatchSkinComponent catchSkinComponent))
return null; return null;
@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
this.GetAnimation("fruit-ryuuta", true, true, true); this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter: case CatchSkinComponents.CatchComboCounter:
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. if (providesComboCounter)
if (this.HasFont(comboFont)) return new LegacyCatchComboCounter(Source);
return new LegacyComboCounter(Source);
break; break;
} }

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
/// <summary> /// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary> /// </summary>
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
{ {
private readonly LegacyRollingCounter counter; private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion; private readonly LegacyRollingCounter explosion;
public LegacyComboCounter(ISkin skin) public LegacyCatchComboCounter(ISkin skin)
{ {
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score"; var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f; var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; 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) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);

View File

@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary> /// </summary>
public int TotalColumns => Stages.Sum(g => g.Columns); 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> /// <summary>
/// Creates a new <see cref="ManiaBeatmap"/>. /// Creates a new <see cref="ManiaBeatmap"/>.
/// </summary> /// </summary>
/// <param name="defaultStage">The initial stages.</param> /// <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); Stages.Add(defaultStage);
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
} }
public override IEnumerable<BeatmapStatistic> GetStatistics() public override IEnumerable<BeatmapStatistic> GetStatistics()

View File

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

View File

@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
public class ManiaDifficultyAttributes : DifficultyAttributes public class ManiaDifficultyAttributes : DifficultyAttributes
{ {
public double GreatHitWindow; 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. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; 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.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills; using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty 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 const double star_scaling_factor = 0.018;
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
} }
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) 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 return new ManiaDifficultyAttributes
{ {
StarRating = difficultyValue(skills) * star_scaling_factor, StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods, Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future // 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), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills 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) protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{ {
for (int i = 1; i < beatmap.HitObjects.Count; i++) var sortedObjects = beatmap.HitObjects.ToArray();
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
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; new Strain(((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();
}
protected override Mod[] DifficultyAdjustmentMods protected override Mod[] DifficultyAdjustmentMods
{ {
@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
new ManiaModKey3(), new ManiaModKey3(),
new ManiaModKey4(), new ManiaModKey4(),
new ManiaModKey5(), new ManiaModKey5(),
new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
new ManiaModKey6(), new ManiaModKey6(),
new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
new ManiaModKey7(), new ManiaModKey7(),
new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
new ManiaModKey8(), new ManiaModKey8(),
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
new ManiaModKey9(), new ManiaModKey9(),
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
}).ToArray(); }).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(ManiaModKey7),
typeof(ManiaModKey8), typeof(ManiaModKey8),
typeof(ManiaModKey9), typeof(ManiaModKey9),
typeof(ManiaModKey10),
}.Except(new[] { GetType() }).ToArray(); }.Except(new[] { GetType() }).ToArray();
} }
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore(); private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
[TestCaseSource(nameof(allBeatmaps))] [TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name) public void TestEncodeDecodeStability(string name)

View File

@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA); 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 private class ModA : Mod
{ {
public override string Name => nameof(ModA); public override string Name => nameof(ModA);
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) }; 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 private class ModIncompatibleWithA : Mod
{ {
public override string Name => $"Incompatible With {nameof(ModA)}"; public override string Name => $"Incompatible With {nameof(ModA)}";

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,2 @@
[General]
Version: 1.0

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneComboCounter : SkinnableTestScene
{
private IEnumerable<SkinnableComboCounter> comboCounters => CreatedDrawables.OfType<SkinnableComboCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() =>
{
var comboCounter = new SkinnableComboCounter();
comboCounter.Current.Value = 1;
return comboCounter;
}));
}
[Test]
public void TestComboCounterIncrementing()
{
AddRepeatStep("increase combo", () =>
{
foreach (var counter in comboCounters)
counter.Current.Value++;
}, 10);
AddStep("reset combo", () =>
{
foreach (var counter in comboCounters)
counter.Current.Value = 0;
});
}
}
}

View File

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

View File

@ -2,23 +2,29 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene public class TestSceneHUDOverlay : SkinnableTestScene
{ {
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
@ -26,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[Test]
public void TestComboCounterIncrementing()
{
createNew();
AddRepeatStep("increase combo", () =>
{
foreach (var hud in hudOverlays)
hud.ComboCounter.Current.Value++;
}, 10);
AddStep("reset combo", () =>
{
foreach (var hud in hudOverlays)
hud.ComboCounter.Current.Value = 0;
});
}
[Test] [Test]
public void TestShownByDefault() public void TestShownByDefault()
{ {
@ -45,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
AddUntilStep("wait for load", () => hudOverlay.IsAlive); AddUntilStep("wait for load", () => hudOverlay.IsAlive);
AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1); AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
} }
[Test] [Test]
@ -53,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
createNew(); createNew();
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
@ -89,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set keycounter visible false", () => AddStep("set keycounter visible false", () =>
{ {
config.Set<bool>(OsuSetting.KeyOverlay, false); config.Set<bool>(OsuSetting.KeyOverlay, false);
hudOverlay.KeyCounter.AlwaysVisible.Value = false; hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
}); });
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
@ -107,13 +131,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("create overlay", () => AddStep("create overlay", () =>
{ {
Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>()); SetContents(() =>
{
hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
action?.Invoke(hudOverlay); hudOverlay.ComboCounter.Current.Value = 1;
action?.Invoke(hudOverlay);
return hudOverlay;
});
}); });
} }
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
} }
} }

View File

@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private BarHitErrorMeter barMeter; private BarHitErrorMeter barMeter;
private BarHitErrorMeter barMeter2; private BarHitErrorMeter barMeter2;
private BarHitErrorMeter barMeter3;
private ColourHitErrorMeter colourMeter; private ColourHitErrorMeter colourMeter;
private ColourHitErrorMeter colourMeter2; private ColourHitErrorMeter colourMeter2;
private ColourHitErrorMeter colourMeter3;
private HitWindows hitWindows; private HitWindows hitWindows;
public TestSceneHitErrorMeter() public TestSceneHitErrorMeter()
@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}); });
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Rotation = 270,
});
Add(colourMeter = new ColourHitErrorMeter(hitWindows) Add(colourMeter = new ColourHitErrorMeter(hitWindows)
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 } Margin = new MarginPadding { Left = 50 }
}); });
Add(colourMeter3 = new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Rotation = 270,
Margin = new MarginPadding { Left = 50 }
});
} }
private void newJudgement(double offset = 0) private void newJudgement(double offset = 0)
@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay
barMeter.OnNewJudgement(judgement); barMeter.OnNewJudgement(judgement);
barMeter2.OnNewJudgement(judgement); barMeter2.OnNewJudgement(judgement);
barMeter3.OnNewJudgement(judgement);
colourMeter.OnNewJudgement(judgement); colourMeter.OnNewJudgement(judgement);
colourMeter2.OnNewJudgement(judgement); colourMeter2.OnNewJudgement(judgement);
colourMeter3.OnNewJudgement(judgement);
} }
} }
} }

View File

@ -1,68 +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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneScoreCounter : OsuTestScene
{
public TestSceneScoreCounter()
{
int numerator = 0, denominator = 0;
ScoreCounter score = new ScoreCounter(7)
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Margin = new MarginPadding(20),
};
Add(score);
ComboCounter comboCounter = new StandardComboCounter
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Margin = new MarginPadding(10),
};
Add(comboCounter);
PercentageCounter accuracyCounter = new PercentageCounter
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Position = new Vector2(-20, 60),
};
Add(accuracyCounter);
AddStep(@"Reset all", delegate
{
score.Current.Value = 0;
comboCounter.Current.Value = 0;
numerator = denominator = 0;
accuracyCounter.SetFraction(0, 0);
});
AddStep(@"Hit! :D", delegate
{
score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current.Value > 0 ? comboCounter.Current.Value - 1 : 0) / 25.0);
comboCounter.Increment();
numerator++;
denominator++;
accuracyCounter.SetFraction(numerator, denominator);
});
AddStep(@"miss...", delegate
{
comboCounter.Current.Value = 0;
denominator++;
accuracyCounter.SetFraction(numerator, denominator);
});
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene
{
private IEnumerable<SkinnableAccuracyCounter> accuracyCounters => CreatedDrawables.OfType<SkinnableAccuracyCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() =>
{
var accuracyCounter = new SkinnableAccuracyCounter();
accuracyCounter.Current.Value = 1;
return accuracyCounter;
}));
}
[Test]
public void TestChangingAccuracy()
{
AddStep(@"Reset all", delegate
{
foreach (var s in accuracyCounters)
s.Current.Value = 1;
});
AddStep(@"Hit! :D", delegate
{
foreach (var s in accuracyCounters)
s.Current.Value -= 0.023f;
});
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
{
private IEnumerable<SkinnableHealthDisplay> healthDisplays => CreatedDrawables.OfType<SkinnableHealthDisplay>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create health displays", () =>
{
SetContents(() => new SkinnableHealthDisplay());
});
AddStep(@"Reset all", delegate
{
foreach (var s in healthDisplays)
s.Current.Value = 1;
});
}
[Test]
public void TestHealthDisplayIncrementing()
{
AddRepeatStep(@"decrease hp", delegate
{
foreach (var healthDisplay in healthDisplays)
healthDisplay.Current.Value -= 0.08f;
}, 10);
AddRepeatStep(@"increase hp without flash", delegate
{
foreach (var healthDisplay in healthDisplays)
healthDisplay.Current.Value += 0.1f;
}, 3);
AddRepeatStep(@"increase hp with flash", delegate
{
foreach (var healthDisplay in healthDisplays)
{
healthDisplay.Current.Value += 0.1f;
healthDisplay.Flash(new JudgementResult(null, new OsuJudgement()));
}
}, 3);
}
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableScoreCounter : SkinnableTestScene
{
private IEnumerable<SkinnableScoreCounter> scoreCounters => CreatedDrawables.OfType<SkinnableScoreCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() =>
{
var comboCounter = new SkinnableScoreCounter();
comboCounter.Current.Value = 1;
return comboCounter;
}));
}
[Test]
public void TestScoreCounterIncrementing()
{
AddStep(@"Reset all", delegate
{
foreach (var s in scoreCounters)
s.Current.Value = 0;
});
AddStep(@"Hit! :D", delegate
{
foreach (var s in scoreCounters)
s.Current.Value += 300;
});
}
}
}

View File

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

View File

@ -401,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false)); AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz"); AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!")); AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal));
} }
[Test] [Test]

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -18,10 +19,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneModSettings : OsuTestScene public class TestSceneModSettings : OsuManualInputManagerTestScene
{ {
private TestModSelectOverlay modSelect; 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)); 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() private void createModSelect()
{ {
AddStep("create mod select", () => AddStep("create mod select", () =>
@ -121,9 +158,16 @@ namespace osu.Game.Tests.Visual.UserInterface
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); 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) => public void SelectMod(Mod mod) =>
ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) GetModButton(mod).SelectNext(1);
.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
public void SetModSettingsWidth(float newWidth) =>
ModSettingsContainer.Width = newWidth;
} }
public class TestRulesetInfo : RulesetInfo public class TestRulesetInfo : RulesetInfo

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -59,7 +60,7 @@ namespace osu.Game.Tests
get get
{ {
using (var reader = getZipReader()) using (var reader = getZipReader())
return reader.Filenames.First(f => f.EndsWith(".mp3")); return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal));
} }
} }
@ -73,7 +74,7 @@ namespace osu.Game.Tests
protected override Beatmap CreateBeatmap() protected override Beatmap CreateBeatmap()
{ {
using (var reader = getZipReader()) using (var reader = getZipReader())
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")))) using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal))))
using (var beatmapReader = new LineBufferedReader(beatmapStream)) using (var beatmapReader = new LineBufferedReader(beatmapStream))
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader); return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
} }

View File

@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Screens.Drawings
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
continue; continue;
if (line.ToUpperInvariant().StartsWith("GROUP")) if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal))
continue; continue;
// ReSharper disable once AccessToModifiedClosure // ReSharper disable once AccessToModifiedClosure

View File

@ -98,7 +98,7 @@ namespace osu.Game.Beatmaps
[JsonIgnore] [JsonIgnore]
public string StoredBookmarks public string StoredBookmarks
{ {
get => string.Join(",", Bookmarks); get => string.Join(',', Bookmarks);
set set
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))

View File

@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
@ -36,6 +37,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary> /// </summary>
[ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
{ {
/// <summary> /// <summary>
@ -389,7 +391,7 @@ namespace osu.Game.Beatmaps
protected override BeatmapSetInfo CreateModel(ArchiveReader reader) protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{ {
// let's make sure there are actually .osu files to import. // let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(mapName)) if (string.IsNullOrEmpty(mapName))
{ {
@ -417,7 +419,7 @@ namespace osu.Game.Beatmaps
{ {
var beatmapInfos = new List<BeatmapInfo>(); var beatmapInfos = new List<BeatmapInfo>();
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu"))) foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
{ {
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
using (var ms = new MemoryStream()) // we need a memory stream so we can seek using (var ms = new MemoryStream()) // we need a memory stream so we can seek

View File

@ -13,6 +13,7 @@ using osu.Framework.Development;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -23,6 +24,7 @@ namespace osu.Game.Beatmaps
{ {
public partial class BeatmapManager public partial class BeatmapManager
{ {
[ExcludeFromDynamicCompile]
private class BeatmapOnlineLookupQueue : IDisposable private class BeatmapOnlineLookupQueue : IDisposable
{ {
private readonly IAPIProvider api; private readonly IAPIProvider api;

View File

@ -8,6 +8,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -17,6 +18,7 @@ namespace osu.Game.Beatmaps
{ {
public partial class BeatmapManager public partial class BeatmapManager
{ {
[ExcludeFromDynamicCompile]
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{ {
private readonly IResourceStore<byte[]> store; private readonly IResourceStore<byte[]> store;

View File

@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; } public string Hash { get; set; }
public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb"))?.Filename; public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
public List<BeatmapSetFileInfo> Files { get; set; } public List<BeatmapSetFileInfo> Files { get; set; }

View File

@ -307,12 +307,7 @@ namespace osu.Game.Beatmaps.Formats
double start = getOffsetTime(Parsing.ParseDouble(split[1])); double start = getOffsetTime(Parsing.ParseDouble(split[1]));
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
var breakEvent = new BreakPeriod(start, end); beatmap.Breaks.Add(new BreakPeriod(start, end));
if (!breakEvent.HasEffect)
return;
beatmap.Breaks.Add(breakEvent);
break; break;
} }
} }

View File

@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
var pair = SplitKeyVal(line); var pair = SplitKeyVal(line);
bool isCombo = pair.Key.StartsWith(@"Combo"); bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
string[] split = pair.Value.Split(','); string[] split = pair.Value.Split(',');

View File

@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing
public double Duration => EndTime - StartTime; public double Duration => EndTime - StartTime;
/// <summary> /// <summary>
/// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap. /// Whether the break has any effect.
/// </summary> /// </summary>
public bool HasEffect => Duration >= MIN_BREAK_DURATION; public bool HasEffect => Duration >= MIN_BREAK_DURATION;

View File

@ -16,7 +16,10 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")] [Description("Hit Error (right)")]
HitErrorRight, HitErrorRight,
[Description("Hit Error (both)")] [Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Hit Error (left+right)")]
HitErrorBoth, HitErrorBoth,
[Description("Colour (left)")] [Description("Colour (left)")]
@ -25,7 +28,10 @@ namespace osu.Game.Configuration
[Description("Colour (right)")] [Description("Colour (right)")]
ColourRight, ColourRight,
[Description("Colour (both)")] [Description("Colour (left+right)")]
ColourBoth, ColourBoth,
[Description("Colour (bottom)")]
ColourBottom,
} }
} }

View File

@ -279,7 +279,7 @@ namespace osu.Game.Database
// for now, concatenate all .osu files in the set to create a unique hash. // for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream(); MemoryStream hashable = new MemoryStream();
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)).OrderBy(f => f.Filename)) foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
{ {
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath)) using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable); s.CopyTo(hashable);
@ -593,7 +593,7 @@ namespace osu.Game.Database
var fileInfos = new List<TFileModel>(); var fileInfos = new List<TFileModel>();
string prefix = reader.Filenames.GetCommonPrefix(); string prefix = reader.Filenames.GetCommonPrefix();
if (!(prefix.EndsWith("/") || prefix.EndsWith("\\"))) if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
prefix = string.Empty; prefix = string.Empty;
// import files to manager // import files to manager

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Utils; using osu.Game.Utils;
@ -28,9 +27,6 @@ namespace osu.Game.Graphics.UserInterface
Current.Value = DisplayedCount = 1.0f; Current.Value = DisplayedCount = 1.0f;
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override string FormatCount(double count) => count.FormatAccuracy(); protected override string FormatCount(double count) => count.FormatAccuracy();
protected override double GetProportionalDuration(double currentValue, double newValue) protected override double GetProportionalDuration(double currentValue, double newValue)

View File

@ -1,18 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class ScoreCounter : RollingCounter<double> public abstract class ScoreCounter : RollingCounter<double>, IScoreCounter
{ {
protected override double RollingDuration => 1000; protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out; protected override Easing RollingEasing => Easing.Out;
public bool UseCommaSeparator; /// <summary>
/// Whether comma separators should be displayed.
/// </summary>
public bool UseCommaSeparator { get; }
/// <summary> /// <summary>
/// How many leading zeroes the counter has. /// How many leading zeroes the counter has.
@ -23,14 +26,13 @@ namespace osu.Game.Graphics.UserInterface
/// Displays score. /// Displays score.
/// </summary> /// </summary>
/// <param name="leading">How many leading zeroes the counter will have.</param> /// <param name="leading">How many leading zeroes the counter will have.</param>
public ScoreCounter(uint leading = 0) /// <param name="useCommaSeparator">Whether comma separators should be displayed.</param>
protected ScoreCounter(uint leading = 0, bool useCommaSeparator = false)
{ {
UseCommaSeparator = useCommaSeparator;
LeadingZeroes = leading; LeadingZeroes = leading;
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override double GetProportionalDuration(double currentValue, double newValue) protected override double GetProportionalDuration(double currentValue, double newValue)
{ {
return currentValue > newValue ? currentValue - newValue : newValue - currentValue; return currentValue > newValue ? currentValue - newValue : newValue - currentValue;

View File

@ -196,7 +196,7 @@ namespace osu.Game.Online.Chat
if (target == null) if (target == null)
return; return;
var parameters = text.Split(new[] { ' ' }, 2); var parameters = text.Split(' ', 2);
string command = parameters[0]; string command = parameters[0];
string content = parameters.Length == 2 ? parameters[1] : string.Empty; string content = parameters.Length == 2 ? parameters[1] : string.Empty;

View File

@ -111,7 +111,7 @@ namespace osu.Game.Online.Chat
public static LinkDetails GetLinkDetails(string url) public static LinkDetails GetLinkDetails(string url)
{ {
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); var args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':'); args[0] = args[0].TrimEnd(':');
switch (args[0]) switch (args[0])

View File

@ -181,7 +181,7 @@ namespace osu.Game
if (args?.Length > 0) if (args?.Length > 0)
{ {
var paths = args.Where(a => !a.StartsWith(@"-")).ToArray(); var paths = args.Where(a => !a.StartsWith('-')).ToArray();
if (paths.Length > 0) if (paths.Length > 0)
Task.Run(() => Import(paths)); Task.Run(() => Import(paths));
} }
@ -289,7 +289,7 @@ namespace osu.Game
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{ {
if (url.StartsWith("/")) if (url.StartsWith('/'))
url = $"{API.Endpoint}{url}"; url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url); externalLinkOpener.OpenUrlExternally(url);

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -45,9 +44,7 @@ namespace osu.Game.Overlays.Mods
protected readonly FillFlowContainer<ModSection> ModSectionsContainer; protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
protected readonly FillFlowContainer<ModControlSection> ModSettingsContent; protected readonly ModSettingsContainer ModSettingsContainer;
protected readonly Container ModSettingsContainer;
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); 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, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
@ -292,29 +289,11 @@ namespace osu.Game.Overlays.Mods
Width = 0.25f, Width = 0.25f,
Alpha = 0, Alpha = 0,
X = -100, X = -100,
Children = new Drawable[] SelectedMods = { BindTarget = SelectedMods },
{
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),
}
}
}
} }
}; };
((IBindable<bool>)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -423,8 +402,6 @@ namespace osu.Game.Overlays.Mods
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList()); section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
updateMods(); updateMods();
updateModSettings(mods);
} }
private void updateMods() private void updateMods()
@ -445,25 +422,6 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(Color4.White, 200); 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) private void modButtonPressed(Mod selectedMod)
{ {
if (selectedMod != null) 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

@ -135,7 +135,6 @@ namespace osu.Game.Overlays.Profile.Header
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}"); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}");
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord);
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat"); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat");
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Lastfm, user.Lastfm, $@"https://last.fm/users/{user.Lastfm}");
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website);
// If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding // If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding
@ -149,7 +148,7 @@ namespace osu.Game.Overlays.Profile.Header
if (string.IsNullOrEmpty(content)) return false; if (string.IsNullOrEmpty(content)) return false;
// newlines could be contained in API returned user content. // newlines could be contained in API returned user content.
content = content.Replace("\n", " "); content = content.Replace('\n', ' ');
bottomLinkContainer.AddIcon(icon, text => bottomLinkContainer.AddIcon(icon, text =>
{ {

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Difficulty
if (!beatmap.HitObjects.Any()) if (!beatmap.HitObjects.Any())
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); 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; double sectionLength = SectionLength * clockRate;
@ -100,15 +100,24 @@ namespace osu.Game.Rulesets.Difficulty
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); 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> /// <summary>
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty. /// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
/// </summary> /// </summary>
public Mod[] CreateDifficultyAdjustmentModCombinations() 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) switch (currentSetCount)
{ {
case 0: case 0:
@ -128,18 +137,43 @@ namespace osu.Game.Rulesets.Difficulty
break; break;
} }
// Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod // Apply the rest of the remaining mods recursively.
// combinations in further recursions, so a moving subset is used to eliminate this effect for (int i = 0; i < remainingMods.Length; i++)
for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++)
{ {
var adjustmentMod = adjustmentSet[i]; var (nextSet, nextCount) = flatten(remainingMods.Span[i]);
if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod))))
// 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; 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; 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> /// <summary>

View File

@ -41,7 +41,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// </summary> /// </summary>
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet 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 double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List<double> strainPeaks = new List<double>(); private readonly List<double> strainPeaks = new List<double>();
@ -51,10 +55,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// </summary> /// </summary>
public void Process(DifficultyHitObject current) public void Process(DifficultyHitObject current)
{ {
currentStrain *= strainDecay(current.DeltaTime); CurrentStrain *= strainDecay(current.DeltaTime);
currentStrain += StrainValueOf(current) * SkillMultiplier; CurrentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
Previous.Push(current); Previous.Push(current);
} }
@ -71,15 +75,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// <summary> /// <summary>
/// Sets the initial strain level for a new section. /// Sets the initial strain level for a new section.
/// </summary> /// </summary>
/// <param name="offset">The beginning of the new section in milliseconds.</param> /// <param name="time">The beginning of the new section in milliseconds.</param>
public void StartNewSectionFrom(double offset) 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. // 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. // 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) 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> /// <summary>
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s. /// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
/// </summary> /// </summary>

View File

@ -107,6 +107,9 @@ namespace osu.Game.Rulesets.Mods
{ {
foreach (var breakPeriod in Breaks) foreach (var breakPeriod in Breaks)
{ {
if (!breakPeriod.HasEffect)
continue;
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue; if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION); this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);

View File

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

View File

@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
string[] ss = split[5].Split(':'); string[] ss = split[5].Split(':');
endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0])); endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0]));
readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo);
} }
result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime);

View File

@ -96,11 +96,13 @@ namespace osu.Game.Rulesets
context.SaveChanges(); context.SaveChanges();
// add any other modes // add any other modes
var existingRulesets = context.RulesetInfo.ToList();
foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{ {
// todo: StartsWith can be changed to Equals on 2020-11-08 // todo: StartsWith can be changed to Equals on 2020-11-08
// This is to give users enough time to have their database use new abbreviated info). // This is to give users enough time to have their database use new abbreviated info).
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null) if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
context.RulesetInfo.Add(r.RulesetInfo); context.RulesetInfo.Add(r.RulesetInfo);
} }

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. /// 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. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks.
/// </summary> /// </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; private readonly double gameplayStartTime;
/// <summary> /// <summary>
@ -35,7 +38,6 @@ namespace osu.Game.Rulesets.UI
public GameplayClock GameplayClock => stabilityGameplayClock; public GameplayClock GameplayClock => stabilityGameplayClock;
[Cached(typeof(GameplayClock))] [Cached(typeof(GameplayClock))]
[Cached(typeof(ISamplePlaybackDisabler))]
private readonly StabilityGameplayClock stabilityGameplayClock; private readonly StabilityGameplayClock stabilityGameplayClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue) public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
@ -102,6 +104,8 @@ namespace osu.Game.Rulesets.UI
requireMoreUpdateLoops = true; requireMoreUpdateLoops = true;
validState = !GameplayClock.IsPaused.Value; validState = !GameplayClock.IsPaused.Value;
samplePlaybackDisabled.Value = stabilityGameplayClock.ShouldDisableSamplePlayback;
int loops = 0; int loops = 0;
while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames) while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames)
@ -224,6 +228,8 @@ namespace osu.Game.Rulesets.UI
public ReplayInputHandler ReplayInputHandler { get; set; } public ReplayInputHandler ReplayInputHandler { get; set; }
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
private class StabilityGameplayClock : GameplayClock private class StabilityGameplayClock : GameplayClock
{ {
public GameplayClock ParentGameplayClock; 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. // handle the case where playback is catching up to real-time.
base.ShouldDisableSamplePlayback base.ShouldDisableSamplePlayback
|| ParentSampleDisabler?.SamplePlaybackDisabled.Value == true || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true

View File

@ -57,7 +57,7 @@ namespace osu.Game.Scoring
if (archive == null) if (archive == null)
return null; return null;
using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr")))) using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase))))
{ {
try try
{ {

View File

@ -79,9 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementNewCombo() private void updatePlacementNewCombo()
{ {
if (currentPlacement == null) return; if (currentPlacement?.HitObject is IHasComboInformation c)
if (currentPlacement.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True; c.NewCombo = NewCombo.Value == TernaryState.True;
} }

View File

@ -597,10 +597,20 @@ namespace osu.Game.Screens.Edit
{ {
double amount = e.ShiftPressed ? 4 : 1; double amount = e.ShiftPressed ? 4 : 1;
bool trackPlaying = clock.IsRunning;
if (trackPlaying)
{
// generally users are not looking to perform tiny seeks when the track is playing,
// so seeks should always be by one full beat, bypassing the beatDivisor.
// this multiplication undoes the division that will be applied in the underlying seek operation.
amount *= beatDivisor.Value;
}
if (direction < 1) if (direction < 1)
clock.SeekBackward(!clock.IsRunning, amount); clock.SeekBackward(!trackPlaying, amount);
else else
clock.SeekForward(!clock.IsRunning, amount); clock.SeekForward(!trackPlaying, amount);
} }
private void exportBeatmap() private void exportBeatmap()

View File

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

View File

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

View File

@ -1,200 +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.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Play.HUD
{
public abstract class ComboCounter : Container
{
public BindableInt Current = new BindableInt
{
MinValue = 0,
};
public bool IsRolling { get; protected set; }
protected SpriteText PopOutCount;
protected virtual double PopOutDuration => 150;
protected virtual float PopOutScale => 2.0f;
protected virtual Easing PopOutEasing => Easing.None;
protected virtual float PopOutInitialAlpha => 0.75f;
protected virtual double FadeOutDuration => 100;
/// <summary>
/// Duration in milliseconds for the counter roll-up animation for each element.
/// </summary>
protected virtual double RollingDuration => 20;
/// <summary>
/// Easing for the counter rollover animation.
/// </summary>
protected Easing RollingEasing => Easing.None;
protected SpriteText DisplayedCountSpriteText;
private int previousValue;
/// <summary>
/// Base of all combo counters.
/// </summary>
protected ComboCounter()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
DisplayedCountSpriteText = new OsuSpriteText
{
Alpha = 0,
},
PopOutCount = new OsuSpriteText
{
Alpha = 0,
Margin = new MarginPadding(0.05f),
}
};
TextSize = 80;
Current.ValueChanged += combo => updateCount(combo.NewValue == 0);
}
protected override void LoadComplete()
{
base.LoadComplete();
DisplayedCountSpriteText.Text = FormatCount(Current.Value);
DisplayedCountSpriteText.Anchor = Anchor;
DisplayedCountSpriteText.Origin = Origin;
StopRolling();
}
private int displayedCount;
/// <summary>
/// Value shown at the current moment.
/// </summary>
public virtual int DisplayedCount
{
get => displayedCount;
protected set
{
if (displayedCount.Equals(value))
return;
updateDisplayedCount(displayedCount, value, IsRolling);
}
}
private float textSize;
public float TextSize
{
get => textSize;
set
{
textSize = value;
DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(size: TextSize);
PopOutCount.Font = PopOutCount.Font.With(size: TextSize);
}
}
/// <summary>
/// Increments the combo by an amount.
/// </summary>
/// <param name="amount"></param>
public void Increment(int amount = 1)
{
Current.Value += amount;
}
/// <summary>
/// Stops rollover animation, forcing the displayed count to be the actual count.
/// </summary>
public void StopRolling()
{
updateCount(false);
}
protected virtual string FormatCount(int count)
{
return count.ToString();
}
protected virtual void OnCountRolling(int currentValue, int newValue)
{
transformRoll(currentValue, newValue);
}
protected virtual void OnCountIncrement(int currentValue, int newValue)
{
DisplayedCount = newValue;
}
protected virtual void OnCountChange(int currentValue, int newValue)
{
DisplayedCount = newValue;
}
private double getProportionalDuration(int currentValue, int newValue)
{
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
return difference * RollingDuration;
}
private void updateDisplayedCount(int currentValue, int newValue, bool rolling)
{
displayedCount = newValue;
if (rolling)
OnDisplayedCountRolling(currentValue, newValue);
else if (currentValue + 1 == newValue)
OnDisplayedCountIncrement(newValue);
else
OnDisplayedCountChange(newValue);
}
private void updateCount(bool rolling)
{
int prev = previousValue;
previousValue = Current.Value;
if (!IsLoaded)
return;
if (!rolling)
{
FinishTransforms(false, nameof(DisplayedCount));
IsRolling = false;
DisplayedCount = prev;
if (prev + 1 == Current.Value)
OnCountIncrement(prev, Current.Value);
else
OnCountChange(prev, Current.Value);
}
else
{
OnCountRolling(displayedCount, Current.Value);
IsRolling = true;
}
}
private void transformRoll(int currentValue, int newValue)
{
this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), RollingEasing);
}
protected abstract void OnDisplayedCountRolling(int currentValue, int newValue);
protected abstract void OnDisplayedCountIncrement(int newValue);
protected abstract void OnDisplayedCountChange(int newValue);
}
}

View File

@ -0,0 +1,43 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter
{
private readonly Vector2 offset = new Vector2(-20, 5);
public DefaultAccuracyCounter()
{
Origin = Anchor.TopRight;
Anchor = Anchor.TopRight;
}
[Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
}
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Anchor = Anchor.TopLeft;
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset;
}
}
}
}

View File

@ -4,18 +4,23 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Screens.Play.HUD
{ {
/// <summary> public class DefaultComboCounter : RollingCounter<int>, IComboCounter
/// Used as an accuracy counter. Represented visually as a percentage.
/// </summary>
public class SimpleComboCounter : RollingCounter<int>
{ {
private readonly Vector2 offset = new Vector2(20, 5);
protected override double RollingDuration => 750; protected override double RollingDuration => 750;
public SimpleComboCounter() [Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }
public DefaultComboCounter()
{ {
Current.Value = DisplayedCount = 0; Current.Value = DisplayedCount = 0;
} }
@ -23,6 +28,17 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter; private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset;
}
}
protected override string FormatCount(int count) protected override string FormatCount(int count)
{ {
return $@"{count}x"; return $@"{count}x";

View File

@ -16,7 +16,7 @@ using osu.Framework.Utils;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class StandardHealthDisplay : HealthDisplay, IHasAccentColour public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour
{ {
/// <summary> /// <summary>
/// The base opacity of the glow. /// The base opacity of the glow.
@ -71,8 +71,12 @@ namespace osu.Game.Screens.Play.HUD
} }
} }
public StandardHealthDisplay() public DefaultHealthDisplay()
{ {
Size = new Vector2(1, 5);
RelativeSizeAxes = Axes.X;
Margin = new MarginPadding { Top = 20 };
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -103,13 +107,7 @@ namespace osu.Game.Screens.Play.HUD
GlowColour = colours.BlueDarker; GlowColour = colours.BlueDarker;
} }
public void Flash(JudgementResult result) public override void Flash(JudgementResult result) => Scheduler.AddOnce(flash);
{
if (!result.IsHit)
return;
Scheduler.AddOnce(flash);
}
private void flash() private void flash()
{ {

View File

@ -0,0 +1,35 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultScoreCounter : ScoreCounter
{
public DefaultScoreCounter()
: base(6)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
}
[Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
// todo: check if default once health display is skinnable
hud?.ShowHealthbar.BindValueChanged(healthBar =>
{
this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING);
}, true);
}
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -12,14 +13,18 @@ namespace osu.Game.Screens.Play.HUD
/// A container for components displaying the current player health. /// A container for components displaying the current player health.
/// Gets bound automatically to the <see cref="HealthProcessor"/> when inserted to <see cref="DrawableRuleset.Overlays"/> hierarchy. /// Gets bound automatically to the <see cref="HealthProcessor"/> when inserted to <see cref="DrawableRuleset.Overlays"/> hierarchy.
/// </summary> /// </summary>
public abstract class HealthDisplay : Container public abstract class HealthDisplay : Container, IHealthDisplay
{ {
public readonly BindableDouble Current = new BindableDouble(1) public Bindable<double> Current { get; } = new BindableDouble(1)
{ {
MinValue = 0, MinValue = 0,
MaxValue = 1 MaxValue = 1
}; };
public virtual void Flash(JudgementResult result)
{
}
/// <summary> /// <summary>
/// Bind the tracked fields of <see cref="HealthProcessor"/> to this health display. /// Bind the tracked fields of <see cref="HealthProcessor"/> to this health display.
/// </summary> /// </summary>

View File

@ -66,54 +66,69 @@ namespace osu.Game.Screens.Play.HUD
switch (type.NewValue) switch (type.NewValue)
{ {
case ScoreMeterType.HitErrorBoth: case ScoreMeterType.HitErrorBoth:
createBar(false); createBar(Anchor.CentreLeft);
createBar(true); createBar(Anchor.CentreRight);
break; break;
case ScoreMeterType.HitErrorLeft: case ScoreMeterType.HitErrorLeft:
createBar(false); createBar(Anchor.CentreLeft);
break; break;
case ScoreMeterType.HitErrorRight: case ScoreMeterType.HitErrorRight:
createBar(true); createBar(Anchor.CentreRight);
break;
case ScoreMeterType.HitErrorBottom:
createBar(Anchor.BottomCentre);
break; break;
case ScoreMeterType.ColourBoth: case ScoreMeterType.ColourBoth:
createColour(false); createColour(Anchor.CentreLeft);
createColour(true); createColour(Anchor.CentreRight);
break; break;
case ScoreMeterType.ColourLeft: case ScoreMeterType.ColourLeft:
createColour(false); createColour(Anchor.CentreLeft);
break; break;
case ScoreMeterType.ColourRight: case ScoreMeterType.ColourRight:
createColour(true); createColour(Anchor.CentreRight);
break;
case ScoreMeterType.ColourBottom:
createColour(Anchor.BottomCentre);
break; break;
} }
} }
private void createBar(bool rightAligned) private void createBar(Anchor anchor)
{ {
bool rightAligned = (anchor & Anchor.x2) > 0;
bool bottomAligned = (anchor & Anchor.y2) > 0;
var display = new BarHitErrorMeter(hitWindows, rightAligned) var display = new BarHitErrorMeter(hitWindows, rightAligned)
{ {
Margin = new MarginPadding(margin), Margin = new MarginPadding(margin),
Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, Anchor = anchor,
Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, Origin = bottomAligned ? Anchor.CentreLeft : anchor,
Alpha = 0, Alpha = 0,
Rotation = bottomAligned ? 270 : 0
}; };
completeDisplayLoading(display); completeDisplayLoading(display);
} }
private void createColour(bool rightAligned) private void createColour(Anchor anchor)
{ {
bool bottomAligned = (anchor & Anchor.y2) > 0;
var display = new ColourHitErrorMeter(hitWindows) var display = new ColourHitErrorMeter(hitWindows)
{ {
Margin = new MarginPadding(margin), Margin = new MarginPadding(margin),
Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, Anchor = anchor,
Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, Origin = bottomAligned ? Anchor.CentreLeft : anchor,
Alpha = 0, Alpha = 0,
Rotation = bottomAligned ? 270 : 0
}; };
completeDisplayLoading(display); completeDisplayLoading(display);

View File

@ -99,7 +99,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Size = new Vector2(10), Size = new Vector2(10),
Icon = FontAwesome.Solid.ShippingFast, Icon = FontAwesome.Solid.ShippingFast,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.Centre,
// undo any layout rotation to display the icon the correct orientation
Rotation = -Rotation,
}, },
new SpriteIcon new SpriteIcon
{ {
@ -107,7 +109,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Size = new Vector2(10), Size = new Vector2(10),
Icon = FontAwesome.Solid.Bicycle, Icon = FontAwesome.Solid.Bicycle,
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.Centre,
// undo any layout rotation to display the icon the correct orientation
Rotation = -Rotation,
} }
} }
}, },

View File

@ -0,0 +1,19 @@
// 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.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a accuracy counter.
/// </summary>
public interface IAccuracyCounter : IDrawable
{
/// <summary>
/// The current accuracy to be displayed.
/// </summary>
Bindable<double> Current { get; }
}
}

View File

@ -0,0 +1,19 @@
// 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.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a combo counter.
/// </summary>
public interface IComboCounter : IDrawable
{
/// <summary>
/// The current combo to be displayed.
/// </summary>
Bindable<int> Current { get; }
}
}

View File

@ -0,0 +1,26 @@
// 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.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a health display.
/// </summary>
public interface IHealthDisplay : IDrawable
{
/// <summary>
/// The current health to be displayed.
/// </summary>
Bindable<double> Current { get; }
/// <summary>
/// Flash the display for a specified result type.
/// </summary>
/// <param name="result">The result type.</param>
void Flash(JudgementResult result);
}
}

View File

@ -0,0 +1,19 @@
// 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.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a score counter.
/// </summary>
public interface IScoreCounter : IDrawable
{
/// <summary>
/// The current score to be displayed.
/// </summary>
Bindable<double> Current { get; }
}
}

View File

@ -0,0 +1,252 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary>
public class LegacyComboCounter : CompositeDrawable, IComboCounter
{
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, };
private uint scheduledPopOutCurrentId;
private const double pop_out_duration = 150;
private const Easing pop_out_easing = Easing.None;
private const double fade_out_duration = 100;
/// <summary>
/// Duration in milliseconds for the counter roll-up animation for each element.
/// </summary>
private const double rolling_duration = 20;
private Drawable popOutCount;
private Drawable displayedCountSpriteText;
private int previousValue;
private int displayedCount;
private bool isRolling;
[Resolved]
private ISkinSource skin { get; set; }
public LegacyComboCounter()
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
Margin = new MarginPadding(10);
Scale = new Vector2(1.2f);
}
/// <summary>
/// Value shown at the current moment.
/// </summary>
public virtual int DisplayedCount
{
get => displayedCount;
private set
{
if (displayedCount.Equals(value))
return;
if (isRolling)
onDisplayedCountRolling(displayedCount, value);
else if (displayedCount + 1 == value)
onDisplayedCountIncrement(value);
else
onDisplayedCountChange(value);
displayedCount = value;
}
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new[]
{
displayedCountSpriteText = createSpriteText().With(s =>
{
s.Alpha = 0;
}),
popOutCount = createSpriteText().With(s =>
{
s.Alpha = 0;
s.Margin = new MarginPadding(0.05f);
})
};
Current.ValueChanged += combo => updateCount(combo.NewValue == 0);
}
protected override void LoadComplete()
{
base.LoadComplete();
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
displayedCountSpriteText.Anchor = Anchor;
displayedCountSpriteText.Origin = Origin;
popOutCount.Origin = Origin;
popOutCount.Anchor = Anchor;
updateCount(false);
}
private void updateCount(bool rolling)
{
int prev = previousValue;
previousValue = Current.Value;
if (!IsLoaded)
return;
if (!rolling)
{
FinishTransforms(false, nameof(DisplayedCount));
isRolling = false;
DisplayedCount = prev;
if (prev + 1 == Current.Value)
onCountIncrement(prev, Current.Value);
else
onCountChange(prev, Current.Value);
}
else
{
onCountRolling(displayedCount, Current.Value);
isRolling = true;
}
}
private void transformPopOut(int newValue)
{
((IHasText)popOutCount).Text = formatCount(newValue);
popOutCount.ScaleTo(1.6f);
popOutCount.FadeTo(0.75f);
popOutCount.MoveTo(Vector2.Zero);
popOutCount.ScaleTo(1, pop_out_duration, pop_out_easing);
popOutCount.FadeOut(pop_out_duration, pop_out_easing);
popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration, pop_out_easing);
}
private void transformNoPopOut(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
displayedCountSpriteText.ScaleTo(1);
}
private void transformPopOutSmall(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
displayedCountSpriteText.ScaleTo(1.1f);
displayedCountSpriteText.ScaleTo(1, pop_out_duration, pop_out_easing);
}
private void scheduledPopOutSmall(uint id)
{
// Too late; scheduled task invalidated
if (id != scheduledPopOutCurrentId)
return;
DisplayedCount++;
}
private void onCountIncrement(int currentValue, int newValue)
{
scheduledPopOutCurrentId++;
if (DisplayedCount < currentValue)
DisplayedCount++;
displayedCountSpriteText.Show();
transformPopOut(newValue);
uint newTaskId = scheduledPopOutCurrentId;
Scheduler.AddDelayed(delegate
{
scheduledPopOutSmall(newTaskId);
}, pop_out_duration);
}
private void onCountRolling(int currentValue, int newValue)
{
scheduledPopOutCurrentId++;
// Hides displayed count if was increasing from 0 to 1 but didn't finish
if (currentValue == 0 && newValue == 0)
displayedCountSpriteText.FadeOut(fade_out_duration);
transformRoll(currentValue, newValue);
}
private void onCountChange(int currentValue, int newValue)
{
scheduledPopOutCurrentId++;
if (newValue == 0)
displayedCountSpriteText.FadeOut();
DisplayedCount = newValue;
}
private void onDisplayedCountRolling(int currentValue, int newValue)
{
if (newValue == 0)
displayedCountSpriteText.FadeOut(fade_out_duration);
else
displayedCountSpriteText.Show();
transformNoPopOut(newValue);
}
private void onDisplayedCountChange(int newValue)
{
displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1);
transformNoPopOut(newValue);
}
private void onDisplayedCountIncrement(int newValue)
{
displayedCountSpriteText.Show();
transformPopOutSmall(newValue);
}
private void transformRoll(int currentValue, int newValue) =>
this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), Easing.None);
private string formatCount(int count) => $@"{count}x";
private double getProportionalDuration(int currentValue, int newValue)
{
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
return difference * rolling_duration;
}
private OsuSpriteText createSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboText));
}
}

View File

@ -48,22 +48,29 @@ namespace osu.Game.Screens.Play.HUD
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Children = new Drawable[] Child = new FillFlowContainer
{ {
iconsContainer = new ReverseChildIDFillFlowContainer<ModIcon> Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
Anchor = Anchor.TopCentre, iconsContainer = new ReverseChildIDFillFlowContainer<ModIcon>
Origin = Anchor.TopCentre, {
AutoSizeAxes = Axes.Both, Anchor = Anchor.TopCentre,
Direction = FillDirection.Horizontal, Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
},
unrankedText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"/ UNRANKED /",
Font = OsuFont.Numeric.With(size: 12)
}
}, },
unrankedText = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Text = @"/ UNRANKED /",
Font = OsuFont.Numeric.With(size: 12)
}
}; };
Current.ValueChanged += mods => Current.ValueChanged += mods =>

View File

@ -20,14 +20,13 @@ namespace osu.Game.Screens.Play.HUD
public readonly VisualSettings VisualSettings; public readonly VisualSettings VisualSettings;
//public readonly CollectionSettings CollectionSettings;
//public readonly DiscussionSettings DiscussionSettings;
public PlayerSettingsOverlay() public PlayerSettingsOverlay()
{ {
AlwaysPresent = true; AlwaysPresent = true;
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
AutoSizeAxes = Axes.Both;
Child = new FillFlowContainer<PlayerSettingsGroup> Child = new FillFlowContainer<PlayerSettingsGroup>
{ {
@ -36,7 +35,6 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20), Spacing = new Vector2(0, 20),
Margin = new MarginPadding { Top = 100, Right = 10 },
Children = new PlayerSettingsGroup[] Children = new PlayerSettingsGroup[]
{ {
//CollectionSettings = new CollectionSettings(), //CollectionSettings = new CollectionSettings(),

View File

@ -0,0 +1,29 @@
// 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.Framework.Bindables;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public class SkinnableAccuracyCounter : SkinnableDrawable, IAccuracyCounter
{
public Bindable<double> Current { get; } = new Bindable<double>();
public SkinnableAccuracyCounter()
: base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter())
{
CentreComponent = false;
}
private IAccuracyCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IAccuracyCounter;
skinnedCounter?.Current.BindTo(Current);
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Framework.Bindables;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public class SkinnableComboCounter : SkinnableDrawable, IComboCounter
{
public Bindable<int> Current { get; } = new Bindable<int>();
public SkinnableComboCounter()
: base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter())
{
CentreComponent = false;
}
private IComboCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IComboCounter;
skinnedCounter?.Current.BindTo(Current);
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Framework.Bindables;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public class SkinnableScoreCounter : SkinnableDrawable, IScoreCounter
{
public Bindable<double> Current { get; } = new Bindable<double>();
public SkinnableScoreCounter()
: base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter())
{
CentreComponent = false;
}
private IScoreCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IScoreCounter;
skinnedCounter?.Current.BindTo(Current);
}
}
}

Some files were not shown because too many files have changed in this diff Show More