1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:47:27 +08:00

Merge branch 'master' into export

This commit is contained in:
Bartłomiej Dach 2023-05-06 20:12:53 +02:00
commit 65b4b198a4
No known key found for this signature in database
42 changed files with 1352 additions and 382 deletions

View File

@ -191,6 +191,8 @@ csharp_style_prefer_index_operator = false:silent
csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
csharp_style_namespace_declarations = block_scoped:warning
[*.{yaml,yml}]
insert_final_newline = true
indent_style = space

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.418.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.506.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -15,6 +15,10 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -22,6 +26,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Configuration;
namespace osu.Game.Rulesets.Osu.Tests
{
@ -30,6 +35,27 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
[SetUpSteps]
public void SetUpSteps()
{
AddToggleStep("toggle snaking", v =>
{
snakingIn.Value = v;
snakingOut.Value = v;
});
}
[BackgroundDependencyLoader]
private void load()
{
var config = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn);
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
[Test]
public void TestVariousSliders()
{

View File

@ -87,12 +87,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void UpdateInitialTransforms()
{
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
animDuration = Math.Min(300, HitObject.SpanDuration);
this.Animate(
d => d.FadeIn(animDuration),
d => d.ScaleTo(0.5f).ScaleTo(1f, animDuration * 2, Easing.OutElasticHalf)
);
this
.FadeOut()
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
.FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration);
}
protected override void UpdateHitStateTransforms(ArmedState state)

View File

@ -91,7 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateInitialTransforms();
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
CirclePiece
.FadeOut()
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
.FadeIn(HitObject.TimeFadeIn);
}
protected override void UpdateHitStateTransforms(ArmedState state)

View File

@ -39,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects
}
else
{
// taken from osu-stable
const float first_end_circle_preempt_adjust = 2 / 3f;
// The first end circle should fade in with the slider.
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
TimePreempt += StartTime - slider.StartTime;
}
}

View File

@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI
localCursorContainer?.Expire();
localCursorContainer = null;
GameplayCursor?.ActiveCursor?.Show();
GameplayCursor?.ActiveCursor.Show();
}
protected override bool OnHover(HoverEvent e) => true;

View File

@ -49,5 +49,31 @@ namespace osu.Game.Tests.Mods
Assert.That(mod3, Is.EqualTo(mod2));
Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2));
}
[Test]
public void TestModWithMultipleSettings()
{
var ruleset = new OsuRuleset();
var mod1 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 0 } };
var mod2 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } };
var mod3 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } };
var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
var doubleConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
var doubleConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
Assert.That(mod1, Is.Not.EqualTo(mod2));
Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doubleConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod2));
Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod3));
Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod3));
Assert.That(mod3, Is.EqualTo(mod2));
Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2));
}
}
}

View File

@ -2,6 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@ -10,7 +13,7 @@ namespace osu.Game.Tests.Mods
public class ModSettingsTest
{
[Test]
public void TestModSettingsUnboundWhenCopied()
public void TestModSettingsUnboundWhenCloned()
{
var original = new OsuModDoubleTime();
var copy = (OsuModDoubleTime)original.DeepClone();
@ -22,7 +25,7 @@ namespace osu.Game.Tests.Mods
}
[Test]
public void TestMultiModSettingsUnboundWhenCopied()
public void TestMultiModSettingsUnboundWhenCloned()
{
var original = new MultiMod(new OsuModDoubleTime());
var copy = (MultiMod)original.DeepClone();
@ -32,5 +35,67 @@ namespace osu.Game.Tests.Mods
Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0));
Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5));
}
[Test]
public void TestDifferentTypeSettingsKeptWhenCopied()
{
const double setting_change = 50.4;
var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } };
var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } };
var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } };
modDouble.CopyCommonSettingsFrom(modBool);
modDouble.CopyCommonSettingsFrom(modInt);
modBool.CopyCommonSettingsFrom(modDouble);
modBool.CopyCommonSettingsFrom(modInt);
modInt.CopyCommonSettingsFrom(modDouble);
modInt.CopyCommonSettingsFrom(modBool);
Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change));
Assert.That(modBool.TestSetting.Value, Is.EqualTo(true));
Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2));
}
[Test]
public void TestDefaultValueKeptWhenCopied()
{
var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } };
var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } };
modBoolFalse.CopyCommonSettingsFrom(modBoolTrue);
Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false));
Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value));
}
private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod
{
public override string Acronym => "NMD";
public override BindableNumber<double> TestSetting { get; } = new BindableDouble();
}
private class TestNonMatchingSettingTypeModInt : TestNonMatchingSettingTypeMod
{
public override string Acronym => "NMI";
public override BindableNumber<int> TestSetting { get; } = new BindableInt();
}
private class TestNonMatchingSettingTypeModBool : TestNonMatchingSettingTypeMod
{
public override string Acronym => "NMB";
public override Bindable<bool> TestSetting { get; } = new BindableBool();
}
private abstract class TestNonMatchingSettingTypeMod : Mod
{
public override string Name => "Non-matching setting type mod";
public override LocalisableString Description => "Description";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Conversion;
[SettingSource("Test setting")]
public abstract IBindable TestSetting { get; }
}
}
}

View File

