1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 02:22:56 +08:00

Merge branch 'master' into autolink-md

This commit is contained in:
Gagah Pangeran Rosfatiputra 2021-07-13 15:04:36 +07:00 committed by GitHub
commit 1c69da09d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1214 additions and 289 deletions

View File

@ -52,10 +52,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.707.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.713.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.2.1" />
<PackageReference Include="Realm" Version="10.3.0" />
</ItemGroup>
</Project>

View File

@ -68,6 +68,8 @@ namespace osu.Desktop.Updater
return false;
}
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
@ -98,7 +100,6 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
{
@ -110,6 +111,7 @@ namespace osu.Desktop.Updater
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
}
finally
{

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class CatchEditorTestSceneContainer : Container
{
[Cached(typeof(Playfield))]
public readonly ScrollingPlayfield Playfield;
protected override Container<Drawable> Content { get; }
public CatchEditorTestSceneContainer()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Width = CatchPlayfield.WIDTH;
Height = 1000;
Padding = new MarginPadding
{
Bottom = 100
};
InternalChildren = new Drawable[]
{
new ScrollingTestContainer(ScrollingDirection.Down)
{
TimeRange = 1000,
RelativeSizeAxes = Axes.Both,
Child = Playfield = new TestCatchPlayfield
{
RelativeSizeAxes = Axes.Both
}
},
new PlayfieldBorder
{
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full },
Clock = new FramedClock(new StopwatchClock(true))
},
Content = new Container
{
RelativeSizeAxes = Axes.Both
}
};
}
private class TestCatchPlayfield : CatchEditorPlayfield
{
public TestCatchPlayfield()
: base(new BeatmapDifficulty { CircleSize = 0 })
{
}
}
}
}

View File

@ -0,0 +1,79 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
protected const double TIME_SNAP = 100;
protected DrawableCatchHitObject LastObject;
protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchPlacementBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
contentContainer.Playfield.Clock = new FramedClock(new ManualClock());
}
[SetUp]
public void Setup() => Schedule(() =>
{
HitObjectContainer.Clear();
ResetPlacement();
LastObject = null;
});
protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
{
float y = HitObjectContainer.PositionAtTime(time);
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
InputManager.MoveMouseTo(pos);
});
protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () =>
{
InputManager.Click(button);
});
protected IEnumerable<FruitOutline> FruitOutlines => Content.ChildrenOfType<FruitOutline>();
// Unused because AddHitObject is overriden
protected override Container CreateHitObjectContainer() => new Container();
protected override void AddHitObject(DrawableHitObject hitObject)
{
LastObject = (DrawableCatchHitObject)hitObject;
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
return result;
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchSelectionBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
}
}
}

View File

@ -0,0 +1,87 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override void AddHitObject(DrawableHitObject hitObject)
{
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
base.AddHitObject(hitObject);
}
[Test]
public void TestBasicPlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestReversePlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestFinishWithZeroDuration()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is not placed", () => LastObject == null);
AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting);
}
[Test]
public void TestOpacity()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
AddMoveStep(200, 0);
AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1));
AddMoveStep(100, 0);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
}
private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType<TimeSpanOutline>().Single();
}
}

View File

@ -0,0 +1,44 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test]
public void TestFruitPlacementPosition()
{
const double time = 300;
const float x = CatchPlayfield.CENTER_X;
AddMoveStep(time, x);
AddClickStep(MouseButton.Left);
AddAssert("outline position is correct", () =>
{
var outline = FruitOutlines.Single();
return Precision.AlmostEquals(outline.X, x) &&
Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time));
});
AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time));
AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x));
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
public TestSceneJuiceStreamSelectionBlueprint()
{
var hitObject = new JuiceStream
{
OriginalX = 100,
StartTime = 100,
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(200, 100),
new Vector2(0, 200),
}),
};
var controlPoint = new ControlPointInfo();
controlPoint.Add(0, new TimingControlPoint
{
BeatLength = 100
});
hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
}
}
}

View File

