diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 09ddc420a7..7a70257f3a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -187,28 +187,19 @@ namespace osu.Game.Rulesets.Osu.Edit if (b.IsSelected) continue; - var hitObject = (OsuHitObject)b.Item; + var snapPositions = b.ScreenSpaceSnapPoints; - Vector2? snap = checkSnap(hitObject.Position); - if (snap == null && hitObject.Position != hitObject.EndPosition) - snap = checkSnap(hitObject.EndPosition); + if (!snapPositions.Any()) + continue; - if (snap != null) + var closestSnapPosition = snapPositions.MinBy(p => Vector2.Distance(p, screenSpacePosition)); + + if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius) { // only return distance portion, since time is not really valid - snapResult = new SnapResult(snap.Value, null, playfield); + snapResult = new SnapResult(closestSnapPosition, null, playfield); return true; } - - Vector2? checkSnap(Vector2 checkPos) - { - Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos); - - if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius) - return checkScreenPos; - - return null; - } } snapResult = null; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumTouchInputArea.cs index 6514b760bb..5fb85df82b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumTouchInputArea.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumTouchInputArea.cs @@ -2,8 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Testing; +using osu.Game.Rulesets.Taiko.Configuration; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Tests.Visual; @@ -14,36 +17,48 @@ namespace osu.Game.Rulesets.Taiko.Tests { private DrumTouchInputArea drumTouchInputArea = null!; - [SetUpSteps] - public void SetUpSteps() + private readonly Bindable controlScheme = new Bindable(); + + [BackgroundDependencyLoader] + private void load() { - AddStep("create drum", () => + var config = (TaikoRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); + config.BindWith(TaikoRulesetSetting.TouchControlScheme, controlScheme); + } + + private void createDrum() + { + Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo) { - Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo) + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new InputDrum { - new InputDrum - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Height = 0.2f, - }, - drumTouchInputArea = new DrumTouchInputArea - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = 0.2f, }, - }; - }); + drumTouchInputArea = new DrumTouchInputArea + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + } + } + }; } [Test] public void TestDrum() { + AddStep("create drum", createDrum); AddStep("show drum", () => drumTouchInputArea.Show()); + + AddStep("change scheme (kddk)", () => controlScheme.Value = TaikoTouchControlScheme.KDDK); + AddStep("change scheme (kkdd)", () => controlScheme.Value = TaikoTouchControlScheme.KKDD); + AddStep("change scheme (ddkk)", () => controlScheme.Value = TaikoTouchControlScheme.DDKK); } + + protected override Ruleset CreateRuleset() => new TaikoRuleset(); } } diff --git a/osu.Game.Rulesets.Taiko/Configuration/TaikoRulesetConfigManager.cs b/osu.Game.Rulesets.Taiko/Configuration/TaikoRulesetConfigManager.cs new file mode 100644 index 0000000000..c3bc7f6439 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Configuration/TaikoRulesetConfigManager.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Configuration; +using osu.Game.Rulesets.Configuration; + +namespace osu.Game.Rulesets.Taiko.Configuration +{ + public class TaikoRulesetConfigManager : RulesetConfigManager + { + public TaikoRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null) + : base(settings, ruleset, variant) + { + } + + protected override void InitialiseDefaults() + { + base.InitialiseDefaults(); + + SetDefault(TaikoRulesetSetting.TouchControlScheme, TaikoTouchControlScheme.KDDK); + } + } + + public enum TaikoRulesetSetting + { + TouchControlScheme + } +} diff --git a/osu.Game.Rulesets.Taiko/Configuration/TaikoTouchControlScheme.cs b/osu.Game.Rulesets.Taiko/Configuration/TaikoTouchControlScheme.cs new file mode 100644 index 0000000000..74e4a53746 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Configuration/TaikoTouchControlScheme.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Taiko.Configuration +{ + public enum TaikoTouchControlScheme + { + KDDK, + DDKK, + KKDD + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 1d0f772db0..9944c0c6b7 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -28,9 +28,13 @@ using osu.Game.Rulesets.Taiko.Skinning.Argon; using osu.Game.Rulesets.Taiko.Skinning.Legacy; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; +using osu.Game.Overlays.Settings; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; +using osu.Game.Rulesets.Configuration; +using osu.Game.Configuration; +using osu.Game.Rulesets.Taiko.Configuration; namespace osu.Game.Rulesets.Taiko { @@ -194,6 +198,10 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); + public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo); + + public override RulesetSettingsSubsection CreateSettings() => new TaikoSettingsSubsection(this); + protected override IEnumerable GetValidHitResults() { return new[] diff --git a/osu.Game.Rulesets.Taiko/TaikoSettingsSubsection.cs b/osu.Game.Rulesets.Taiko/TaikoSettingsSubsection.cs new file mode 100644 index 0000000000..9fe861c08c --- /dev/null +++ b/osu.Game.Rulesets.Taiko/TaikoSettingsSubsection.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . 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.Localisation; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Taiko.Configuration; + +namespace osu.Game.Rulesets.Taiko +{ + public partial class TaikoSettingsSubsection : RulesetSettingsSubsection + { + protected override LocalisableString Header => "osu!taiko"; + + public TaikoSettingsSubsection(TaikoRuleset ruleset) + : base(ruleset) + { + } + + [BackgroundDependencyLoader] + private void load() + { + var config = (TaikoRulesetConfigManager)Config; + + Children = new Drawable[] + { + new SettingsEnumDropdown + { + LabelText = "Touch control scheme", + Current = config.GetBindable(TaikoRulesetSetting.TouchControlScheme) + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs index 0232c10d65..29ccd96675 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Rulesets.Taiko.Configuration; using osuTK; using osuTK.Graphics; @@ -31,15 +34,18 @@ namespace osu.Game.Rulesets.Taiko.UI private Container mainContent = null!; - private QuarterCircle leftCentre = null!; - private QuarterCircle rightCentre = null!; - private QuarterCircle leftRim = null!; - private QuarterCircle rightRim = null!; + private DrumSegment leftCentre = null!; + private DrumSegment rightCentre = null!; + private DrumSegment leftRim = null!; + private DrumSegment rightRim = null!; + + private readonly Bindable configTouchControlScheme = new Bindable(); [BackgroundDependencyLoader] - private void load(TaikoInputManager taikoInputManager, OsuColour colours) + private void load(TaikoInputManager taikoInputManager, TaikoRulesetConfigManager config) { Debug.Assert(taikoInputManager.KeyBindingContainer != null); + keyBindingContainer = taikoInputManager.KeyBindingContainer; // Container should handle input everywhere. @@ -65,27 +71,27 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - leftRim = new QuarterCircle(TaikoAction.LeftRim, colours.Blue) + leftRim = new DrumSegment { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomRight, X = -2, }, - rightRim = new QuarterCircle(TaikoAction.RightRim, colours.Blue) + rightRim = new DrumSegment { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomRight, X = 2, Rotation = 90, }, - leftCentre = new QuarterCircle(TaikoAction.LeftCentre, colours.Pink) + leftCentre = new DrumSegment { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomRight, X = -2, Scale = new Vector2(centre_region), }, - rightCentre = new QuarterCircle(TaikoAction.RightCentre, colours.Pink) + rightCentre = new DrumSegment { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomRight, @@ -98,6 +104,17 @@ namespace osu.Game.Rulesets.Taiko.UI } }, }; + + config.BindWith(TaikoRulesetSetting.TouchControlScheme, configTouchControlScheme); + configTouchControlScheme.BindValueChanged(scheme => + { + var actions = getOrderedActionsForScheme(scheme.NewValue); + + leftRim.Action = actions[0]; + leftCentre.Action = actions[1]; + rightCentre.Action = actions[2]; + rightRim.Action = actions[3]; + }, true); } protected override bool OnKeyDown(KeyDownEvent e) @@ -119,11 +136,47 @@ namespace osu.Game.Rulesets.Taiko.UI base.OnTouchUp(e); } + private static TaikoAction[] getOrderedActionsForScheme(TaikoTouchControlScheme scheme) + { + switch (scheme) + { + case TaikoTouchControlScheme.KDDK: + return new[] + { + TaikoAction.LeftRim, + TaikoAction.LeftCentre, + TaikoAction.RightCentre, + TaikoAction.RightRim + }; + + case TaikoTouchControlScheme.DDKK: + return new[] + { + TaikoAction.LeftCentre, + TaikoAction.RightCentre, + TaikoAction.LeftRim, + TaikoAction.RightRim + }; + + case TaikoTouchControlScheme.KKDD: + return new[] + { + TaikoAction.LeftRim, + TaikoAction.RightRim, + TaikoAction.LeftCentre, + TaikoAction.RightCentre + }; + + default: + throw new ArgumentOutOfRangeException(nameof(scheme), scheme, null); + } + } + private void handleDown(object source, Vector2 position) { Show(); - TaikoAction taikoAction = getTaikoActionFromInput(position); + TaikoAction taikoAction = getTaikoActionFromPosition(position); // Not too sure how this can happen, but let's avoid throwing. if (trackedActions.ContainsKey(source)) @@ -139,18 +192,15 @@ namespace osu.Game.Rulesets.Taiko.UI trackedActions.Remove(source); } - private bool validMouse(MouseButtonEvent e) => - leftRim.Contains(e.ScreenSpaceMouseDownPosition) || rightRim.Contains(e.ScreenSpaceMouseDownPosition); - - private TaikoAction getTaikoActionFromInput(Vector2 inputPosition) + private TaikoAction getTaikoActionFromPosition(Vector2 inputPosition) { bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition); bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2; if (leftSide) - return centreHit ? TaikoAction.LeftCentre : TaikoAction.LeftRim; + return centreHit ? leftCentre.Action : leftRim.Action; - return centreHit ? TaikoAction.RightCentre : TaikoAction.RightRim; + return centreHit ? rightCentre.Action : rightRim.Action; } protected override void PopIn() @@ -163,23 +213,42 @@ namespace osu.Game.Rulesets.Taiko.UI mainContent.FadeOut(300); } - private partial class QuarterCircle : CompositeDrawable, IKeyBindingHandler + private partial class DrumSegment : CompositeDrawable, IKeyBindingHandler { - private readonly Circle overlay; + private TaikoAction action; - private readonly TaikoAction handledAction; + public TaikoAction Action + { + get => action; + set + { + if (action == value) + return; - private readonly Circle circle; + action = value; + updateColoursFromAction(); + } + } + + private Circle overlay = null!; + + private Circle circle = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos); - public QuarterCircle(TaikoAction handledAction, Color4 colour) + public DrumSegment() { - this.handledAction = handledAction; RelativeSizeAxes = Axes.Both; FillMode = FillMode.Fit; + } + [BackgroundDependencyLoader] + private void load() + { InternalChildren = new Drawable[] { new Container @@ -191,7 +260,6 @@ namespace osu.Game.Rulesets.Taiko.UI circle = new Circle { RelativeSizeAxes = Axes.Both, - Colour = colour.Multiply(1.4f).Darken(2.8f), Alpha = 0.8f, Scale = new Vector2(2), }, @@ -200,7 +268,6 @@ namespace osu.Game.Rulesets.Taiko.UI Alpha = 0, RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, - Colour = colour, Scale = new Vector2(2), } } @@ -208,18 +275,52 @@ namespace osu.Game.Rulesets.Taiko.UI }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + updateColoursFromAction(); + } + public bool OnPressed(KeyBindingPressEvent e) { - if (e.Action == handledAction) + if (e.Action == Action) overlay.FadeTo(1f, 80, Easing.OutQuint); return false; } public void OnReleased(KeyBindingReleaseEvent e) { - if (e.Action == handledAction) + if (e.Action == Action) overlay.FadeOut(1000, Easing.OutQuint); } + + private void updateColoursFromAction() + { + if (!IsLoaded) + return; + + var colour = getColourFromTaikoAction(Action); + + circle.Colour = colour.Multiply(1.4f).Darken(2.8f); + overlay.Colour = colour; + } + + private Color4 getColourFromTaikoAction(TaikoAction handledAction) + { + switch (handledAction) + { + case TaikoAction.LeftRim: + case TaikoAction.RightRim: + return colours.Blue; + + case TaikoAction.LeftCentre: + case TaikoAction.RightCentre: + return colours.Pink; + } + + throw new ArgumentOutOfRangeException(); + } } } } diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 6750d74a08..68ece56d8a 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -294,8 +294,14 @@ namespace osu.Game.Graphics.Backgrounds vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); } + // Due to triangles having various sizes we would need to set a different "texelSize" value for each of them, which is insanely expensive, thus we should use one single value. + // texelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them. + // But we still need to specify at least something, because otherwise other shader usages will override this value. + float texelSize = 0f; + shader.Bind(); shader.GetUniform("thickness").UpdateValue(ref fill); + shader.GetUniform("texelSize").UpdateValue(ref texelSize); foreach (TriangleParticle particle in parts) { diff --git a/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs b/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs new file mode 100644 index 0000000000..2f77a287a0 --- /dev/null +++ b/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class BarHitErrorMeterStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.HUD.BarHitErrorMeter"; + + /// + /// "Judgement line thickness" + /// + public static LocalisableString JudgementLineThickness => new TranslatableString(getKey(@"judgement_line_thickness"), "Judgement line thickness"); + + /// + /// "How thick the individual lines should be." + /// + public static LocalisableString JudgementLineThicknessDescription => new TranslatableString(getKey(@"judgement_line_thickness_description"), "How thick the individual lines should be."); + + /// + /// "Show colour bars" + /// + public static LocalisableString ColourBarVisibility => new TranslatableString(getKey(@"colour_bar_visibility"), "Show colour bars"); + + /// + /// "Show moving average arrow" + /// + public static LocalisableString ShowMovingAverage => new TranslatableString(getKey(@"show_moving_average"), "Show moving average arrow"); + + /// + /// "Whether an arrow should move beneath the bar showing the average error." + /// + public static LocalisableString ShowMovingAverageDescription => new TranslatableString(getKey(@"show_moving_average_description"), "Whether an arrow should move beneath the bar showing the average error."); + + /// + /// "Centre marker style" + /// + public static LocalisableString CentreMarkerStyle => new TranslatableString(getKey(@"centre_marker_style"), "Centre marker style"); + + /// + /// "How to signify the centre of the display" + /// + public static LocalisableString CentreMarkerStyleDescription => new TranslatableString(getKey(@"centre_marker_style_description"), "How to signify the centre of the display"); + + /// + /// "None" + /// + public static LocalisableString CentreMarkerStylesNone => new TranslatableString(getKey(@"centre_marker_styles_none"), "None"); + + /// + /// "Circle" + /// + public static LocalisableString CentreMarkerStylesCircle => new TranslatableString(getKey(@"centre_marker_styles_circle"), "Circle"); + + /// + /// "Line" + /// + public static LocalisableString CentreMarkerStylesLine => new TranslatableString(getKey(@"centre_marker_styles_line"), "Line"); + + /// + /// "Label style" + /// + public static LocalisableString LabelStyle => new TranslatableString(getKey(@"label_style"), "Label style"); + + /// + /// "How to show early/late extremities" + /// + public static LocalisableString LabelStyleDescription => new TranslatableString(getKey(@"label_style_description"), "How to show early/late extremities"); + + /// + /// "None" + /// + public static LocalisableString LabelStylesNone => new TranslatableString(getKey(@"label_styles_none"), "None"); + + /// + /// "Icons" + /// + public static LocalisableString LabelStylesIcons => new TranslatableString(getKey(@"label_styles_icons"), "Icons"); + + /// + /// "Text" + /// + public static LocalisableString LabelStylesText => new TranslatableString(getKey(@"label_styles_text"), "Text"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/HUD/ColourHitErrorMeterStrings.cs b/osu.Game/Localisation/HUD/ColourHitErrorMeterStrings.cs new file mode 100644 index 0000000000..8fdcb34a49 --- /dev/null +++ b/osu.Game/Localisation/HUD/ColourHitErrorMeterStrings.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class ColourHitErrorMeterStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.HUD.ColourHitError"; + + /// + /// "Judgement count" + /// + public static LocalisableString JudgementCount => new TranslatableString(getKey(@"judgement_count"), "Judgement count"); + + /// + /// "The number of displayed judgements" + /// + public static LocalisableString JudgementCountDescription => new TranslatableString(getKey(@"judgement_count_description"), "The number of displayed judgements"); + + /// + /// "Judgement spacing" + /// + public static LocalisableString JudgementSpacing => new TranslatableString(getKey(@"judgement_spacing"), "Judgement spacing"); + + /// + /// "The space between each displayed judgement" + /// + public static LocalisableString JudgementSpacingDescription => new TranslatableString(getKey(@"judgement_spacing_description"), "The space between each displayed judgement"); + + /// + /// "Judgement shape" + /// + public static LocalisableString JudgementShape => new TranslatableString(getKey(@"judgement_shape"), "Judgement shape"); + + /// + /// "The shape of each displayed judgement" + /// + public static LocalisableString JudgementShapeDescription => new TranslatableString(getKey(@"judgement_shape_description"), "The shape of each displayed judgement"); + + /// + /// "Circle" + /// + public static LocalisableString ShapeStyleCircle => new TranslatableString(getKey(@"shape_style_cricle"), "Circle"); + + /// + /// "Square" + /// + public static LocalisableString ShapeStyleSquare => new TranslatableString(getKey(@"shape_style_square"), "Square"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/HUD/GameplayAccuracyCounterStrings.cs b/osu.Game/Localisation/HUD/GameplayAccuracyCounterStrings.cs new file mode 100644 index 0000000000..ec7f4a1af3 --- /dev/null +++ b/osu.Game/Localisation/HUD/GameplayAccuracyCounterStrings.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class GameplayAccuracyCounterStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.HUD.GameplayAccuracyCounter"; + + /// + /// "Accuracy display mode" + /// + public static LocalisableString AccuracyDisplay => new TranslatableString(getKey(@"accuracy_display"), "Accuracy display mode"); + + /// + /// "Which accuracy mode should be displayed." + /// + public static LocalisableString AccuracyDisplayDescription => new TranslatableString(getKey(@"accuracy_display_description"), "Which accuracy mode should be displayed."); + + /// + /// "Standard" + /// + public static LocalisableString AccuracyDisplayModeStandard => new TranslatableString(getKey(@"accuracy_display_mode_standard"), "Standard"); + + /// + /// "Maximum achievable" + /// + public static LocalisableString AccuracyDisplayModeMax => new TranslatableString(getKey(@"accuracy_display_mode_max"), "Maximum achievable"); + + /// + /// "Minimum achievable" + /// + public static LocalisableString AccuracyDisplayModeMin => new TranslatableString(getKey(@"accuracy_display_mode_min"), "Minimum achievable"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/HUD/JudgementCounterDisplayStrings.cs b/osu.Game/Localisation/HUD/JudgementCounterDisplayStrings.cs new file mode 100644 index 0000000000..b1c756e48e --- /dev/null +++ b/osu.Game/Localisation/HUD/JudgementCounterDisplayStrings.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class JudgementCounterDisplayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.HUD.JudgementCounterDisplay"; + + /// + /// "Display mode" + /// + public static LocalisableString JudgementDisplayMode => new TranslatableString(getKey(@"judgement_display_mode"), "Display mode"); + + /// + /// "Counter direction" + /// + public static LocalisableString FlowDirection => new TranslatableString(getKey(@"flow_direction"), "Counter direction"); + + /// + /// "Show judgement names" + /// + public static LocalisableString ShowJudgementNames => new TranslatableString(getKey(@"show_judgement_names"), "Show judgement names"); + + /// + /// "Show max judgement" + /// + public static LocalisableString ShowMaxJudgement => new TranslatableString(getKey(@"show_max_judgement"), "Show max judgement"); + + /// + /// "Simple" + /// + public static LocalisableString JudgementDisplayModeSimple => new TranslatableString(getKey(@"judgement_display_mode_simple"), "Simple"); + + /// + /// "Normal" + /// + public static LocalisableString JudgementDisplayModeNormal => new TranslatableString(getKey(@"judgement_display_mode_normal"), "Normal"); + + /// + /// "All" + /// + public static LocalisableString JudgementDisplayModeAll => new TranslatableString(getKey(@"judgement_display_mode_all"), "All"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/HUD/SongProgressStrings.cs b/osu.Game/Localisation/HUD/SongProgressStrings.cs new file mode 100644 index 0000000000..4c621e8e8c --- /dev/null +++ b/osu.Game/Localisation/HUD/SongProgressStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class SongProgressStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.HUD.SongProgress"; + + /// + /// "Show difficulty graph" + /// + public static LocalisableString ShowGraph => new TranslatableString(getKey(@"show_graph"), "Show difficulty graph"); + + /// + /// "Whether a graph displaying difficulty throughout the beatmap should be shown" + /// + public static LocalisableString ShowGraphDescription => new TranslatableString(getKey(@"show_graph_description"), "Whether a graph displaying difficulty throughout the beatmap should be shown"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs new file mode 100644 index 0000000000..b2e2285faf --- /dev/null +++ b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.SkinComponents +{ + public static class BeatmapAttributeTextStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SkinComponents.BeatmapAttributeText"; + + /// + /// "Attribute" + /// + public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute"); + + /// + /// "The attribute to be displayed." + /// + public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed."); + + /// + /// "Template" + /// + public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template"); + + /// + /// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)." + /// + public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs new file mode 100644 index 0000000000..547df86fc7 --- /dev/null +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.SkinComponents +{ + public static class SkinnableComponentStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SkinComponents.SkinnableComponentStrings"; + + /// + /// "Sprite name" + /// + public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), "Sprite name"); + + /// + /// "The filename of the sprite" + /// + public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), "The filename of the sprite"); + + /// + /// "Font" + /// + public static LocalisableString Font => new TranslatableString(getKey(@"font"), "Font"); + + /// + /// "The font to use." + /// + public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), "The font to use."); + + /// + /// "Text" + /// + public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), "Text"); + + /// + /// "The text to be displayed." + /// + public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed."); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs index b2e5f3aeb6..5cf2e5b5c5 100644 --- a/osu.Game/Localisation/SkinEditorStrings.cs +++ b/osu.Game/Localisation/SkinEditorStrings.cs @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Settings(string arg0) => new TranslatableString(getKey(@"settings"), @"Settings ({0})", arg0); + /// + /// "Currently editing" + /// + public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), "Currently editing"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 03917b75a4..4ac86924f8 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -36,21 +36,24 @@ namespace osu.Game.Overlays.Profile AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; - Masking = true; - CornerRadius = 10; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0, 1), - Radius = 3, - Colour = Colour4.Black.Opacity(0.25f) - }; - InternalChildren = new Drawable[] { - background = new Box + new Container { RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0, 1), + Radius = 3, + Colour = Colour4.Black.Opacity(0.25f) + }, + Child = background = new Box + { + RelativeSizeAxes = Axes.Both, + }, }, new FillFlowContainer { diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index 6017a98944..a8d64c1de8 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -40,7 +40,7 @@ namespace osu.Game.Overlays.SkinEditor RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(2) + Spacing = new Vector2(EditorSidebar.PADDING) }; reloadComponents(); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 51cca96f4a..3a10e58707 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -131,7 +131,7 @@ namespace osu.Game.Overlays.SkinEditor { Items = new[] { - new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, Save), + new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, revert), new EditorMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), @@ -284,13 +284,13 @@ namespace osu.Game.Overlays.SkinEditor headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16)); headerText.NewParagraph(); - headerText.AddText("Currently editing ", cp => + headerText.AddText(SkinEditorStrings.CurrentlyEditing, cp => { cp.Font = OsuFont.Default.With(size: 12); cp.Colour = colours.Yellow; }); - headerText.AddText($"{currentSkin.Value.SkinInfo}", cp => + headerText.AddText($" {currentSkin.Value.SkinInfo}", cp => { cp.Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold); cp.Colour = colours.Yellow; @@ -367,7 +367,7 @@ namespace osu.Game.Overlays.SkinEditor protected void Redo() => changeHandler?.RestoreState(1); - public void Save() + public void Save(bool userTriggered = true) { if (!hasBegunMutating) return; @@ -377,8 +377,9 @@ namespace osu.Game.Overlays.SkinEditor foreach (var t in targetContainers) currentSkin.Value.UpdateDrawableTarget(t); - skins.Save(skins.CurrentSkin.Value); - onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString() ?? "Unknown")); + // In the case the save was user triggered, always show the save message to make them feel confident. + if (skins.Save(skins.CurrentSkin.Value) || userTriggered) + onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString() ?? "Unknown")); } protected override bool OnHover(HoverEvent e) => true; diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 4eff87c5f4..c87e60e47f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.SkinEditor if (skinEditor == null) return; - skinEditor.Save(); + skinEditor.Save(userTriggered: false); // ensure the toolbar is re-hidden even if a new screen decides to try and show it. updateComponentVisibility(); diff --git a/osu.Game/Screens/Edit/Components/EditorSidebar.cs b/osu.Game/Screens/Edit/Components/EditorSidebar.cs index 9a8c5115dd..24e21ceafe 100644 --- a/osu.Game/Screens/Edit/Components/EditorSidebar.cs +++ b/osu.Game/Screens/Edit/Components/EditorSidebar.cs @@ -18,6 +18,8 @@ namespace osu.Game.Screens.Edit.Components { public const float WIDTH = 250; + public const float PADDING = 3; + private readonly Box background; protected override Container Content { get; } @@ -35,13 +37,13 @@ namespace osu.Game.Screens.Edit.Components }, new OsuScrollContainer { - Padding = new MarginPadding { Left = 20 }, ScrollbarOverlapsContent = false, RelativeSizeAxes = Axes.Both, Child = Content = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(PADDING), Direction = FillDirection.Vertical, }, } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 836fceea22..453e4b9130 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -91,6 +91,8 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.DrawableObject = drawableObject; } + private bool nudgeMovementActive; + protected override bool OnKeyDown(KeyDownEvent e) { if (e.ControlPressed) @@ -98,19 +100,19 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Key) { case Key.Left: - moveSelection(new Vector2(-1, 0)); + nudgeSelection(new Vector2(-1, 0)); return true; case Key.Right: - moveSelection(new Vector2(1, 0)); + nudgeSelection(new Vector2(1, 0)); return true; case Key.Up: - moveSelection(new Vector2(0, -1)); + nudgeSelection(new Vector2(0, -1)); return true; case Key.Down: - moveSelection(new Vector2(0, 1)); + nudgeSelection(new Vector2(0, 1)); return true; } } @@ -118,12 +120,29 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (nudgeMovementActive && !e.ControlPressed) + { + Beatmap.EndChange(); + nudgeMovementActive = false; + } + } + /// /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). /// /// - private void moveSelection(Vector2 delta) + private void nudgeSelection(Vector2 delta) { + if (!nudgeMovementActive) + { + nudgeMovementActive = true; + Beatmap.BeginChange(); + } + var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); if (firstBlueprint == null) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index bfee6d295e..9dce8996c3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Play.HUD @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD private const float bar_height = 10; - [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")] + [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] public Bindable ShowGraph { get; } = new BindableBool(true); [Resolved] diff --git a/osu.Game/Screens/Play/HUD/BPMCounter.cs b/osu.Game/Screens/Play/HUD/BPMCounter.cs index c07b203341..500ab0169f 100644 --- a/osu.Game/Screens/Play/HUD/BPMCounter.cs +++ b/osu.Game/Screens/Play/HUD/BPMCounter.cs @@ -37,11 +37,8 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); - //We don't want it going to 0 when we pause. so we block the updates - if (gameplayClock.IsPaused.Value) return; - // We want to check Rate every update to cover windup/down - Current.Value = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.Rate; + Current.Value = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.GetTrueGameplayRate(); } protected override OsuSpriteText CreateSpriteText() diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 91a34fe374..6eed563703 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Objects; using osuTK; @@ -26,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD private readonly DefaultSongProgressGraph graph; private readonly SongProgressInfo info; - [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")] + [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] public Bindable ShowGraph { get; } = new BindableBool(true); [Resolved] diff --git a/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs index ca7fef8f73..9da032e489 100644 --- a/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs @@ -1,18 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { public abstract partial class GameplayAccuracyCounter : PercentageCounter { - [SettingSource("Accuracy display mode", "Which accuracy mode should be displayed.")] + [SettingSource(typeof(GameplayAccuracyCounterStrings), nameof(GameplayAccuracyCounterStrings.AccuracyDisplay), nameof(GameplayAccuracyCounterStrings.AccuracyDisplayDescription))] public Bindable AccuracyDisplay { get; } = new Bindable(); [Resolved] @@ -51,13 +52,13 @@ namespace osu.Game.Screens.Play.HUD public enum AccuracyDisplayMode { - [Description("Standard")] + [LocalisableDescription(typeof(GameplayAccuracyCounterStrings), nameof(GameplayAccuracyCounterStrings.AccuracyDisplayModeStandard))] Standard, - [Description("Maximum achievable")] + [LocalisableDescription(typeof(GameplayAccuracyCounterStrings), nameof(GameplayAccuracyCounterStrings.AccuracyDisplayModeMax))] MaximumAchievable, - [Description("Minimum achievable")] + [LocalisableDescription(typeof(GameplayAccuracyCounterStrings), nameof(GameplayAccuracyCounterStrings.AccuracyDisplayModeMin))] MinimumAchievable } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 0337f66bd9..eb5221aa45 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -12,10 +12,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; @@ -25,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Cached] public partial class BarHitErrorMeter : HitErrorMeter { - [SettingSource("Judgement line thickness", "How thick the individual lines should be.")] + [SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.JudgementLineThickness), nameof(BarHitErrorMeterStrings.JudgementLineThicknessDescription))] public BindableNumber JudgementLineThickness { get; } = new BindableNumber(4) { MinValue = 1, @@ -33,16 +35,16 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters Precision = 0.1f, }; - [SettingSource("Show colour bars")] + [SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.ColourBarVisibility))] public Bindable ColourBarVisibility { get; } = new Bindable(true); - [SettingSource("Show moving average arrow", "Whether an arrow should move beneath the bar showing the average error.")] + [SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.ShowMovingAverage), nameof(BarHitErrorMeterStrings.ShowMovingAverageDescription))] public Bindable ShowMovingAverage { get; } = new BindableBool(true); - [SettingSource("Centre marker style", "How to signify the centre of the display")] + [SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.CentreMarkerStyle), nameof(BarHitErrorMeterStrings.CentreMarkerStyleDescription))] public Bindable CentreMarkerStyle { get; } = new Bindable(CentreMarkerStyles.Circle); - [SettingSource("Label style", "How to show early/late extremities")] + [SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.LabelStyle), nameof(BarHitErrorMeterStrings.LabelStyleDescription))] public Bindable LabelStyle { get; } = new Bindable(LabelStyles.Icons); private const int judgement_line_width = 14; @@ -487,15 +489,25 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters public enum CentreMarkerStyles { + [LocalisableDescription(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.CentreMarkerStylesNone))] None, + + [LocalisableDescription(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.CentreMarkerStylesCircle))] Circle, + + [LocalisableDescription(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.CentreMarkerStylesLine))] Line } public enum LabelStyles { + [LocalisableDescription(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.LabelStylesNone))] None, + + [LocalisableDescription(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.LabelStylesIcons))] Icons, + + [LocalisableDescription(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.LabelStylesText))] Text } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index ec5dc5f52f..5793713fca 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -9,7 +9,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; @@ -23,21 +25,21 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private const int animation_duration = 200; private const int drawable_judgement_size = 8; - [SettingSource("Judgement count", "The number of displayed judgements")] + [SettingSource(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.JudgementCount), nameof(ColourHitErrorMeterStrings.JudgementCountDescription))] public BindableNumber JudgementCount { get; } = new BindableNumber(20) { MinValue = 1, MaxValue = 50, }; - [SettingSource("Judgement spacing", "The space between each displayed judgement")] + [SettingSource(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.JudgementSpacing), nameof(ColourHitErrorMeterStrings.JudgementSpacingDescription))] public BindableNumber JudgementSpacing { get; } = new BindableNumber(2) { MinValue = 0, MaxValue = 10, }; - [SettingSource("Judgement shape", "The shape of each displayed judgement")] + [SettingSource(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.JudgementShape), nameof(ColourHitErrorMeterStrings.JudgementShapeDescription))] public Bindable JudgementShape { get; } = new Bindable(); private readonly JudgementFlow judgementsFlow; @@ -192,7 +194,10 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters public enum ShapeStyle { + [LocalisableDescription(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.ShapeStyleCircle))] Circle, + + [LocalisableDescription(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.ShapeStyleSquare))] Square } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 82f24c736e..80d2e0863f 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -6,7 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; @@ -19,16 +21,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public bool UsesFixedAnchor { get; set; } - [SettingSource("Display mode")] + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayMode))] public Bindable Mode { get; set; } = new Bindable(); - [SettingSource("Counter direction")] + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))] public Bindable FlowDirection { get; set; } = new Bindable(); - [SettingSource("Show judgement names")] + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowJudgementNames))] public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true); - [SettingSource("Show max judgement")] + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowMaxJudgement))] public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true); [Resolved] @@ -130,8 +132,13 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public enum DisplayMode { + [LocalisableDescription(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayModeSimple))] Simple, + + [LocalisableDescription(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayModeNormal))] Normal, + + [LocalisableDescription(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayModeAll))] All } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index e05d950369..f08d14720b 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -47,8 +47,9 @@ namespace osu.Game.Screens.Select.Carousel private Sprite background = null!; - private Action? startRequested; - private Action? editRequested; + private MenuItem[]? mainMenuItems; + + private Action? selectRequested; private Action? hideRequested; private Triangles triangles = null!; @@ -84,9 +85,8 @@ namespace osu.Game.Screens.Select.Carousel if (songSelect != null) { - startRequested = b => songSelect.FinaliseSelection(b); - if (songSelect.AllowEditing) - editRequested = songSelect.Edit; + mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(beatmapInfo); + selectRequested = b => songSelect.FinaliseSelection(b); } if (manager != null) @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnClick(ClickEvent e) { if (Item?.State.Value == CarouselItemState.Selected) - startRequested?.Invoke(beatmapInfo); + selectRequested?.Invoke(beatmapInfo); return base.OnClick(e); } @@ -227,11 +227,8 @@ namespace osu.Game.Screens.Select.Carousel { List items = new List(); - if (startRequested != null) - items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmapInfo))); - - if (editRequested != null) - items.Add(new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Standard, () => editRequested(beatmapInfo))); + if (mainMenuItems != null) + items.AddRange(mainMenuItems); if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 9edca1d0c0..b99d949b43 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -4,10 +4,14 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -30,6 +34,12 @@ namespace osu.Game.Screens.Select public override bool AllowExternalScreenChange => true; + public override MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(BeatmapInfo beatmap) => new MenuItem[] + { + new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => FinaliseSelection(beatmap)), + new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => Edit(beatmap)) + }; + protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap(); private PlayBeatmapDetailArea playBeatmapDetailArea = null!; @@ -37,7 +47,7 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load(OsuColour colours) { - BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); + BeatmapOptions.AddButton(ButtonSystemStrings.Edit.ToSentence(), @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); } protected void PresentScore(ScoreInfo score) => diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 41e722ae88..8786821c77 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1,19 +1,31 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -21,23 +33,12 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; using osu.Game.Screens.Select.Options; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; using osuTK.Input; -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; -using osu.Game.Collections; -using osu.Game.Graphics.UserInterface; -using System.Diagnostics; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Game.Configuration; -using osu.Game.Screens.Play; -using osu.Game.Skinning; namespace osu.Game.Screens.Select { @@ -83,6 +84,17 @@ namespace osu.Game.Screens.Select public bool BeatmapSetsLoaded => IsLoaded && Carousel.BeatmapSetsLoaded; + /// + /// Creates any "action" menu items for the provided beatmap (ie. "Select", "Play", "Edit"). + /// These will always be placed at the top of the context menu, with common items added below them. + /// + /// The beatmap to create items for. + /// The menu items. + public virtual MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(BeatmapInfo beatmap) => new MenuItem[] + { + new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(beatmap)) + }; + [Resolved] private Bindable> selectedMods { get; set; } = null!; diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 68bb1e7ddc..2c16a67cac 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -18,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; +using osu.Game.Localisation.SkinComponents; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Skinning.Components @@ -25,10 +26,10 @@ namespace osu.Game.Skinning.Components [UsedImplicitly] public partial class BeatmapAttributeText : FontAdjustableSkinComponent { - [SettingSource("Attribute", "The attribute to be displayed.")] + [SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Attribute), nameof(BeatmapAttributeTextStrings.AttributeDescription))] public Bindable Attribute { get; } = new Bindable(BeatmapAttribute.StarRating); - [SettingSource("Template", "Supports {Label} and {Value}, but also including arbitrary attributes like {StarRating} (see attribute list for supported values).")] + [SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Template), nameof(BeatmapAttributeTextStrings.TemplateDescription))] public Bindable Template { get; set; } = new Bindable("{Label}: {Value}"); [Resolved] diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index d87fb125bb..936f6a529b 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -8,13 +8,14 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation.SkinComponents; namespace osu.Game.Skinning.Components { [UsedImplicitly] public partial class TextElement : FontAdjustableSkinComponent { - [SettingSource("Text", "The text to be displayed.")] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextElementText), nameof(SkinnableComponentStrings.TextElementTextDescription))] public Bindable Text { get; } = new Bindable("Circles!"); private readonly OsuSpriteText text; diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index ee73417bfe..2e41b35abb 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Localisation.SkinComponents; namespace osu.Game.Skinning { @@ -16,7 +17,7 @@ namespace osu.Game.Skinning { public bool UsesFixedAnchor { get; set; } - [SettingSource("Font", "The font to use.")] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); /// diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 1685562cc7..8ce265719c 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -179,8 +179,14 @@ namespace osu.Game.Skinning private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources); - public void Save(Skin skin) + /// + /// Save a skin, serialising any changes to skin layouts to relevant JSON structures. + /// + /// Whether any change actually occurred. + public bool Save(Skin skin) { + bool hadChanges = false; + skin.SkinInfo.PerformWrite(s => { // Update for safety @@ -212,8 +218,14 @@ namespace osu.Game.Skinning } } - s.Hash = ComputeHash(s); + string newHash = ComputeHash(s); + + hadChanges = newHash != s.Hash; + + s.Hash = newHash; }); + + return hadChanges; } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index a4cf83b32e..fca7dc0f5e 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -192,12 +192,16 @@ namespace osu.Game.Skinning }); } - public void Save(Skin skin) + /// + /// Save a skin, serialising any changes to skin layouts to relevant JSON structures. + /// + /// Whether any change actually occurred. + public bool Save(Skin skin) { if (!skin.SkinInfo.IsManaged) throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first."); - skinImporter.Save(skin); + return skinImporter.Save(skin); } /// diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index a66f3e0549..c3449562c3 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation.SkinComponents; using osu.Game.Overlays.Settings; using osuTK; @@ -27,7 +28,7 @@ namespace osu.Game.Skinning [Resolved] private TextureStore textures { get; set; } = null!; - [SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.SpriteName), nameof(SkinnableComponentStrings.SpriteNameDescription), SettingControlType = typeof(SpriteSelectorControl))] public Bindable SpriteName { get; } = new Bindable(string.Empty); [Resolved] diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d4ea0abbb5..cdb3d9b66b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - +