Merge branch 'master' into fix-replay-date

This commit is contained in:
Dan Balasescu 2021-07-19 21:58:37 +09:00 committed by GitHub
commit 5ef1fe6948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 226 additions and 97 deletions

View File

@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken) protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{ {
var positionData = obj as IHasXPosition; var xPositionData = obj as IHasXPosition;
var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo; var comboData = obj as IHasCombo;
switch (obj) switch (obj)
@ -36,10 +37,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path, Path = curveData.Path,
NodeSamples = curveData.NodeSamples, NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount, RepeatCount = curveData.RepeatCount,
X = positionData?.X ?? 0, X = xPositionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0, ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
}.Yield(); }.Yield();
case IHasDuration endTime: case IHasDuration endTime:
@ -59,7 +61,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples, Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0, ComboOffset = comboData?.ComboOffset ?? 0,
X = positionData?.X ?? 0 X = xPositionData?.X ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
}.Yield(); }.Yield();
} }
} }

View File

@ -19,9 +19,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
get get
{ {
float x = HitObject.OriginalX; Vector2 position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
float y = HitObjectContainer.PositionAtTime(HitObject.StartTime); return HitObjectContainer.ToScreenSpace(position + new Vector2(0, HitObjectContainer.DrawHeight));
return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
} }
} }

View File

@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@ -28,10 +26,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
Colour = osuColour.Yellow; Colour = osuColour.Yellow;
} }
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null) public void UpdateFrom(CatchHitObject hitObject)
{ {
X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
Scale = new Vector2(hitObject.Scale); Scale = new Vector2(hitObject.Scale);
} }
} }

View File

@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
X = parentHitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
}
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject) public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{ {
nestedHitObjects.Clear(); nestedHitObjects.Clear();
@ -43,7 +37,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
var hitObject = nestedHitObjects[i]; var hitObject = nestedHitObjects[i];
var outline = (FruitOutline)InternalChildren[i]; var outline = (FruitOutline)InternalChildren[i];
outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject); outline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, hitObject) - Position;
outline.UpdateFrom(hitObject);
outline.Scale *= hitObject is Droplet ? 0.5f : 1; outline.Scale *= hitObject is Droplet ? 0.5f : 1;
} }
} }

View File

@ -33,12 +33,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
}; };
} }
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
{
X = hitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
}
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{ {
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);

View File

@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
base.Update(); base.Update();
outline.UpdateFrom(HitObjectContainer, HitObject); outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
outline.UpdateFrom(HitObject);
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -20,8 +20,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
base.Update(); base.Update();
if (IsSelected) if (!IsSelected) return;
outline.UpdateFrom(HitObjectContainer, HitObject);
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
outline.UpdateFrom(HitObject);
} }
} }
} }

View File

@ -49,8 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
if (!IsSelected) return; if (!IsSelected) return;
scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject); nestedOutlineContainer.Position = scrollingPath.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
if (pathCache.IsValid) return; if (pathCache.IsValid) return;

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
/// <summary>
/// Utility functions used by the editor.
/// </summary>
public static class CatchHitObjectUtils
{
/// <summary>
/// Get the position of the hit object in the playfield based on <see cref="CatchHitObject.OriginalX"/> and <see cref="HitObject.StartTime"/>.
/// </summary>
public static Vector2 GetStartPosition(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
{
return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime));
}
}
}

View File

@ -9,10 +9,11 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public abstract class CatchHitObject : HitObject, IHasXPosition, IHasComboInformation public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
@ -31,8 +32,6 @@ namespace osu.Game.Rulesets.Catch.Objects
set => OriginalXBindable.Value = value; set => OriginalXBindable.Value = value;
} }
float IHasXPosition.X => OriginalXBindable.Value;
public readonly Bindable<float> XOffsetBindable = new Bindable<float>(); public readonly Bindable<float> XOffsetBindable = new Bindable<float>();
/// <summary> /// <summary>
@ -131,5 +130,24 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
#region Hit object conversion
// The half of the height of the osu! playfield.
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
/// <summary>
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
/// </summary>
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
float IHasXPosition.X => OriginalX;
float IHasYPosition.Y => LegacyConvertedY;
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
#endregion
} }
} }

View File