@ -12,37 +12,29 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get
@ -61,20 +53,12 @@ namespace osu.Game.Rulesets.Catch.Mods
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty)
{
base.TransferSettings(difficulty);
TransferSetting(CircleSize, difficulty.CircleSize);
TransferSetting(ApproachRate, difficulty.ApproachRate);
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -11,34 +10,26 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get
@ -55,20 +46,12 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty)
{
base.TransferSettings(difficulty);
TransferSetting(CircleSize, difficulty.CircleSize);
TransferSetting(ApproachRate, difficulty.ApproachRate);
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -11,14 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ScrollSpeed { get; } = new BindableFloat
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable ScrollSpeed { get; } = new DifficultyBindable
{
Precision = 0.05f,
MinValue = 0.25f,
MaxValue = 4,
Default = 1,
Value = 1,
ReadCurrentFromDifficulty = _ => 1,
};
public override string SettingDescription
@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
base.ApplySettings(difficulty);
ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll);
if (ScrollSpeed.Value != null) difficulty.SliderMultiplier *= ScrollSpeed.Value.Value;
}
}
}

View File

@ -0,0 +1,165 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
{
[TestFixture]
public class ModDifficultyAdjustTest
{
private TestModDifficultyAdjust testMod;
[SetUp]
public void Setup()
{
testMod = new TestModDifficultyAdjust();
}
[Test]
public void TestUnchangedSettingsFollowAppliedDifficulty()
{
var result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
Assert.That(result.DrainRate, Is.EqualTo(10));
Assert.That(result.OverallDifficulty, Is.EqualTo(10));
result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 1,
OverallDifficulty = 1
});
Assert.That(result.DrainRate, Is.EqualTo(1));
Assert.That(result.OverallDifficulty, Is.EqualTo(1));
}
[Test]
public void TestChangedSettingsOverrideAppliedDifficulty()
{
testMod.OverallDifficulty.Value = 4;
var result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
Assert.That(result.DrainRate, Is.EqualTo(10));
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 1,
OverallDifficulty = 1
});
Assert.That(result.DrainRate, Is.EqualTo(1));
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
}
[Test]
public void TestChangedSettingsRetainedWhenSameValueIsApplied()
{
testMod.OverallDifficulty.Value = 4;
// Apply and de-apply the same value as the mod.
applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
var result = applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 10 });
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
}
[Test]
public void TestChangedSettingSerialisedWhenSameValueIsApplied()
{
applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
testMod.OverallDifficulty.Value = 4;
var result = (TestModDifficultyAdjust)new APIMod(testMod).ToMod(new TestRuleset());
Assert.That(result.OverallDifficulty.Value, Is.EqualTo(4));
}
[Test]
public void TestChangedSettingsRevertedToDefault()
{
applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
testMod.OverallDifficulty.Value = 4;
testMod.ResetSettingsToDefaults();
Assert.That(testMod.DrainRate.Value, Is.Null);
Assert.That(testMod.OverallDifficulty.Value, Is.Null);
var applied = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
}
/// <summary>
/// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/>
/// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance.
/// </summary>
private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty)
{
// ensure that ReadFromDifficulty doesn't pollute the values.
var newDifficulty = difficulty.Clone();
testMod.ReadFromDifficulty(difficulty);
testMod.ApplyToDifficulty(newDifficulty);
return newDifficulty;
}
private class TestModDifficultyAdjust : ModDifficultyAdjust
{
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type)
{
if (type == ModType.DifficultyIncrease)
yield return new TestModDifficultyAdjust();
}
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{
throw new System.NotImplementedException();
}
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
{
throw new System.NotImplementedException();
}
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap)
{
throw new System.NotImplementedException();
}
public override string Description => string.Empty;
public override string ShortName => string.Empty;
}
}
}

View File

