同步更新

This commit is contained in:
LA
2026-01-17 21:21:02 +08:00
parent a8a839741f
commit 0e6fd45637
48 changed files with 811 additions and 360 deletions

View File

@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -32,7 +33,7 @@ namespace osu.Desktop.Security
{
public ElevatedPrivilegesNotification()
{
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator");
}
[BackgroundDependencyLoader]

View File

@@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.LAsEzExtensions.Audio;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Scoring;

View File

@@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PolygonGenerationPopover : OsuPopover
{
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
private SliderWithTextBoxInput<int> repeatCountInput = null!;
private SliderWithTextBoxInput<int> pointInput = null!;
private FormSliderBar<double> distanceSnapInput { get; set; } = null!;
private FormSliderBar<int> offsetAngleInput { get; set; } = null!;
private FormSliderBar<int> repeatCountInput { get; set; } = null!;
private FormSliderBar<int> pointInput { get; set; } = null!;
private RoundedButton commitButton = null!;
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
@@ -64,11 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
distanceSnapInput = new FormSliderBar<double>
{
Caption = "Distance snap",
Current = new BindableNumber<double>(1)
{
MinValue = 0.1,
@@ -76,37 +77,40 @@ namespace osu.Game.Rulesets.Osu.Edit
Precision = 0.1,
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
},
Instantaneous = true
TabbableContentContainer = this
},
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
offsetAngleInput = new FormSliderBar<int>
{
Caption = "Offset angle",
Current = new BindableNumber<int>
{
MinValue = 0,
MaxValue = 180,
Precision = 1
},
Instantaneous = true
TabbableContentContainer = this
},
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
repeatCountInput = new FormSliderBar<int>
{
Caption = "Repeats",
Current = new BindableNumber<int>(1)
{
MinValue = 1,
MaxValue = 10,
Precision = 1
},
Instantaneous = true
TabbableContentContainer = this
},
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
pointInput = new FormSliderBar<int>
{
Caption = "Vertices",
Current = new BindableNumber<int>(3)
{
MinValue = 3,
MaxValue = 32,
Precision = 1,
},
Instantaneous = true
TabbableContentContainer = this
},
commitButton = new RoundedButton
{

View File

@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private BindableNumber<float> xBindable = null!;
private BindableNumber<float> yBindable = null!;
private SliderWithTextBoxInput<float> xInput = null!;
private FormSliderBar<float> xInput { get; set; } = null!;
private OsuCheckbox relativeCheckbox = null!;
public PreciseMovementPopover()
@@ -52,31 +52,31 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
xInput = new SliderWithTextBoxInput<float>("X:")
xInput = new FormSliderBar<float>
{
Caption = "X",
Current = xBindable = new BindableNumber<float>
{
Precision = 1,
},
Instantaneous = true,
TabbableContentContainer = this,
TabbableContentContainer = this
},
new SliderWithTextBoxInput<float>("Y:")
new FormSliderBar<float>
{
Caption = "Y",
Current = yBindable = new BindableNumber<float>
{
Precision = 1,
},
Instantaneous = true,
TabbableContentContainer = this,
TabbableContentContainer = this
},
relativeCheckbox = new OsuCheckbox(false)
{
RelativeSizeAxes = Axes.X,
LabelText = "Relative movement",
LabelText = "Relative movement"
}
}
};

View File

@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, EditorOrigin.GridCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private FormSliderBar<float> angleInput { get; set; } = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
private RadioButton gridCentreButton = null!;
@@ -54,11 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
angleInput = new SliderWithTextBoxInput<float>("Angle (degrees):")
angleInput = new FormSliderBar<float>
{
Caption = "Angle (degrees)",
Current = new BindableNumber<float>
{
MinValue = -360,
@@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Precision = 1
},
KeyboardStep = 1f,
Instantaneous = true
TabbableContentContainer = this
},
rotationOrigin = new EditorRadioButtonCollection
{

View File

@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, EditorOrigin.GridCentre, true, true));
private SliderWithTextBoxInput<float> scaleInput = null!;
private FormSliderBar<float> scaleInput { get; set; } = null!;
private BindableNumber<float> scaleInputBindable = null!;
private EditorRadioButtonCollection scaleOrigin = null!;
@@ -66,11 +66,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
scaleInput = new SliderWithTextBoxInput<float>("Scale:")
scaleInput = new FormSliderBar<float>
{
Caption = "Scale",
Current = scaleInputBindable = new BindableNumber<float>
{
MinValue = 0.05f,
@@ -80,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Default = 1,
},
KeyboardStep = 0.01f,
Instantaneous = true
TabbableContentContainer = this
},
scaleOrigin = new EditorRadioButtonCollection
{

View File

@@ -287,9 +287,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (IsHovered)
{
// 记录延迟追踪按键输入
osu.Game.LAsEzExtensions.Audio.InputAudioLatencyTracker.Instance?.RecordKeyPress(e.Action == OsuAction.LeftButton ? osuTK.Input.Key.Z : osuTK.Input.Key.X);
Hit();
HitAction ??= e.Action;
return true;

View File

@@ -10,8 +10,6 @@ using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.LAsEzExtensions.Audio;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -96,31 +94,6 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
// Record input for latency tracking (map TaikoAction to a representative Key)
try
{
Key mapped = Key.Unknown;
switch (e.Action)
{
case TaikoAction.LeftCentre:
mapped = Key.D;
break;
case TaikoAction.RightCentre:
mapped = Key.J;
break;
case TaikoAction.LeftRim:
mapped = Key.F;
break;
case TaikoAction.RightRim:
mapped = Key.K;
break;
}
if (mapped != Key.Unknown)
InputAudioLatencyTracker.Instance?.RecordKeyPress(mapped);
}
catch { }
Play(triggerSource, hitType, strong);
lastHitTime = Time.Current;

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 osu.Framework.Allocation;
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.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFormButton : ThemeComparisonTestScene
{
public TestSceneFormButton()
: base(false)
{
}
protected override Drawable CreateContent() => new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new BackgroundBox
{
RelativeSizeAxes = Axes.Both,
},
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
new FormButton
{
Caption = "Button with default style",
Action = () => { },
},
new FormButton
{
Caption = "Button with default style",
Enabled = { Value = false },
},
new FormButton
{
Caption = "Button with custom style",
BackgroundColour = new OsuColour().DangerousButtonColour,
ButtonIcon = FontAwesome.Solid.Hamburger,
Action = () => { },
},
new FormButton
{
Caption = "Button with custom style",
BackgroundColour = new OsuColour().DangerousButtonColour,
ButtonIcon = FontAwesome.Solid.Hamburger,
Enabled = { Value = false },
},
new FormButton
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
BackgroundColour = new OsuColour().Blue3,
ButtonIcon = FontAwesome.Solid.Book,
Action = () => { },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
Action = () => { },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
Enabled = { Value = false },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().DangerousButtonColour,
Action = () => { },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().DangerousButtonColour,
Enabled = { Value = false },
},
new FormButton
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().Blue3,
Action = () => { },
},
new FormButton
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().Blue3,
Enabled = { Value = false },
},
},
},
},
}
}
};
private partial class BackgroundBox : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
}
}
}
}