@ -13,8 +13,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
public TestSceneEditorComposeRadioButtons() public TestSceneEditorComposeRadioButtons()
{ {
RadioButtonCollection collection; EditorRadioButtonCollection collection;
Add(collection = new RadioButtonCollection Add(collection = new EditorRadioButtonCollection
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -2,17 +2,24 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Timing; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -20,8 +27,15 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture] [TestFixture]
public class TestSceneHitObjectComposer : EditorClockTestScene public class TestSceneHitObjectComposer : EditorClockTestScene
{ {
[BackgroundDependencyLoader] private OsuHitObjectComposer hitObjectComposer;
private void load() private EditorBeatmapContainer editorBeatmapContainer;
private EditorBeatmap editorBeatmap => editorBeatmapContainer.EditorBeatmap;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create beatmap", () =>
{ {
Beatmap.Value = CreateWorkingBeatmap(new Beatmap Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{ {
@ -41,16 +55,61 @@ namespace osu.Game.Tests.Visual.Editing
} }
}, },
}); });
});
var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo)); AddStep("Create composer", () =>
{
Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value)
{
Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset())
};
});
}
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; [Test]
Dependencies.CacheAs<IAdjustableClock>(clock); public void TestPlacementOnlyWorksWithTiming()
Dependencies.CacheAs<IFrameBasedClock>(clock); {
Dependencies.CacheAs(editorBeatmap); AddStep("clear all control points", () => editorBeatmap.ControlPointInfo.Clear());
Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap);
Child = new OsuHitObjectComposer(new OsuRuleset()); AddAssert("Tool is selection", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is SelectTool);
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Click());
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
}
public class EditorBeatmapContainer : Container
{
private readonly WorkingBeatmap working;
public EditorBeatmap EditorBeatmap { get; private set; }
public EditorBeatmapContainer(WorkingBeatmap working)
{
this.working = working;
RelativeSizeAxes = Axes.Both;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
EditorBeatmap = new EditorBeatmap(working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
dependencies.CacheAs(EditorBeatmap);
dependencies.CacheAs<IBeatSnapProvider>(EditorBeatmap);
return dependencies;
}
protected override void LoadComplete()
{
base.LoadComplete();
Add(EditorBeatmap);
}
} }
} }
} }

View File

