Merge branch 'master' into beatmap-carousel-refactor-less-alloc-overhead
2
.idea/.idea.osu.Desktop/.idea/modules.xml
generated
@ -2,7 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1013.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1019.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
34
osu.Android/GameplayScreenRotationLocker.cs
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)]
|
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
|
||||||
public class OsuGameActivity : AndroidGameActivity
|
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)
|
protected override void OnCreate(Bundle savedInstanceState)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
|
|
||||||
@ -11,6 +12,15 @@ namespace osu.Android
|
|||||||
{
|
{
|
||||||
public class OsuGameAndroid : OsuGame
|
public class OsuGameAndroid : OsuGame
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly OsuGameActivity gameActivity;
|
||||||
|
|
||||||
|
public OsuGameAndroid(OsuGameActivity activity)
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
gameActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
public override Version AssemblyVersion
|
public override Version AssemblyVersion
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -55,6 +65,12 @@ namespace osu.Android
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
|
||||||
|
}
|
||||||
|
|
||||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
<AndroidLinkTool>r8</AndroidLinkTool>
|
<AndroidLinkTool>r8</AndroidLinkTool>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="GameplayScreenRotationLocker.cs" />
|
||||||
<Compile Include="OsuGameActivity.cs" />
|
<Compile Include="OsuGameActivity.cs" />
|
||||||
<Compile Include="OsuGameAndroid.cs" />
|
<Compile Include="OsuGameAndroid.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -125,12 +125,14 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
base.SetHost(host);
|
base.SetHost(host);
|
||||||
|
|
||||||
|
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||||
|
|
||||||
switch (host.Window)
|
switch (host.Window)
|
||||||
{
|
{
|
||||||
// Legacy osuTK DesktopGameWindow
|
// Legacy osuTK DesktopGameWindow
|
||||||
case DesktopGameWindow desktopGameWindow:
|
case DesktopGameWindow desktopGameWindow:
|
||||||
desktopGameWindow.CursorState |= CursorState.Hidden;
|
desktopGameWindow.CursorState |= CursorState.Hidden;
|
||||||
desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
|
desktopGameWindow.SetIconFromStream(iconStream);
|
||||||
desktopGameWindow.Title = Name;
|
desktopGameWindow.Title = Name;
|
||||||
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
|
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
|
||||||
break;
|
break;
|
||||||
@ -138,6 +140,7 @@ namespace osu.Desktop
|
|||||||
// SDL2 DesktopWindow
|
// SDL2 DesktopWindow
|
||||||
case DesktopWindow desktopWindow:
|
case DesktopWindow desktopWindow:
|
||||||
desktopWindow.CursorState.Value |= CursorState.Hidden;
|
desktopWindow.CursorState.Value |= CursorState.Hidden;
|
||||||
|
desktopWindow.SetIconFromStream(iconStream);
|
||||||
desktopWindow.Title = Name;
|
desktopWindow.Title = Name;
|
||||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||||
break;
|
break;
|
||||||
|
@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(4f),
|
Scale = new Vector2(4f),
|
||||||
}, skin);
|
}, skin);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("get trails container", () =>
|
||||||
|
{
|
||||||
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
|
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
|
||||||
catcherArea.MovableCatcher.SetHyperDashState(2);
|
catcherArea.MovableCatcher.SetHyperDashState(2);
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
{
|
{
|
||||||
public class CatchLegacySkinTransformer : LegacySkinTransformer
|
public class CatchLegacySkinTransformer : LegacySkinTransformer
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
||||||
|
/// </summary>
|
||||||
|
private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score");
|
||||||
|
|
||||||
public CatchLegacySkinTransformer(ISkinSource source)
|
public CatchLegacySkinTransformer(ISkinSource source)
|
||||||
: base(source)
|
: base(source)
|
||||||
{
|
{
|
||||||
@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
|
|
||||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||||
{
|
{
|
||||||
|
if (component is HUDSkinComponent hudComponent)
|
||||||
|
{
|
||||||
|
switch (hudComponent.Component)
|
||||||
|
{
|
||||||
|
case HUDSkinComponents.ComboCounter:
|
||||||
|
// catch may provide its own combo counter; hide the default.
|
||||||
|
return providesComboCounter ? Drawable.Empty() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!(component is CatchSkinComponent catchSkinComponent))
|
if (!(component is CatchSkinComponent catchSkinComponent))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
this.GetAnimation("fruit-ryuuta", true, true, true);
|
this.GetAnimation("fruit-ryuuta", true, true, true);
|
||||||
|
|
||||||
case CatchSkinComponents.CatchComboCounter:
|
case CatchSkinComponents.CatchComboCounter:
|
||||||
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
|
||||||
|
|
||||||
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
if (providesComboCounter)
|
||||||
if (this.HasFont(comboFont))
|
return new LegacyCatchComboCounter(Source);
|
||||||
return new LegacyComboCounter(Source);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
|
public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
|
||||||
{
|
{
|
||||||
private readonly LegacyRollingCounter counter;
|
private readonly LegacyRollingCounter counter;
|
||||||
|
|
||||||
private readonly LegacyRollingCounter explosion;
|
private readonly LegacyRollingCounter explosion;
|
||||||
|
|
||||||
public LegacyComboCounter(ISkin skin)
|
public LegacyCatchComboCounter(ISkin skin)
|
||||||
{
|
{
|
||||||
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
||||||
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
|
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
|
@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
|
trails = new CatcherTrailDisplay(this);
|
||||||
|
|
||||||
updateCatcher();
|
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>
|
/// <summary>
|
||||||
/// Creates proxied content to be displayed beneath hitobjects.
|
/// Creates proxied content to be displayed beneath hitobjects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||||
|
|
||||||
[TestCase(2.3683365342338796d, "diffcalc-test")]
|
[TestCase(2.3449735700206298d, "diffcalc-test")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
|
@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalColumns => Stages.Sum(g => g.Columns);
|
public int TotalColumns => Stages.Sum(g => g.Columns);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int OriginalTotalColumns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ManiaBeatmap"/>.
|
/// Creates a new <see cref="ManiaBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="defaultStage">The initial stages.</param>
|
/// <param name="defaultStage">The initial stages.</param>
|
||||||
public ManiaBeatmap(StageDefinition defaultStage)
|
/// <param name="originalTotalColumns">The total number of columns present before any user adjustments. Defaults to the total columns in <paramref name="defaultStage"/>.</param>
|
||||||
|
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
|
||||||
{
|
{
|
||||||
Stages.Add(defaultStage);
|
Stages.Add(defaultStage);
|
||||||
|
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<BeatmapStatistic> GetStatistics()
|
public override IEnumerable<BeatmapStatistic> GetStatistics()
|
||||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
public bool Dual;
|
public bool Dual;
|
||||||
public readonly bool IsForCurrentRuleset;
|
public readonly bool IsForCurrentRuleset;
|
||||||
|
|
||||||
|
private readonly int originalTargetColumns;
|
||||||
|
|
||||||
// Internal for testing purposes
|
// Internal for testing purposes
|
||||||
internal FastRandom Random { get; private set; }
|
internal FastRandom Random { get; private set; }
|
||||||
|
|
||||||
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
else
|
else
|
||||||
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
originalTargetColumns = TargetColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||||
@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
|
|
||||||
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
||||||
{
|
{
|
||||||
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
|
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
|
||||||
|
|
||||||
if (Dual)
|
if (Dual)
|
||||||
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
|
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
|
||||||
|
@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||||
{
|
{
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
|
public double ScoreMultiplier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
|||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Skills;
|
using osu.Game.Rulesets.Mania.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.MathUtils;
|
||||||
using osu.Game.Rulesets.Mania.Mods;
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Scoring;
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
private const double star_scaling_factor = 0.018;
|
private const double star_scaling_factor = 0.018;
|
||||||
|
|
||||||
private readonly bool isForCurrentRuleset;
|
private readonly bool isForCurrentRuleset;
|
||||||
|
private readonly double originalOverallDifficulty;
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||||
|
originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
return new ManiaDifficultyAttributes
|
return new ManiaDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = difficultyValue(skills) * star_scaling_factor,
|
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
|
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||||
|
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
|
||||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||||
Skills = skills
|
Skills = skills
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private double difficultyValue(Skill[] skills)
|
|
||||||
{
|
|
||||||
// Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
|
|
||||||
var overall = skills.OfType<Overall>().Single();
|
|
||||||
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
|
|
||||||
|
|
||||||
foreach (var individual in skills.OfType<Individual>())
|
|
||||||
{
|
|
||||||
for (int i = 0; i < individual.StrainPeaks.Count; i++)
|
|
||||||
{
|
|
||||||
double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
|
|
||||||
|
|
||||||
if (aggregate > aggregatePeaks[i])
|
|
||||||
aggregatePeaks[i] = aggregate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
|
||||||
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
|
|
||||||
// Difficulty is the weighted sum of the highest strains from every section.
|
|
||||||
foreach (double strain in aggregatePeaks)
|
|
||||||
{
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||||
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
|
|
||||||
|
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||||
|
|
||||||
|
for (int i = 1; i < sortedObjects.Length; i++)
|
||||||
|
yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||||
|
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||||
{
|
{
|
||||||
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
|
new Strain(((ManiaBeatmap)beatmap).TotalColumns)
|
||||||
|
};
|
||||||
var skills = new List<Skill> { new Overall(columnCount) };
|
|
||||||
|
|
||||||
for (int i = 0; i < columnCount; i++)
|
|
||||||
skills.Add(new Individual(i, columnCount));
|
|
||||||
|
|
||||||
return skills.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods
|
protected override Mod[] DifficultyAdjustmentMods
|
||||||
{
|
{
|
||||||
@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
new ManiaModKey3(),
|
new ManiaModKey3(),
|
||||||
new ManiaModKey4(),
|
new ManiaModKey4(),
|
||||||
new ManiaModKey5(),
|
new ManiaModKey5(),
|
||||||
|
new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
|
||||||
new ManiaModKey6(),
|
new ManiaModKey6(),
|
||||||
|
new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
|
||||||
new ManiaModKey7(),
|
new ManiaModKey7(),
|
||||||
|
new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
|
||||||
new ManiaModKey8(),
|
new ManiaModKey8(),
|
||||||
|
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
|
||||||
new ManiaModKey9(),
|
new ManiaModKey9(),
|
||||||
|
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getHitWindow300(Mod[] mods)
|
||||||
|
{
|
||||||
|
if (isForCurrentRuleset)
|
||||||
|
{
|
||||||
|
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
|
||||||
|
return applyModAdjustments(34 + 3 * od, mods);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Round(originalOverallDifficulty) > 4)
|
||||||
|
return applyModAdjustments(34, mods);
|
||||||
|
|
||||||
|
return applyModAdjustments(47, mods);
|
||||||
|
|
||||||
|
static int applyModAdjustments(double value, Mod[] mods)
|
||||||
|
{
|
||||||
|
if (mods.Any(m => m is ManiaModHardRock))
|
||||||
|
value /= 1.4;
|
||||||
|
else if (mods.Any(m => m is ManiaModEasy))
|
||||||
|
value *= 1.4;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is ManiaModDoubleTime))
|
||||||
|
value *= 1.5;
|
||||||
|
else if (mods.Any(m => m is ManiaModHalfTime))
|
||||||
|
value *= 0.75;
|
||||||
|
|
||||||
|
return (int)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
|
||||||
|
{
|
||||||
|
double scoreMultiplier = 1;
|
||||||
|
|
||||||
|
foreach (var m in mods)
|
||||||
|
{
|
||||||
|
switch (m)
|
||||||
|
{
|
||||||
|
case ManiaModNoFail _:
|
||||||
|
case ManiaModEasy _:
|
||||||
|
case ManiaModHalfTime _:
|
||||||
|
scoreMultiplier *= 0.5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||||
|
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
|
||||||
|
|
||||||
|
if (diff > 0)
|
||||||
|
scoreMultiplier *= 0.9;
|
||||||
|
else if (diff < 0)
|
||||||
|
scoreMultiplier *= 0.9 + 0.04 * diff;
|
||||||
|
|
||||||
|
return scoreMultiplier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Individual : Skill
|
|
||||||
{
|
|
||||||
protected override double SkillMultiplier => 1;
|
|
||||||
protected override double StrainDecayBase => 0.125;
|
|
||||||
|
|
||||||
private readonly double[] holdEndTimes;
|
|
||||||
|
|
||||||
private readonly int column;
|
|
||||||
|
|
||||||
public Individual(int column, int columnCount)
|
|
||||||
{
|
|
||||||
this.column = column;
|
|
||||||
|
|
||||||
holdEndTimes = new double[columnCount];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
|
||||||
var endTime = maniaCurrent.BaseObject.GetEndTime();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (maniaCurrent.BaseObject.Column != column)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// We give a slight bonus if something is held meanwhile
|
|
||||||
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Overall : Skill
|
|
||||||
{
|
|
||||||
protected override double SkillMultiplier => 1;
|
|
||||||
protected override double StrainDecayBase => 0.3;
|
|
||||||
|
|
||||||
private readonly double[] holdEndTimes;
|
|
||||||
|
|
||||||
private readonly int columnCount;
|
|
||||||
|
|
||||||
public Overall(int columnCount)
|
|
||||||
{
|
|
||||||
this.columnCount = columnCount;
|
|
||||||
|
|
||||||
holdEndTimes = new double[columnCount];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
|
||||||
var endTime = maniaCurrent.BaseObject.GetEndTime();
|
|
||||||
|
|
||||||
double holdFactor = 1.0; // Factor in case something else is held
|
|
||||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
|
||||||
|
|
||||||
for (int i = 0; i < columnCount; i++)
|
|
||||||
{
|
|
||||||
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
|
|
||||||
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
|
|
||||||
holdAddition = 1.0;
|
|
||||||
|
|
||||||
// ... this addition only is valid if there is _no_ other note with the same ending.
|
|
||||||
// Releasing multiple notes at the same time is just as easy as releasing one
|
|
||||||
if (endTime == holdEndTimes[i])
|
|
||||||
holdAddition = 0;
|
|
||||||
|
|
||||||
// We give a slight bonus if something is held meanwhile
|
|
||||||
if (holdEndTimes[i] > endTime)
|
|
||||||
holdFactor = 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
|
|
||||||
|
|
||||||
return (1 + holdAddition) * holdFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
80
osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Strain : Skill
|
||||||
|
{
|
||||||
|
private const double individual_decay_base = 0.125;
|
||||||
|
private const double overall_decay_base = 0.30;
|
||||||
|
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 1;
|
||||||
|
|
||||||
|
private readonly double[] holdEndTimes;
|
||||||
|
private readonly double[] individualStrains;
|
||||||
|
|
||||||
|
private double individualStrain;
|
||||||
|
private double overallStrain;
|
||||||
|
|
||||||
|
public Strain(int totalColumns)
|
||||||
|
{
|
||||||
|
holdEndTimes = new double[totalColumns];
|
||||||
|
individualStrains = new double[totalColumns];
|
||||||
|
overallStrain = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
|
var endTime = maniaCurrent.BaseObject.GetEndTime();
|
||||||
|
var column = maniaCurrent.BaseObject.Column;
|
||||||
|
|
||||||
|
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||||
|
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||||
|
|
||||||
|
// Fill up the holdEndTimes array
|
||||||
|
for (int i = 0; i < holdEndTimes.Length; ++i)
|
||||||
|
{
|
||||||
|
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
|
||||||
|
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
|
||||||
|
holdAddition = 1.0;
|
||||||
|
|
||||||
|
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
|
||||||
|
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
|
||||||
|
holdAddition = 0;
|
||||||
|
|
||||||
|
// We give a slight bonus to everything if something is held meanwhile
|
||||||
|
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
|
||||||
|
holdFactor = 1.25;
|
||||||
|
|
||||||
|
// Decay individual strains
|
||||||
|
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
holdEndTimes[column] = endTime;
|
||||||
|
|
||||||
|
// Increase individual strain in own column
|
||||||
|
individualStrains[column] += 2.0 * holdFactor;
|
||||||
|
individualStrain = individualStrains[column];
|
||||||
|
|
||||||
|
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
|
||||||
|
|
||||||
|
return individualStrain + overallStrain - CurrentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double GetPeakStrain(double offset)
|
||||||
|
=> applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
|
||||||
|
+ applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
|
||||||
|
|
||||||
|
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||||
|
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||||
|
}
|
||||||
|
}
|
165
osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.MathUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to .NET4.0 unstable sorting methods.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
|
||||||
|
/// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class LegacySortHelper<T>
|
||||||
|
{
|
||||||
|
private const int quick_sort_depth_threshold = 32;
|
||||||
|
|
||||||
|
public static void Sort(T[] keys, IComparer<T> comparer)
|
||||||
|
{
|
||||||
|
if (keys == null)
|
||||||
|
throw new ArgumentNullException(nameof(keys));
|
||||||
|
|
||||||
|
if (keys.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
comparer ??= Comparer<T>.Default;
|
||||||
|
depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer<T> comparer, int depthLimit)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (depthLimit == 0)
|
||||||
|
{
|
||||||
|
heapsort(keys, left, right, comparer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = left;
|
||||||
|
int j = right;
|
||||||
|
|
||||||
|
// pre-sort the low, middle (pivot), and high values in place.
|
||||||
|
// this improves performance in the face of already sorted data, or
|
||||||
|
// data that is made up of multiple sorted runs appended together.
|
||||||
|
int middle = i + ((j - i) >> 1);
|
||||||
|
swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
|
||||||
|
swapIfGreater(keys, comparer, i, j); // swap the low with the high
|
||||||
|
swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
|
||||||
|
|
||||||
|
T x = keys[middle];
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
while (comparer.Compare(keys[i], x) < 0) i++;
|
||||||
|
while (comparer.Compare(x, keys[j]) < 0) j--;
|
||||||
|
Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
|
||||||
|
if (i > j) break;
|
||||||
|
|
||||||
|
if (i < j)
|
||||||
|
{
|
||||||
|
T key = keys[i];
|
||||||
|
keys[i] = keys[j];
|
||||||
|
keys[j] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
} while (i <= j);
|
||||||
|
|
||||||
|
// The next iteration of the while loop is to "recursively" sort the larger half of the array and the
|
||||||
|
// following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
|
||||||
|
// both sorts see the new value.
|
||||||
|
depthLimit--;
|
||||||
|
|
||||||
|
if (j - left <= right - i)
|
||||||
|
{
|
||||||
|
if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
|
||||||
|
left = i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
|
||||||
|
right = j;
|
||||||
|
}
|
||||||
|
} while (left < right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void heapsort(T[] keys, int lo, int hi, IComparer<T> comparer)
|
||||||
|
{
|
||||||
|
Contract.Requires(keys != null);
|
||||||
|
Contract.Requires(comparer != null);
|
||||||
|
Contract.Requires(lo >= 0);
|
||||||
|
Contract.Requires(hi > lo);
|
||||||
|
Contract.Requires(hi < keys.Length);
|
||||||
|
|
||||||
|
int n = hi - lo + 1;
|
||||||
|
|
||||||
|
for (int i = n / 2; i >= 1; i = i - 1)
|
||||||
|
{
|
||||||
|
downHeap(keys, i, n, lo, comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = n; i > 1; i = i - 1)
|
||||||
|
{
|
||||||
|
swap(keys, lo, lo + i - 1);
|
||||||
|
downHeap(keys, 1, i - 1, lo, comparer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void downHeap(T[] keys, int i, int n, int lo, IComparer<T> comparer)
|
||||||
|
{
|
||||||
|
Contract.Requires(keys != null);
|
||||||
|
Contract.Requires(comparer != null);
|
||||||
|
Contract.Requires(lo >= 0);
|
||||||
|
Contract.Requires(lo < keys.Length);
|
||||||
|
|
||||||
|
T d = keys[lo + i - 1];
|
||||||
|
|
||||||
|
while (i <= n / 2)
|
||||||
|
{
|
||||||
|
var child = 2 * i;
|
||||||
|
|
||||||
|
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
|
||||||
|
{
|
||||||
|
child++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
|
||||||
|
break;
|
||||||
|
|
||||||
|
keys[lo + i - 1] = keys[lo + child - 1];
|
||||||
|
i = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys[lo + i - 1] = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swap(T[] a, int i, int j)
|
||||||
|
{
|
||||||
|
if (i != j)
|
||||||
|
{
|
||||||
|
T t = a[i];
|
||||||
|
a[i] = a[j];
|
||||||
|
a[j] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
|
||||||
|
{
|
||||||
|
if (a != b)
|
||||||
|
{
|
||||||
|
if (comparer.Compare(keys[a], keys[b]) > 0)
|
||||||
|
{
|
||||||
|
T key = keys[a];
|
||||||
|
keys[a] = keys[b];
|
||||||
|
keys[b] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
typeof(ManiaModKey7),
|
typeof(ManiaModKey7),
|
||||||
typeof(ManiaModKey8),
|
typeof(ManiaModKey8),
|
||||||
typeof(ManiaModKey9),
|
typeof(ManiaModKey9),
|
||||||
|
typeof(ManiaModKey10),
|
||||||
}.Except(new[] { GetType() }).ToArray();
|
}.Except(new[] { GetType() }).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
|
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
|
||||||
|
|
||||||
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu"));
|
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
|
||||||
|
|
||||||
[TestCaseSource(nameof(allBeatmaps))]
|
[TestCaseSource(nameof(allBeatmaps))]
|
||||||
public void TestEncodeDecodeStability(string name)
|
public void TestEncodeDecodeStability(string name)
|
||||||
|
@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
var osu = LoadOsuIntoHost(host);
|
var osu = LoadOsuIntoHost(host);
|
||||||
var storage = osu.Dependencies.Get<Storage>();
|
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.
|
// 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(".");
|
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(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
|
||||||
Assert.That(storage.ExistsDirectory(Path.Combine("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(File.Exists(Path.Combine(originalDirectory, file)));
|
||||||
Assert.That(storage.Exists(file), Is.False);
|
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(Directory.Exists(Path.Combine(originalDirectory, dir)));
|
||||||
Assert.That(storage.ExistsDirectory(dir), Is.False);
|
Assert.That(storage.ExistsDirectory(dir), Is.False);
|
||||||
|
@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiModFlattening()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(4, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
Assert.IsTrue(combinations[3] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleThroughMultiMod()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(3, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleWithSameInstanceViaMultiMod()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(3, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
||||||
|
}
|
||||||
|
|
||||||
private class ModA : Mod
|
private class ModA : Mod
|
||||||
{
|
{
|
||||||
public override string Name => nameof(ModA);
|
public override string Name => nameof(ModA);
|
||||||
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ModC : Mod
|
||||||
|
{
|
||||||
|
public override string Name => nameof(ModC);
|
||||||
|
public override string Acronym => nameof(ModC);
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
}
|
||||||
|
|
||||||
private class ModIncompatibleWithA : Mod
|
private class ModIncompatibleWithA : Mod
|
||||||
{
|
{
|
||||||
public override string Name => $"Incompatible With {nameof(ModA)}";
|
public override string Name => $"Incompatible With {nameof(ModA)}";
|
||||||
|
BIN
osu.Game.Tests/Resources/old-skin/score-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-3.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-4.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-5.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-6.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-7.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-8.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-9.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-comma.png
Normal file
After Width: | Height: | Size: 865 B |
BIN
osu.Game.Tests/Resources/old-skin/score-dot.png
Normal file
After Width: | Height: | Size: 771 B |
BIN
osu.Game.Tests/Resources/old-skin/score-percent.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-x.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-bg.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png
Normal file
After Width: | Height: | Size: 465 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png
Normal file
After Width: | Height: | Size: 475 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png
Normal file
After Width: | Height: | Size: 466 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png
Normal file
After Width: | Height: | Size: 464 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-ki.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
2
osu.Game.Tests/Resources/old-skin/skin.ini
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[General]
|
||||||
|
Version: 1.0
|
@ -53,5 +53,263 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
|
|
||||||
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddStep("get variables", () =>
|
AddStep("get variables", () =>
|
||||||
{
|
{
|
||||||
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock;
|
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First();
|
||||||
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
||||||
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
|
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
|
||||||
});
|
});
|
||||||
|
@ -2,23 +2,29 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
|
public class TestSceneHUDOverlay : SkinnableTestScene
|
||||||
{
|
{
|
||||||
private HUDOverlay hudOverlay;
|
private HUDOverlay hudOverlay;
|
||||||
|
|
||||||
|
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
||||||
|
|
||||||
// best way to check without exposing.
|
// best way to check without exposing.
|
||||||
private Drawable hideTarget => hudOverlay.KeyCounter;
|
private Drawable hideTarget => hudOverlay.KeyCounter;
|
||||||
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
|
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
|
||||||
@ -26,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; }
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestComboCounterIncrementing()
|
||||||
|
{
|
||||||
|
createNew();
|
||||||
|
|
||||||
|
AddRepeatStep("increase combo", () =>
|
||||||
|
{
|
||||||
|
foreach (var hud in hudOverlays)
|
||||||
|
hud.ComboCounter.Current.Value++;
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
AddStep("reset combo", () =>
|
||||||
|
{
|
||||||
|
foreach (var hud in hudOverlays)
|
||||||
|
hud.ComboCounter.Current.Value = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestShownByDefault()
|
public void TestShownByDefault()
|
||||||
{
|
{
|
||||||
@ -45,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
|
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
|
||||||
AddUntilStep("wait for load", () => hudOverlay.IsAlive);
|
AddUntilStep("wait for load", () => hudOverlay.IsAlive);
|
||||||
AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1);
|
AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -53,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
createNew();
|
createNew();
|
||||||
|
|
||||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
|
||||||
|
|
||||||
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
||||||
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
|
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
|
||||||
@ -89,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("set keycounter visible false", () =>
|
AddStep("set keycounter visible false", () =>
|
||||||
{
|
{
|
||||||
config.Set<bool>(OsuSetting.KeyOverlay, false);
|
config.Set<bool>(OsuSetting.KeyOverlay, false);
|
||||||
hudOverlay.KeyCounter.AlwaysVisible.Value = false;
|
hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
|
||||||
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
||||||
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
|
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
|
||||||
|
|
||||||
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
|
AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
|
||||||
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
|
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
|
||||||
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
|
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
|
||||||
|
|
||||||
@ -107,13 +131,22 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
AddStep("create overlay", () =>
|
AddStep("create overlay", () =>
|
||||||
{
|
{
|
||||||
Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
|
SetContents(() =>
|
||||||
|
{
|
||||||
|
hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
|
||||||
|
|
||||||
// Add any key just to display the key counter visually.
|
// Add any key just to display the key counter visually.
|
||||||
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
|
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
|
||||||
|
|
||||||
|
hudOverlay.ComboCounter.Current.Value = 1;
|
||||||
|
|
||||||
action?.Invoke(hudOverlay);
|
action?.Invoke(hudOverlay);
|
||||||
|
|
||||||
|
return hudOverlay;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
private BarHitErrorMeter barMeter;
|
private BarHitErrorMeter barMeter;
|
||||||
private BarHitErrorMeter barMeter2;
|
private BarHitErrorMeter barMeter2;
|
||||||
|
private BarHitErrorMeter barMeter3;
|
||||||
private ColourHitErrorMeter colourMeter;
|
private ColourHitErrorMeter colourMeter;
|
||||||
private ColourHitErrorMeter colourMeter2;
|
private ColourHitErrorMeter colourMeter2;
|
||||||
|
private ColourHitErrorMeter colourMeter3;
|
||||||
private HitWindows hitWindows;
|
private HitWindows hitWindows;
|
||||||
|
|
||||||
public TestSceneHitErrorMeter()
|
public TestSceneHitErrorMeter()
|
||||||
@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomCentre,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Rotation = 270,
|
||||||
|
});
|
||||||
|
|
||||||
Add(colourMeter = new ColourHitErrorMeter(hitWindows)
|
Add(colourMeter = new ColourHitErrorMeter(hitWindows)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Margin = new MarginPadding { Left = 50 }
|
Margin = new MarginPadding { Left = 50 }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Add(colourMeter3 = new ColourHitErrorMeter(hitWindows)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomCentre,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Rotation = 270,
|
||||||
|
Margin = new MarginPadding { Left = 50 }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void newJudgement(double offset = 0)
|
private void newJudgement(double offset = 0)
|
||||||
@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
barMeter.OnNewJudgement(judgement);
|
barMeter.OnNewJudgement(judgement);
|
||||||
barMeter2.OnNewJudgement(judgement);
|
barMeter2.OnNewJudgement(judgement);
|
||||||
|
barMeter3.OnNewJudgement(judgement);
|
||||||
colourMeter.OnNewJudgement(judgement);
|
colourMeter.OnNewJudgement(judgement);
|
||||||
colourMeter2.OnNewJudgement(judgement);
|
colourMeter2.OnNewJudgement(judgement);
|
||||||
|
colourMeter3.OnNewJudgement(judgement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.OpenGL.Textures;
|
using osu.Framework.Graphics.OpenGL.Textures;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -22,27 +21,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
public class TestSceneSkinnableSound : OsuTestScene
|
public class TestSceneSkinnableSound : OsuTestScene
|
||||||
{
|
{
|
||||||
[Cached(typeof(ISamplePlaybackDisabler))]
|
|
||||||
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
|
||||||
|
|
||||||
private TestSkinSourceContainer skinSource;
|
private TestSkinSourceContainer skinSource;
|
||||||
private PausableSkinnableSound skinnableSound;
|
private PausableSkinnableSound skinnableSound;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("setup hierarchy", () =>
|
||||||
{
|
{
|
||||||
gameplayClock.IsPaused.Value = false;
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
skinSource = new TestSkinSourceContainer
|
skinSource = new TestSkinSourceContainer
|
||||||
{
|
{
|
||||||
Clock = gameplayClock,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
|
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStoppedSoundDoesntResumeAfterPause()
|
public void TestStoppedSoundDoesntResumeAfterPause()
|
||||||
@ -62,8 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||||
|
|
||||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
|
||||||
|
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||||
|
|
||||||
AddWaitStep("wait a bit", 5);
|
AddWaitStep("wait a bit", 5);
|
||||||
AddAssert("sample not playing", () => !sample.Playing);
|
AddAssert("sample not playing", () => !sample.Playing);
|
||||||
@ -82,8 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddUntilStep("wait for sample to start playing", () => sample.Playing);
|
AddUntilStep("wait for sample to start playing", () => sample.Playing);
|
||||||
|
|
||||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||||
|
|
||||||
|
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||||
|
AddUntilStep("wait for sample to start playing", () => sample.Playing);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -98,10 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddAssert("sample playing", () => sample.Playing);
|
AddAssert("sample playing", () => sample.Playing);
|
||||||
|
|
||||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
|
||||||
|
|
||||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
AddUntilStep("sample not playing", () => !sample.Playing);
|
||||||
|
|
||||||
|
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||||
|
|
||||||
AddAssert("sample not playing", () => !sample.Playing);
|
AddAssert("sample not playing", () => !sample.Playing);
|
||||||
AddAssert("sample not playing", () => !sample.Playing);
|
AddAssert("sample not playing", () => !sample.Playing);
|
||||||
@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddAssert("sample playing", () => sample.Playing);
|
AddAssert("sample playing", () => sample.Playing);
|
||||||
|
|
||||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||||
|
|
||||||
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
|
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
|
||||||
@ -133,20 +134,25 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("new sample stopped", () => !sample.Playing);
|
AddAssert("new sample stopped", () => !sample.Playing);
|
||||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||||
|
|
||||||
AddWaitStep("wait a bit", 5);
|
AddWaitStep("wait a bit", 5);
|
||||||
AddAssert("new sample not played", () => !sample.Playing);
|
AddAssert("new sample not played", () => !sample.Playing);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cached(typeof(ISkinSource))]
|
[Cached(typeof(ISkinSource))]
|
||||||
private class TestSkinSourceContainer : Container, ISkinSource
|
[Cached(typeof(ISamplePlaybackDisabler))]
|
||||||
|
private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ISkinSource source { get; set; }
|
private ISkinSource source { get; set; }
|
||||||
|
|
||||||
public event Action SourceChanged;
|
public event Action SourceChanged;
|
||||||
|
|
||||||
|
public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>();
|
||||||
|
|
||||||
|
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
|
||||||
|
|
||||||
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
|
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
|
||||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
|
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||||
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
|
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
|
||||||
|
@ -401,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
|
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
|
||||||
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
|
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
|
||||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||||
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
|
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -18,10 +19,11 @@ using osu.Game.Rulesets.Difficulty;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
public class TestSceneModSettings : OsuTestScene
|
public class TestSceneModSettings : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private TestModSelectOverlay modSelect;
|
private TestModSelectOverlay modSelect;
|
||||||
|
|
||||||
@ -95,6 +97,41 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
|
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiModSettingsUnboundWhenCopied()
|
||||||
|
{
|
||||||
|
MultiMod original = null;
|
||||||
|
MultiMod copy = null;
|
||||||
|
|
||||||
|
AddStep("create mods", () =>
|
||||||
|
{
|
||||||
|
original = new MultiMod(new OsuModDoubleTime());
|
||||||
|
copy = (MultiMod)original.CreateCopy();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
|
||||||
|
|
||||||
|
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
|
||||||
|
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCustomisationMenuNoClickthrough()
|
||||||
|
{
|
||||||
|
createModSelect();
|
||||||
|
openModSelect();
|
||||||
|
|
||||||
|
AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
|
||||||
|
AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
|
||||||
|
AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1);
|
||||||
|
AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
|
||||||
|
AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
|
||||||
|
AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
|
||||||
|
AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
|
||||||
|
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
private void createModSelect()
|
private void createModSelect()
|
||||||
{
|
{
|
||||||
AddStep("create mod select", () =>
|
AddStep("create mod select", () =>
|
||||||
@ -121,9 +158,16 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
|
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
|
||||||
|
|
||||||
|
public ModButton GetModButton(Mod mod)
|
||||||
|
{
|
||||||
|
return ModSectionsContainer.ChildrenOfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
|
||||||
|
}
|
||||||
|
|
||||||
public void SelectMod(Mod mod) =>
|
public void SelectMod(Mod mod) =>
|
||||||
ModSectionsContainer.Children.Single(s => s.ModType == mod.Type)
|
GetModButton(mod).SelectNext(1);
|
||||||
.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
|
|
||||||
|
public void SetModSettingsWidth(float newWidth) =>
|
||||||
|
ModSettingsContainer.Width = newWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRulesetInfo : RulesetInfo
|
public class TestRulesetInfo : RulesetInfo
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
@ -59,7 +60,7 @@ namespace osu.Game.Tests
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
using (var reader = getZipReader())
|
using (var reader = getZipReader())
|
||||||
return reader.Filenames.First(f => f.EndsWith(".mp3"));
|
return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ namespace osu.Game.Tests
|
|||||||
protected override Beatmap CreateBeatmap()
|
protected override Beatmap CreateBeatmap()
|
||||||
{
|
{
|
||||||
using (var reader = getZipReader())
|
using (var reader = getZipReader())
|
||||||
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))))
|
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal))))
|
||||||
using (var beatmapReader = new LineBufferedReader(beatmapStream))
|
using (var beatmapReader = new LineBufferedReader(beatmapStream))
|
||||||
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
|
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Graphics.Video;
|
using osu.Framework.Graphics.Video;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Tournament.IO;
|
||||||
|
|
||||||
namespace osu.Game.Tournament.Components
|
namespace osu.Game.Tournament.Components
|
||||||
{
|
{
|
||||||
@ -17,7 +18,6 @@ namespace osu.Game.Tournament.Components
|
|||||||
private readonly string filename;
|
private readonly string filename;
|
||||||
private readonly bool drawFallbackGradient;
|
private readonly bool drawFallbackGradient;
|
||||||
private Video video;
|
private Video video;
|
||||||
|
|
||||||
private ManualClock manualClock;
|
private ManualClock manualClock;
|
||||||
|
|
||||||
public TourneyVideo(string filename, bool drawFallbackGradient = false)
|
public TourneyVideo(string filename, bool drawFallbackGradient = false)
|
||||||
@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[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)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
72
osu.Game.Tournament/IO/TournamentStorage.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,12 +4,12 @@
|
|||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Platform;
|
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)
|
public TournamentVideoResourceStore(Storage storage)
|
||||||
: base(new StorageBackedResourceStore(storage), "tournament")
|
: base(new StorageBackedResourceStore(storage), "videos")
|
||||||
{
|
{
|
||||||
AddExtension("m4v");
|
AddExtension("m4v");
|
||||||
AddExtension("avi");
|
AddExtension("avi");
|
@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
|||||||
if (string.IsNullOrEmpty(line))
|
if (string.IsNullOrEmpty(line))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (line.ToUpperInvariant().StartsWith("GROUP"))
|
if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// ReSharper disable once AccessToModifiedClosure
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
@ -8,11 +8,12 @@ using Newtonsoft.Json;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.IO.Stores;
|
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Tournament.IPC;
|
using osu.Game.Tournament.IPC;
|
||||||
|
using osu.Game.Tournament.IO;
|
||||||
using osu.Game.Tournament.Models;
|
using osu.Game.Tournament.Models;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -23,13 +24,8 @@ namespace osu.Game.Tournament
|
|||||||
public class TournamentGameBase : OsuGameBase
|
public class TournamentGameBase : OsuGameBase
|
||||||
{
|
{
|
||||||
private const string bracket_filename = "bracket.json";
|
private const string bracket_filename = "bracket.json";
|
||||||
|
|
||||||
private LadderInfo ladder;
|
private LadderInfo ladder;
|
||||||
|
private TournamentStorage storage;
|
||||||
private Storage storage;
|
|
||||||
|
|
||||||
private TournamentStorage tournamentStorage;
|
|
||||||
|
|
||||||
private DependencyContainer dependencies;
|
private DependencyContainer dependencies;
|
||||||
private FileBasedIPC ipc;
|
private FileBasedIPC ipc;
|
||||||
|
|
||||||
@ -39,15 +35,14 @@ namespace osu.Game.Tournament
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(Storage storage)
|
private void load(Storage baseStorage)
|
||||||
{
|
{
|
||||||
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
|
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));
|
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
|
||||||
|
|
||||||
this.storage = storage;
|
|
||||||
|
|
||||||
readBracket();
|
readBracket();
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Beatmaps
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string StoredBookmarks
|
public string StoredBookmarks
|
||||||
{
|
{
|
||||||
get => string.Join(",", Bookmarks);
|
get => string.Join(',', Bookmarks);
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
|
@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures;
|
|||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
@ -36,6 +37,7 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
|
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -389,7 +391,7 @@ namespace osu.Game.Beatmaps
|
|||||||
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
|
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
|
||||||
{
|
{
|
||||||
// let's make sure there are actually .osu files to import.
|
// let's make sure there are actually .osu files to import.
|
||||||
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
|
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(mapName))
|
if (string.IsNullOrEmpty(mapName))
|
||||||
{
|
{
|
||||||
@ -417,7 +419,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
var beatmapInfos = new List<BeatmapInfo>();
|
var beatmapInfos = new List<BeatmapInfo>();
|
||||||
|
|
||||||
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu")))
|
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
|
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
|
||||||
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
|
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Development;
|
|||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
@ -23,6 +24,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
public partial class BeatmapManager
|
public partial class BeatmapManager
|
||||||
{
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
private class BeatmapOnlineLookupQueue : IDisposable
|
private class BeatmapOnlineLookupQueue : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IAPIProvider api;
|
private readonly IAPIProvider api;
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Audio.Track;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -17,6 +18,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
public partial class BeatmapManager
|
public partial class BeatmapManager
|
||||||
{
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
|
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
|
||||||
{
|
{
|
||||||
private readonly IResourceStore<byte[]> store;
|
private readonly IResourceStore<byte[]> store;
|
||||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public string Hash { get; set; }
|
public string Hash { get; set; }
|
||||||
|
|
||||||
public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb"))?.Filename;
|
public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
|
||||||
|
|
||||||
public List<BeatmapSetFileInfo> Files { get; set; }
|
public List<BeatmapSetFileInfo> Files { get; set; }
|
||||||
|
|
||||||
|
@ -307,12 +307,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
|
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
|
||||||
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
|
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
|
||||||
|
|
||||||
var breakEvent = new BreakPeriod(start, end);
|
beatmap.Breaks.Add(new BreakPeriod(start, end));
|
||||||
|
|
||||||
if (!breakEvent.HasEffect)
|
|
||||||
return;
|
|
||||||
|
|
||||||
beatmap.Breaks.Add(breakEvent);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
var pair = SplitKeyVal(line);
|
var pair = SplitKeyVal(line);
|
||||||
|
|
||||||
bool isCombo = pair.Key.StartsWith(@"Combo");
|
bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
|
||||||
|
|
||||||
string[] split = pair.Value.Split(',');
|
string[] split = pair.Value.Split(',');
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing
|
|||||||
public double Duration => EndTime - StartTime;
|
public double Duration => EndTime - StartTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap.
|
/// Whether the break has any effect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
|
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
|
||||||
|
|
||||||
|
@ -16,7 +16,10 @@ namespace osu.Game.Configuration
|
|||||||
[Description("Hit Error (right)")]
|
[Description("Hit Error (right)")]
|
||||||
HitErrorRight,
|
HitErrorRight,
|
||||||
|
|
||||||
[Description("Hit Error (both)")]
|
[Description("Hit Error (bottom)")]
|
||||||
|
HitErrorBottom,
|
||||||
|
|
||||||
|
[Description("Hit Error (left+right)")]
|
||||||
HitErrorBoth,
|
HitErrorBoth,
|
||||||
|
|
||||||
[Description("Colour (left)")]
|
[Description("Colour (left)")]
|
||||||
@ -25,7 +28,10 @@ namespace osu.Game.Configuration
|
|||||||
[Description("Colour (right)")]
|
[Description("Colour (right)")]
|
||||||
ColourRight,
|
ColourRight,
|
||||||
|
|
||||||
[Description("Colour (both)")]
|
[Description("Colour (left+right)")]
|
||||||
ColourBoth,
|
ColourBoth,
|
||||||
|
|
||||||
|
[Description("Colour (bottom)")]
|
||||||
|
ColourBottom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,7 +279,7 @@ namespace osu.Game.Database
|
|||||||
// for now, concatenate all .osu files in the set to create a unique hash.
|
// for now, concatenate all .osu files in the set to create a unique hash.
|
||||||
MemoryStream hashable = new MemoryStream();
|
MemoryStream hashable = new MemoryStream();
|
||||||
|
|
||||||
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)).OrderBy(f => f.Filename))
|
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
|
||||||
{
|
{
|
||||||
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
|
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
|
||||||
s.CopyTo(hashable);
|
s.CopyTo(hashable);
|
||||||
@ -593,7 +593,7 @@ namespace osu.Game.Database
|
|||||||
var fileInfos = new List<TFileModel>();
|
var fileInfos = new List<TFileModel>();
|
||||||
|
|
||||||
string prefix = reader.Filenames.GetCommonPrefix();
|
string prefix = reader.Filenames.GetCommonPrefix();
|
||||||
if (!(prefix.EndsWith("/") || prefix.EndsWith("\\")))
|
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
|
||||||
prefix = string.Empty;
|
prefix = string.Empty;
|
||||||
|
|
||||||
// import files to manager
|
// import files to manager
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
@ -28,9 +27,6 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Current.Value = DisplayedCount = 1.0f;
|
Current.Value = DisplayedCount = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours) => Colour = colours.BlueLighter;
|
|
||||||
|
|
||||||
protected override string FormatCount(double count) => count.FormatAccuracy();
|
protected override string FormatCount(double count) => count.FormatAccuracy();
|
||||||
|
|
||||||
protected override double GetProportionalDuration(double currentValue, double newValue)
|
protected override double GetProportionalDuration(double currentValue, double newValue)
|
||||||
|
@ -56,8 +56,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
displayedCount = value;
|
displayedCount = value;
|
||||||
if (displayedCountSpriteText != null)
|
UpdateDisplay();
|
||||||
displayedCountSpriteText.Text = FormatCount(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,10 +72,17 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
displayedCountSpriteText = CreateSpriteText();
|
displayedCountSpriteText = CreateSpriteText();
|
||||||
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
|
|
||||||
|
UpdateDisplay();
|
||||||
Child = displayedCountSpriteText;
|
Child = displayedCountSpriteText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void UpdateDisplay()
|
||||||
|
{
|
||||||
|
if (displayedCountSpriteText != null)
|
||||||
|
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
@ -1,35 +1,37 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public class ScoreCounter : RollingCounter<double>
|
public abstract class ScoreCounter : RollingCounter<double>, IScoreCounter
|
||||||
{
|
{
|
||||||
protected override double RollingDuration => 1000;
|
protected override double RollingDuration => 1000;
|
||||||
protected override Easing RollingEasing => Easing.Out;
|
protected override Easing RollingEasing => Easing.Out;
|
||||||
|
|
||||||
public bool UseCommaSeparator;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many leading zeroes the counter has.
|
/// Whether comma separators should be displayed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint LeadingZeroes { get; }
|
public bool UseCommaSeparator { get; }
|
||||||
|
|
||||||
|
public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Displays score.
|
/// Displays score.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="leading">How many leading zeroes the counter will have.</param>
|
/// <param name="leading">How many leading zeroes the counter will have.</param>
|
||||||
public ScoreCounter(uint leading = 0)
|
/// <param name="useCommaSeparator">Whether comma separators should be displayed.</param>
|
||||||
|
protected ScoreCounter(int leading = 0, bool useCommaSeparator = false)
|
||||||
{
|
{
|
||||||
LeadingZeroes = leading;
|
UseCommaSeparator = useCommaSeparator;
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
RequiredDisplayDigits.Value = leading;
|
||||||
private void load(OsuColour colours) => Colour = colours.BlueLighter;
|
RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay());
|
||||||
|
}
|
||||||
|
|
||||||
protected override double GetProportionalDuration(double currentValue, double newValue)
|
protected override double GetProportionalDuration(double currentValue, double newValue)
|
||||||
{
|
{
|
||||||
@ -38,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected override string FormatCount(double count)
|
protected override string FormatCount(double count)
|
||||||
{
|
{
|
||||||
string format = new string('0', (int)LeadingZeroes);
|
string format = new string('0', RequiredDisplayDigits.Value);
|
||||||
|
|
||||||
if (UseCommaSeparator)
|
if (UseCommaSeparator)
|
||||||
{
|
{
|
||||||
|
@ -73,8 +73,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
},
|
},
|
||||||
new Container
|
new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
// top right works better when the vertical height of the component changes smoothly (avoids weird layout animations).
|
||||||
Origin = Anchor.CentreRight,
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Child = Component = CreateComponent().With(d =>
|
Child = Component = CreateComponent().With(d =>
|
||||||
|
132
osu.Game/IO/MigratableStorage.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -13,7 +10,7 @@ using osu.Game.Configuration;
|
|||||||
|
|
||||||
namespace osu.Game.IO
|
namespace osu.Game.IO
|
||||||
{
|
{
|
||||||
public class OsuStorage : WrappedStorage
|
public class OsuStorage : MigratableStorage
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
|
/// 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 StorageConfigManager storageConfig;
|
||||||
private readonly Storage defaultStorage;
|
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",
|
"framework.ini",
|
||||||
"storage.ini"
|
"storage.ini"
|
||||||
@ -103,106 +100,11 @@ namespace osu.Game.IO
|
|||||||
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
|
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string newLocation)
|
public override void Migrate(Storage newStorage)
|
||||||
{
|
{
|
||||||
var source = new DirectoryInfo(GetFullPath("."));
|
base.Migrate(newStorage);
|
||||||
var destination = new DirectoryInfo(newLocation);
|
storageConfig.Set(StorageConfig.FullPath, 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", 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);
|
|
||||||
storageConfig.Save();
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ namespace osu.Game.Online.Chat
|
|||||||
if (target == null)
|
if (target == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var parameters = text.Split(new[] { ' ' }, 2);
|
var parameters = text.Split(' ', 2);
|
||||||
string command = parameters[0];
|
string command = parameters[0];
|
||||||
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
|
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
public static LinkDetails GetLinkDetails(string url)
|
public static LinkDetails GetLinkDetails(string url)
|
||||||
{
|
{
|
||||||
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
var args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
args[0] = args[0].TrimEnd(':');
|
args[0] = args[0].TrimEnd(':');
|
||||||
|
|
||||||
switch (args[0])
|
switch (args[0])
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
@ -150,9 +151,9 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
switch (placeholderState = value)
|
switch (placeholderState = value)
|
||||||
{
|
{
|
||||||
case PlaceholderState.NetworkFailure:
|
case PlaceholderState.NetworkFailure:
|
||||||
replacePlaceholder(new RetrievalFailurePlaceholder
|
replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
|
||||||
{
|
{
|
||||||
OnRetry = UpdateScores,
|
Action = UpdateScores,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -103,6 +103,20 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public readonly Bindable<int> Position = new Bindable<int>(-1);
|
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)
|
public void CopyFrom(Room other)
|
||||||
{
|
{
|
||||||
RoomID.Value = other.RoomID.Value;
|
RoomID.Value = other.RoomID.Value;
|
||||||
|
38
osu.Game/Online/Placeholders/ClickablePlaceholder.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,45 +2,20 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
namespace osu.Game.Online.Placeholders
|
namespace osu.Game.Online.Placeholders
|
||||||
{
|
{
|
||||||
public sealed class LoginPlaceholder : Placeholder
|
public sealed class LoginPlaceholder : ClickablePlaceholder
|
||||||
{
|
{
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private LoginOverlay login { get; set; }
|
private LoginOverlay login { get; set; }
|
||||||
|
|
||||||
public LoginPlaceholder(string actionMessage)
|
public LoginPlaceholder(string actionMessage)
|
||||||
|
: base(actionMessage, FontAwesome.Solid.UserLock)
|
||||||
{
|
{
|
||||||
AddIcon(FontAwesome.Solid.UserLock, cp =>
|
Action = () => login?.Show();
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
if (args?.Length > 0)
|
if (args?.Length > 0)
|
||||||
{
|
{
|
||||||
var paths = args.Where(a => !a.StartsWith(@"-")).ToArray();
|
var paths = args.Where(a => !a.StartsWith('-')).ToArray();
|
||||||
if (paths.Length > 0)
|
if (paths.Length > 0)
|
||||||
Task.Run(() => Import(paths));
|
Task.Run(() => Import(paths));
|
||||||
}
|
}
|
||||||
@ -289,7 +289,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
|
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
|
||||||
{
|
{
|
||||||
if (url.StartsWith("/"))
|
if (url.StartsWith('/'))
|
||||||
url = $"{API.Endpoint}{url}";
|
url = $"{API.Endpoint}{url}";
|
||||||
|
|
||||||
externalLinkOpener.OpenUrlExternally(url);
|
externalLinkOpener.OpenUrlExternally(url);
|
||||||
|
@ -371,8 +371,10 @@ namespace osu.Game
|
|||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
RulesetStore?.Dispose();
|
RulesetStore?.Dispose();
|
||||||
BeatmapManager?.Dispose();
|
BeatmapManager?.Dispose();
|
||||||
|
LocalConfig?.Dispose();
|
||||||
|
|
||||||
contextFactory.FlushConnections();
|
contextFactory.FlushConnections();
|
||||||
}
|
}
|
||||||
@ -406,7 +408,7 @@ namespace osu.Game
|
|||||||
public void Migrate(string path)
|
public void Migrate(string path)
|
||||||
{
|
{
|
||||||
contextFactory.FlushConnections();
|
contextFactory.FlushConnections();
|
||||||
(Storage as OsuStorage)?.Migrate(path);
|
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
@ -45,9 +44,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
|
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
|
||||||
|
|
||||||
protected readonly FillFlowContainer<ModControlSection> ModSettingsContent;
|
protected readonly ModSettingsContainer ModSettingsContainer;
|
||||||
|
|
||||||
protected readonly Container ModSettingsContainer;
|
|
||||||
|
|
||||||
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||||
|
|
||||||
@ -284,7 +281,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ModSettingsContainer = new Container
|
ModSettingsContainer = new ModSettingsContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.BottomRight,
|
Anchor = Anchor.BottomRight,
|
||||||
@ -292,29 +289,11 @@ namespace osu.Game.Overlays.Mods
|
|||||||
Width = 0.25f,
|
Width = 0.25f,
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
X = -100,
|
X = -100,
|
||||||
Children = new Drawable[]
|
SelectedMods = { BindTarget = SelectedMods },
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = new Color4(0, 0, 0, 192)
|
|
||||||
},
|
|
||||||
new OsuScrollContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Child = ModSettingsContent = new FillFlowContainer<ModControlSection>
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Spacing = new Vector2(0f, 10f),
|
|
||||||
Padding = new MarginPadding(20),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
((IBindable<bool>)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
@ -423,8 +402,6 @@ namespace osu.Game.Overlays.Mods
|
|||||||
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
|
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
|
||||||
|
|
||||||
updateMods();
|
updateMods();
|
||||||
|
|
||||||
updateModSettings(mods);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMods()
|
private void updateMods()
|
||||||
@ -445,25 +422,6 @@ namespace osu.Game.Overlays.Mods
|
|||||||
MultiplierLabel.FadeColour(Color4.White, 200);
|
MultiplierLabel.FadeColour(Color4.White, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateModSettings(ValueChangedEvent<IReadOnlyList<Mod>> selectedMods)
|
|
||||||
{
|
|
||||||
ModSettingsContent.Clear();
|
|
||||||
|
|
||||||
foreach (var mod in selectedMods.NewValue)
|
|
||||||
{
|
|
||||||
var settings = mod.CreateSettingsControls().ToList();
|
|
||||||
if (settings.Count > 0)
|
|
||||||
ModSettingsContent.Add(new ModControlSection(mod, settings));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasSettings = ModSettingsContent.Count > 0;
|
|
||||||
|
|
||||||
CustomiseButton.Enabled.Value = hasSettings;
|
|
||||||
|
|
||||||
if (!hasSettings)
|
|
||||||
ModSettingsContainer.Hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void modButtonPressed(Mod selectedMod)
|
private void modButtonPressed(Mod selectedMod)
|
||||||
{
|
{
|
||||||
if (selectedMod != null)
|
if (selectedMod != null)
|
||||||
|
84
osu.Game/Overlays/Mods/ModSettingsContainer.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.Mods
|
||||||
|
{
|
||||||
|
public class ModSettingsContainer : Container
|
||||||
|
{
|
||||||
|
public readonly IBindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||||
|
|
||||||
|
public IBindable<bool> HasSettingsForSelection => hasSettingsForSelection;
|
||||||
|
|
||||||
|
private readonly Bindable<bool> hasSettingsForSelection = new Bindable<bool>();
|
||||||
|
|
||||||
|
private readonly FillFlowContainer<ModControlSection> modSettingsContent;
|
||||||
|
|
||||||
|
public ModSettingsContainer()
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = new Color4(0, 0, 0, 192)
|
||||||
|
},
|
||||||
|
new OsuScrollContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = modSettingsContent = new FillFlowContainer<ModControlSection>
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(0f, 10f),
|
||||||
|
Padding = new MarginPadding(20),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
SelectedMods.BindValueChanged(modsChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||||
|
{
|
||||||
|
modSettingsContent.Clear();
|
||||||
|
|
||||||
|
foreach (var mod in mods.NewValue)
|
||||||
|
{
|
||||||
|
var settings = mod.CreateSettingsControls().ToList();
|
||||||
|
if (settings.Count > 0)
|
||||||
|
modSettingsContent.Add(new ModControlSection(mod, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasSettings = modSettingsContent.Count > 0;
|
||||||
|
|
||||||
|
if (!hasSettings)
|
||||||
|
Hide();
|
||||||
|
|
||||||
|
hasSettingsForSelection.Value = hasSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||||
|
protected override bool OnHover(HoverEvent e) => true;
|
||||||
|
}
|
||||||
|
}
|
@ -135,7 +135,6 @@ namespace osu.Game.Overlays.Profile.Header
|
|||||||
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}");
|
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}");
|
||||||
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord);
|
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord);
|
||||||
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat");
|
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat");
|
||||||
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Lastfm, user.Lastfm, $@"https://last.fm/users/{user.Lastfm}");
|
|
||||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website);
|
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website);
|
||||||
|
|
||||||
// If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding
|
// If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding
|
||||||
@ -149,7 +148,7 @@ namespace osu.Game.Overlays.Profile.Header
|
|||||||
if (string.IsNullOrEmpty(content)) return false;
|
if (string.IsNullOrEmpty(content)) return false;
|
||||||
|
|
||||||
// newlines could be contained in API returned user content.
|
// newlines could be contained in API returned user content.
|
||||||
content = content.Replace("\n", " ");
|
content = content.Replace('\n', ' ');
|
||||||
|
|
||||||
bottomLinkContainer.AddIcon(icon, text =>
|
bottomLinkContainer.AddIcon(icon, text =>
|
||||||
{
|
{
|
||||||
|
@ -76,7 +76,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
new SettingsEnumDropdown<ScoringMode>
|
new SettingsEnumDropdown<ScoringMode>
|
||||||
{
|
{
|
||||||
LabelText = "Score display mode",
|
LabelText = "Score display mode",
|
||||||
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
|
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode),
|
||||||
|
Keywords = new[] { "scoring" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
if (!beatmap.HitObjects.Any())
|
if (!beatmap.HitObjects.Any())
|
||||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||||
|
|
||||||
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
|
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
|
||||||
|
|
||||||
double sectionLength = SectionLength * clockRate;
|
double sectionLength = SectionLength * clockRate;
|
||||||
|
|
||||||
@ -100,15 +100,24 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorts a given set of <see cref="DifficultyHitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The <see cref="DifficultyHitObject"/>s to sort.</param>
|
||||||
|
/// <returns>The sorted <see cref="DifficultyHitObject"/>s.</returns>
|
||||||
|
protected virtual IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input)
|
||||||
|
=> input.OrderBy(h => h.BaseObject.StartTime);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
|
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
||||||
{
|
{
|
||||||
return createDifficultyAdjustmentModCombinations(Array.Empty<Mod>(), DifficultyAdjustmentMods).ToArray();
|
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
|
||||||
|
|
||||||
IEnumerable<Mod> createDifficultyAdjustmentModCombinations(IEnumerable<Mod> currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0)
|
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
|
||||||
{
|
{
|
||||||
|
// Return the current set.
|
||||||
switch (currentSetCount)
|
switch (currentSetCount)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
@ -128,18 +137,43 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod
|
// Apply the rest of the remaining mods recursively.
|
||||||
// combinations in further recursions, so a moving subset is used to eliminate this effect
|
for (int i = 0; i < remainingMods.Length; i++)
|
||||||
for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++)
|
|
||||||
{
|
{
|
||||||
var adjustmentMod = adjustmentSet[i];
|
var (nextSet, nextCount) = flatten(remainingMods.Span[i]);
|
||||||
if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod))))
|
|
||||||
|
// Check if any mods in the next set are incompatible with any of the current set.
|
||||||
|
if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1))
|
// Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
|
||||||
|
if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType())))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If all's good, attach the next set to the current set and recurse further.
|
||||||
|
foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount))
|
||||||
yield return combo;
|
yield return combo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
|
||||||
|
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
|
||||||
|
{
|
||||||
|
if (!(mod is MultiMod multi))
|
||||||
|
return (mod.Yield(), 1);
|
||||||
|
|
||||||
|
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
foreach (var nested in multi.Mods)
|
||||||
|
{
|
||||||
|
var (nestedSet, nestedCount) = flatten(nested);
|
||||||
|
set = set.Concat(nestedSet);
|
||||||
|
count += nestedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (set, count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -41,7 +41,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
|
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
|
||||||
|
|
||||||
private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
|
/// <summary>
|
||||||
|
/// The current strain level.
|
||||||
|
/// </summary>
|
||||||
|
protected double CurrentStrain { get; private set; } = 1;
|
||||||
|
|
||||||
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
||||||
|
|
||||||
private readonly List<double> strainPeaks = new List<double>();
|
private readonly List<double> strainPeaks = new List<double>();
|
||||||
@ -51,10 +55,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Process(DifficultyHitObject current)
|
public void Process(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
CurrentStrain *= strainDecay(current.DeltaTime);
|
||||||
currentStrain += StrainValueOf(current) * SkillMultiplier;
|
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
|
||||||
|
|
||||||
currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
|
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
|
||||||
|
|
||||||
Previous.Push(current);
|
Previous.Push(current);
|
||||||
}
|
}
|
||||||
@ -71,15 +75,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the initial strain level for a new section.
|
/// Sets the initial strain level for a new section.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="offset">The beginning of the new section in milliseconds.</param>
|
/// <param name="time">The beginning of the new section in milliseconds.</param>
|
||||||
public void StartNewSectionFrom(double offset)
|
public void StartNewSectionFrom(double time)
|
||||||
{
|
{
|
||||||
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
|
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
|
||||||
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
||||||
if (Previous.Count > 0)
|
if (Previous.Count > 0)
|
||||||
currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime);
|
currentSectionPeak = GetPeakStrain(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the peak strain at a point in time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The time to retrieve the peak strain at.</param>
|
||||||
|
/// <returns>The peak strain.</returns>
|
||||||
|
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
|
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -107,6 +107,9 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
foreach (var breakPeriod in Breaks)
|
foreach (var breakPeriod in Breaks)
|
||||||
{
|
{
|
||||||
|
if (!breakPeriod.HasEffect)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
|
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
|
||||||
|
|
||||||
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
|
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
|
||||||
|
@ -6,7 +6,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mods
|
namespace osu.Game.Rulesets.Mods
|
||||||
{
|
{
|
||||||
public class MultiMod : Mod
|
public sealed class MultiMod : Mod
|
||||||
{
|
{
|
||||||
public override string Name => string.Empty;
|
public override string Name => string.Empty;
|
||||||
public override string Acronym => string.Empty;
|
public override string Acronym => string.Empty;
|
||||||
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
Mods = mods;
|
Mods = mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
|
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|