View File

@@ -64,9 +64,11 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
[Test]
public void TestNubDoubleClickRevertToDefault()
[TestCase(false)]
[TestCase(true)]
public void TestNubDoubleClickRevertToDefault(bool transferValueOnCommit)
{
OsuSpriteText text;
FormSliderBar<float> slider = null!;
AddStep("create content", () =>
@@ -81,9 +83,11 @@ namespace osu.Game.Tests.Visual.UserInterface
Spacing = new Vector2(10),
Children = new Drawable[]
{
text = new OsuSpriteText(),
slider = new FormSliderBar<float>
{
Caption = "Slider",
TransferValueOnCommit = transferValueOnCommit,
Current = new BindableFloat
{
MinValue = 0,
@@ -94,6 +98,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
}
};
slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true);
});
AddStep("set slider to 1", () => slider.Current.Value = 1);

View File

@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class EmptyToast : Toast
{
public EmptyToast()
: base("", "", "")
: base("", "")
{
}
}
@@ -104,8 +104,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class LengthyToast : Toast
{
public LengthyToast()
: base("Toast with a very very very long text", "A very very very very very very long text also", "A very very very very very long shortcut")
: base("Toast with a very very very long text", "A very very very very very very long text also")
{
ExtraText = "A very very very very very long shortcut";
}
}

View File

@@ -1,131 +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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene
{
private SliderWithTextBoxInput<float> sliderWithTextBoxInput = null!;
private OsuSliderBar<float> slider => sliderWithTextBoxInput.ChildrenOfType<OsuSliderBar<float>>().Single();
private Nub nub => sliderWithTextBoxInput.ChildrenOfType<Nub>().Single();
private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType<OsuTextBox>().Single();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput<float>("Test Slider")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Current = new BindableFloat
{
MinValue = -5,
MaxValue = 5,
Precision = 0.2f
}
});
}
[Test]
public void TestNonInstantaneousMode()
{
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
[Test]
public void TestInstantaneousMode()
{
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
}
}

View File

@@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap,
GameHost? host)
{
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host, Realm);
}
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);

View File

@@ -47,12 +47,13 @@ namespace osu.Game.Beatmaps
private readonly LargeTextureStore beatmapPanelTextureStore;
private readonly ITrackStore trackStore;
private readonly IResourceStore<byte[]> files;
private readonly RealmAccess realm;
[CanBeNull]
private readonly GameHost host;
public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null,
GameHost host = null)
GameHost host = null, RealmAccess realm = null)
{
DefaultBeatmap = defaultBeatmap;
@@ -63,6 +64,7 @@ namespace osu.Game.Beatmaps
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files)));
this.trackStore = trackStore;
this.realm = realm;
}
public void Invalidate(BeatmapSetInfo info)
@@ -118,7 +120,7 @@ namespace osu.Game.Beatmaps
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager;
RealmAccess IStorageResourceProvider.RealmAccess => null!;
RealmAccess IStorageResourceProvider.RealmAccess => realm;
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);

View File

@@ -18,10 +18,10 @@ namespace osu.Game.Configuration
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
BeatmapWithStoryboard,
[Description("'EzResource/Webm/*.webm' from local folders")]
[Description("'EzResource/Webm/*' from osu data folder ")]
WebmSource,
[Description("'EzResource/BG/*.png' from local folders")]
[Description("'EzResource/BG/*' from local folders")]
Slides,
}
}

View File

@@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -198,7 +199,7 @@ namespace osu.Game.Database
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = $"Exporting {itemFilename}...",
Text = NotificationsStrings.FileExportOngoing(itemFilename),
};
PostNotification?.Invoke(notification);
@@ -225,7 +226,7 @@ namespace osu.Game.Database
throw;
}
notification.CompletionText = $"Exported {itemFilename}! Click to view.";
notification.CompletionText = NotificationsStrings.FileExportFinished(itemFilename);
notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename);
notification.State = ProgressNotificationState.Completed;
});

View File

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
using Realms;
@@ -83,7 +84,7 @@ namespace osu.Game.Database
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = $"Exporting {itemFilename}...",
Text = NotificationsStrings.FileExportOngoing(itemFilename),
};
PostNotification?.Invoke(notification);
@@ -106,7 +107,7 @@ namespace osu.Game.Database
throw;
}
notification.CompletionText = $"Exported {itemFilename}! Click to view.";
notification.CompletionText = NotificationsStrings.FileExportFinished(itemFilename);
notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename);
notification.State = ProgressNotificationState.Completed;
}

View File