@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
@ -182,6 +185,64 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("all boxes still selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(2));
}
[Test]
public void TestUndoEditHistory()
{
SkinComponentsContainer firstTarget = null!;
TestSkinEditorChangeHandler changeHandler = null!;
byte[] defaultState = null!;
IEnumerable<ISerialisableDrawable> testComponents = null!;
AddStep("Load necessary things", () =>
{
firstTarget = Player.ChildrenOfType<SkinComponentsContainer>().First();
changeHandler = new TestSkinEditorChangeHandler(firstTarget);
changeHandler.SaveState();
defaultState = changeHandler.GetCurrentState();
testComponents = new[]
{
targetContainer.Components.First(),
targetContainer.Components[targetContainer.Components.Count / 2],
targetContainer.Components.Last()
};
});
AddStep("Press undo", () => InputManager.Keys(PlatformAction.Undo));
AddAssert("Nothing changed", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
AddStep("Add components", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
revertAndCheckUnchanged();
AddStep("Move components", () =>
{
changeHandler.BeginChange();
testComponents.ForEach(c => ((Drawable)c).Position += Vector2.One);
changeHandler.EndChange();
});
revertAndCheckUnchanged();
AddStep("Select components", () => skinEditor.SelectedComponents.AddRange(testComponents));
AddStep("Bring to front", () => skinEditor.BringSelectionToFront());
revertAndCheckUnchanged();
AddStep("Remove components", () => testComponents.ForEach(c => firstTarget.Remove(c, false)));
revertAndCheckUnchanged();
void revertAndCheckUnchanged()
{
AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue));
AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
}
}
[TestCase(false)]
[TestCase(true)]
public void TestBringToFront(bool alterSelectionOrder)
@ -269,5 +330,23 @@ namespace osu.Game.Tests.Visual.Gameplay
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler
{
public TestSkinEditorChangeHandler(Drawable targetScreen)
: base(targetScreen)
{
}
public byte[] GetCurrentState()
{
using var stream = new MemoryStream();
WriteCurrentStateToStream(stream);
byte[] newState = stream.ToArray();
return newState;
}
}
}
}

View File