@ -184,6 +184,9 @@ namespace osu.Game.Tests.NonVisual
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
// some files may have been left behind for whatever reason, but that's not what we're testing here.
customPath = prepareCustomPath();
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
@ -10,6 +11,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
@ -17,17 +19,26 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerScoreSubmission : OsuPlayerTestScene
public class TestScenePlayerScoreSubmission : PlayerTestScene
{
protected override bool AllowFail => allowFail;
private bool allowFail;
private Func<RulesetInfo, IBeatmap> createCustomBeatmap;
private Func<Ruleset> createCustomRuleset;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
protected override bool HasCustomSteps => true;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => createCustomBeatmap?.Invoke(ruleset) ?? createTestBeatmap(ruleset);
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{
var beatmap = (TestBeatmap)base.CreateBeatmap(ruleset);
@ -36,14 +47,12 @@ namespace osu.Game.Tests.Visual.Gameplay
return beatmap;
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
[Test]
public void TestNoSubmissionOnResultsWithNoToken()
{
prepareTokenResponse(false);
CreateTest(() => allowFail = false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -63,7 +72,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTokenResponse(true);
CreateTest(() => allowFail = false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -82,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTokenResponse(false);
CreateTest(() => allowFail = false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -99,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTokenResponse(true);
CreateTest(() => allowFail = true);
createPlayerTest(true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -114,7 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTokenResponse(true);
CreateTest(() => allowFail = true);
createPlayerTest(true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -131,7 +140,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTokenResponse(true);
CreateTest(() => allowFail = false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -144,7 +153,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTokenResponse(true);
CreateTest(() => allowFail = false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -154,18 +163,49 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
}
private void addFakeHit()
[Test]
public void TestNoSubmissionOnLocalBeatmap()
{
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
prepareTokenResponse(true);
AddStep("force successfuly hit", () =>
createPlayerTest(false, r =>
{
Player.ScoreProcessor.RevertResult(Player.Results.First());
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new OsuJudgement())
{
Type = HitResult.Great,
});
var beatmap = createTestBeatmap(r);
beatmap.BeatmapInfo.OnlineBeatmapID = null;
return beatmap;
});
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestNoSubmissionOnCustomRuleset()
{
prepareTokenResponse(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } });
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null)
{
CreateTest(() => AddStep("set up requirements", () =>
{
this.allowFail = allowFail;
createCustomBeatmap = createBeatmap;
createCustomRuleset = createRuleset;
}));
}
private void prepareTokenResponse(bool validToken)
@ -188,5 +228,19 @@ namespace osu.Game.Tests.Visual.Gameplay
};
});
}
private void addFakeHit()
{
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
AddStep("force successfuly hit", () =>
{
Player.ScoreProcessor.RevertResult(Player.Results.First());
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new OsuJudgement())
{
Type = HitResult.Great,
});
});
}
}
}

View File

@ -58,8 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtSongSelectFromPlayerLoader()
{
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
PushAndConfirm(() => new TestPlaySongSelect());
importAndWaitForSongSelect();
AddStep("Press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
@ -72,8 +71,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtMenuFromPlayerLoader()
{
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
PushAndConfirm(() => new TestPlaySongSelect());
importAndWaitForSongSelect();
AddStep("Press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
@ -172,6 +170,13 @@ namespace osu.Game.Tests.Visual.Navigation
}
}
private void importAndWaitForSongSelect()
{
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
PushAndConfirm(() => new TestPlaySongSelect());
AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineBeatmapSetID == 241526);
}
public class DialogBlockingScreen : OsuScreen
{
[Resolved]

View File

@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays;
@ -17,28 +16,65 @@ namespace osu.Game.Tests.Visual.Settings
[Test]
public void TestRestoreDefaultValueButtonVisibility()
{
TestSettingsTextBox textBox = null;
SettingsTextBox textBox = null;
RestoreDefaultValueButton<string> restoreDefaultValueButton = null;
AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox
AddStep("create settings item", () =>
{
Current = new Bindable<string>
Child = textBox = new SettingsTextBox
{
Default = "test",
Value = "test"
}
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
};
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
});
AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value from default", () => textBox.Current.Value = "non-default");
AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0);
AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddStep("restore default", () => textBox.Current.SetDefault());
AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
private class TestSettingsTextBox : SettingsTextBox
/// <summary>
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
/// </summary>
[TestCase(4.2f)]
[TestCase(9.9f)]
public void TestRestoreDefaultValueButtonPrecision(float initialValue)
{
public Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
BindableFloat current = null;
SettingsSlider<float> sliderBar = null;
RestoreDefaultValueButton<float> restoreDefaultValueButton = null;
AddStep("create settings item", () =>
{
Child = sliderBar = new SettingsSlider<float>
{
Current = current = new BindableFloat(initialValue)
{
MinValue = 0f,
MaxValue = 10f,
Precision = 0.1f,
}
};
restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single();
});
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f);
AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddStep("restore default", () => sliderBar.Current.SetDefault());
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
}
}
}