@@ -17,6 +17,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -169,7 +170,7 @@ namespace osu.Game.Graphics
notificationOverlay.Post(new SimpleNotification
{
Text = $"Screenshot {filename} saved!",
Text = NotificationsStrings.ScreenshotSaved(filename),
Activated = () =>
{
storage.PresentFileExternally(filename);

View File

@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -12,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -28,62 +30,133 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Sets text inside the button.
/// </summary>
public LocalisableString ButtonText { get; init; }
public Action? Action { get; init; }
/// <summary>
/// Sets a custom button icon. Not shown when <see cref="ButtonText"/> is set.
/// </summary>
public IconUsage ButtonIcon { get; init; } = FontAwesome.Solid.ChevronRight;
private readonly Color4? backgroundColour;
/// <summary>
/// Sets a custom background colour for the button.
/// </summary>
public Color4? BackgroundColour
{
get => backgroundColour;
init
{
backgroundColour = value;
if (IsLoaded)
updateState();
}
}
/// <summary>
/// The action to invoke when the button is clicked.
/// </summary>
public Action? Action { get; set; }
/// <summary>
/// Whether the button is enabled.
/// </summary>
public readonly BindableBool Enabled = new BindableBool(true);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Container content = null!;
private Box background = null!;
private OsuTextFlowContainer text = null!;
private Button button = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Height = 50;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
InternalChildren = new Drawable[]
InternalChild = content = new Container
{
new Box
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 5,
CornerExponent = 2.5f,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
background = new Box
{
Left = 9,
Right = 5,
Vertical = 5,
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
Children = new Drawable[]
new TrianglesV2
{
new OsuTextFlowContainer
SpawnRatio = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background5),
},
new HoverClickSounds(HoverSampleSet.Button)
{
Enabled = { BindTarget = Enabled },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.45f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Caption,
Left = 9,
Right = 5,
Vertical = 5,
},
new Button
Children = new Drawable[]
{
Action = Action,
Text = ButtonText,
RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X,
Width = ButtonText == default ? 90 : 0.45f,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
text = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Caption,
},
button = new Button
{
Action = () => Action?.Invoke(),
Text = ButtonText,
Icon = ButtonIcon,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Enabled = { BindTarget = Enabled },
}
},
},
},
}
};
if (ButtonText == default)
{
text.Padding = new MarginPadding { Right = 100 };
button.Width = 90;
}
else
{
text.Width = 0.55f;
text.Padding = new MarginPadding { Right = 10 };
button.RelativeSizeAxes = Axes.X;
button.Width = 0.45f;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(_ => updateState(), true);
}
protected override bool OnHover(HoverEvent e)
@@ -98,12 +171,34 @@ namespace osu.Game.Graphics.UserInterfaceV2
updateState();
}
protected override bool OnClick(ClickEvent e)
{
if (Enabled.Value)
{
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
button.TriggerClick();
}
return true;
}
private void updateState()
{
BorderThickness = IsHovered ? 2 : 0;
text.Colour = Enabled.Value ? colourProvider.Content1 : colourProvider.Background1;
if (IsHovered)
BorderColour = colourProvider.Light4;
background.FadeColour(IsHovered
? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4)
: colourProvider.Background5, 200, Easing.OutQuint);
content.BorderThickness = IsHovered ? 2 : 0;
if (BackgroundColour != null)
{
button.BackgroundColour = BackgroundColour.Value;
content.BorderColour = Enabled.Value ? BackgroundColour.Value : Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1);
}
else
content.BorderColour = Enabled.Value ? colourProvider.Light4 : colourProvider.Dark1;
}
public partial class Button : OsuButton
@@ -125,6 +220,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
public IconUsage Icon { get; init; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider overlayColourProvider)
{
@@ -135,7 +232,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Add(new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronRight,
Icon = Icon,
Size = new Vector2(16),
Shadow = true,
Anchor = Anchor.Centre,

View File

@@ -40,6 +40,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
private FormDropdownHeader header = null!;
private const float header_menu_spacing = 5;
[BackgroundDependencyLoader]
private void load()
{
@@ -47,6 +49,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
header.Caption = Caption;
header.HintText = HintText;
// there's bottom margin applied inside the header to give spacing between the header and the menu.
// however when the menu is closed the extra spacing remains present. to remove it, apply negative bottom padding here.
Margin = new MarginPadding { Bottom = -header_menu_spacing };
}
protected override void LoadComplete()
@@ -145,6 +151,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
Masking = true;
CornerRadius = 5;
Margin = new MarginPadding { Bottom = header_menu_spacing };
Foreground.Padding = new MarginPadding(9);
Foreground.Children = new Drawable[]
{
@@ -278,11 +286,26 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void load(OverlayColourProvider colourProvider)
{
ItemsContainer.Padding = new MarginPadding(9);
Margin = new MarginPadding { Top = 5 };
MaskingContainer.BorderThickness = 2;
MaskingContainer.BorderColour = colourProvider.Highlight1;
}
protected override void AnimateOpen()
{
base.AnimateOpen();
// there's negative bottom margin applied on the whole dropdown control to remove extra spacing when the menu is closed.
// however, when the menu is open, we want spacing between the menu and the next control below it. therefore apply bottom margin here.
// we use a transform to keep the open animation smooth while margin is adjusted.
this.TransformTo(nameof(Margin), new MarginPadding { Bottom = header_menu_spacing }, 300, Easing.OutQuint);
}
protected override void AnimateClose()
{
base.AnimateClose();
this.TransformTo(nameof(Margin), new MarginPadding { Bottom = 0 }, 300, Easing.OutQuint);
}
}
}

View File

@@ -134,6 +134,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
private readonly Bindable<Language> currentLanguage = new Bindable<Language>();
public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true;
public FormSliderBar()
{
LabelFormat ??= defaultLabelFormat;
@@ -233,6 +235,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
TooltipFormat = TooltipFormat,
DisplayAsPercentage = DisplayAsPercentage,
PlaySamplesOnAdjust = PlaySamplesOnAdjust,
ResetToDefault = () =>
{
if (!IsDisabled)
SetDefault();
}
}
},
},
@@ -323,7 +330,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(textBox.Current.Value);
bindableDouble.Value = double.Parse(textBox.Current.Value) / (DisplayAsPercentage ? 100 : 1);
break;
case Bindable<float> bindableFloat:
bindableFloat.Value = float.Parse(textBox.Current.Value) / (DisplayAsPercentage ? 100 : 1);
break;
default:
@@ -390,7 +401,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
if (updatingFromTextBox) return;
textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS);
if (DisplayAsPercentage)
{
double floatValue = double.CreateTruncating(currentNumberInstantaneous.Value);
if (currentNumberInstantaneous.Value is int)
floatValue /= 100;
textBox.Text = floatValue.ToStandardFormattedString(Math.Max(0, OsuSliderBar<T>.MAX_DECIMAL_DIGITS - 2));
}
else
textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS);
valueLabel.Text = LabelFormat(currentNumberInstantaneous.Value);
}
@@ -400,9 +422,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
public BindableBool Focused { get; } = new BindableBool();
public BindableBool IsDragging { get; set; } = new BindableBool();
public BindableBool IsDragging { get; } = new BindableBool();
public Action? OnCommit { get; set; }
public Action? ResetToDefault { get; init; }
public Action? OnCommit { get; init; }
public sealed override LocalisableString TooltipText => base.TooltipText;
@@ -453,11 +477,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Padding = new MarginPadding { Horizontal = RangePadding, },
Child = nub = new InnerSliderNub
{
ResetToDefault = () =>
{
if (!Current.Disabled)
Current.SetDefault();
}
ResetToDefault = ResetToDefault,
}
},
sounds = new HoverClickSounds()