@ -8,10 +8,12 @@ using System.Linq;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@ -30,6 +32,7 @@ using osu.Game.Overlays.Chat.Listing;
using osu.Game.Overlays.Chat.ChannelList;
using osuTK;
using osuTK.Input;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.Online
{
@ -53,6 +56,9 @@ namespace osu.Game.Tests.Visual.Online
private int currentMessageId;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim();
[SetUp]
public void SetUp() => Schedule(() =>
{
@ -576,6 +582,75 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestChatReport()
{
ChatReportRequest request = null;
AddStep("Show overlay with channel", () =>
{
chatOverlay.Show();
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
});
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
waitForChannel1Visible();
AddStep("Setup request handling", () =>
{
requestLock.Reset();
dummyAPI.HandleRequest = r =>
{
if (!(r is ChatReportRequest req))
return false;
Task.Run(() =>
{
request = req;
requestLock.Wait(10000);
req.TriggerSuccess();
});
return true;
};
});
AddStep("Show report popover", () => this.ChildrenOfType<ChatLine>().First().ShowPopover());
AddStep("Set report reason to other", () =>
{
var reason = this.ChildrenOfType<OsuEnumDropdown<ChatReportReason>>().Single();
reason.Current.Value = ChatReportReason.Other;
});
AddStep("Try to report", () =>
{
var btn = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<RoundedButton>().Single();
InputManager.MoveMouseTo(btn);
InputManager.Click(MouseButton.Left);
});
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportChatPopover>().Any());
AddStep("Set report data", () =>
{
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
field.Current.Value = "test other";
});
AddStep("Try to report", () =>
{
var btn = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<RoundedButton>().Single();
InputManager.MoveMouseTo(btn);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("Overlay closed", () => !this.ChildrenOfType<ReportChatPopover>().Any());
AddStep("Complete request", () => requestLock.Set());
AddUntilStep("Request sent", () => request != null);
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
}
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));

View File

@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click delete", () =>
{
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().Single();
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(1);
InputManager.MoveMouseTo(deleteItem);
InputManager.Click(MouseButton.Left);
});
@ -261,6 +261,137 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("preset soft-deleted", () => Realm.Run(r => r.All<ModPreset>().Count(preset => preset.DeletePending) == 1));
}
[Test]
public void TestEditPresetName()
{
ModPresetColumn modPresetColumn = null!;
string presetName = null!;
ModPresetPanel panel = null!;
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddStep("right click first panel", () =>
{
panel = this.ChildrenOfType<ModPresetPanel>().First();
presetName = panel.Preset.Value.Name;
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Right);
});
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click edit", () =>
{
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
InputManager.MoveMouseTo(editItem);
InputManager.Click(MouseButton.Left);
});
OsuPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
AddStep("clear preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "");
AddStep("attempt preset edit", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName);
AddUntilStep("popover is unchanged", () => this.ChildrenOfType<OsuPopover>().FirstOrDefault() == popover);
AddStep("edit preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "something new");
AddStep("attempt preset edit", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName);
}
[Test]
public void TestEditPresetMod()
{
ModPresetColumn modPresetColumn = null!;
var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() };
List<Mod> previousMod = null!;
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddStep("right click first panel", () =>
{
var panel = this.ChildrenOfType<ModPresetPanel>().First();
previousMod = panel.Preset.Value.Mods.ToList();
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Right);
});
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click edit", () =>
{
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
InputManager.MoveMouseTo(editItem);
InputManager.Click(MouseButton.Left);
});
OsuPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
AddStep("click use current mods", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(0));
InputManager.Click(MouseButton.Left);
});
AddStep("attempt preset edit", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("preset mod not changed", () =>
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(previousMod));
AddStep("select mods", () => SelectedMods.Value = mods);
AddStep("right click first panel", () =>
{
var panel = this.ChildrenOfType<ModPresetPanel>().First();
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Right);
});
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click edit", () =>
{
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
InputManager.MoveMouseTo(editItem);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
AddStep("click use current mods", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(0));
InputManager.Click(MouseButton.Left);
});
AddStep("attempt preset edit", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("preset mod is changed", () =>
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods));
}
private ICollection<ModPreset> createTestPresets() => new[]
{
new ModPreset

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Input;
@ -385,6 +386,50 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("no mod selected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestKeepSharedSettingsFromSimilarMods()
{
const float setting_change = 1.2f;
createScreen();
changeRuleset(0);
AddStep("select difficulty adjust mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModDifficultyAdjust>()! });
changeRuleset(0);
AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModDifficultyAdjust);
AddStep("change mod settings", () =>
{
var osuMod = getSelectedMod<OsuModDifficultyAdjust>();
osuMod.ExtendedLimits.Value = true;
osuMod.CircleSize.Value = setting_change;
osuMod.DrainRate.Value = setting_change;
osuMod.OverallDifficulty.Value = setting_change;
osuMod.ApproachRate.Value = setting_change;
});
changeRuleset(1);
AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust);
AddAssert("shared settings preserved", () =>
{
var taikoMod = getSelectedMod<TaikoModDifficultyAdjust>();
return taikoMod.ExtendedLimits.Value &&
taikoMod.DrainRate.Value == setting_change &&
taikoMod.OverallDifficulty.Value == setting_change;
});
AddAssert("non-shared settings remain default", () =>
{
var taikoMod = getSelectedMod<TaikoModDifficultyAdjust>();
return taikoMod.ScrollSpeed.IsDefault;
});
}
[Test]
public void TestExternallySetCustomizedMod()
{
@ -617,6 +662,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active);
}
private T getSelectedMod<T>() where T : Mod => SelectedMods.Value.OfType<T>().Single();
private ModPanel getPanelForMod(Type modType)
=> modSelectOverlay.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);

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.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
/// <summary>
/// A generic popover for sending an online report about something.
/// </summary>
/// <typeparam name="TReportReason">An enumeration type with all valid reasons for the report.</typeparam>
public abstract partial class ReportPopover<TReportReason> : OsuPopover
where TReportReason : struct, Enum
{
/// <summary>
/// The action to run when the report is finalised.
/// The arguments to this action are: the reason for the report, and an optional additional comment.
/// </summary>
public Action<TReportReason, string>? Action;
private OsuEnumDropdown<TReportReason> reasonDropdown = null!;
private OsuTextBox commentsTextBox = null!;
private RoundedButton submitButton = null!;
private readonly LocalisableString header;
/// <summary>
/// Creates a new <see cref="ReportPopover{TReportReason}"/>.
/// </summary>
/// <param name="headerString">The text to display in the header of the popover.</param>
protected ReportPopover(LocalisableString headerString)
{
header = headerString;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
Direction = FillDirection.Vertical,
Width = 500,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new SpriteIcon
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Icon = FontAwesome.Solid.ExclamationTriangle,
Size = new Vector2(36),
},
new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = header,
Font = OsuFont.Torus.With(size: 25),
Margin = new MarginPadding { Bottom = 10 }
},
new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = UsersStrings.ReportReason,
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Child = reasonDropdown = new OsuEnumDropdown<TReportReason>
{
RelativeSizeAxes = Axes.X
}
},
new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = UsersStrings.ReportComments,
},
commentsTextBox = new OsuTextBox
{
RelativeSizeAxes = Axes.X,
PlaceholderText = UsersStrings.ReportPlaceholder,
},
submitButton = new RoundedButton
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Width = 200,
BackgroundColour = colours.Red3,
Text = UsersStrings.ReportActionsSend,
Action = () =>
{
Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text);
this.HidePopover();
},
Margin = new MarginPadding { Bottom = 5, Top = 10 },
}
}
};
commentsTextBox.Current.BindValueChanged(_ => updateStatus());
reasonDropdown.Current.BindValueChanged(_ => updateStatus());
updateStatus();
}
private void updateStatus()
{
submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || !IsCommentRequired(reasonDropdown.Current.Value);
}
/// <summary>
/// Determines whether an additional comment is required for submitting the report with the supplied <paramref name="reason"/>.
/// </summary>
protected virtual bool IsCommentRequired(TReportReason reason) => true;
}
}

View File

@ -34,6 +34,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString AddPreset => new TranslatableString(getKey(@"add_preset"), @"Add preset");
/// <summary>
/// "Use current mods"
/// </summary>
public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Overlays.Chat;
namespace osu.Game.Online.API.Requests
{
public class ChatReportRequest : APIRequest
{
public readonly long? MessageId;
public readonly ChatReportReason Reason;
public readonly string Comment;
public ChatReportRequest(long? id, ChatReportReason reason, string comment)
{
MessageId = id;
Reason = reason;
Comment = comment;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter(@"reportable_type", @"message");
req.AddParameter(@"reportable_id", $"{MessageId}");
req.AddParameter(@"reason", Reason.ToString());
req.AddParameter(@"comments", Comment);
return req;
}
protected override string Target => @"reports";
}
}

View File