View File

@ -31,10 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
[Test]
public void TestLocal([Values("Beatmap", "Some long title and stuff")]
string title,
[Values("Trial", "Some1's very hardest difficulty")]
string version)
public void TestLocal(
[Values("Beatmap", "Some long title and stuff")]
string title,
[Values("Trial", "Some1's very hardest difficulty")]
string version)
{
showMetadataForBeatmap(() => CreateWorkingBeatmap(new Beatmap
{

View File

@ -0,0 +1,225 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModDifficultyAdjustSettings : OsuManualInputManagerTestScene
{
private OsuModDifficultyAdjust modDifficultyAdjust;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create control", () =>
{
modDifficultyAdjust = new OsuModDifficultyAdjust();
Child = new Container
{
Size = new Vector2(300),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ChildrenEnumerable = modDifficultyAdjust.CreateSettingsControls(),
},
}
};
});
}
[Test]
public void TestFollowsBeatmapDefaultsVisually()
{
setBeatmapWithDifficultyParameters(5);
checkSliderAtValue("Circle Size", 5);
checkBindableAtValue("Circle Size", null);
setBeatmapWithDifficultyParameters(8);
checkSliderAtValue("Circle Size", 8);
checkBindableAtValue("Circle Size", null);
}
[Test]
public void TestOutOfRangeValueStillApplied()
{
AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11);
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
// this is a no-op, just showing that it won't reset the value during deserialisation.
setExtendedLimits(false);
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
// setting extended limits will reset the serialisation exception.
// this should be fine as the goal is to allow, at most, the value of extended limits.
setExtendedLimits(true);
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
}
[Test]
public void TestExtendedLimits()
{
setSliderValue("Circle Size", 99);
checkSliderAtValue("Circle Size", 10);
checkBindableAtValue("Circle Size", 10);
setExtendedLimits(true);
checkSliderAtValue("Circle Size", 10);
checkBindableAtValue("Circle Size", 10);
setSliderValue("Circle Size", 99);
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
setExtendedLimits(false);
checkSliderAtValue("Circle Size", 10);
checkBindableAtValue("Circle Size", 10);
}
[Test]
public void TestUserOverrideMaintainedOnBeatmapChange()
{
setSliderValue("Circle Size", 9);
setBeatmapWithDifficultyParameters(2);
checkSliderAtValue("Circle Size", 9);
checkBindableAtValue("Circle Size", 9);
}
[Test]
public void TestResetToDefault()
{
setBeatmapWithDifficultyParameters(2);
setSliderValue("Circle Size", 9);
checkSliderAtValue("Circle Size", 9);
checkBindableAtValue("Circle Size", 9);
resetToDefault("Circle Size");
checkSliderAtValue("Circle Size", 2);
checkBindableAtValue("Circle Size", null);
}
[Test]
public void TestUserOverrideMaintainedOnMatchingBeatmapValue()
{
setBeatmapWithDifficultyParameters(3);
checkSliderAtValue("Circle Size", 3);
checkBindableAtValue("Circle Size", null);
// need to initially change it away from the current beatmap value to trigger an override.
setSliderValue("Circle Size", 4);
setSliderValue("Circle Size", 3);
checkSliderAtValue("Circle Size", 3);
checkBindableAtValue("Circle Size", 3);
setBeatmapWithDifficultyParameters(4);
checkSliderAtValue("Circle Size", 3);
checkBindableAtValue("Circle Size", 3);
}
[Test]
public void TestResetToDefaults()
{
setBeatmapWithDifficultyParameters(5);
setSliderValue("Circle Size", 3);
setExtendedLimits(true);
checkSliderAtValue("Circle Size", 3);
checkBindableAtValue("Circle Size", 3);
AddStep("reset mod settings", () => modDifficultyAdjust.ResetSettingsToDefaults());
checkSliderAtValue("Circle Size", 5);
checkBindableAtValue("Circle Size", null);
}
private void resetToDefault(string name)
{
AddStep($"Reset {name} to default", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.Current.SetDefault());
}
private void setExtendedLimits(bool status) =>
AddStep($"Set extended limits {status}", () => modDifficultyAdjust.ExtendedLimits.Value = status);
private void setSliderValue(string name, float value)
{
AddStep($"Set {name} slider to {value}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.ChildrenOfType<SettingsSlider<float>>().First().Current.Value = value);
}
private void checkBindableAtValue(string name, float? expectedValue)
{
AddAssert($"Bindable {name} is {(expectedValue?.ToString() ?? "null")}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.Current.Value == expectedValue);
}
private void checkSliderAtValue(string name, float expectedValue)
{
AddAssert($"Slider {name} at {expectedValue}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.ChildrenOfType<SettingsSlider<float>>().First().Current.Value == expectedValue);
}
private void setBeatmapWithDifficultyParameters(float value)
{
AddStep($"set beatmap with all {value}", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty
{
OverallDifficulty = value,
CircleSize = value,
DrainRate = value,
ApproachRate = value,
}
}
}));
}
}
}

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show());
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 5);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
}
[Test]

