1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 06:47:24 +08:00

Merge branch 'master' into beatmap-carousel-less-diffcalc-stutter

This commit is contained in:
Dan Balasescu 2020-10-20 14:26:16 +09:00 committed by GitHub
commit 13060b8575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 3188 additions and 1176 deletions

View File

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<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>
</component>
</project>

View File

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

View File

@ -0,0 +1,34 @@
// 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 Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game;
namespace osu.Android
{
public class GameplayScreenRotationLocker : Component
{
private Bindable<bool> localUserPlaying;
[Resolved]
private OsuGameActivity gameActivity { get; set; }
[BackgroundDependencyLoader]
private void load(OsuGame game)
{
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<bool> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
});
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState)
{

View File

@ -4,6 +4,7 @@
using System;
using Android.App;
using Android.OS;
using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
@ -11,6 +12,15 @@ namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
[Cached]
private readonly OsuGameActivity gameActivity;
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
}
public override Version AssemblyVersion
{
get
@ -55,6 +65,12 @@ namespace osu.Android
}
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
}

View File

@ -21,6 +21,7 @@
<AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" />
<Compile Include="OsuGameAndroid.cs" />
</ItemGroup>
@ -53,4 +54,4 @@
<AndroidResource Include="Resources\drawable\lazer.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>
</Project>

View File

@ -125,12 +125,14 @@ namespace osu.Desktop
{
base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
switch (host.Window)
{
// Legacy osuTK DesktopGameWindow
case DesktopGameWindow desktopGameWindow:
desktopGameWindow.CursorState |= CursorState.Hidden;
desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
desktopGameWindow.SetIconFromStream(iconStream);
desktopGameWindow.Title = Name;
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
break;
@ -138,6 +140,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
break;

View File

@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
Origin = Anchor.Centre,
Scale = new Vector2(4f),
}, skin);
});
AddStep("get trails container", () =>
{
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
catcherArea.MovableCatcher.SetHyperDashState(2);
});

View File

@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
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)
: base(source)
{
@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
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))
return null;
@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
this.GetAnimation("fruit-ryuuta", true, true, true);
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 (this.HasFont(comboFont))
return new LegacyComboCounter(Source);
if (providesComboCounter)
return new LegacyCatchComboCounter(Source);
break;
}

View File

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

View File

@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
}
};
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
trails = new CatcherTrailDisplay(this);
updateCatcher();
}
protected override void LoadComplete()
{
base.LoadComplete();
// don't add in above load as we may potentially modify a parent in an unsafe manner.
trailsTarget.Add(trails);
}
/// <summary>
/// Creates proxied content to be displayed beneath hitobjects.
/// </summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Individual : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.125;
private readonly double[] holdEndTimes;
private readonly int column;
public Individual(int column, int columnCount)
{
this.column = column;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
try
{
if (maniaCurrent.BaseObject.Column != column)
return 0;
// We give a slight bonus if something is held meanwhile
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
}
finally
{
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
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))]
public void TestEncodeDecodeStability(string name)

View File

@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
var osuStorage = storage as MigratableStorage;
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
string originalDirectory = storage.GetFullPath(".");
@ -137,13 +138,15 @@ namespace osu.Game.Tests.NonVisual
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES)
Assert.That(osuStorage, Is.Not.Null);
foreach (var file in osuStorage.IgnoreFiles)
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
foreach (var dir in osuStorage.IgnoreDirectories)
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False);

View File

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

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