@ -58,7 +58,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
using File = System.IO.File;
using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
@ -626,15 +625,22 @@ namespace osu.Game
return;
}
var previouslySelectedMods = SelectedMods.Value.ToArray();
if (!SelectedMods.Disabled)
SelectedMods.Value = Array.Empty<Mod>();
AvailableMods.Value = dict;
if (!SelectedMods.Disabled)
SelectedMods.Value = previouslySelectedMods.Select(m => instance.CreateModFromAcronym(m.Acronym)).Where(m => m != null).ToArray();
if (SelectedMods.Disabled)
return;
var convertedMods = SelectedMods.Value.Select(mod =>
{
var newMod = instance.CreateModFromAcronym(mod.Acronym);
newMod?.CopyCommonSettingsFrom(mod);
return newMod;
}).Where(newMod => newMod != null).ToList();
if (!ModUtils.CheckValidForGameplay(convertedMods, out var invalid))
invalid.ForEach(newMod => convertedMods.Remove(newMod));
SelectedMods.Value = convertedMods;
void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
}

View File

@ -1,25 +1,28 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Chat
{
public partial class ChatLine : CompositeDrawable
public partial class ChatLine : CompositeDrawable, IHasPopover
{
private Message message = null!;
@ -55,7 +58,7 @@ namespace osu.Game.Overlays.Chat
private readonly OsuSpriteText drawableTimestamp;
private readonly DrawableUsername drawableUsername;
private readonly DrawableChatUsername drawableUsername;
private readonly LinkFlowContainer drawableContentFlow;
@ -92,7 +95,7 @@ namespace osu.Game.Overlays.Chat
Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true,
},
drawableUsername = new DrawableUsername(message.Sender)
drawableUsername = new DrawableChatUsername(message.Sender)
{
Width = UsernameWidth,
FontSize = FontSize,
@ -100,6 +103,7 @@ namespace osu.Game.Overlays.Chat
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Margin = new MarginPadding { Horizontal = Spacing },
ReportRequested = this.ShowPopover,
},
drawableContentFlow = new LinkFlowContainer(styleMessageContent)
{
@ -128,6 +132,8 @@ namespace osu.Game.Overlays.Chat
FinishTransforms(true);
}
public Popover GetPopover() => new ReportChatPopover(message);
/// <summary>
/// Performs a highlight animation on this <see cref="ChatLine"/>.
/// </summary>

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Chat
{
/// <remarks>
/// References:
/// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L50
/// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L39
/// </remarks>
public enum ChatReportReason
{
[Description("Insulting People")]
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsInsults))]
Insults,
[Description("Spam")]
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsSpam))]
Spam,
[Description("Unwanted Content")]
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))]
UnwantedContent,
[Description("Nonsense")]
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsNonsense))]
Nonsense,
[Description("Other")]
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsOther))]
Other
}
}

View File

@ -29,8 +29,10 @@ using ChatStrings = osu.Game.Localisation.ChatStrings;
namespace osu.Game.Overlays.Chat
{
public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu
public partial class DrawableChatUsername : OsuClickableContainer, IHasContextMenu
{
public Action? ReportRequested;
public Color4 AccentColour { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
@ -75,7 +77,7 @@ namespace osu.Game.Overlays.Chat
private readonly Drawable colouredDrawable;
public DrawableUsername(APIUser user)
public DrawableChatUsername(APIUser user)
{
this.user = user;
@ -169,6 +171,9 @@ namespace osu.Game.Overlays.Chat
}));
}
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested));
return items.ToArray();
}
}

View File

@ -0,0 +1,41 @@
// 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.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Chat
{
public partial class ReportChatPopover : ReportPopover<ChatReportReason>
{
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private ChannelManager channelManager { get; set; } = null!;
private readonly Message message;
public ReportChatPopover(Message message)
: base(ReportStrings.UserTitle(message.Sender?.Username ?? @"Someone"))
{
this.message = message;
Action = report;
}
protected override bool IsCommentRequired(ChatReportReason reason) => reason == ChatReportReason.Other;
private void report(ChatReportReason reason, string comments)
{
var request = new ChatReportRequest(message.Id, reason, comments);
request.Success += () => channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString()));
api.Queue(request);
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
@ -134,9 +135,13 @@ namespace osu.Game.Overlays
},
Children = new Drawable[]
{
currentChannelContainer = new Container<DrawableChannel>
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
}
},
loading = new LoadingLayer(true),
channelListing = new ChannelListing

View File

@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Comments
link.AddLink(ReportStrings.CommentButton.ToLower(), this.ShowPopover);
}
public Popover GetPopover() => new ReportCommentPopover(comment)
{
Action = report
};
private void report(CommentReportReason reason, string comments)
{
var request = new CommentReportRequest(comment.Id, reason, comments);
@ -83,10 +88,5 @@ namespace osu.Game.Overlays.Comments
api.Queue(request);
}
public Popover GetPopover() => new ReportCommentPopover(comment)
{
Action = report
};
}
}

View File

