Merge branch 'master' into freemods

This commit is contained in:
smoogipoo 2021-02-05 00:27:14 +09:00
commit cf5233c6ab
18 changed files with 515 additions and 66 deletions

View File

@ -0,0 +1,31 @@
// 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.Allocation;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModConstantSpeed : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestConstantScroll() => CreateModTest(new ModTestData
{
Mod = new ManiaModConstantSpeed(),
PassCondition = () =>
{
var hitObject = Player.ChildrenOfType<DrawableManiaHitObject>().FirstOrDefault();
return hitObject?.Dependencies.Get<IScrollingInfo>().Algorithm is ConstantScrollAlgorithm;
}
});
}
}

View File

@ -238,6 +238,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModMirror(), new ManiaModMirror(),
new ManiaModDifficultyAdjust(), new ManiaModDifficultyAdjust(),
new ManiaModInvert(), new ManiaModInvert(),
new ManiaModConstantSpeed()
}; };
case ModType.Automation: case ModType.Automation:

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModConstantSpeed : Mod, IApplicableToDrawableRuleset<ManiaHitObject>
{
public override string Name => "Constant Speed";
public override string Acronym => "CS";
public override double ScoreMultiplier => 1;
public override string Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override ModType Type => ModType.Conversion;
public override bool Ranked => false;
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
maniaRuleset.ScrollMethod = ScrollVisualisationMethod.Constant;
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -11,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -49,6 +51,22 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
public ScrollVisualisationMethod ScrollMethod
{
get => scrollMethod;
set
{
if (IsLoaded)
throw new InvalidOperationException($"Can't alter {nameof(ScrollMethod)} after ruleset is already loaded");
scrollMethod = value;
}
}
private ScrollVisualisationMethod scrollMethod = ScrollVisualisationMethod.Sequential;
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly Bindable<double> configTimeRange = new BindableDouble(); private readonly Bindable<double> configTimeRange = new BindableDouble();

View File

@ -3,8 +3,12 @@
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -58,5 +62,45 @@ namespace osu.Game.Tests.NonVisual
AddStep("end operation", () => operation.Dispose()); AddStep("end operation", () => operation.Dispose());
AddAssert("operation is ended", () => !operationInProgress.Value); AddAssert("operation is ended", () => !operationInProgress.Value);
} }
[Test]
public void TestOperationDisposalAfterScreenExit()
{
TestScreenWithTracker screen = null;
OsuScreenStack stack;
IDisposable operation = null;
AddStep("create screen with tracker", () =>
{
Child = stack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both
};
stack.Push(screen = new TestScreenWithTracker());
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("begin operation", () => operation = screen.OngoingOperationTracker.BeginOperation());
AddAssert("operation in progress", () => screen.OngoingOperationTracker.InProgress.Value);
AddStep("dispose after screen exit", () =>
{
screen.Exit();
operation.Dispose();
});
AddAssert("operation ended", () => !screen.OngoingOperationTracker.InProgress.Value);
}
private class TestScreenWithTracker : OsuScreen
{
public OngoingOperationTracker OngoingOperationTracker { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
InternalChild = OngoingOperationTracker = new OngoingOperationTracker();
}
}
} }
} }

View File