@ -53,5 +53,263 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
}
/// <summary>
/// Test to see that all <see cref="HitResult"/>s contribute to score portions in correct amounts.
/// </summary>
/// <param name="scoringMode">Scoring mode to test.</param>
/// <param name="hitResult">The <see cref="HitResult"/> that will be applied to selected hit objects.</param>
/// <param name="maxResult">The maximum <see cref="HitResult"/> achievable.</param>
/// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
/// <remarks>
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
/// <para>
/// For standardised scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// 1_000_000 * (((3 * <paramref name="hitResult"/>) / (4 * <paramref name="maxResult"/>)) * 30% + (bestCombo / maxCombo) * 70%)
/// </para>
/// <para>
/// For classic scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// <paramref name="hitResult"/> / <paramref name="maxResult"/> * 936
/// where 936 is simplified from:
/// 75% * 4 * 300 * (1 + 1/25)
/// </para>
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points)
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4))
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? minResult : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
}
/// <remarks>
/// This test uses a beatmap with four small ticks and one object with the <see cref="Judgement.MaxResult"/> of <see cref="HitResult.Ok"/>.
/// Its goal is to ensure that with the <see cref="ScoringMode"/> of <see cref="ScoringMode.Standardised"/>,
/// small ticks contribute to the accuracy portion, but not the combo portion.
/// In contrast, <see cref="ScoringMode.Classic"/> does not have separate combo and accuracy portion (they are multiplied by each other).
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable<HitObject> hitObjects = Enumerable
.Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
.Append(new TestHitObject(HitResult.Ok));
IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = hitObjects.ToList()
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? HitResult.SmallTickMiss : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
{
Type = HitResult.Ok
};
scoreProcessor.ApplyResult(lastJudgementResult);
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
}
[Test]
public void TestEmptyBeatmap(
[Values(ScoringMode.Standardised, ScoringMode.Classic)]
ScoringMode scoringMode)
{
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value));
}
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
[TestCase(HitResult.Meh, HitResult.Miss)]
[TestCase(HitResult.Ok, HitResult.Miss)]
[TestCase(HitResult.Good, HitResult.Miss)]
[TestCase(HitResult.Great, HitResult.Miss)]
[TestCase(HitResult.Perfect, HitResult.Miss)]
[TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
{
Assert.AreEqual(expectedMinResult, new TestJudgement(hitResult).MinResult);
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, false)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, false)]
[TestCase(HitResult.LargeBonus, false)]
public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.AffectsCombo());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, true)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, false)]
[TestCase(HitResult.LargeBonus, false)]
public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.AffectsAccuracy());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, false)]
[TestCase(HitResult.Meh, false)]
[TestCase(HitResult.Ok, false)]
[TestCase(HitResult.Good, false)]
[TestCase(HitResult.Great, false)]
[TestCase(HitResult.Perfect, false)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, false)]
[TestCase(HitResult.LargeTickMiss, false)]
[TestCase(HitResult.LargeTickHit, false)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsBonus(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsBonus());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, true)]
[TestCase(HitResult.Miss, false)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, false)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsHit(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsHit());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, true)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsScorable(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
public TestJudgement(HitResult maxResult)
{
MaxResult = maxResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
public override Judgement CreateJudgement()
{
return new TestJudgement(maxResult);
}
public TestHitObject(HitResult maxResult)
{
this.maxResult = maxResult;
}
}
}
}

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", () =>
{
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock;
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First();
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
});

View File

@ -2,23 +2,29 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
public class TestSceneHUDOverlay : SkinnableTestScene
{
private HUDOverlay hudOverlay;
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
@ -26,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
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]
public void TestShownByDefault()
{
@ -45,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
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]
@ -53,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
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);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
@ -89,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set keycounter visible 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);
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);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
@ -107,13 +131,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{
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.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
// Add any key just to display the key counter visually.
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 barMeter2;
private BarHitErrorMeter barMeter3;
private ColourHitErrorMeter colourMeter;
private ColourHitErrorMeter colourMeter2;
private ColourHitErrorMeter colourMeter3;
private HitWindows hitWindows;
public TestSceneHitErrorMeter()
@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft,
});
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Rotation = 270,
});
Add(colourMeter = new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.CentreRight,
@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft,
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)
@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay
barMeter.OnNewJudgement(judgement);
barMeter2.OnNewJudgement(judgement);
barMeter3.OnNewJudgement(judgement);
colourMeter.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,54 @@
// 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.Extensions.IEnumerableExtensions;
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;
});
}
[Test]
public void TestVeryLargeScore()
{
AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_000_000_000));
}
}
}

View File

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

View File

@ -401,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
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]

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using osu.Framework.Audio;
@ -59,7 +60,7 @@ namespace osu.Game.Tests
get
{
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()
{
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))
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
}