@ -1,111 +1,17 @@
// 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.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Overlays.Comments
{
public partial class ReportCommentPopover : OsuPopover
public partial class ReportCommentPopover : ReportPopover<CommentReportReason>
{
public Action<CommentReportReason, string>? Action;
private readonly Comment? comment;
private OsuEnumDropdown<CommentReportReason> reasonDropdown = null!;
private OsuTextBox commentsTextBox = null!;
private RoundedButton submitButton = null!;
public ReportCommentPopover(Comment? comment)
: base(ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"))
{
this.comment = comment;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
Direction = FillDirection.Vertical,
Width = 500,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new SpriteIcon
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Icon = FontAwesome.Solid.ExclamationTriangle,
Size = new Vector2(36),
},
new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"),
Font = OsuFont.Torus.With(size: 25),
Margin = new MarginPadding { Bottom = 10 }
},
new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = UsersStrings.ReportReason,
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Child = reasonDropdown = new OsuEnumDropdown<CommentReportReason>
{
RelativeSizeAxes = Axes.X
}
},
new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = UsersStrings.ReportComments,
},
commentsTextBox = new OsuTextBox
{
RelativeSizeAxes = Axes.X,
PlaceholderText = UsersStrings.ReportPlaceholder,
},
submitButton = new RoundedButton
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Width = 200,
BackgroundColour = colours.Red3,
Text = UsersStrings.ReportActionsSend,
Action = () =>
{
Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text);
this.HidePopover();
},
Margin = new MarginPadding { Bottom = 5, Top = 10 },
}
}
};
commentsTextBox.Current.BindValueChanged(e =>
{
submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue);
}, true);
}
}
}

View File

@ -9,7 +9,6 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@ -67,7 +66,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = ModSelectOverlayStrings.AddPreset,
Action = tryCreatePreset
Action = createPreset
}
}
};
@ -89,16 +88,15 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
nameTextBox.Current.BindValueChanged(s =>
{
createButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue);
}, true);
}
private void tryCreatePreset()
private void createPreset()
{
if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value))
{
Body.Shake();
return;
}
realm.Write(r => r.Add(new ModPreset
{
Name = nameTextBox.Current.Value,

View File

@ -0,0 +1,172 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
internal partial class EditPresetPopover : OsuPopover
{
private LabelledTextBox nameTextBox = null!;
private LabelledTextBox descriptionTextBox = null!;
private ShearedButton useCurrentModsButton = null!;
private ShearedButton saveButton = null!;
private FillFlowContainer scrollContent = null!;
private readonly Live<ModPreset> preset;
private HashSet<Mod> saveableMods;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public EditPresetPopover(Live<ModPreset> preset)
{
this.preset = preset;
saveableMods = preset.PerformRead(p => p.Mods).ToHashSet();
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 300,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(7),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
nameTextBox = new LabelledTextBox
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Label = CommonStrings.Name,
TabbableContentContainer = this,
Current = { Value = preset.PerformRead(p => p.Name) },
},
descriptionTextBox = new LabelledTextBox
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Label = CommonStrings.Description,
TabbableContentContainer = this,
Current = { Value = preset.PerformRead(p => p.Description) },
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.X,
Height = 100,
Padding = new MarginPadding(7),
Child = scrollContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(7),
Spacing = new Vector2(7),
}
},
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(7),
Children = new Drawable[]
{
useCurrentModsButton = new ShearedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = ModSelectOverlayStrings.UseCurrentMods,
DarkerColour = colours.Blue1,
LighterColour = colours.Blue0,
TextColour = colourProvider.Background6,
Action = useCurrentMods,
},
saveButton = new ShearedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = Resources.Localisation.Web.CommonStrings.ButtonsSave,
DarkerColour = colours.Orange1,
LighterColour = colours.Orange0,
TextColour = colourProvider.Background6,
Action = save,
},
}
}
}
};
Body.BorderThickness = 3;
Body.BorderColour = colours.Orange1;
selectedMods.BindValueChanged(_ => updateState(), true);
nameTextBox.Current.BindValueChanged(s =>
{
saveButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue);
}, true);
}
private void useCurrentMods()
{
saveableMods = selectedMods.Value.ToHashSet();
updateState();
}
private void updateState()
{
scrollContent.ChildrenEnumerable = saveableMods.Select(mod => new ModPresetRow(mod));
useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved();
}
private bool checkSelectedModsDiffersFromSaved()
{
if (!selectedMods.Value.Any())
return false;
return !saveableMods.SetEquals(selectedMods.Value);
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
}
private void save()
{
preset.PerformWrite(s =>
{
s.Name = nameTextBox.Current.Value;
s.Description = descriptionTextBox.Current.Value;
s.Mods = saveableMods;
});
this.HidePopover();
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
@ -17,7 +18,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>, IHasContextMenu
public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>, IHasContextMenu, IHasPopover
{
public readonly Live<ModPreset> Preset;
@ -91,7 +92,8 @@ namespace osu.Game.Overlays.Mods
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset)))
new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover),
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))),
};
#endregion
@ -102,5 +104,7 @@ namespace osu.Game.Overlays.Mods
settingChangeTracker?.Dispose();
}
public Popover GetPopover() => new EditPresetPopover(Preset);
}
}

View File

@ -0,0 +1,64 @@
// 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.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public partial class ModPresetRow : FillFlowContainer
{
public ModPresetRow(Mod mod)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(4);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new ModSwitchTiny(mod)
{
Active = { Value = true },
Scale = new Vector2(0.6f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
}
}
};
if (!string.IsNullOrEmpty(mod.SettingDescription))
{
AddInternal(new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 14 },
Text = mod.SettingDescription
});
}
}
}
}

View File