View File

@@ -146,14 +146,9 @@ Click to see what's new!", version);
public static LocalisableString FriendOffline(string info) => new TranslatableString(getKey(@"friend_offline"), @"Offline: {0}", info);
/// <summary>
/// "Connection to API was lost. Can't continue with online play."
/// "Connection to online services was interrupted. osu! will be operating with limited functionality."
/// </summary>
public static LocalisableString APIDisconnect => new TranslatableString(getKey(@"api_disconnect"), @"Connection to API was lost. Can't continue with online play.");
/// <summary>
/// "Connection to the multiplayer server was lost. Exiting multiplayer."
/// </summary>
public static LocalisableString MultiplayerDisconnect => new TranslatableString(getKey(@"multiplayer_disconnect"), @"Connection to the multiplayer server was lost. Exiting multiplayer.");
public static LocalisableString APIConnectionInterrupted => new TranslatableString(getKey(@"api_connection_interrupted"), @"Connection to online services was interrupted. osu! will be operating with limited functionality.");
/// <summary>
/// "You have been logged out on this device due to a login to your account on another device."
@@ -215,6 +210,56 @@ Click to see what's new!", version);
/// </summary>
public static LocalisableString ActionInterruptedByDialog => new TranslatableString(getKey(@"action_interrupted_by_dialog"), @"An action was interrupted due to a dialog being displayed.");
/// <summary>
/// "Exporting {0}..."
/// </summary>
public static LocalisableString FileExportOngoing(string filename) => new TranslatableString(getKey(@"file_export_ongoing"), @"Exporting {0}...", filename);
/// <summary>
/// "Exported {0}! Click to view."
/// </summary>
public static LocalisableString FileExportFinished(string filename) => new TranslatableString(getKey(@"file_export_finished"), @"Exported {0}! Click to view.", filename);
/// <summary>
/// "Exporting logs..."
/// </summary>
public static LocalisableString LogsExportOngoing => new TranslatableString(getKey(@"logs_export_ongoing"), @"Exporting logs...");
/// <summary>
/// "Exported logs! Click to view."
/// </summary>
public static LocalisableString LogsExportFinished => new TranslatableString(getKey(@"logs_export_finished"), @"Exported logs! Click to view.");
/// <summary>
/// "Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."
/// </summary>
public static LocalisableString ElevatedPrivileges(LocalisableString user) => new TranslatableString(getKey(@"elevated_privileges"), @"Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.", user);
/// <summary>
/// "Screenshot {0} saved!"
/// </summary>
public static LocalisableString ScreenshotSaved(string filename) => new TranslatableString(getKey(@"screenshot_saved"), @"Screenshot {0} saved!", filename);
/// <summary>
/// "The multiplayer server will be right back..."
/// </summary>
public static LocalisableString MultiplayerServerShuttingDownImmediately => new TranslatableString(getKey(@"multiplayer_server_shutting_down_immediately"), @"The multiplayer server will be right back...");
/// <summary>
/// "The multiplayer server is restarting in {0}."
/// </summary>
public static LocalisableString MultiplayerServerShuttingDownRemaining(string remainingTime) => new TranslatableString(getKey(@"multiplayer_server_shutting_down_remaining"), @"The multiplayer server is restarting in {0}.", remainingTime);
/// <summary>
/// "Created new collection &quot;{0}&quot; with {1} beatmaps."
/// </summary>
public static LocalisableString CollectionCreated(string name, int beatmapsCount) => new TranslatableString(getKey(@"collection_created"), @"Created new collection ""{0}"" with {1} beatmaps.", name, beatmapsCount);
/// <summary>
/// "Added {0} beatmaps to collection &quot;{1}&quot;."
/// </summary>
public static LocalisableString CollectionBeatmapsAdded(string name, int beatmapsCount) => new TranslatableString(getKey(@"collection_beatmaps_added"), @"Added {0} beatmaps to collection ""{1}"".", beatmapsCount, name);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Framework.Extensions.ExceptionExtensions;
@@ -20,13 +21,23 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(t.Exception != null);
Exception exception = t.Exception.AsSingular();
onError?.Invoke(exception);
if (exception is WebSocketException wse && wse.Message == @"The remote party closed the WebSocket connection without completing the close handshake.")
{
// OnlineStatusNotifier is already letting users know about interruptions to connections.
// Silence these because it gets very spammy otherwise.
return;
}
if (exception.GetHubExceptionMessage() is string message)
{
// Hub exceptions generally contain something we can show the user directly.
Logger.Log(message, level: LogLevel.Important);
else
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
return;
}
onError?.Invoke(exception);
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
}
else
{

View File

@@ -5,6 +5,7 @@ using System;
using Humanizer.Localisation;
using osu.Framework.Allocation;
using osu.Framework.Threading;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
@@ -53,10 +54,10 @@ namespace osu.Game.Online.Multiplayer
if (remaining.TotalSeconds <= 5)
{
updateDelegate?.Cancel();
Text = "The multiplayer server will be right back...";
Text = NotificationsStrings.MultiplayerServerShuttingDownImmediately;
}
else
Text = $"The multiplayer server is restarting in {HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second)}.";
Text = NotificationsStrings.MultiplayerServerShuttingDownRemaining(HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second));
}
}
}