View File

@ -20,15 +20,26 @@ namespace osu.Game.Overlays
{
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
// this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button.
public override bool AcceptsFocus => true;
// this is intentionally not using BindableWithCurrent, as it can use the wrong IsDefault implementation when passed a BindableNumber.
// using GetBoundCopy() ensures that the received bindable is of the exact same type as the source bindable and uses the proper IsDefault implementation.
private Bindable<T> current;
public Bindable<T> Current
{
get => current.Current;
set => current.Current = value;
get => current;
set
{
current?.UnbindAll();
current = value.GetBoundCopy();
current.ValueChanged += _ => UpdateState();
current.DefaultChanged += _ => UpdateState();
current.DisabledChanged += _ => UpdateState();
UpdateState();
}
}
private Color4 buttonColour;
@ -62,18 +73,14 @@ namespace osu.Game.Overlays
Action += () =>
{
if (!current.Disabled) current.SetDefault();
if (!current.Disabled)
current.SetDefault();
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.ValueChanged += _ => UpdateState();
Current.DisabledChanged += _ => UpdateState();
Current.DefaultChanged += _ => UpdateState();
UpdateState();
}

View File

@ -101,10 +101,10 @@ namespace osu.Game.Overlays.Settings
public event Action SettingChanged;
private readonly RestoreDefaultValueButton<T> restoreDefaultButton;
protected SettingsItem()
{
RestoreDefaultValueButton<T> restoreDefaultButton;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS };
@ -126,14 +126,19 @@ namespace osu.Game.Overlays.Settings
// all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is
// never loaded, but requires bindable storage.
if (controlWithCurrent != null)
{
controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke();
controlWithCurrent.Current.DisabledChanged += _ => updateDisabled();
if (controlWithCurrent == null)
throw new ArgumentException(@$"Control created via {nameof(CreateControl)} must implement {nameof(IHasCurrentValue<T>)}");
if (ShowsDefaultIndicator)
restoreDefaultButton.Current = controlWithCurrent.Current;
}
controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke();
controlWithCurrent.Current.DisabledChanged += _ => updateDisabled();
}
protected override void LoadComplete()
{
base.LoadComplete();
if (ShowsDefaultIndicator)
restoreDefaultButton.Current = controlWithCurrent.Current;
}
private void updateDisabled()

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
protected virtual bool AlwaysShowWhenSelected => false;
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
protected override bool ShouldBeAlive => (DrawableObject?.IsAlive == true && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
protected HitObjectSelectionBlueprint(HitObject hitObject)
: base(hitObject)

View File

@ -0,0 +1,112 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods
{
public class DifficultyAdjustSettingsControl : SettingsItem<float?>
{
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
/// <summary>
/// Used to track the display value on the setting slider.
/// </summary>
/// <remarks>
/// When the mod is overriding a default, this will match the value of <see cref="Current"/>.
/// When there is no override (ie. <see cref="Current"/> is null), this value will match the beatmap provided default via <see cref="updateCurrentFromSlider"/>.
/// </remarks>
private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>();
protected override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent);
/// <summary>
/// Guards against beatmap values displayed on slider bars being transferred to user override.
/// </summary>
private bool isInternalChange;
private DifficultyBindable difficultyBindable;
public override Bindable<float?> Current
{
get => base.Current;
set
{
// Intercept and extract the internal number bindable from DifficultyBindable.
// This will provide bounds and precision specifications for the slider bar.
difficultyBindable = ((DifficultyBindable)value).GetBoundCopy();
sliderDisplayCurrent.BindTo(difficultyBindable.CurrentNumber);
base.Current = difficultyBindable;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(current => updateCurrentFromSlider());
beatmap.BindValueChanged(b => updateCurrentFromSlider(), true);
sliderDisplayCurrent.BindValueChanged(number =>
{
// this handles the transfer of the slider value to the main bindable.
// as such, should be skipped if the slider is being updated via updateFromDifficulty().
if (!isInternalChange)
Current.Value = number.NewValue;
});
}
private void updateCurrentFromSlider()
{
if (Current.Value != null)
{
// a user override has been added or updated.
sliderDisplayCurrent.Value = Current.Value.Value;
return;
}
var difficulty = beatmap.Value.BeatmapInfo.BaseDifficulty;
if (difficulty == null)
return;
// generally should always be implemented, else the slider will have a zero default.
if (difficultyBindable.ReadCurrentFromDifficulty == null)
return;
isInternalChange = true;
sliderDisplayCurrent.Value = difficultyBindable.ReadCurrentFromDifficulty(difficulty);
isInternalChange = false;
}
private class SliderControl : CompositeDrawable, IHasCurrentValue<float?>
{
// This is required as SettingsItem relies heavily on this bindable for internal use.
// The actual update flow is done via the bindable provided in the constructor.
public Bindable<float?> Current { get; set; } = new Bindable<float?>();
public SliderControl(BindableNumber<float> currentNumber)
{
InternalChildren = new Drawable[]
{
new SettingsSlider<float>
{
ShowsDefaultIndicator = false,
Current = currentNumber,
}
};
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
}
}
}
}

View File

@ -0,0 +1,133 @@
// 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.Bindables;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
{
public class DifficultyBindable : Bindable<float?>
{
/// <summary>
/// Whether the extended limits should be applied to this bindable.
/// </summary>
public readonly BindableBool ExtendedLimits = new BindableBool();
/// <summary>
/// An internal numeric bindable to hold and propagate min/max/precision.
/// The value of this bindable should not be set.
/// </summary>
internal readonly BindableFloat CurrentNumber = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
};
/// <summary>
/// A function that can extract the current value of this setting from a beatmap difficulty for display purposes.
/// </summary>
public Func<BeatmapDifficulty, float> ReadCurrentFromDifficulty;
public float Precision
{
set => CurrentNumber.Precision = value;
}
public float MinValue
{
set => CurrentNumber.MinValue = value;
}
private float maxValue;
public float MaxValue
{
set
{
if (value == maxValue)
return;
maxValue = value;
updateMaxValue();
}
}
private float? extendedMaxValue;
/// <summary>
/// The maximum value to be used when extended limits are applied.
/// </summary>
public float? ExtendedMaxValue
{
set
{
if (value == extendedMaxValue)
return;
extendedMaxValue = value;
updateMaxValue();
}
}
public DifficultyBindable()
: this(null)
{
}
public DifficultyBindable(float? defaultValue = null)
: base(defaultValue)
{
ExtendedLimits.BindValueChanged(_ => updateMaxValue());
}
public override float? Value
{
get => base.Value;
set
{
// Ensure that in the case serialisation runs in the wrong order (and limit extensions aren't applied yet) the deserialised value is still propagated.
if (value != null)
CurrentNumber.MaxValue = MathF.Max(CurrentNumber.MaxValue, value.Value);
base.Value = value;
}
}
private void updateMaxValue()
{
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
}
public override void BindTo(Bindable<float?> them)
{
if (!(them is DifficultyBindable otherDifficultyBindable))
throw new InvalidOperationException($"Cannot bind to a non-{nameof(DifficultyBindable)}.");
ReadCurrentFromDifficulty = otherDifficultyBindable.ReadCurrentFromDifficulty;
// the following max value copies are only safe as long as these values are effectively constants.
MaxValue = otherDifficultyBindable.maxValue;
ExtendedMaxValue = otherDifficultyBindable.extendedMaxValue;
ExtendedLimits.BindTarget = otherDifficultyBindable.ExtendedLimits;
// the actual values need to be copied after the max value constraints.
CurrentNumber.BindTarget = otherDifficultyBindable.CurrentNumber;
base.BindTo(them);
}
public override void UnbindFrom(IUnbindable them)
{
if (!(them is DifficultyBindable otherDifficultyBindable))
throw new InvalidOperationException($"Cannot unbind from a non-{nameof(DifficultyBindable)}.");
base.UnbindFrom(them);
CurrentNumber.UnbindFrom(otherDifficultyBindable.CurrentNumber);
ExtendedLimits.UnbindFrom(otherDifficultyBindable.ExtendedLimits);
}
public new DifficultyBindable GetBoundCopy() => new DifficultyBindable { BindTarget = this };
}
}