@ -6,11 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
@ -61,55 +57,5 @@ namespace osu.Game.Overlays.Mods
protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private partial class ModPresetRow : FillFlowContainer
{
public ModPresetRow(Mod mod)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(4);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new ModSwitchTiny(mod)
{
Active = { Value = true },
Scale = new Vector2(0.6f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
}
}
};
if (!string.IsNullOrEmpty(mod.SettingDescription))
{
AddInternal(new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 14 },
Text = mod.SettingDescription
});
}
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -56,20 +57,53 @@ namespace osu.Game.Overlays.SkinEditor
if (deserializedContent == null)
return;
SerialisedDrawableInfo[] skinnableInfo = deserializedContent.ToArray();
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray();
SerialisedDrawableInfo[] skinnableInfos = deserializedContent.ToArray();
ISerialisableDrawable[] targetComponents = firstTarget.Components.ToArray();
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
// Store components based on type for later reuse
var componentsPerTypeLookup = new Dictionary<Type, Queue<Drawable>>();
foreach (ISerialisableDrawable component in targetComponents)
{
// Perform a naive full reload for now.
firstTarget.Reload(skinnableInfo);
Type lookup = component.GetType();
if (!componentsPerTypeLookup.TryGetValue(lookup, out Queue<Drawable>? componentsOfSameType))
componentsPerTypeLookup.Add(lookup, componentsOfSameType = new Queue<Drawable>());
componentsOfSameType.Enqueue((Drawable)component);
}
else
{
int i = 0;
foreach (var drawable in targetComponents)
drawable.ApplySerialisedInfo(skinnableInfo[i++]);
for (int i = targetComponents.Length - 1; i >= 0; i--)
firstTarget.Remove(targetComponents[i], false);
foreach (var skinnableInfo in skinnableInfos)
{
Type lookup = skinnableInfo.Type;
if (!componentsPerTypeLookup.TryGetValue(lookup, out Queue<Drawable>? componentsOfSameType))
{
firstTarget.Add((ISerialisableDrawable)skinnableInfo.CreateInstance());
continue;
}
// Wherever possible, attempt to reuse existing component instances.
if (componentsOfSameType.TryDequeue(out Drawable? component))
{
component.ApplySerialisedInfo(skinnableInfo);
}
else
{
component = skinnableInfo.CreateInstance();
}
firstTarget.Add((ISerialisableDrawable)component);
}
// Dispose components which were not reused.
foreach ((Type _, Queue<Drawable> typeComponents) in componentsPerTypeLookup)
{
foreach (var component in typeComponents)
component.Dispose();
}
}
}

View File