@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Online namespace osu.Game.Tests.Online
{ {
[TestFixture] [TestFixture]
public class TestAPIModSerialization public class TestAPIModJsonSerialization
{ {
[Test] [Test]
public void TestAcronymIsPreserved() public void TestAcronymIsPreserved()

View File

@ -0,0 +1,139 @@
// 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 MessagePack;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
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.Online
{
[TestFixture]
public class TestAPIModMessagePackSerialization
{
[Test]
public void TestAcronymIsPreserved()
{
var apiMod = new APIMod(new TestMod());
var deserialized = MessagePackSerializer.Deserialize<APIMod>(MessagePackSerializer.Serialize(apiMod));
Assert.That(deserialized.Acronym, Is.EqualTo(apiMod.Acronym));
}
[Test]
public void TestRawSettingIsPreserved()
{
var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } });
var deserialized = MessagePackSerializer.Deserialize<APIMod>(MessagePackSerializer.Serialize(apiMod));
Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(2.0));
}
[Test]
public void TestConvertedModHasCorrectSetting()
{
var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } });
var deserialized = MessagePackSerializer.Deserialize<APIMod>(MessagePackSerializer.Serialize(apiMod));
var converted = (TestMod)deserialized.ToMod(new TestRuleset());
Assert.That(converted.TestSetting.Value, Is.EqualTo(2));
}
[Test]
public void TestDeserialiseTimeRampMod()
{
// Create the mod with values different from default.
var apiMod = new APIMod(new TestModTimeRamp
{
AdjustPitch = { Value = false },
InitialRate = { Value = 1.25 },
FinalRate = { Value = 0.25 }
});
var deserialised = MessagePackSerializer.Deserialize<APIMod>(MessagePackSerializer.Serialize(apiMod));
var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset());
Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false));
Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25));
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
{
new TestMod(),
new TestModTimeRamp(),
};
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 { get; } = string.Empty;
public override string ShortName { get; } = string.Empty;
}
private class TestMod : Mod
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
public BindableNumber<double> TestSetting { get; } = new BindableDouble
{
MinValue = 0,
MaxValue = 10,
Default = 5,
Precision = 0.01,
};
}
private class TestModTimeRamp : ModTimeRamp
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
}
}
}

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -46,6 +47,32 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show()); AddStep("show", () => modSelect.Show());
} }
[Test]
public void TestAnimationFlushOnClose()
{
changeRuleset(0);
AddStep("Select all fun mods", () =>
{
modSelect.ModSectionsContainer
.Single(c => c.ModType == ModType.DifficultyIncrease)
.SelectAll();
});
AddUntilStep("many mods selected", () => modDisplay.Current.Value.Count >= 5);
AddStep("trigger deselect and close overlay", () =>
{
modSelect.ModSectionsContainer
.Single(c => c.ModType == ModType.DifficultyIncrease)
.DeselectAll();
modSelect.Hide();
});
AddAssert("all mods deselected", () => modDisplay.Current.Value.Count == 0);
}
[Test] [Test]
public void TestOsuMods() public void TestOsuMods()
{ {
@ -145,11 +172,11 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); AddAssert("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime)); AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime));
AddAssert("double time not visible", () => modSelect.ChildrenOfType<ModButton>().All(b => !b.Mods.Any(m => m is OsuModDoubleTime))); AddUntilStep("double time not visible", () => modSelect.ChildrenOfType<ModButton>().All(b => !b.Mods.Any(m => m is OsuModDoubleTime)));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore))); AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
AddStep("make double time valid again", () => modSelect.IsValidMod = m => true); AddStep("make double time valid again", () => modSelect.IsValidMod = m => true);
AddAssert("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); AddUntilStep("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore))); AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
} }
@ -312,6 +339,9 @@ namespace osu.Game.Tests.Visual.UserInterface
public bool AllLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); public bool AllLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
public new FillFlowContainer<ModSection> ModSectionsContainer =>
base.ModSectionsContainer;
public ModButton GetModButton(Mod mod) public ModButton GetModButton(Mod mod)
{ {
var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type); var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type);

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -48,7 +49,7 @@ namespace osu.Game.Graphics.UserInterface
private SampleChannel sampleChecked; private SampleChannel sampleChecked;
private SampleChannel sampleUnchecked; private SampleChannel sampleUnchecked;
public OsuCheckbox() public OsuCheckbox(bool nubOnRight = true)
{ {
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -57,26 +58,42 @@ namespace osu.Game.Graphics.UserInterface
Children = new Drawable[] Children = new Drawable[]
{ {
labelText = new OsuTextFlowContainer labelText = new OsuTextFlowContainer(ApplyLabelParameters)
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding }
},
Nub = new Nub
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = nub_padding },
}, },
Nub = new Nub(),
new HoverClickSounds() new HoverClickSounds()
}; };
if (nubOnRight)
{
Nub.Anchor = Anchor.CentreRight;
Nub.Origin = Anchor.CentreRight;
Nub.Margin = new MarginPadding { Right = nub_padding };
labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
else
{
Nub.Anchor = Anchor.CentreLeft;
Nub.Origin = Anchor.CentreLeft;
Nub.Margin = new MarginPadding { Left = nub_padding };
labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
Nub.Current.BindTo(Current); Nub.Current.BindTo(Current);
Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1; Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
} }
/// <summary>
/// A function which can be overridden to change the parameters of the label's text.
/// </summary>
protected virtual void ApplyLabelParameters(SpriteText text)
{
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {

View File

@ -23,6 +23,7 @@ namespace osu.Game.Online.API
[JsonProperty("settings")] [JsonProperty("settings")]
[Key(1)] [Key(1)]
[MessagePackFormatter(typeof(ModSettingsDictionaryFormatter))]
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>(); public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
[JsonConstructor] [JsonConstructor]

View File

@ -0,0 +1,67 @@
// 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.Buffers;
using System.Collections.Generic;
using System.Text;
using MessagePack;
using MessagePack.Formatters;
using osu.Framework.Bindables;
namespace osu.Game.Online.API
{
public class ModSettingsDictionaryFormatter : IMessagePackFormatter<Dictionary<string, object>>
{
public void Serialize(ref MessagePackWriter writer, Dictionary<string, object> value, MessagePackSerializerOptions options)
{
var primitiveFormatter = PrimitiveObjectFormatter.Instance;
writer.WriteArrayHeader(value.Count);
foreach (var kvp in value)
{
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
writer.WriteString(in stringBytes);
switch (kvp.Value)
{
case Bindable<double> d:
primitiveFormatter.Serialize(ref writer, d.Value, options);
break;
case Bindable<int> i:
primitiveFormatter.Serialize(ref writer, i.Value, options);
break;
case Bindable<float> f:
primitiveFormatter.Serialize(ref writer, f.Value, options);
break;
case Bindable<bool> b:
primitiveFormatter.Serialize(ref writer, b.Value, options);
break;
default:
// fall back for non-bindable cases.
primitiveFormatter.Serialize(ref writer, kvp.Value, options);
break;
}
}
}
public Dictionary<string, object> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
var output = new Dictionary<string, object>();
int itemCount = reader.ReadArrayHeader();
for (int i = 0; i < itemCount; i++)
{
output[reader.ReadString()] =
PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options);
}
return output;
}
}
}