View File

@ -0,0 +1,169 @@
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Tournament.Configuration;
using osu.Game.Tests;
namespace osu.Game.Tournament.Tests.NonVisual
{
[TestFixture]
public class CustomTourneyDirectoryTest
{
[Test]
public void TestDefaultDirectory()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestCustomDirectory()
{
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
{
string osuDesktopStorage = basePath(nameof(TestCustomDirectory));
const string custom_tournament = "custom";
// need access before the game has constructed its own storage yet.
Storage storage = new DesktopStorage(osuDesktopStorage, host);
// manual cleaning so we can prepare a config file.
storage.DeleteDirectory(string.Empty);
using (var storageConfig = new TournamentStorageManager(storage))
storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament);
try
{
var osu = loadOsu(host);
storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", custom_tournament)));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigration()
{
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration.
{
string osuRoot = basePath(nameof(TestMigration));
string configFile = Path.Combine(osuRoot, "tournament.ini");
if (File.Exists(configFile))
File.Delete(configFile);
// Recreate the old setup that uses "tournament" as the base path.
string oldPath = Path.Combine(osuRoot, "tournament");
string videosPath = Path.Combine(oldPath, "videos");
string modsPath = Path.Combine(oldPath, "mods");
string flagsPath = Path.Combine(oldPath, "flags");
Directory.CreateDirectory(videosPath);
Directory.CreateDirectory(modsPath);
Directory.CreateDirectory(flagsPath);
// Define testing files corresponding to the specific file migrations that are needed
string bracketFile = Path.Combine(osuRoot, "bracket.json");
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
// Define sample files to test recursive copying
string videoFile = Path.Combine(videosPath, "video.mp4");
string modFile = Path.Combine(modsPath, "mod.png");
string flagFile = Path.Combine(flagsPath, "flag.png");
File.WriteAllText(bracketFile, "{}");
File.WriteAllText(drawingsConfig, "test");
File.WriteAllText(drawingsFile, "test");
File.WriteAllText(drawingsResult, "test");
File.WriteAllText(videoFile, "test");
File.WriteAllText(modFile, "test");
File.WriteAllText(flagFile, "test");
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
videosPath = Path.Combine(migratedPath, "videos");
modsPath = Path.Combine(migratedPath, "mods");
flagsPath = Path.Combine(migratedPath, "flags");
videoFile = Path.Combine(videosPath, "video.mp4");
modFile = Path.Combine(modsPath, "mod.png");
flagFile = Path.Combine(flagsPath, "flag.png");
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
Assert.True(storage.Exists("bracket.json"));
Assert.True(storage.Exists("drawings.txt"));
Assert.True(storage.Exists("drawings_results.txt"));
Assert.True(storage.Exists("drawings.ini"));
Assert.True(storage.Exists(videoFile));
Assert.True(storage.Exists(modFile));
Assert.True(storage.Exists(flagFile));
}
finally
{
host.Storage.Delete("tournament.ini");
host.Storage.DeleteDirectory("tournaments");
host.Exit();
}
}
}
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance);
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video;
using osu.Framework.Timing;
using osu.Game.Graphics;
using osu.Game.Tournament.IO;
namespace osu.Game.Tournament.Components
{
@ -17,7 +18,6 @@ namespace osu.Game.Tournament.Components
private readonly string filename;
private readonly bool drawFallbackGradient;
private Video video;
private ManualClock manualClock;
public TourneyVideo(string filename, bool drawFallbackGradient = false)
@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components
}
[BackgroundDependencyLoader]
private void load(TournamentStorage storage)
private void load(TournamentVideoResourceStore storage)
{
var stream = storage.GetStream($@"videos/{filename}");
var stream = storage.GetStream(filename);
if (stream != null)
{

View File

@ -0,0 +1,23 @@
// 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.Configuration;
using osu.Framework.Platform;
namespace osu.Game.Tournament.Configuration
{
public class TournamentStorageManager : IniConfigManager<StorageConfig>
{
protected override string Filename => "tournament.ini";
public TournamentStorageManager(Storage storage)
: base(storage)
{
}
}
public enum StorageConfig
{
CurrentTournament,
}
}

View File

@ -0,0 +1,72 @@
// 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.Logging;
using osu.Framework.Platform;
using osu.Game.IO;
using System.IO;
using osu.Game.Tournament.Configuration;
namespace osu.Game.Tournament.IO
{
public class TournamentStorage : MigratableStorage
{
private const string default_tournament = "default";
private readonly Storage storage;
private readonly TournamentStorageManager storageConfig;
public TournamentStorage(Storage storage)
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
{
this.storage = storage;
storageConfig = new TournamentStorageManager(storage);
if (storage.Exists("tournament.ini"))
{
ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
}
else
Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament));
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
}
public override void Migrate(Storage newStorage)
{
// this migration only happens once on moving to the per-tournament storage system.
// listed files are those known at that point in time.
// this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
var source = new DirectoryInfo(storage.GetFullPath("tournament"));
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
if (source.Exists)
{
Logger.Log("Migrating tournament assets to default tournament storage.");
CopyRecursive(source, destination);
DeleteRecursive(source);
}
moveFileIfExists("bracket.json", destination);
moveFileIfExists("drawings.txt", destination);
moveFileIfExists("drawings_results.txt", destination);
moveFileIfExists("drawings.ini", destination);
ChangeTargetStorage(newStorage);
storageConfig.Set(StorageConfig.CurrentTournament, default_tournament);
storageConfig.Save();
}
private void moveFileIfExists(string file, DirectoryInfo destination)
{
if (!storage.Exists(file))
return;
Logger.Log($"Migrating {file} to default tournament storage.");
var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
fileInfo.Delete();
}
}
}

View File

@ -4,12 +4,12 @@
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
namespace osu.Game.Tournament
namespace osu.Game.Tournament.IO
{
internal class TournamentStorage : NamespacedResourceStore<byte[]>
public class TournamentVideoResourceStore : NamespacedResourceStore<byte[]>
{
public TournamentStorage(Storage storage)
: base(new StorageBackedResourceStore(storage), "tournament")
public TournamentVideoResourceStore(Storage storage)
: base(new StorageBackedResourceStore(storage), "videos")
{
AddExtension("m4v");
AddExtension("avi");

View File

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

View File

@ -8,11 +8,12 @@ using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.IO;
using osu.Game.Tournament.Models;
using osu.Game.Users;
using osuTK.Input;
@ -23,13 +24,8 @@ namespace osu.Game.Tournament
public class TournamentGameBase : OsuGameBase
{
private const string bracket_filename = "bracket.json";
private LadderInfo ladder;
private Storage storage;
private TournamentStorage tournamentStorage;
private TournamentStorage storage;
private DependencyContainer dependencies;
private FileBasedIPC ipc;
@ -39,15 +35,14 @@ namespace osu.Game.Tournament
}
[BackgroundDependencyLoader]
private void load(Storage storage)
private void load(Storage baseStorage)
{
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage));
dependencies.CacheAs<Storage>(storage = new TournamentStorage(baseStorage));
dependencies.Cache(new TournamentVideoResourceStore(storage));
Textures.AddStore(new TextureLoaderStore(tournamentStorage));
this.storage = storage;
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
readBracket();

View File

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

View File

@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO;
@ -36,6 +37,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
[ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
{
/// <summary>
@ -389,7 +391,7 @@ namespace osu.Game.Beatmaps
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{
// 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))
{
@ -417,7 +419,7 @@ namespace osu.Game.Beatmaps
{
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 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.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@ -23,6 +24,7 @@ namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
[ExcludeFromDynamicCompile]
private class BeatmapOnlineLookupQueue : IDisposable
{
private readonly IAPIProvider api;

View File

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

View File

@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
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; }

View File

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

View File

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

View File

@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing
public double Duration => EndTime - StartTime;
/// <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>
public bool HasEffect => Duration >= MIN_BREAK_DURATION;

View File

@ -16,7 +16,10 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")]
HitErrorRight,
[Description("Hit Error (both)")]
[Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Hit Error (left+right)")]
HitErrorBoth,
[Description("Colour (left)")]
@ -25,7 +28,10 @@ namespace osu.Game.Configuration
[Description("Colour (right)")]
ColourRight,
[Description("Colour (both)")]
[Description("Colour (left+right)")]
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.
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))
s.CopyTo(hashable);
@ -593,7 +593,7 @@ namespace osu.Game.Database
var fileInfos = new List<TFileModel>();
string prefix = reader.Filenames.GetCommonPrefix();
if (!(prefix.EndsWith("/") || prefix.EndsWith("\\")))
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
prefix = string.Empty;
// import files to manager

View File

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

View File

@ -56,8 +56,7 @@ namespace osu.Game.Graphics.UserInterface
return;
displayedCount = value;
if (displayedCountSpriteText != null)
displayedCountSpriteText.Text = FormatCount(value);
UpdateDisplay();
}
}
@ -73,10 +72,17 @@ namespace osu.Game.Graphics.UserInterface
private void load()
{
displayedCountSpriteText = CreateSpriteText();
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
UpdateDisplay();
Child = displayedCountSpriteText;
}
protected void UpdateDisplay()
{
if (displayedCountSpriteText != null)
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
}
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -1,35 +1,37 @@
// 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.Game.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Graphics.UserInterface
{
public class ScoreCounter : RollingCounter<double>
public abstract class ScoreCounter : RollingCounter<double>, IScoreCounter
{
protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out;
public bool UseCommaSeparator;
/// <summary>
/// How many leading zeroes the counter has.
/// Whether comma separators should be displayed.
/// </summary>
public uint LeadingZeroes { get; }
public bool UseCommaSeparator { get; }
public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>();
/// <summary>
/// Displays score.
/// </summary>
/// <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(int leading = 0, bool useCommaSeparator = false)
{
LeadingZeroes = leading;
}
UseCommaSeparator = useCommaSeparator;
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter;
RequiredDisplayDigits.Value = leading;
RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay());
}
protected override double GetProportionalDuration(double currentValue, double newValue)
{
@ -38,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface
protected override string FormatCount(double count)
{
string format = new string('0', (int)LeadingZeroes);
string format = new string('0', RequiredDisplayDigits.Value);
if (UseCommaSeparator)
{

View File

@ -73,8 +73,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// top right works better when the vertical height of the component changes smoothly (avoids weird layout animations).
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Component = CreateComponent().With(d =>

View File

@ -0,0 +1,132 @@
// 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.IO;
using System.Linq;
using System.Threading;
using osu.Framework.Platform;
namespace osu.Game.IO
{
/// <summary>
/// A <see cref="WrappedStorage"/> that is migratable to different locations.
/// </summary>
public abstract class MigratableStorage : WrappedStorage
{
/// <summary>
/// A relative list of directory paths which should not be migrated.
/// </summary>
public virtual string[] IgnoreDirectories => Array.Empty<string>();
/// <summary>
/// A relative list of file paths which should not be migrated.
/// </summary>
public virtual string[] IgnoreFiles => Array.Empty<string>();
protected MigratableStorage(Storage storage, string subPath = null)
: base(storage, subPath)
{
}
/// <summary>
/// A general purpose migration method to move the storage to a different location.
/// <param name="newStorage">The target storage of the migration.</param>
/// </summary>
public virtual void Migrate(Storage newStorage)
{
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
// using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
if (sourceUri == destinationUri)
throw new ArgumentException("Destination provided is already the current location", destination.FullName);
if (sourceUri.IsBaseOf(destinationUri))
throw new ArgumentException("Destination provided is inside the source", destination.FullName);
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
throw new ArgumentException("Destination provided already has files or directories present", destination.FullName);
}
CopyRecursive(source, destination);
ChangeTargetStorage(newStorage);
DeleteRecursive(source);
}
protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
{
foreach (System.IO.FileInfo fi in target.GetFiles())
{
if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
continue;
AttemptOperation(() => fi.Delete());
}
foreach (DirectoryInfo dir in target.GetDirectories())
{
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
continue;
AttemptOperation(() => dir.Delete(true));
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
AttemptOperation(target.Delete);
}
protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
{
// based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
if (!destination.Exists)
Directory.CreateDirectory(destination.FullName);
foreach (System.IO.FileInfo fi in source.GetFiles())
{
if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
continue;
AttemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
}
foreach (DirectoryInfo dir in source.GetDirectories())
{
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
continue;
CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
}
}
/// <summary>
/// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="attempts">The number of attempts (250ms wait between each).</param>
protected static void AttemptOperation(Action action, int attempts = 10)
{
while (true)
{
try
{
action();
return;
}
catch (Exception)
{
if (attempts-- == 0)
throw;
}
Thread.Sleep(250);
}
}
}
}

View File

@ -1,11 +1,8 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -13,7 +10,7 @@ using osu.Game.Configuration;
namespace osu.Game.IO
{
public class OsuStorage : WrappedStorage
public class OsuStorage : MigratableStorage
{
/// <summary>
/// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
@ -36,9 +33,9 @@ namespace osu.Game.IO
private readonly StorageConfigManager storageConfig;
private readonly Storage defaultStorage;
public static readonly string[] IGNORE_DIRECTORIES = { "cache" };
public override string[] IgnoreDirectories => new[] { "cache" };
public static readonly string[] IGNORE_FILES =
public override string[] IgnoreFiles => new[]
{
"framework.ini",
"storage.ini"
@ -103,106 +100,11 @@ namespace osu.Game.IO
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
}
public void Migrate(string newLocation)
public override void Migrate(Storage newStorage)
{
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newLocation);
// using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
if (sourceUri == destinationUri)
throw new ArgumentException("Destination provided is already the current location", nameof(newLocation));
if (sourceUri.IsBaseOf(destinationUri))
throw new ArgumentException("Destination provided is inside the source", nameof(newLocation));
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation));
deleteRecursive(destination);
}
copyRecursive(source, destination);
ChangeTargetStorage(host.GetStorage(newLocation));
storageConfig.Set(StorageConfig.FullPath, newLocation);
base.Migrate(newStorage);
storageConfig.Set(StorageConfig.FullPath, newStorage.GetFullPath("."));
storageConfig.Save();
deleteRecursive(source);
}
private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
{
foreach (System.IO.FileInfo fi in target.GetFiles())
{
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
attemptOperation(() => fi.Delete());
}
foreach (DirectoryInfo dir in target.GetDirectories())
{
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
attemptOperation(() => dir.Delete(true));
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
attemptOperation(target.Delete);
}
private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
{
// based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
Directory.CreateDirectory(destination.FullName);
foreach (System.IO.FileInfo fi in source.GetFiles())
{
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
}
foreach (DirectoryInfo dir in source.GetDirectories())
{
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
}
}
/// <summary>
/// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="attempts">The number of attempts (250ms wait between each).</param>
private static void attemptOperation(Action action, int attempts = 10)
{
while (true)
{
try
{
action();
return;
}
catch (Exception)
{
if (attempts-- == 0)
throw;
}
Thread.Sleep(250);
}
}
}

View File

@ -196,7 +196,7 @@ namespace osu.Game.Online.Chat
if (target == null)
return;
var parameters = text.Split(new[] { ' ' }, 2);
var parameters = text.Split(' ', 2);
string command = parameters[0];
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)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])

View File

@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
@ -150,9 +151,9 @@ namespace osu.Game.Online.Leaderboards
switch (placeholderState = value)
{
case PlaceholderState.NetworkFailure:
replacePlaceholder(new RetrievalFailurePlaceholder
replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
{
OnRetry = UpdateScores,
Action = UpdateScores,
});
break;

View File

@ -1,65 +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;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Placeholders;
using osuTK;
namespace osu.Game.Online.Leaderboards
{
public class RetrievalFailurePlaceholder : Placeholder
{
public Action OnRetry;
public RetrievalFailurePlaceholder()
{
AddArbitraryDrawable(new RetryButton
{
Action = () => OnRetry?.Invoke(),
Padding = new MarginPadding { Right = 10 }
});
AddText(@"Couldn't retrieve scores!");
}
public class RetryButton : OsuHoverContainer
{
private readonly SpriteIcon icon;
public new Action Action;
public RetryButton()
{
AutoSizeAxes = Axes.Both;
Child = new OsuClickableContainer
{
AutoSizeAxes = Axes.Both,
Action = () => Action?.Invoke(),
Child = icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Sync,
Size = new Vector2(TEXT_SIZE),
Shadow = true,
},
};
}
protected override bool OnMouseDown(MouseDownEvent e)
{
icon.ScaleTo(0.8f, 4000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
icon.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
}
}
}

View File

@ -103,6 +103,20 @@ namespace osu.Game.Online.Multiplayer
[JsonIgnore]
public readonly Bindable<int> Position = new Bindable<int>(-1);
/// <summary>
/// Create a copy of this room without online information.
/// Should be used to create a local copy of a room for submitting in the future.
/// </summary>
public Room CreateCopy()
{
var copy = new Room();
copy.CopyFrom(this);
copy.RoomID.Value = null;
return copy;
}
public void CopyFrom(Room other)
{
RoomID.Value = other.RoomID.Value;

View File

@ -0,0 +1,38 @@
// 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.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Online.Placeholders
{
public class ClickablePlaceholder : Placeholder
{
public Action Action;
public ClickablePlaceholder(string actionMessage, IconUsage icon)
{
OsuTextFlowContainer textFlow;
AddArbitraryDrawable(new OsuAnimatedButton
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Child = textFlow = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE))
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Margin = new Framework.Graphics.MarginPadding(5)
},
Action = () => Action?.Invoke()
});
textFlow.AddIcon(icon, i =>
{
i.Padding = new Framework.Graphics.MarginPadding { Right = 10 };
});
textFlow.AddText(actionMessage);
}
}
}

View File

@ -2,45 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
namespace osu.Game.Online.Placeholders
{
public sealed class LoginPlaceholder : Placeholder
public sealed class LoginPlaceholder : ClickablePlaceholder
{
[Resolved(CanBeNull = true)]
private LoginOverlay login { get; set; }
public LoginPlaceholder(string actionMessage)
: base(actionMessage, FontAwesome.Solid.UserLock)
{
AddIcon(FontAwesome.Solid.UserLock, cp =>
{
cp.Font = cp.Font.With(size: TEXT_SIZE);
cp.Padding = new MarginPadding { Right = 10 };
});
AddText(actionMessage);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
this.ScaleTo(0.8f, 4000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
this.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
protected override bool OnClick(ClickEvent e)
{
login?.Show();
return base.OnClick(e);
Action = () => login?.Show();
}
}
}

View File

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

View File

@ -371,8 +371,10 @@ namespace osu.Game
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
RulesetStore?.Dispose();
BeatmapManager?.Dispose();
LocalConfig?.Dispose();
contextFactory.FlushConnections();
}
@ -406,7 +408,7 @@ namespace osu.Game
public void Migrate(string path)
{
contextFactory.FlushConnections();
(Storage as OsuStorage)?.Migrate(path);
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
}
}

View File

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

View File

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

View File

@ -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.Discord, user.Discord);
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);
// 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;
// newlines could be contained in API returned user content.
content = content.Replace("\n", " ");
content = content.Replace('\n', ' ');
bottomLinkContainer.AddIcon(icon, text =>
{

View File

@ -76,7 +76,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsEnumDropdown<ScoringMode>
{
LabelText = "Score display mode",
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode),
Keywords = new[] { "scoring" }
}
};

View File

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

View File

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

View File

@ -107,6 +107,9 @@ namespace osu.Game.Rulesets.Mods
{
foreach (var breakPeriod in Breaks)
{
if (!breakPeriod.HasEffect)
continue;
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
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
{
public class MultiMod : Mod
public sealed class MultiMod : Mod
{
public override string Name => string.Empty;
public override string Acronym => string.Empty;
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods
Mods = mods;
}
public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
}
}

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