@ -1,146 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
namespace osu.Game.Rulesets.Edit
{
internal partial class HitObjectInspector : CompositeDrawable
{
private OsuTextFlowContainer inspectorText = null!;
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
InternalChild = inspectorText = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
EditorBeatmap.TransactionBegan += updateInspectorText;
EditorBeatmap.TransactionEnded += updateInspectorText;
updateInspectorText();
}
private ScheduledDelegate? rollingTextUpdate;
private void updateInspectorText()
{
inspectorText.Clear();
rollingTextUpdate?.Cancel();
rollingTextUpdate = null;
switch (EditorBeatmap.SelectedHitObjects.Count)
{
case 0:
addValue("No selection");
break;
case 1:
var selected = EditorBeatmap.SelectedHitObjects.Single();
addHeader("Type");
addValue($"{selected.GetType().ReadableName()}");
addHeader("Time");
addValue($"{selected.StartTime:#,0.##}ms");
switch (selected)
{
case IHasPosition pos:
addHeader("Position");
addValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
break;
case IHasXPosition x:
addHeader("Position");
addValue($"x:{x.X:#,0.##} ");
break;
case IHasYPosition y:
addHeader("Position");
addValue($"y:{y.Y:#,0.##}");
break;
}
if (selected is IHasDistance distance)
{
addHeader("Distance");
addValue($"{distance.Distance:#,0.##}px");
}
if (selected is IHasRepeats repeats)
{
addHeader("Repeats");
addValue($"{repeats.RepeatCount:#,0.##}");
}
if (selected is IHasDuration duration)
{
addHeader("End Time");
addValue($"{duration.EndTime:#,0.##}ms");
addHeader("Duration");
addValue($"{duration.Duration:#,0.##}ms");
}
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
// This is a good middle-ground for the time being.
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
break;
default:
addHeader("Selected Objects");
addValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
addHeader("Start Time");
addValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
addHeader("End Time");
addValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
break;
}
void addHeader(string header) => inspectorText.AddParagraph($"{header}: ", s =>
{
s.Padding = new MarginPadding { Top = 2 };
s.Font = s.Font.With(size: 12);
s.Colour = colourProvider.Content2;
});
void addValue(string value) => inspectorText.AddParagraph(value, s =>
{
s.Font = s.Font.With(weight: FontWeight.SemiBold);
s.Colour = colourProvider.Content1;
});
}
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Rulesets.UI;
using osu.Game.Utils;
@ -113,21 +114,29 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual Type[] IncompatibleMods => Array.Empty<Type>();
private IReadOnlyList<IBindable>? settingsBacking;
private IReadOnlyDictionary<string, IBindable>? settingsBacking;
/// <summary>
/// A list of the all <see cref="IBindable"/> settings within this mod.
/// All <see cref="IBindable"/> settings within this mod.
/// </summary>
internal IReadOnlyList<IBindable> Settings =>
/// <remarks>
/// The settings are returned in ascending key order as per <see cref="SettingsMap"/>.
/// The ordering is intentionally enforced manually, as ordering of <see cref="Dictionary{TKey,TValue}.Values"/> is unspecified.
/// </remarks>
internal IEnumerable<IBindable> SettingsBindables => SettingsMap.OrderBy(pair => pair.Key).Select(pair => pair.Value);
/// <summary>
/// Provides mapping of names to <see cref="IBindable"/>s of all settings within this mod.
/// </summary>
internal IReadOnlyDictionary<string, IBindable> SettingsMap =>
settingsBacking ??= this.GetSettingsSourceProperties()
.Select(p => p.Item2.GetValue(this))
.Cast<IBindable>()
.ToList();
.Select(p => p.Item2)
.ToDictionary(property => property.Name.ToSnakeCase(), property => (IBindable)property.GetValue(this)!);
/// <summary>
/// Whether all settings in this mod are set to their default state.
/// </summary>
protected virtual bool UsesDefaultConfiguration => Settings.All(s => s.IsDefault);
protected virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault);
/// <summary>
/// Creates a copy of this <see cref="Mod"/> initialised to a default state.
@ -148,15 +157,53 @@ namespace osu.Game.Rulesets.Mods
if (source.GetType() != GetType())
throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source));
foreach (var (_, prop) in this.GetSettingsSourceProperties())
foreach (var (_, property) in this.GetSettingsSourceProperties())
{
var targetBindable = (IBindable)prop.GetValue(this)!;
var sourceBindable = (IBindable)prop.GetValue(source)!;
var targetBindable = (IBindable)property.GetValue(this)!;
var sourceBindable = (IBindable)property.GetValue(source)!;
CopyAdjustedSetting(targetBindable, sourceBindable);
}
}
/// <summary>
/// This method copies the values of all settings from <paramref name="source"/> that share the same names with this mod instance.
/// The most frequent use of this is when switching rulesets, in order to preserve values of common settings during the switch.
/// </summary>
/// <remarks>
/// The values are copied directly, without adjusting for possibly different allowed ranges of values.
/// If the value of a setting is not valid for this instance due to not falling inside of the allowed range, it will be clamped accordingly.
/// </remarks>
/// <param name="source">The mod to extract settings from.</param>
public void CopyCommonSettingsFrom(Mod source)
{
if (source.UsesDefaultConfiguration)
return;
foreach (var (name, targetSetting) in SettingsMap)
{
if (!source.SettingsMap.TryGetValue(name, out IBindable? sourceSetting))
continue;
if (sourceSetting.IsDefault)
continue;
var targetBindableType = targetSetting.GetType();
var sourceBindableType = sourceSetting.GetType();
// if either the target is assignable to the source or the source is assignable to the target,
// then we presume that the data types contained in both bindables are compatible and we can proceed with the copy.
// this handles cases like `Bindable<int>` and `BindableInt`.
if (!targetBindableType.IsAssignableFrom(sourceBindableType) && !sourceBindableType.IsAssignableFrom(targetBindableType))
continue;
// TODO: special case for handling number types
PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable<bool>.Value))!;
property.SetValue(targetSetting, property.GetValue(sourceSetting));
}
}
/// <summary>
/// When creating copies or clones of a Mod, this method will be called
/// to copy explicitly adjusted user settings from <paramref name="target"/>.
@ -191,7 +238,7 @@ namespace osu.Game.Rulesets.Mods
if (ReferenceEquals(this, other)) return true;
return GetType() == other.GetType() &&
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
SettingsBindables.SequenceEqual(other.SettingsBindables, ModSettingsEqualityComparer.Default);
}
public override int GetHashCode()
@ -200,7 +247,7 @@ namespace osu.Game.Rulesets.Mods
hashCode.Add(GetType());
foreach (var setting in Settings)
foreach (var setting in SettingsBindables)
hashCode.Add(setting.GetUnderlyingSettingValue());
return hashCode.ToHashCode();

View File

@ -3,17 +3,18 @@
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Objects.Types;
/// <summary>
/// A HitObject that has a slider velocity multiplier.
/// </summary>
public interface IHasSliderVelocity
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// The slider velocity multiplier.
/// A HitObject that has a slider velocity multiplier.
/// </summary>
double SliderVelocity { get; set; }
public interface IHasSliderVelocity
{
/// <summary>
/// The slider velocity multiplier.
/// </summary>
double SliderVelocity { get; set; }
BindableNumber<double> SliderVelocityBindable { get; }
BindableNumber<double> SliderVelocityBindable { get; }
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Compose.Components
{
internal partial class EditorInspector : CompositeDrawable
{
protected OsuTextFlowContainer InspectorText = null!;
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
InternalChild = InspectorText = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
}
protected void AddHeader(string header) => InspectorText.AddParagraph($"{header}: ", s =>
{
s.Padding = new MarginPadding { Top = 2 };
s.Font = s.Font.With(size: 12);
s.Colour = colourProvider.Content2;
});
protected void AddValue(string value) => InspectorText.AddParagraph(value, s =>
{
s.Font = s.Font.With(weight: FontWeight.SemiBold);
s.Colour = colourProvider.Content1;
});
}
}

View File