@ -251,11 +251,8 @@ namespace osu.Game.Beatmaps.Formats
switch (beatmap.BeatmapInfo.RulesetID) switch (beatmap.BeatmapInfo.RulesetID)
{ {
case 0: case 0:
position = ((IHasPosition)hitObject).Position;
break;
case 2: case 2:
position.X = ((IHasXPosition)hitObject).X; position = ((IHasPosition)hitObject).Position;
break; break;
case 3: case 3:

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
@ -63,10 +64,12 @@ namespace osu.Game.Rulesets.Edit
private InputManager inputManager; private InputManager inputManager;
private RadioButtonCollection toolboxCollection; private EditorRadioButtonCollection toolboxCollection;
private FillFlowContainer togglesCollection; private FillFlowContainer togglesCollection;
private IBindable<bool> hasTiming;
protected HitObjectComposer(Ruleset ruleset) protected HitObjectComposer(Ruleset ruleset)
{ {
Ruleset = ruleset; Ruleset = ruleset;
@ -126,7 +129,7 @@ namespace osu.Game.Rulesets.Edit
{ {
new ToolboxGroup("toolbox (1-9)") new ToolboxGroup("toolbox (1-9)")
{ {
Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
}, },
new ToolboxGroup("toggles (Q~P)") new ToolboxGroup("toggles (Q~P)")
{ {
@ -160,6 +163,14 @@ namespace osu.Game.Rulesets.Edit
base.LoadComplete(); base.LoadComplete();
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
hasTiming = EditorBeatmap.HasTiming.GetBoundCopy();
hasTiming.BindValueChanged(timing =>
{
// it's important this is performed before the similar code in EditorRadioButton disables the button.
if (!timing.NewValue)
setSelectTool();
});
} }
public override Playfield Playfield => drawableRulesetWrapper.Playfield; public override Playfield Playfield => drawableRulesetWrapper.Playfield;
@ -219,6 +230,7 @@ namespace osu.Game.Rulesets.Edit
if (item != null) if (item != null)
{ {
if (!item.Selected.Disabled)
item.Select(); item.Select();
return true; return true;
} }

View File

@ -2,15 +2,20 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Catch namespace osu.Game.Rulesets.Objects.Legacy.Catch
{ {
/// <summary> /// <summary>
/// Legacy osu!catch Hit-type, used for parsing Beatmaps. /// Legacy osu!catch Hit-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertHit : ConvertHitObject, IHasCombo, IHasXPosition internal sealed class ConvertHit : ConvertHitObject, IHasPosition, IHasCombo
{ {
public float X { get; set; } public float X => Position.X;
public float Y => Position.Y;
public Vector2 Position { get; set; }
public bool NewCombo { get; set; } public bool NewCombo { get; set; }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
return new ConvertHit return new ConvertHit
{ {
X = position.X, Position = position,
NewCombo = newCombo, NewCombo = newCombo,
ComboOffset = comboOffset ComboOffset = comboOffset
}; };
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
return new ConvertSlider return new ConvertSlider
{ {
X = position.X, Position = position,
NewCombo = FirstObject || newCombo, NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset, ComboOffset = comboOffset,
Path = new SliderPath(controlPoints, length), Path = new SliderPath(controlPoints, length),

View File

@ -2,15 +2,20 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Catch namespace osu.Game.Rulesets.Objects.Legacy.Catch
{ {
/// <summary> /// <summary>
/// Legacy osu!catch Slider-type, used for parsing Beatmaps. /// Legacy osu!catch Slider-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition, IHasCombo internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo
{ {
public float X { get; set; } public float X => Position.X;
public float Y => Position.Y;
public Vector2 Position { get; set; }
public bool NewCombo { get; set; } public bool NewCombo { get; set; }

View File

@ -5,9 +5,11 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -16,26 +18,30 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.RadioButtons namespace osu.Game.Screens.Edit.Components.RadioButtons
{ {
public class DrawableRadioButton : OsuButton public class EditorRadioButton : OsuButton, IHasTooltip
{ {
/// <summary> /// <summary>
/// Invoked when this <see cref="DrawableRadioButton"/> has been selected. /// Invoked when this <see cref="EditorRadioButton"/> has been selected.
/// </summary> /// </summary>
public Action<RadioButton> Selected; public Action<RadioButton> Selected;
public readonly RadioButton Button;
private Color4 defaultBackgroundColour; private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour; private Color4 defaultBubbleColour;
private Color4 selectedBackgroundColour; private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour; private Color4 selectedBubbleColour;
private Drawable icon; private Drawable icon;
private readonly RadioButton button;
public DrawableRadioButton(RadioButton button) [Resolved(canBeNull: true)]
private EditorBeatmap editorBeatmap { get; set; }
public EditorRadioButton(RadioButton button)
{ {
this.button = button; Button = button;
Text = button.Item.ToString(); Text = button.Label;
Action = button.Select; Action = button.Select;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -57,7 +63,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Colour = Color4.Black.Opacity(0.5f) Colour = Color4.Black.Opacity(0.5f)
}; };
Add(icon = (button.CreateIcon?.Invoke() ?? new Circle()).With(b => Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{ {
b.Blending = BlendingParameters.Additive; b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft; b.Anchor = Anchor.CentreLeft;
@ -71,13 +77,16 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
{ {
base.LoadComplete(); base.LoadComplete();
button.Selected.ValueChanged += selected => Button.Selected.ValueChanged += selected =>
{ {
updateSelectionState(); updateSelectionState();
if (selected.NewValue) if (selected.NewValue)
Selected?.Invoke(button); Selected?.Invoke(Button);
}; };
editorBeatmap?.HasTiming.BindValueChanged(hasTiming => Button.Selected.Disabled = !hasTiming.NewValue, true);
Button.Selected.BindDisabledChanged(disabled => Enabled.Value = !disabled, true);
updateSelectionState(); updateSelectionState();
} }
@ -86,8 +95,8 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
if (!IsLoaded) if (!IsLoaded)
return; return;
BackgroundColour = button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour; BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = button.Selected.Value ? selectedBubbleColour : defaultBubbleColour; icon.Colour = Button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
} }
protected override SpriteText CreateText() => new OsuSpriteText protected override SpriteText CreateText() => new OsuSpriteText
@ -97,5 +106,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
X = 40f X = 40f
}; };
public LocalisableString TooltipText => Enabled.Value ? string.Empty : "Add at least one timing point first!";
} }
} }

View File

@ -9,7 +9,7 @@ using osuTK;
namespace osu.Game.Screens.Edit.Components.RadioButtons namespace osu.Game.Screens.Edit.Components.RadioButtons
{ {
public class RadioButtonCollection : CompositeDrawable public class EditorRadioButtonCollection : CompositeDrawable
{ {
private IReadOnlyList<RadioButton> items; private IReadOnlyList<RadioButton> items;
@ -28,13 +28,13 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
} }
} }
private readonly FlowContainer<DrawableRadioButton> buttonContainer; private readonly FlowContainer<EditorRadioButton> buttonContainer;
public RadioButtonCollection() public EditorRadioButtonCollection()
{ {
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
InternalChild = buttonContainer = new FillFlowContainer<DrawableRadioButton> InternalChild = buttonContainer = new FillFlowContainer<EditorRadioButton>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -58,7 +58,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
currentlySelected = null; currentlySelected = null;
}; };
buttonContainer.Add(new DrawableRadioButton(button)); buttonContainer.Add(new EditorRadioButton(button));
} }
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// <summary> /// <summary>
/// The item related to this button. /// The item related to this button.
/// </summary> /// </summary>
public object Item; public string Label;
/// <summary> /// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used. /// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
@ -26,21 +26,14 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
private readonly Action action; private readonly Action action;
public RadioButton(object item, Action action, Func<Drawable> createIcon = null) public RadioButton(string label, Action action, Func<Drawable> createIcon = null)
{ {
Item = item; Label = label;
CreateIcon = createIcon; CreateIcon = createIcon;
this.action = action; this.action = action;
Selected = new BindableBool(); Selected = new BindableBool();
} }
public RadioButton(string item)
: this(item, null)
{
Item = item;
action = null;
}
/// <summary> /// <summary>
/// Selects this <see cref="RadioButton"/>. /// Selects this <see cref="RadioButton"/>.
/// </summary> /// </summary>

View File

@ -46,12 +46,22 @@ namespace osu.Game.Screens.Edit
public readonly IBeatmap PlayableBeatmap; public readonly IBeatmap PlayableBeatmap;
/// <summary>
/// Whether at least one timing control point is present and providing timing information.
/// </summary>
public IBindable<bool> HasTiming => hasTiming;
private readonly Bindable<bool> hasTiming = new Bindable<bool>();
[CanBeNull] [CanBeNull]
public readonly ISkin BeatmapSkin; public readonly ISkin BeatmapSkin;
[Resolved] [Resolved]
private BindableBeatDivisor beatDivisor { get; set; } private BindableBeatDivisor beatDivisor { get; set; }
[Resolved]
private EditorClock editorClock { get; set; }
private readonly IBeatmapProcessor beatmapProcessor; private readonly IBeatmapProcessor beatmapProcessor;
private readonly Dictionary<HitObject, Bindable<double>> startTimeBindables = new Dictionary<HitObject, Bindable<double>>(); private readonly Dictionary<HitObject, Bindable<double>> startTimeBindables = new Dictionary<HitObject, Bindable<double>>();
@ -238,6 +248,8 @@ namespace osu.Game.Screens.Edit
if (batchPendingUpdates.Count > 0) if (batchPendingUpdates.Count > 0)
UpdateState(); UpdateState();
hasTiming.Value = !ReferenceEquals(ControlPointInfo.TimingPointAt(editorClock.CurrentTime), TimingControlPoint.DEFAULT);
} }
protected override void UpdateState() protected override void UpdateState()

View File

@ -955,7 +955,11 @@ namespace osu.Game.Screens.Play
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
if (prepareScoreForDisplayTask == null) if (prepareScoreForDisplayTask == null)
{
Score.ScoreInfo.Passed = false; Score.ScoreInfo.Passed = false;
// potentially should be ScoreRank.F instead? this is the best alternative for now.
Score.ScoreInfo.Rank = ScoreRank.D;
}
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits. // To resolve test failures, forcefully end playing synchronously when this screen exits.