View File

@ -33,6 +33,8 @@ namespace osu.Game.Overlays.Mods
private CancellationTokenSource modsLoadCts; private CancellationTokenSource modsLoadCts;
protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0;
/// <summary> /// <summary>
/// True when all mod icons have completed loading. /// True when all mod icons have completed loading.
/// </summary> /// </summary>
@ -49,7 +51,11 @@ namespace osu.Game.Overlays.Mods
return new ModButton(m) return new ModButton(m)
{ {
SelectionChanged = Action, SelectionChanged = mod =>
{
ModButtonStateChanged(mod);
Action?.Invoke(mod);
},
}; };
}).ToArray(); }).ToArray();
@ -78,6 +84,10 @@ namespace osu.Game.Overlays.Mods
} }
} }
protected virtual void ModButtonStateChanged(Mod mod)
{
}
private ModButton[] buttons = Array.Empty<ModButton>(); private ModButton[] buttons = Array.Empty<ModButton>();
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
@ -94,43 +104,75 @@ namespace osu.Game.Overlays.Mods
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
private const double initial_multiple_selection_delay = 120;
private double selectionDelay = initial_multiple_selection_delay;
private double lastSelection;
private readonly Queue<Action> pendingSelectionOperations = new Queue<Action>();
protected override void Update()
{
base.Update();
if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay)
{
if (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
{
dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
selectionDelay = Math.Max(30, selectionDelay * 0.8f);
lastSelection = Time.Current;
}
else
{
// reset the selection delay after all animations have been completed.
// this will cause the next action to be immediately performed.
selectionDelay = initial_multiple_selection_delay;
}
}
}
/// <summary> /// <summary>
/// Selects all mods. /// Selects all mods.
/// </summary> /// </summary>
public void SelectAll() public void SelectAll()
{ {
pendingSelectionOperations.Clear();
foreach (var button in buttons.Where(b => !b.Selected)) foreach (var button in buttons.Where(b => !b.Selected))
button.SelectAt(0); pendingSelectionOperations.Enqueue(() => button.SelectAt(0));
} }
/// <summary> /// <summary>
/// Deselects all mods. /// Deselects all mods.
/// </summary> /// </summary>
/// <param name="immediate">Set to true to bypass animations and update selections immediately.</param> public void DeselectAll()
public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate); {
pendingSelectionOperations.Clear();
DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null));
}
/// <summary> /// <summary>
/// Deselect one or more mods in this section. /// Deselect one or more mods in this section.
/// </summary> /// </summary>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param> /// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
/// <param name="immediate">Set to true to bypass animations and update selections immediately.</param> /// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false) public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false)
{ {
int delay = 0;
foreach (var button in buttons) foreach (var button in buttons)
{ {
Mod selected = button.SelectedMod; if (button.SelectedMod == null) continue;
if (selected == null) continue;
foreach (var type in modTypes) foreach (var type in modTypes)
{ {
if (type.IsInstanceOfType(selected)) if (type.IsInstanceOfType(button.SelectedMod))
{ {
if (immediate) if (immediate)
button.Deselect(); button.Deselect();
else else
Scheduler.AddDelayed(button.Deselect, delay += 50); pendingSelectionOperations.Enqueue(button.Deselect);
} }
} }
} }
@ -197,5 +239,14 @@ namespace osu.Game.Overlays.Mods
Font = OsuFont.GetFont(weight: FontWeight.Bold), Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = text Text = text
}; };
/// <summary>
/// Play out all remaining animations immediately to leave mods in a good (final) state.
/// </summary>
public void FlushAnimation()
{
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
dequeuedAction();
}
} }
} }