View File

@@ -17,6 +17,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.Play;
namespace osu.Game.Online
{
@@ -75,22 +76,16 @@ namespace osu.Game.Online
apiState.BindValueChanged(state =>
{
if (state.NewValue == APIState.Online)
switch (state.NewValue)
{
userNotified = false;
return;
}
case APIState.Online:
userNotified = false;
return;
if (userNotified) return;
if (state.NewValue == APIState.Offline && getCurrentScreen() is OnlinePlayScreen)
{
userNotified = true;
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = NotificationsStrings.APIDisconnect,
});
case APIState.Offline:
if (getCurrentScreen() is OnlinePlayScreen)
notifyApiDisconnection();
break;
}
});
@@ -102,22 +97,37 @@ namespace osu.Game.Online
return;
}
if (userNotified) return;
if (multiplayerClient.Room != null)
{
userNotified = true;
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = NotificationsStrings.MultiplayerDisconnect,
});
}
notifyApiDisconnection();
}));
spectatorState.BindValueChanged(_ =>
spectatorState.BindValueChanged(connected => Schedule(() =>
{
// TODO: handle spectator server failure somehow?
if (connected.NewValue)
{
userNotified = false;
return;
}
switch (getCurrentScreen())
{
case SpectatorPlayer: // obvious issues
case SubmittingPlayer: // replay sending issues
notifyApiDisconnection();
break;
}
}));
}
private void notifyApiDisconnection()
{
if (userNotified) return;
userNotified = true;
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = NotificationsStrings.APIConnectionInterrupted,
});
}

View File

@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -86,7 +85,7 @@ namespace osu.Game.Overlays.Music
private readonly GlobalAction action;
public MusicActionToast(LocalisableString value, GlobalAction action)
: base(ToastStrings.MusicPlayback, value, string.Empty)
: base(ToastStrings.MusicPlayback, value)
{
this.action = action;
}
@@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader]
private void load(RealmKeyBindingStore keyBindingStore)
{
ShortcutText.Text = keyBindingStore.GetBindingsStringFor(action).ToUpper();
ExtraText = keyBindingStore.GetBindingsStringFor(action);
}
}
}

View File

@@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD
public partial class CopiedToClipboardToast : Toast
{
public CopiedToClipboardToast()
: base(CommonStrings.General, ToastStrings.CopiedToClipboard, "")
: base(CommonStrings.General, ToastStrings.CopiedToClipboard)
{
}
}

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 osu.Framework.Allocation;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
@@ -9,9 +10,15 @@ namespace osu.Game.Overlays.OSD
{
public partial class SpeedChangeToast : Toast
{
public SpeedChangeToast(RealmKeyBindingStore keyBindingStore, double newSpeed)
: base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed))
public SpeedChangeToast(double newSpeed)
: base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed))
{
}
[BackgroundDependencyLoader]
private void load(RealmKeyBindingStore keyBindingStore)
{
ExtraText = keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed);
}
}
}

View File

@@ -10,23 +10,30 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays.OSD
{
public abstract partial class Toast : Container
{
/// <summary>
/// Extra text to be shown at the bottom of the toast. Usually a key binding if available.
/// </summary>
public LocalisableString ExtraText
{
get => extraText.Text;
set => extraText.Text = value.ToUpper();
}
private const int toast_minimum_width = 240;
private readonly Container content;
protected override Container<Drawable> Content => content;
protected readonly OsuSpriteText ValueText;
protected readonly OsuSpriteText ValueSpriteText;
private readonly OsuSpriteText extraText;
protected readonly OsuSpriteText ShortcutText;
protected Toast(LocalisableString description, LocalisableString value, LocalisableString shortcut)
protected Toast(LocalisableString description, LocalisableString value)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@@ -65,7 +72,7 @@ namespace osu.Game.Overlays.OSD
Origin = Anchor.TopCentre,
Text = description.ToUpper()
},
ValueText = new OsuSpriteText
ValueSpriteText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Padding = new MarginPadding { Horizontal = 10 },
@@ -74,15 +81,14 @@ namespace osu.Game.Overlays.OSD
Origin = Anchor.Centre,
Text = value
},
ShortcutText = new OsuSpriteText
extraText = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Name = "Shortcut",
Name = "Extra Text",
Alpha = 0.3f,
Margin = new MarginPadding { Bottom = 15, Horizontal = 10 },
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = string.IsNullOrEmpty(shortcut.ToString()) ? ToastStrings.NoKeyBound.ToUpper() : shortcut.ToUpper()
},
};
}

View File

@@ -35,8 +35,10 @@ namespace osu.Game.Overlays.OSD
private Bindable<double?> lastPlaybackTime;
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
: base(description.Name, description.Value)
{
ExtraText = description.Shortcut;
FillFlowContainer<OptionLight> optionLights;
Children = new Drawable[]
@@ -75,7 +77,7 @@ namespace osu.Game.Overlays.OSD
break;
}
ValueText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
ValueSpriteText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
for (int i = 0; i < optionCount; i++)
optionLights.Add(new OptionLight { Glowing = i == selectedOption });

View File

@@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = "Exporting logs...",
Text = NotificationsStrings.LogsExportOngoing,
};
notifications?.Post(notification);
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
throw;
}
notification.CompletionText = "Exported logs! Click to view.";
notification.CompletionText = NotificationsStrings.LogsExportFinished;
notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename);
notification.State = ProgressNotificationState.Completed;