@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Threading;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Compose.Components
{
internal partial class HitObjectInspector : EditorInspector
{
protected override void LoadComplete()
{
base.LoadComplete();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
EditorBeatmap.TransactionBegan += updateInspectorText;
EditorBeatmap.TransactionEnded += updateInspectorText;
updateInspectorText();
}
private ScheduledDelegate? rollingTextUpdate;
private void updateInspectorText()
{
InspectorText.Clear();
rollingTextUpdate?.Cancel();
rollingTextUpdate = null;
switch (EditorBeatmap.SelectedHitObjects.Count)
{
case 0:
AddValue("No selection");
break;
case 1:
var selected = EditorBeatmap.SelectedHitObjects.Single();
AddHeader("Type");
AddValue($"{selected.GetType().ReadableName()}");
AddHeader("Time");
AddValue($"{selected.StartTime:#,0.##}ms");
switch (selected)
{
case IHasPosition pos:
AddHeader("Position");
AddValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
break;
case IHasXPosition x:
AddHeader("Position");
AddValue($"x:{x.X:#,0.##} ");
break;
case IHasYPosition y:
AddHeader("Position");
AddValue($"y:{y.Y:#,0.##}");
break;
}
if (selected is IHasDistance distance)
{
AddHeader("Distance");
AddValue($"{distance.Distance:#,0.##}px");
}
if (selected is IHasSliderVelocity sliderVelocity)
{
AddHeader("Slider Velocity");
AddValue($"{sliderVelocity.SliderVelocity:#,0.00}x");
}
if (selected is IHasRepeats repeats)
{
AddHeader("Repeats");
AddValue($"{repeats.RepeatCount:#,0.##}");
}
if (selected is IHasDuration duration)
{
AddHeader("End Time");
AddValue($"{duration.EndTime:#,0.##}ms");
AddHeader("Duration");
AddValue($"{duration.Duration:#,0.##}ms");
}
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
// This is a good middle-ground for the time being.
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
break;
default:
AddHeader("Selected Objects");
AddValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
AddHeader("Start Time");
AddValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
AddHeader("End Time");
AddValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
break;
}
}
}
}

View File

@ -95,7 +95,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Text = "Hold shift while dragging the end of an object to adjust velocity while snapping."
}
},
new SliderVelocityInspector(),
}
}
};
@ -105,7 +106,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray();
// even if there are multiple objects selected, we can still display a value if they all have the same value.
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable : null;
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1
? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable
: null;
if (selectedPointBindable != null)
{
@ -139,4 +142,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
}
internal partial class SliderVelocityInspector : EditorInspector
{
[BackgroundDependencyLoader]
private void load()
{
EditorBeatmap.TransactionBegan += updateInspectorText;
EditorBeatmap.TransactionEnded += updateInspectorText;
updateInspectorText();
}
private void updateInspectorText()
{
InspectorText.Clear();
double[] sliderVelocities = EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>().Select(sv => sv.SliderVelocity).OrderBy(v => v).ToArray();
if (sliderVelocities.Length < 2)
return;
double? modeSliderVelocity = sliderVelocities.GroupBy(v => v).MaxBy(v => v.Count())?.Key;
double? medianSliderVelocity = sliderVelocities[sliderVelocities.Length / 2];
AddHeader("Average velocity");
AddValue($"{medianSliderVelocity:#,0.00}x");
AddHeader("Most used velocity");
AddValue($"{modeSliderVelocity:#,0.00}x");
AddHeader("Velocity range");
AddValue($"{sliderVelocities.First():#,0.00}x - {sliderVelocities.Last():#,0.00}x");
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
EditorBeatmap.TransactionBegan -= updateInspectorText;
EditorBeatmap.TransactionEnded -= updateInspectorText;
}
}
}

View File

@ -131,7 +131,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.DarkBlue,
Size = new Vector2(0.96f)
Size = OsuLogo.SCALE_ADJUST,
},
new Circle
{

View File

@ -35,6 +35,12 @@ namespace osu.Game.Screens.Menu
private const double transition_length = 300;
/// <summary>
/// The osu! logo sprite has a shadow included in its texture.
/// This adjustment vector is used to match the precise edge of the border of the logo.
/// </summary>
public static readonly Vector2 SCALE_ADJUST = new Vector2(0.96f);
private readonly Sprite logo;
private readonly CircularContainer logoContainer;
private readonly Container logoBounceContainer;
@ -150,7 +156,7 @@ namespace osu.Game.Screens.Menu
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Alpha = visualizer_default_alpha,
Size = new Vector2(0.96f)
Size = SCALE_ADJUST
},
new Container
{
@ -162,7 +168,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.88f),
Scale = SCALE_ADJUST,
Masking = true,
Children = new Drawable[]
{
@ -406,7 +412,7 @@ namespace osu.Game.Screens.Menu
public void Impact()
{
impactContainer.FadeOutFromOne(250, Easing.In);
impactContainer.ScaleTo(0.96f);
impactContainer.ScaleTo(SCALE_ADJUST);
impactContainer.ScaleTo(1.12f, 250);
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.Break
set
{
icon.Size = value;
base.Size = value + BlurSigma * 2.5f;
base.Size = value + BlurSigma * 5;
ForceRedraw();
}
get => base.Size;

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.418.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.506.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.417.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -16,6 +16,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.418.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.506.0" />
</ItemGroup>
</Project>

View File

@ -277,6 +277,7 @@
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Explicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/LOCAL_FUNCTION_BODY/@EntryValue">ExpressionBody</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">BlockBody</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/NAMESPACE_BODY/@EntryValue">BlockScoped</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_EVIDENT/@EntryValue">ExplicitlyTyped</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/USE_HEURISTICS_FOR_BODY_STYLE/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>