View File

@ -1,13 +1,12 @@
// 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.Beatmaps;
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using System.Linq;
namespace osu.Game.Rulesets.Mods
{
@ -33,24 +32,24 @@ namespace osu.Game.Rulesets.Mods
protected const int LAST_SETTING_ORDER = 2;
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
public BindableNumber<float> DrainRate { get; } = new BindableFloatWithLimitExtension
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable DrainRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.DrainRate,
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloatWithLimitExtension
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
};
[SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
@ -58,17 +57,11 @@ namespace osu.Game.Rulesets.Mods
protected ModDifficultyAdjust()
{
ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
}
/// <summary>
/// Changes the difficulty adjustment limits. Occurs when the value of <see cref="ExtendedLimits"/> is changed.
/// </summary>
/// <param name="extended">Whether limits should extend beyond sane ranges.</param>
protected virtual void ApplyLimits(bool extended)
{
DrainRate.MaxValue = extended ? 11 : 10;
OverallDifficulty.MaxValue = extended ? 11 : 10;
foreach (var (_, property) in this.GetOrderedSettingsSourceProperties())
{
if (property.GetValue(this) is DifficultyBindable diffAdjustBindable)
diffAdjustBindable.ExtendedLimits.BindTo(ExtendedLimits);
}
}
public override string SettingDescription
@ -86,146 +79,20 @@ namespace osu.Game.Rulesets.Mods
}
}
private BeatmapDifficulty difficulty;
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
{
if (this.difficulty == null || this.difficulty.ID != difficulty.ID)
{
TransferSettings(difficulty);
this.difficulty = difficulty;
}
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty);
/// <summary>
/// Transfer initial settings from the beatmap to settings.
/// </summary>
/// <param name="difficulty">The beatmap's initial values.</param>
protected virtual void TransferSettings(BeatmapDifficulty difficulty)
{
TransferSetting(DrainRate, difficulty.DrainRate);
TransferSetting(OverallDifficulty, difficulty.OverallDifficulty);
}
private readonly Dictionary<IBindable, bool> userChangedSettings = new Dictionary<IBindable, bool>();
/// <summary>
/// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable.
/// Only performs the transfer if the user is not currently overriding.
/// </summary>
protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault)
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
{
bindable.UnbindEvents();
userChangedSettings.TryAdd(bindable, false);
bindable.Default = beatmapDefault;
// users generally choose a difficulty setting and want it to stick across multiple beatmap changes.
// we only want to value transfer if the user hasn't changed the value previously.
if (!userChangedSettings[bindable])
bindable.Value = beatmapDefault;
bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault;
}
internal override void CopyAdjustedSetting(IBindable target, object source)
{
// if the value is non-bindable, it's presumably coming from an external source (like the API) - therefore presume it is not default.
// if the value is bindable, defer to the source's IsDefault to be able to tell.
userChangedSettings[target] = !(source is IBindable bindableSource) || !bindableSource.IsDefault;
base.CopyAdjustedSetting(target, source);
}
/// <summary>
/// Applies a setting from a configuration bindable using <paramref name="applyFunc"/>, if it has been changed by the user.
/// </summary>
protected void ApplySetting<T>(BindableNumber<T> setting, Action<T> applyFunc)
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
{
if (userChangedSettings.TryGetValue(setting, out bool userChangedSetting) && userChangedSetting)
applyFunc.Invoke(setting.Value);
}
/// <summary>
/// Apply all custom settings to the provided beatmap.
/// </summary>
/// <param name="difficulty">The beatmap to have settings applied.</param>
protected virtual void ApplySettings(BeatmapDifficulty difficulty)
{
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
}
public override void ResetSettingsToDefaults()
{
base.ResetSettingsToDefaults();
if (difficulty != null)
{
// base implementation potentially overwrite modified defaults that came from a beatmap selection.
TransferSettings(difficulty);
}
}
/// <summary>
/// A <see cref="BindableDouble"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableDoubleWithLimitExtension : BindableDouble
{
public override double Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
/// <summary>
/// A <see cref="BindableFloat"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableFloatWithLimitExtension : BindableFloat
{
public override float Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
/// <summary>
/// A <see cref="BindableInt"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableIntWithLimitExtension : BindableInt
{
public override int Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value;
if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value;
}
}
}