View File

@@ -772,8 +772,9 @@ namespace osu.Game.Overlays.SkinEditor
private partial class SkinEditorToast : Toast
{
public SkinEditorToast(LocalisableString value, string skinDisplayName)
: base(SkinSettingsStrings.SkinLayoutEditor, value, skinDisplayName)
: base(SkinSettingsStrings.SkinLayoutEditor, value)
{
ExtraText = skinDisplayName;
}
}

View File

@@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Edit
private readonly ValueChangedEvent<double> change;
public DistanceSpacingToast(LocalisableString value, ValueChangedEvent<double> change)
: base(getAction(change).GetLocalisableDescription(), value, string.Empty)
: base(getAction(change).GetLocalisableDescription(), value)
{
this.change = change;
}
@@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(RealmKeyBindingStore keyBindingStore)
{
ShortcutText.Text = keyBindingStore.GetBindingsStringFor(getAction(change)).ToUpper();
ExtraText = keyBindingStore.GetBindingsStringFor(getAction(change));
}
private static GlobalAction getAction(ValueChangedEvent<double> change) => change.NewValue - change.OldValue > 0

View File

@@ -19,7 +19,6 @@ using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.LAsEzExtensions.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Rulesets.Objects.Types;
@@ -741,9 +740,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate;
if (Result.HasResult)
{
UpdateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
}
OnNewResult?.Invoke(this, Result);
}

View File

@@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Components
set => current.Current = value;
}
public Func<FileInfo, string>? SampleAddRequested { get; init; }
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
public Action<string>? SampleRemoveRequested { get; init; }
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
@@ -194,7 +194,7 @@ namespace osu.Game.Screens.Edit.Components
/// <summary>
/// Invoked when a new sample is selected via this button.
/// </summary>
public Func<FileInfo, string>? SampleAddRequested { get; init; }
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
/// <summary>
/// Invoked when a sample removal is selected via this button.
@@ -294,7 +294,18 @@ namespace osu.Game.Screens.Edit.Components
AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button)));
sample = ActualFilename.Value == null ? null : editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
if (ActualFilename.Value != null)
{
// to cover all bases, invalidate the extensionless filename (which gameplay is most likely to use)
// as well as the filename with extension (which we are using here).
editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ExpectedFilename.Value);
editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ActualFilename.Value);
sample = editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
}
else
{
sample = null;
}
}
protected override bool OnHover(HoverEvent e)
@@ -317,7 +328,7 @@ namespace osu.Game.Screens.Edit.Components
return;
this.HidePopover();
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value) ?? selectedFile.Value.ToString();
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString();
}
private void deleteSample()

View File

@@ -1620,8 +1620,9 @@ namespace osu.Game.Screens.Edit
private partial class BeatmapEditorToast : Toast
{
public BeatmapEditorToast(LocalisableString value, string beatmapDisplayName)
: base(InputSettingsStrings.EditorSection, value, beatmapDisplayName)
: base(InputSettingsStrings.EditorSection, value)
{
ExtraText = beatmapDisplayName;
}
}

View File

@@ -101,11 +101,8 @@ namespace osu.Game.Screens.Edit
this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo;
if (beatmapSkin is Skin skin)
{
BeatmapSkin = new EditorBeatmapSkin(skin);
BeatmapSkin.BeatmapSkinChanged += SaveState;
}
if (beatmapSkin is LegacyBeatmapSkin skin)
BeatmapSkin = new EditorBeatmapSkin(this, skin);
beatmapProcessor = new EditorBeatmapProcessor(this, playableBeatmap.BeatmapInfo.Ruleset.CreateInstance());
@@ -532,5 +529,11 @@ namespace osu.Game.Screens.Edit
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;
public int BeatDivisor => beatDivisor?.Value ?? 1;
protected override void Dispose(bool isDisposing)
{
BeatmapSkin?.Dispose();
base.Dispose(isDisposing);
}
}
}

View File