View File

@ -14,7 +14,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.Framework.Threading;
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;
@ -380,6 +379,11 @@ namespace osu.Game.Overlays.Mods
{ {
base.PopOut(); base.PopOut();
foreach (var section in ModSectionsContainer)
{
section.FlushAnimation();
}
FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
@ -496,17 +500,12 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(Color4.White, 200); MultiplierLabel.FadeColour(Color4.White, 200);
} }
private ScheduledDelegate sampleOnDelegate;
private ScheduledDelegate sampleOffDelegate;
private void modButtonPressed(Mod selectedMod) private void modButtonPressed(Mod selectedMod)
{ {
if (selectedMod != null) if (selectedMod != null)
{ {
// Fixes buzzing when multiple mods are selected in the same frame.
sampleOnDelegate?.Cancel();
if (State.Value == Visibility.Visible) if (State.Value == Visibility.Visible)
sampleOnDelegate = Scheduler.Add(() => sampleOn?.Play()); Scheduler.AddOnce(playSelectedSound);
OnModSelected(selectedMod); OnModSelected(selectedMod);
@ -514,15 +513,16 @@ namespace osu.Game.Overlays.Mods
} }
else else
{ {
// Fixes buzzing when multiple mods are deselected in the same frame.
sampleOffDelegate?.Cancel();
if (State.Value == Visibility.Visible) if (State.Value == Visibility.Visible)
sampleOffDelegate = Scheduler.Add(() => sampleOff?.Play()); Scheduler.AddOnce(playDeselectedSound);
} }
refreshSelectedMods(); refreshSelectedMods();
} }
private void playSelectedSound() => sampleOn?.Play();
private void playDeselectedSound() => sampleOff?.Play();
/// <summary> /// <summary>
/// Invoked when a new <see cref="Mod"/> has been selected. /// Invoked when a new <see cref="Mod"/> has been selected.
/// </summary> /// </summary>

View File