View File

@ -6,6 +6,7 @@ using System.Diagnostics;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Scoring;
namespace osu.Game.Screens.Play
@ -27,7 +28,7 @@ namespace osu.Game.Screens.Play
if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
return null;
if (!(Ruleset.Value.ID is int rulesetId))
if (!(Ruleset.Value.ID is int rulesetId) || Ruleset.Value.ID > ILegacyRuleset.MAX_LEGACY_RULESET_ID)
return null;
return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash);

View File

@ -25,8 +25,11 @@ namespace osu.Game.Tests
protected override void SetupForRun()
{
base.SetupForRun();
Storage.DeleteDirectory(string.Empty);
// base call needs to be run *after* storage is emptied, as it updates the (static) logger's storage and may start writing
// log entries from another source if a unit test host is shared over multiple tests, causing a file access denied exception.
base.SetupForRun();
}
}
}

View File

@ -17,11 +17,11 @@ namespace osu.Game.Tests.Visual
public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
{
protected readonly Container HitObjectContainer;
private PlacementBlueprint currentBlueprint;
protected PlacementBlueprint CurrentBlueprint { get; private set; }
protected PlacementBlueprintTestScene()
{
Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
}
[BackgroundDependencyLoader]
@ -63,9 +63,9 @@ namespace osu.Game.Tests.Visual
protected void ResetPlacement()
{
if (currentBlueprint != null)
Remove(currentBlueprint);
Add(currentBlueprint = CreateBlueprint());
if (CurrentBlueprint != null)
Remove(CurrentBlueprint);
Add(CurrentBlueprint = CreateBlueprint());
}
public void Delete(HitObject hitObject)
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual
{
base.Update();
currentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(currentBlueprint));
CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint));
}
protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) =>

View File

@ -57,7 +57,9 @@ namespace osu.Game.Tests.Visual
protected void LoadPlayer()
{
var ruleset = Ruleset.Value.CreateInstance();
var ruleset = CreatePlayerRuleset();
Ruleset.Value = ruleset.RulesetInfo;
var beatmap = CreateBeatmap(ruleset.RulesetInfo);
Beatmap.Value = CreateWorkingBeatmap(beatmap);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual
});
}
protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, DrawableHitObject drawableObject)
protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, [CanBeNull] DrawableHitObject drawableObject = null)
{
Add(blueprint.With(d =>
{

View File

@ -35,8 +35,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.2.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.707.0" />
<PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.713.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
<PackageReference Include="Sentry" Version="3.6.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.707.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.713.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,12 +93,12 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.707.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.713.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.115.0" ExcludeAssets="all" />
<PackageReference Include="Realm" Version="10.2.1" />
<PackageReference Include="Realm" Version="10.3.0" />
</ItemGroup>
</Project>