@@ -18,14 +18,14 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// A beatmap skin which is being edited.
/// </summary>
public class EditorBeatmapSkin : ISkin
public class EditorBeatmapSkin : ISkin, IDisposable
{
public event Action? BeatmapSkinChanged;
/// <summary>
/// The underlying beatmap skin.
/// </summary>
protected internal readonly Skin Skin;
protected internal readonly LegacyBeatmapSkin Skin;
/// <summary>
/// The combo colours of this skin.
@@ -33,10 +33,13 @@ namespace osu.Game.Screens.Edit
/// </summary>
public BindableList<Colour4> ComboColours { get; }
public EditorBeatmapSkin(Skin skin)
{
Skin = skin;
private readonly EditorBeatmap editorBeatmap;
public EditorBeatmapSkin(EditorBeatmap editorBeatmap, LegacyBeatmapSkin skin)
{
this.editorBeatmap = editorBeatmap;
Skin = skin;
ComboColours = new BindableList<Colour4>();
if (Skin.Configuration.ComboColours is IReadOnlyList<Color4> comboColours)
@@ -50,9 +53,14 @@ namespace osu.Game.Screens.Edit
}
ComboColours.BindCollectionChanged((_, _) => updateColours());
if (skin.BeatmapSetResources != null)
skin.BeatmapSetResources.CacheInvalidated += InvokeSkinChanged;
}
private void invokeSkinChanged() => BeatmapSkinChanged?.Invoke();
public void InvokeSkinChanged() => BeatmapSkinChanged?.Invoke();
#region Combo colours
private void updateColours()
{
@@ -60,9 +68,14 @@ namespace osu.Game.Screens.Edit
Skin.Configuration.CustomComboColours.Clear();
for (int i = 0; i < ComboColours.Count; ++i)
Skin.Configuration.CustomComboColours.Add(ComboColours[(ComboColours.Count + i - 1) % ComboColours.Count]);
invokeSkinChanged();
InvokeSkinChanged();
editorBeatmap.SaveState();
}
#endregion
#region Sample sets
public record SampleSet(int SampleSetIndex, string Name)
{
public SampleSet(int sampleSetIndex)
@@ -88,7 +101,7 @@ namespace osu.Game.Screens.Edit
string[] possiblePrefixes = possibleSounds.SelectMany(sound => possibleBanks.Select(bank => $@"{bank}-{sound}")).ToArray();
HashSet<int> indices = new HashSet<int>();
Dictionary<int, SampleSet> sampleSets = new Dictionary<int, SampleSet>();
if (Skin.Samples != null)
{
@@ -96,19 +109,39 @@ namespace osu.Game.Screens.Edit
{
foreach (string possiblePrefix in possiblePrefixes)
{
if (!sample.StartsWith(possiblePrefix, StringComparison.InvariantCultureIgnoreCase))
if (!sample.StartsWith(possiblePrefix, StringComparison.Ordinal))
continue;
string indexString = Path.GetFileNameWithoutExtension(sample)[possiblePrefix.Length..];
int? index = null;
if (string.IsNullOrEmpty(indexString))
indices.Add(1);
if (int.TryParse(indexString, out int index))
indices.Add(index);
index = 1;
if (int.TryParse(indexString, out int parsed))
index = parsed;
if (!index.HasValue)
continue;
SampleSet? sampleSet;
if (!sampleSets.TryGetValue(index.Value, out sampleSet))
sampleSet = sampleSets[index.Value] = new SampleSet(index.Value);
sampleSet.Filenames.Add(sample);
}
}
}
return indices.OrderBy(i => i).Select(i => new SampleSet(i));
return sampleSets.OrderBy(i => i.Key).Select(i => i.Value);
}
#endregion
public void Dispose()
{
if (Skin.BeatmapSetResources != null)
Skin.BeatmapSetResources.CacheInvalidated -= InvokeSkinChanged;
Skin.Dispose();
}
#region Delegated ISkin implementation

View File

@@ -0,0 +1,144 @@
// 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.Globalization;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
public partial class FormSampleSetChooser : FormDropdown<EditorBeatmapSkin.SampleSet?>, IHasPopover
{
private EditorBeatmapSkin? beatmapSkin;
public FormSampleSetChooser()
{
Caption = "Custom sample sets";
}
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
beatmapSkin = editorBeatmap.BeatmapSkin;
}
protected override void LoadComplete()
{
base.LoadComplete();
populateItems();
if (beatmapSkin != null)
beatmapSkin.BeatmapSkinChanged += populateItems;
Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0);
Current.BindValueChanged(val =>
{
if (val.NewValue?.SampleSetIndex == -1)
this.ShowPopover();
});
}
private void populateItems()
{
var items = beatmapSkin?.GetAvailableSampleSets().ToList() ?? [];
items.Add(new EditorBeatmapSkin.SampleSet(-1, "Add new..."));
Items = items;
}
protected override LocalisableString GenerateItemText(EditorBeatmapSkin.SampleSet? item)
{
if (item == null)
return string.Empty;
return base.GenerateItemText(item);
}
public Popover GetPopover() => new NewSampleSetPopover(
Items.Any(i => i?.SampleSetIndex > 0) ? Items.Max(i => i!.SampleSetIndex) : 0,
idx =>
{
if (idx == null)
{
Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0);
return;
}
if (Items.SingleOrDefault(i => i?.SampleSetIndex == idx) is EditorBeatmapSkin.SampleSet existing)
{
Current.Value = existing;
return;
}
var sampleSet = new EditorBeatmapSkin.SampleSet(idx.Value, $@"Custom #{idx}");
var newItems = Items.ToList();
newItems.Insert(newItems.Count - 1, sampleSet);
Items = newItems;
Current.Value = sampleSet;
});
protected override void Dispose(bool isDisposing)
{
if (beatmapSkin != null)
beatmapSkin.BeatmapSkinChanged -= populateItems;
base.Dispose(isDisposing);
}
private partial class NewSampleSetPopover : OsuPopover
{
private readonly int currentLargestIndex;
private readonly Action<int?> onCommit;
private int? committedIndex;
private LabelledNumberBox numberBox = null!;
public NewSampleSetPopover(int currentLargestIndex, Action<int?> onCommit)
{
this.currentLargestIndex = currentLargestIndex;
this.onCommit = onCommit;
}
[BackgroundDependencyLoader]
private void load()
{
Child = numberBox = new LabelledNumberBox
{
RelativeSizeAxes = Axes.None,
Width = 250,
Label = "Sample set index",
Current = { Value = (currentLargestIndex + 1).ToString(CultureInfo.InvariantCulture) }
};
numberBox.OnCommit += (_, _) =>
{
committedIndex = int.Parse(numberBox.Current.Value);
Hide();
};
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
// avoids infinite refocus loop
if (committedIndex == null)
GetContainingFocusManager()?.ChangeFocus(numberBox);
}
public override void Hide()
{
if (State.Value == Visibility.Visible)
onCommit.Invoke(committedIndex > 0 ? committedIndex : null);
base.Hide();
}
}
}
}

View File

@@ -14,6 +14,7 @@ using osu.Game.Localisation;
using osu.Game.Models;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit.Components;
using osu.Game.Utils;
namespace osu.Game.Screens.Edit.Setup
@@ -23,6 +24,8 @@ namespace osu.Game.Screens.Edit.Setup
private FormBeatmapFileSelector audioTrackChooser = null!;
private FormBeatmapFileSelector backgroundChooser = null!;
private readonly Bindable<EditorBeatmapSkin.SampleSet?> currentSampleSet = new Bindable<EditorBeatmapSkin.SampleSet?>();
public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
[Resolved]
@@ -65,6 +68,27 @@ namespace osu.Game.Screens.Edit.Setup
Caption = EditorSetupStrings.AudioTrack,
PlaceholderText = EditorSetupStrings.ClickToSelectTrack,
},
new FormSampleSetChooser
{
Current = { BindTarget = currentSampleSet },
},
new FormSampleSet
{
Current = { BindTarget = currentSampleSet },
SampleAddRequested = (file, targetName) =>
{
string actualFilename = string.Concat(targetName, file.Extension);
using var stream = file.OpenRead();
beatmaps.AddFile(working.Value.BeatmapSetInfo, stream, actualFilename);
return actualFilename;
},
SampleRemoveRequested = filename =>
{
var file = working.Value.BeatmapSetInfo.GetFile(filename);
if (file != null)
beatmaps.DeleteFile(working.Value.BeatmapSetInfo, file);
}
},
};
backgroundChooser.PreviewContainer.Add(headerBackground);