@ -91,7 +91,11 @@ namespace osu.Game.Rulesets.UI.Scrolling
scrollingInfo = new LocalScrollingInfo(); scrollingInfo = new LocalScrollingInfo();
scrollingInfo.Direction.BindTo(Direction); scrollingInfo.Direction.BindTo(Direction);
scrollingInfo.TimeRange.BindTo(TimeRange); scrollingInfo.TimeRange.BindTo(TimeRange);
}
[BackgroundDependencyLoader]
private void load()
{
switch (VisualisationMethod) switch (VisualisationMethod)
{ {
case ScrollVisualisationMethod.Sequential: case ScrollVisualisationMethod.Sequential:
@ -106,11 +110,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
scrollingInfo.Algorithm = new ConstantScrollAlgorithm(); scrollingInfo.Algorithm = new ConstantScrollAlgorithm();
break; break;
} }
}
[BackgroundDependencyLoader]
private void load()
{
double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue; double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue;
double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH;

View File

@ -5,6 +5,8 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -69,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay
private void deselectAll() private void deselectAll()
{ {
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.DeselectAll(true); section.DeselectAll();
} }
protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); protected override ModSection CreateModSection(ModType type) => new FreeModSection(type);
@ -86,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override Drawable CreateHeader(string text) => new Container protected override Drawable CreateHeader(string text) => new Container
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Width = 175, RelativeSizeAxes = Axes.X,
Child = checkbox = new HeaderCheckbox Child = checkbox = new HeaderCheckbox
{ {
LabelText = text, LabelText = text,
@ -96,21 +98,21 @@ namespace osu.Game.Screens.OnlinePlay
private void onCheckboxChanged(bool value) private void onCheckboxChanged(bool value)
{ {
foreach (var button in ButtonsContainer.OfType<ModButton>()) if (value)
{ SelectAll();
if (value) else
button.SelectAt(0); DeselectAll();
else
button.Deselect();
}
} }
protected override void Update() protected override void ModButtonStateChanged(Mod mod)
{ {
base.Update(); base.ModButtonStateChanged(mod);
var validButtons = ButtonsContainer.OfType<ModButton>().Where(b => b.Mod.HasImplementation); if (!SelectionAnimationRunning)
checkbox.Current.Value = validButtons.All(b => b.Selected); {
var validButtons = ButtonsContainer.OfType<ModButton>().Where(b => b.Mod.HasImplementation);
checkbox.Current.Value = validButtons.All(b => b.Selected);
}
} }
} }
@ -120,6 +122,19 @@ namespace osu.Game.Screens.OnlinePlay
protected override bool PlaySoundsOnUserChange => false; protected override bool PlaySoundsOnUserChange => false;
public HeaderCheckbox()
: base(false)
{
}
protected override void ApplyLabelParameters(SpriteText text)
{
base.ApplyLabelParameters(text);
text.Font = OsuFont.GetFont(weight: FontWeight.Bold);
}
protected override void OnUserChange(bool value) protected override void OnUserChange(bool value)
{ {
base.OnUserChange(value); base.OnUserChange(value);

View File

@ -47,26 +47,21 @@ namespace osu.Game.Screens.OnlinePlay
private void endOperationWithKnownLease(LeasedBindable<bool> lease) private void endOperationWithKnownLease(LeasedBindable<bool> lease)
{ {
if (lease != leasedInProgress)
return;
// for extra safety, marshal the end of operation back to the update thread if necessary. // for extra safety, marshal the end of operation back to the update thread if necessary.
Scheduler.Add(() => Scheduler.Add(() =>
{ {
leasedInProgress?.Return(); if (lease != leasedInProgress)
return;
// UnbindAll() is purposefully used instead of Return() - the two do roughly the same thing, with one difference:
// the former won't throw if the lease has already been returned before.
// this matters because framework can unbind the lease via the internal UnbindAllBindables(), which is not always detectable
// (it is in the case of disposal, but not in the case of screen exit - at least not cleanly).
leasedInProgress?.UnbindAll();
leasedInProgress = null; leasedInProgress = null;
}, false); }, false);
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
// base call does an UnbindAllBindables().
// clean up the leased reference here so that it doesn't get returned twice.
leasedInProgress = null;
}
private class OngoingOperation : IDisposable private class OngoingOperation : IDisposable
{ {
private readonly OngoingOperationTracker tracker; private readonly OngoingOperationTracker tracker;

View File

@ -20,11 +20,13 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.6.3" /> <PackageReference Include="DiffPlex" Version="1.6.3" />
<PackageReference Include="Humanizer" Version="2.8.26" /> <PackageReference Include="Humanizer" Version="2.8.26" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.1.10" /> <PackageReference Include="MessagePack" Version="2.2.85" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.1.11" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="3.1.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.128.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.128.0" />

View File

@ -80,6 +80,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.0.3" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.0.3" />
<PackageReference Include="MessagePack" Version="1.7.3.7" />
<PackageReference Include="MessagePack.Annotations" Version="2.2.85" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">