View File

@@ -11,21 +11,24 @@ namespace osu.Game.Screens.Edit.Timing
{
internal partial class EffectSection : Section<EffectControlPoint>
{
private LabelledSwitchButton kiai = null!;
private FormCheckBox kiai = null!;
private SliderWithTextBoxInput<double> scrollSpeedSlider = null!;
private FormSliderBar<double> scrollSpeedSlider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
scrollSpeedSlider = new SliderWithTextBoxInput<double>("Scroll Speed")
kiai = new FormCheckBox { Caption = "Kiai Time" },
scrollSpeedSlider = new FormSliderBar<double>
{
Caption = "Scroll Speed",
Current = new EffectControlPoint().ScrollSpeedBindable,
KeyboardStep = 0.1f
}
KeyboardStep = 0.1f,
TransferValueOnCommit = true,
TabbableContentContainer = this
},
});
}

View File

@@ -17,7 +17,7 @@ using Vector2 = osuTK.Vector2;
namespace osu.Game.Screens.Edit.Timing
{
/// <summary>
/// Analogous to <see cref="SliderWithTextBoxInput{T}"/>, but supports scenarios
/// Analogous to SliderWithTextBoxInput, but supports scenarios
/// where multiple objects with multiple different property values are selected
/// by providing an "indeterminate state".
/// </summary>

View File

@@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -69,10 +70,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
countAfter = c.BeatmapMD5Hashes.Count;
}).ContinueWith(_ => Schedule(() =>
{
LocalisableString message;
if (countBefore == 0)
notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." });
message = NotificationsStrings.CollectionCreated(room.Name, countAfter);
else
notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." });
message = NotificationsStrings.CollectionBeatmapsAdded(room.Name, countAfter - countBefore);
notifications?.Post(new SimpleNotification { Text = message });
}));
};
}

View File

@@ -8,7 +8,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Input;
using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets.Mods;
@@ -21,9 +20,6 @@ namespace osu.Game.Screens.Select
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
[Resolved]
private RealmKeyBindingStore keyBindingStore { get; set; } = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
@@ -56,7 +52,7 @@ namespace osu.Game.Screens.Select
if (Precision.AlmostEquals(targetSpeed, 1, 0.005))
{
selectedMods.Value = selectedMods.Value.Where(m => m is not ModRateAdjust).ToList();
onScreenDisplay?.Display(new SpeedChangeToast(keyBindingStore, targetSpeed));
onScreenDisplay?.Display(new SpeedChangeToast(targetSpeed));
return true;
}
@@ -109,7 +105,7 @@ namespace osu.Game.Screens.Select
return false;
selectedMods.Value = intendedMods;
onScreenDisplay?.Display(new SpeedChangeToast(keyBindingStore, targetMod.SpeedChange.Value));
onScreenDisplay?.Display(new SpeedChangeToast(targetMod.SpeedChange.Value));
return true;
}
}

View File

@@ -206,8 +206,13 @@ namespace osu.Game.Screens.SelectV2
{
base.LoadComplete();
beatmap.BindValueChanged(_ => updateDisplay());
ruleset.BindValueChanged(_ => updateDisplay());
// it is not uncommon for the beatmap and the ruleset to change in conjunction during a single update frame.
// in that process, it is possible for the global bindable triad (beatmap / ruleset / mods) to briefly be partially invalid in combination (e.g. mods invalid for given ruleset).
// `updateDisplay()` will initiate a difficulty calculation, and if it is allowed to run in that invalid intermediate state, it will loudly fail.
// therefore, all changes that may initiate a difficulty calculation are debounced until the next frame to ensure the global bindable state is fully consistent -
// and it's what you'd want to do anyway for performance reasons.
beatmap.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay));
ruleset.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay));
mods.BindValueChanged(m =>
{

View File

@@ -26,6 +26,8 @@ namespace osu.Game.Skinning
// 2. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L158-L196 (user skin textures lookup)
protected override bool AllowHighResolutionSprites => false;
public RealmBackedResourceStore<BeatmapSetInfo>? BeatmapSetResources => FallbackStore as RealmBackedResourceStore<BeatmapSetInfo>;
/// <summary>
/// Construct a new legacy beatmap skin instance.
/// </summary>

View File

@@ -16,6 +16,8 @@ namespace osu.Game.Skinning
public class RealmBackedResourceStore<T> : ResourceStore<byte[]>
where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey
{
public event Action? CacheInvalidated;
private Lazy<Dictionary<string, string>> fileToStoragePathMapping;
private readonly Live<T> liveSource;
@@ -56,7 +58,11 @@ namespace osu.Game.Skinning
private string? getPathForFile(string filename) =>
fileToStoragePathMapping.Value.GetValueOrDefault(filename.ToLowerInvariant());
private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
private void invalidateCache()
{
fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
CacheInvalidated?.Invoke();
}
private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source =>
{

View File

@@ -63,6 +63,8 @@ namespace osu.Game.Skinning
public string Name { get; }
protected IResourceStore<byte[]>? FallbackStore { get; }
/// <summary>
/// Construct a new skin.
/// </summary>
@@ -102,6 +104,7 @@ namespace osu.Game.Skinning
SkinInfo = skin.ToLiveUnmanaged();
}
FallbackStore = fallbackStore;
if (fallbackStore != null)
store.AddStore(fallbackStore);
@@ -339,6 +342,7 @@ namespace osu.Game.Skinning
Textures?.Dispose();
Samples?.Dispose();
FallbackStore?.Dispose();
store.Dispose();
}