Merge branch 'master' into epilepsy-warning

This commit is contained in:
Bartłomiej Dach
2020-10-20 01:06:46 +02:00
1042 changed files with 27494 additions and 8252 deletions

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.IO.Serialization;
using osu.Game.IO.Serialization.Converters;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
{
public class ClipboardContent : IJsonSerializable
{
[JsonConverter(typeof(TypedListConverter<HitObject>))]
public IList<HitObject> HitObjects;
public ClipboardContent()
{
}
public ClipboardContent(EditorBeatmap editorBeatmap)
{
HitObjects = editorBeatmap.SelectedHitObjects.ToList();
}
}
}

View File

@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Components
private const float contents_padding = 15;
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected Track Track => Beatmap.Value.Track;
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Drawable background;
private readonly Container content;
@ -42,9 +43,11 @@ namespace osu.Game.Screens.Edit.Components
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
background.Colour = colours.Gray1;
}
}

View File

@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
Height = 1,
Colour = Color4.White.Opacity(0.2f),
});
Current.Value = EditorScreenMode.Compose;
}
[BackgroundDependencyLoader]

View File

@ -62,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components
}
};
Track?.AddAdjustment(AdjustableProperty.Tempo, tempo);
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true);
}
protected override void Dispose(bool isDisposing)
{
Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
base.Dispose(isDisposing);
}

View File

@ -5,7 +5,6 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@ -29,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour;
private readonly Drawable bubble;
private Drawable icon;
private readonly RadioButton button;
public DrawableRadioButton(RadioButton button)
@ -40,19 +39,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Action = button.Select;
RelativeSizeAxes = Axes.X;
bubble = new CircularContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(0.5f),
X = 10,
Masking = true,
Blending = BlendingParameters.Additive,
Child = new Box { RelativeSizeAxes = Axes.Both }
};
}
[BackgroundDependencyLoader]
@ -73,7 +59,14 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Colour = Color4.Black.Opacity(0.5f)
};
Add(bubble);
Add(icon = (button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
}
protected override void LoadComplete()
@ -96,7 +89,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
return;
BackgroundColour = button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
bubble.Colour = button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
icon.Colour = button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
}
protected override SpriteText CreateText() => new OsuSpriteText

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Edit.Components.RadioButtons
{
@ -19,11 +20,17 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// </summary>
public object Item;
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public readonly Func<Drawable> CreateIcon;
private readonly Action action;
public RadioButton(object item, Action action)
public RadioButton(object item, Action action, Func<Drawable> createIcon = null)
{
Item = item;
CreateIcon = createIcon;
this.action = action;
Selected = new BindableBool();
}

View File

@ -0,0 +1,112 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
internal class DrawableTernaryButton : TriangleButton
{
private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour;
private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour;
private Drawable icon;
public readonly TernaryButton Button;
public DrawableTernaryButton(TernaryButton button)
{
Button = button;
Text = button.Description;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
defaultBackgroundColour = colours.Gray3;
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 2,
Offset = new Vector2(0, 1),
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
}
protected override void LoadComplete()
{
base.LoadComplete();
Button.Bindable.BindValueChanged(selected => updateSelectionState(), true);
Action = onAction;
}
private void onAction()
{
Button.Toggle();
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
icon.Colour = selectedBubbleColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
icon.Colour = defaultBubbleColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
icon.Colour = selectedBubbleColour;
BackgroundColour = selectedBackgroundColour;
break;
}
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public class TernaryButton
{
public readonly Bindable<TernaryState> Bindable;
public readonly string Description;
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public readonly Func<Drawable> CreateIcon;
public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable> createIcon = null)
{
Bindable = bindable;
Description = description;
CreateIcon = createIcon;
}
public void Toggle()
{
switch (Bindable.Value)
{
case TernaryState.False:
case TernaryState.Indeterminate:
Bindable.Value = TernaryState.True;
break;
case TernaryState.True:
Bindable.Value = TernaryState.False;
break;
}
}
}
}

View File

@ -3,8 +3,8 @@
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using System;
using osu.Framework.Allocation;
using osu.Game.Extensions;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components
@ -22,10 +22,12 @@ namespace osu.Game.Screens.Edit.Components
{
trackTimer = new OsuSpriteText
{
Origin = Anchor.BottomLeft,
RelativePositionAxes = Axes.Y,
Font = OsuFont.GetFont(size: 22, fixedWidth: true),
Y = 0.5f,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// intentionally fudged centre to avoid movement of the number portion when
// going negative.
X = -35,
Font = OsuFont.GetFont(size: 25, fixedWidth: true),
}
};
}
@ -33,8 +35,7 @@ namespace osu.Game.Screens.Edit.Components
protected override void Update()
{
base.Update();
trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff");
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
}
}
}

View File

@ -1,70 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
/// <summary>
/// The part of the timeline that displays the control points.
/// </summary>
public class ControlPointPart : TimelinePart
public class ControlPointPart : TimelinePart<GroupVisualisation>
{
private IBindableList<ControlPointGroup> controlPointGroups;
protected override void LoadBeatmap(WorkingBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo;
cpi.TimingPoints.ForEach(addTimingPoint);
// Consider all non-timing points as the same type
cpi.SamplePoints.Select(c => (ControlPoint)c)
.Concat(cpi.EffectPoints)
.Concat(cpi.DifficultyPoints)
.Distinct()
// Non-timing points should not be added where there are timing points
.Where(c => cpi.TimingPointAt(c.Time).Time != c.Time)
.ForEach(addNonTimingPoint);
}
private void addTimingPoint(ControlPoint controlPoint) => Add(new TimingPointVisualisation(controlPoint));
private void addNonTimingPoint(ControlPoint controlPoint) => Add(new NonTimingPointVisualisation(controlPoint));
private class TimingPointVisualisation : ControlPointVisualisation
{
public TimingPointVisualisation(ControlPoint controlPoint)
: base(controlPoint)
controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
controlPointGroups.BindCollectionChanged((sender, args) =>
{
}
switch (args.Action)
{
case NotifyCollectionChangedAction.Reset:
Clear();
break;
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.YellowDark;
}
case NotifyCollectionChangedAction.Add:
foreach (var group in args.NewItems.OfType<ControlPointGroup>())
Add(new GroupVisualisation(group));
break;
private class NonTimingPointVisualisation : ControlPointVisualisation
{
public NonTimingPointVisualisation(ControlPoint controlPoint)
: base(controlPoint)
{
}
case NotifyCollectionChangedAction.Remove:
foreach (var group in args.OldItems.OfType<ControlPointGroup>())
{
var matching = Children.SingleOrDefault(gv => gv.Group == group);
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.Green;
}
matching?.Expire();
}
private abstract class ControlPointVisualisation : PointVisualisation
{
protected ControlPointVisualisation(ControlPoint controlPoint)
: base(controlPoint.Time)
{
}
break;
}
}, true);
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class GroupVisualisation : PointVisualisation
{
public readonly ControlPointGroup Group;
private BindableList<ControlPoint> controlPoints;
[Resolved]
private OsuColour colours { get; set; }
public GroupVisualisation(ControlPointGroup group)
: base(group.Time)
{
Group = group;
}
protected override void LoadComplete()
{
base.LoadComplete();
controlPoints = (BindableList<ControlPoint>)Group.ControlPoints.GetBoundCopy();
controlPoints.BindCollectionChanged((_, __) =>
{
if (controlPoints.Count == 0)
{
Colour = Color4.Transparent;
return;
}
Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green;
}, true);
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osuTK;
using osu.Framework.Graphics;
@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Container<T> content;
protected override Container<T> Content => content;
@ -35,12 +38,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
updateRelativeChildSize();
LoadBeatmap(b.NewValue);
};
Track.ValueChanged += _ => updateRelativeChildSize();
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap)
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
}
private void updateRelativeChildSize()

View File

@ -1,9 +1,9 @@
// 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 osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
{
@ -12,16 +12,23 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// </summary>
public class PointVisualisation : Box
{
public const float WIDTH = 1;
public PointVisualisation(double startTime)
: this()
{
X = (float)startTime;
}
public PointVisualisation()
{
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.Y;
Width = 1;
EdgeSmoothness = new Vector2(1, 0);
RelativePositionAxes = Axes.X;
X = (float)startTime;
RelativeSizeAxes = Axes.Y;
Width = WIDTH;
EdgeSmoothness = new Vector2(WIDTH, 0);
}
}
}

View File

@ -24,15 +24,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A container which provides a "blueprint" display of hitobjects.
/// Includes selection and manipulation support via a <see cref="SelectionHandler"/>.
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler"/>.
/// </summary>
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{
protected DragBox DragBox { get; private set; }
protected Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
public Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
private SelectionHandler selectionHandler;
protected SelectionHandler SelectionHandler { get; private set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
protected EditorBeatmap Beatmap { get; private set; }
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
@ -56,22 +56,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
selectionHandler = CreateSelectionHandler();
selectionHandler.DeselectAll = deselectAll;
SelectionHandler = CreateSelectionHandler();
SelectionHandler.DeselectAll = deselectAll;
AddRangeInternal(new[]
{
DragBox = CreateDragBox(select),
selectionHandler,
DragBox = CreateDragBox(selectBlueprintsFromDragRectangle),
SelectionHandler,
SelectionBlueprints = CreateSelectionBlueprintContainer(),
selectionHandler.CreateProxy(),
SelectionHandler.CreateProxy(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
});
foreach (var obj in beatmap.HitObjects)
foreach (var obj in Beatmap.HitObjects)
AddBlueprintFor(obj);
selectedHitObjects.BindTo(beatmap.SelectedHitObjects);
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
@ -94,15 +94,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
beatmap.HitObjectAdded += AddBlueprintFor;
beatmap.HitObjectRemoved += removeBlueprintFor;
Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += removeBlueprintFor;
}
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
/// <summary>
/// Creates a <see cref="SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// </summary>
protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
// store for double-click handling
clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
clickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
@ -147,7 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
// ensure the blueprint which was hovered for the first click is still the hovered blueprint.
if (clickedBlueprint == null || selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
return false;
editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
@ -201,6 +201,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (isDraggingBlueprint)
{
// handle positional change etc.
foreach (var obj in selectedHitObjects)
Beatmap.Update(obj);
changeHandler?.EndChange();
isDraggingBlueprint = false;
}
@ -208,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (DragBox.State == Visibility.Visible)
{
DragBox.Hide();
selectionHandler.UpdateVisibility();
SelectionHandler.UpdateVisibility();
}
}
@ -217,7 +221,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (e.Key)
{
case Key.Escape:
if (!selectionHandler.SelectedBlueprints.Any())
if (!SelectionHandler.SelectedBlueprints.Any())
return false;
deselectAll();
@ -271,6 +275,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Selected += onBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected;
if (Beatmap.SelectedHitObjects.Contains(hitObject))
blueprint.Select();
SelectionBlueprints.Add(blueprint);
}
@ -295,14 +302,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
bool allowDeselection = e.ControlPressed && e.Button == MouseButton.Left;
// Todo: This is probably incorrectly disallowing multiple selections on stacked objects
if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
if (!allowDeselection && SelectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
return;
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
{
if (blueprint.IsHovered)
{
selectionHandler.HandleSelectionRequested(blueprint, e.CurrentState);
SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState);
clickSelectionBegan = true;
break;
}
@ -326,7 +333,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Select all masks in a given rectangle selection area.
/// </summary>
/// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param>
private void select(RectangleF rect)
private void selectBlueprintsFromDragRectangle(RectangleF rect)
{
foreach (var blueprint in SelectionBlueprints)
{
@ -355,26 +362,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void selectAll()
{
SelectionBlueprints.ToList().ForEach(m => m.Select());
selectionHandler.UpdateVisibility();
SelectionHandler.UpdateVisibility();
}
/// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s.
/// </summary>
private void deselectAll() => selectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void onBlueprintSelected(SelectionBlueprint blueprint)
{
selectionHandler.HandleSelected(blueprint);
SelectionHandler.HandleSelected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 1);
beatmap.SelectedHitObjects.Add(blueprint.HitObject);
}
private void onBlueprintDeselected(SelectionBlueprint blueprint)
{
selectionHandler.HandleDeselected(blueprint);
SelectionHandler.HandleDeselected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
beatmap.SelectedHitObjects.Remove(blueprint.HitObject);
}
#endregion
@ -390,16 +395,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
private void prepareSelectionMovement()
{
if (!selectionHandler.SelectedBlueprints.Any())
if (!SelectionHandler.SelectedBlueprints.Any())
return;
// Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement
// A special case is added for when a click selection occurred before the drag
if (!clickSelectionBegan && !selectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
return;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct
}
@ -424,15 +429,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects.
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
return true;
if (result.Time.HasValue)
{
// Apply the start time at the newly snapped-to position
double offset = result.Time.Value - draggedObject.StartTime;
foreach (HitObject obj in selectionHandler.SelectedHitObjects)
foreach (HitObject obj in Beatmap.SelectedHitObjects)
{
obj.StartTime += offset;
Beatmap.Update(obj);
}
}
return true;
@ -459,10 +468,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.Dispose(isDisposing);
if (beatmap != null)
if (Beatmap != null)
{
beatmap.HitObjectAdded -= AddBlueprintFor;
beatmap.HitObjectRemoved -= removeBlueprintFor;
Beatmap.HitObjectAdded -= AddBlueprintFor;
Beatmap.HitObjectRemoved -= removeBlueprintFor;
}
}
}

View File

@ -3,14 +3,21 @@
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@ -46,6 +53,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
TernaryStates = CreateTernaryButtons().ToArray();
AddInternal(placementBlueprintContainer);
}
@ -54,6 +63,90 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.LoadComplete();
inputManager = GetContainingInputManager();
// updates to selected are handled for us by SelectionHandler.
NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
// we are responsible for current placement blueprint updated based on state changes.
NewCombo.ValueChanged += _ => updatePlacementNewCombo();
// we own SelectionHandler so don't need to worry about making bindable copies (for simplicity)
foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
}
}
private void updatePlacementNewCombo()
{
if (currentPlacement?.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True;
}
private void updatePlacementSamples()
{
if (currentPlacement == null) return;
foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value);
}
private void sampleChanged(string sampleName, TernaryState state)
{
if (currentPlacement == null) return;
var samples = currentPlacement.HitObject.Samples;
var existingSample = samples.FirstOrDefault(s => s.Name == sampleName);
switch (state)
{
case TernaryState.False:
if (existingSample != null)
samples.Remove(existingSample);
break;
case TernaryState.True:
if (existingSample == null)
samples.Add(new HitSampleInfo { Name = sampleName });
break;
}
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public TernaryButton[] TernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
{
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle });
foreach (var kvp in SelectionHandler.SelectionSampleStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
}
private Drawable getIconForSample(string sampleName)
{
switch (sampleName)
{
case HitSampleInfo.HIT_CLAP:
return new SpriteIcon { Icon = FontAwesome.Solid.Hands };
case HitSampleInfo.HIT_WHISTLE:
return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn };
case HitSampleInfo.HIT_FINISH:
return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan };
}
return null;
}
#region Placement
@ -86,7 +179,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
removePlacement();
if (currentPlacement != null)
{
updatePlacementPosition();
}
}
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
@ -104,7 +199,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void AddBlueprintFor(HitObject hitObject)
{
refreshTool();
base.AddBlueprintFor(hitObject);
// on successful placement, the new combo button should be reset as this is the most common user interaction.
if (Beatmap.SelectedHitObjects.Count == 0)
NewCombo.Value = TernaryState.False;
}
private void createPlacement()
@ -115,10 +215,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (blueprint != null)
{
// doing this post-creations as adding the default hit sample should be the case regardless of the ruleset.
blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
updatePlacementPosition();
updatePlacementSamples();
updatePlacementNewCombo();
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
Masking = true,
BorderColour = Color4.White,
BorderThickness = SelectionHandler.BORDER_RADIUS,
BorderThickness = SelectionBox.BORDER_RADIUS,
Child = new Box
{
RelativeSizeAxes = Axes.Both,

View File

@ -0,0 +1,235 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBox : CompositeDrawable
{
public Action<float> OnRotation;
public Action<Vector2, Anchor> OnScale;
public Action<Direction> OnFlip;
public Action OnReverse;
public Action OperationStarted;
public Action OperationEnded;
private bool canReverse;
/// <summary>
/// Whether pattern reversing support should be enabled.
/// </summary>
public bool CanReverse
{
get => canReverse;
set
{
if (canReverse == value) return;
canReverse = value;
recreate();
}
}
private bool canRotate;
/// <summary>
/// Whether rotation support should be enabled.
/// </summary>
public bool CanRotate
{
get => canRotate;
set
{
if (canRotate == value) return;
canRotate = value;
recreate();
}
}
private bool canScaleX;
/// <summary>
/// Whether vertical scale support should be enabled.
/// </summary>
public bool CanScaleX
{
get => canScaleX;
set
{
if (canScaleX == value) return;
canScaleX = value;
recreate();
}
}
private bool canScaleY;
/// <summary>
/// Whether horizontal scale support should be enabled.
/// </summary>
public bool CanScaleY
{
get => canScaleY;
set
{
if (canScaleY == value) return;
canScaleY = value;
recreate();
}
}
private FillFlowContainer buttons;
public const float BORDER_RADIUS = 3;
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
recreate();
}
private void recreate()
{
if (LoadState < LoadState.Loading)
return;
InternalChildren = new Drawable[]
{
new Container
{
Masking = true,
BorderThickness = BORDER_RADIUS,
BorderColour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
},
}
},
buttons = new FillFlowContainer
{
Y = 20,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre
}
};
if (CanScaleX) addXScaleComponents();
if (CanScaleX && CanScaleY) addFullScaleComponents();
if (CanScaleY) addYScaleComponents();
if (CanRotate) addRotationComponents();
if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke());
}
private void addRotationComponents()
{
const float separation = 40;
addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90));
addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90));
AddRangeInternal(new Drawable[]
{
new Box
{
Depth = float.MaxValue,
Colour = colours.YellowLight,
Blending = BlendingParameters.Additive,
Alpha = 0.3f,
Size = new Vector2(BORDER_RADIUS, separation),
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate")
{
Anchor = Anchor.TopCentre,
Y = -separation,
HandleDrag = e => OnRotation?.Invoke(e.Delta.X),
OperationStarted = operationStarted,
OperationEnded = operationEnded
}
});
}
private void addYScaleComponents()
{
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical));
addDragHandle(Anchor.TopCentre);
addDragHandle(Anchor.BottomCentre);
}
private void addFullScaleComponents()
{
addDragHandle(Anchor.TopLeft);
addDragHandle(Anchor.TopRight);
addDragHandle(Anchor.BottomLeft);
addDragHandle(Anchor.BottomRight);
}
private void addXScaleComponents()
{
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal));
addDragHandle(Anchor.CentreLeft);
addDragHandle(Anchor.CentreRight);
}
private void addButton(IconUsage icon, string tooltip, Action action)
{
buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip)
{
OperationStarted = operationStarted,
OperationEnded = operationEnded,
Action = action
});
}
private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle
{
Anchor = anchor,
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
OperationStarted = operationStarted,
OperationEnded = operationEnded
});
private int activeOperations;
private void operationEnded()
{
if (--activeOperations == 0)
OperationEnded?.Invoke();
}
private void operationStarted()
{
if (activeOperations++ == 0)
OperationStarted?.Invoke();
}
}
}

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBoxDragHandle : Container
{
public Action OperationStarted;
public Action OperationEnded;
public Action<DragEvent> HandleDrag { get; set; }
private Circle circle;
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(10);
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UpdateHoverState();
}
protected override bool OnHover(HoverEvent e)
{
UpdateHoverState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
UpdateHoverState();
}
protected bool HandlingMouse;
protected override bool OnMouseDown(MouseDownEvent e)
{
HandlingMouse = true;
UpdateHoverState();
return true;
}
protected override bool OnDragStart(DragStartEvent e)
{
OperationStarted?.Invoke();
return true;
}
protected override void OnDrag(DragEvent e)
{
HandleDrag?.Invoke(e);
base.OnDrag(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
HandlingMouse = false;
OperationEnded?.Invoke();
UpdateHoverState();
base.OnDragEnd(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
HandlingMouse = false;
UpdateHoverState();
base.OnMouseUp(e);
}
protected virtual void UpdateHoverState()
{
circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark);
this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A drag "handle" which shares the visual appearance but behaves more like a clickable button.
/// </summary>
public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip
{
private SpriteIcon icon;
private readonly IconUsage iconUsage;
public Action Action;
public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip)
{
this.iconUsage = iconUsage;
TooltipText = tooltip;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
Size *= 2;
AddInternal(icon = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
Icon = iconUsage,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
protected override bool OnClick(ClickEvent e)
{
OperationStarted?.Invoke();
Action?.Invoke();
OperationEnded?.Invoke();
return true;
}
protected override void UpdateHoverState()
{
base.UpdateHoverState();
icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black;
}
public string TooltipText { get; }
}
}

View File

@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -20,6 +22,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@ -29,22 +32,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{
public const float BORDER_RADIUS = 2;
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
private readonly List<SelectionBlueprint> selectedBlueprints;
public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject);
public int SelectedCount => selectedBlueprints.Count;
private Drawable content;
private OsuSpriteText selectionDetailsText;
[Resolved(CanBeNull = true)]
protected SelectionBox SelectionBox { get; private set; }
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
protected IEditorChangeHandler ChangeHandler { get; private set; }
public SelectionHandler()
{
@ -58,23 +61,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
createStateBindables();
InternalChild = content = new Container
{
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = BORDER_RADIUS,
BorderColour = colours.YellowDark,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
}
},
// todo: should maybe be inside the SelectionBox?
new Container
{
Name = "info text",
@ -93,11 +86,40 @@ namespace osu.Game.Screens.Edit.Compose.Components
Font = OsuFont.Default.With(size: 11)
}
}
}
},
SelectionBox = CreateSelectionBox(),
}
};
}
public SelectionBox CreateSelectionBox()
=> new SelectionBox
{
OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded,
OnRotation = angle => HandleRotation(angle),
OnScale = (amount, anchor) => HandleScale(amount, anchor),
OnFlip = direction => HandleFlip(direction),
OnReverse = () => HandleReverse(),
};
/// <summary>
/// Fired when a drag operation ends from the selection box.
/// </summary>
protected virtual void OnOperationBegan()
{
ChangeHandler.BeginChange();
}
/// <summary>
/// Fired when a drag operation begins from the selection box.
/// </summary>
protected virtual void OnOperationEnded()
{
ChangeHandler.EndChange();
}
#region User Input Handling
/// <summary>
@ -112,7 +134,35 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Whether any <see cref="DrawableHitObject"/>s could be moved.
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
/// </returns>
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true;
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
/// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be rotated.</returns>
public virtual bool HandleRotation(float angle) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being scaled.
/// </summary>
/// <param name="scale">The delta scale to apply, in playfield local coordinates.</param>
/// <param name="anchor">The point of reference where the scale is originating from.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be scaled.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being flipped.
/// </summary>
/// <param name="direction">The direction to flip</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be flipped.</returns>
public virtual bool HandleFlip(Direction direction) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being reversed pattern-wise.
/// </summary>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be reversed.</returns>
public virtual bool HandleReverse() => false;
public bool OnPressed(PlatformAction action)
{
@ -146,7 +196,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
internal void HandleSelected(SelectionBlueprint blueprint)
{
selectedBlueprints.Add(blueprint);
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
// there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once.
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject))
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
UpdateVisibility();
}
@ -158,6 +211,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
internal void HandleDeselected(SelectionBlueprint blueprint)
{
selectedBlueprints.Remove(blueprint);
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
UpdateVisibility();
@ -189,12 +243,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void deleteSelected()
{
changeHandler?.BeginChange();
foreach (var h in selectedBlueprints.ToList())
EditorBeatmap?.Remove(h.HitObject);
changeHandler?.EndChange();
EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject));
}
#endregion
@ -211,11 +260,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
if (count > 0)
{
Show();
OnSelectionChanged();
}
else
Hide();
}
/// <summary>
/// Triggered whenever more than one object is selected, on each change.
/// Should update the selection box's state to match supported operations.
/// </summary>
protected virtual void OnSelectionChanged()
{
}
protected override void Update()
{
base.Update();
@ -250,9 +310,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
changeHandler?.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in SelectedHitObjects)
foreach (var h in EditorBeatmap.SelectedHitObjects)
{
// Make sure there isn't already an existing sample
if (h.Samples.Any(s => s.Name == sampleName))
@ -261,7 +321,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
changeHandler?.EndChange();
EditorBeatmap.EndChange();
}
/// <summary>
/// Set the new combo state of all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="state">Whether to set or unset.</param>
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
public void SetNewCombo(bool state)
{
EditorBeatmap.BeginChange();
foreach (var h in EditorBeatmap.SelectedHitObjects)
{
var comboInfo = h as IHasComboInformation;
if (comboInfo == null || comboInfo.NewCombo == state) continue;
comboInfo.NewCombo = state;
EditorBeatmap.Update(h);
}
EditorBeatmap.EndChange();
}
/// <summary>
@ -270,12 +352,99 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
changeHandler?.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in SelectedHitObjects)
foreach (var h in EditorBeatmap.SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
changeHandler?.EndChange();
EditorBeatmap.EndChange();
}
#endregion
#region Selection State
/// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
/// <summary>
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables()
{
foreach (var sampleName in HitSampleInfo.AllAdditions)
{
var bindable = new Bindable<TernaryState>
{
Description = sampleName.Replace("hit", string.Empty).Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
};
SelectionSampleStates[sampleName] = bindable;
}
// new combo
SelectionNewComboState.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
SetNewCombo(false);
break;
case TernaryState.True:
SetNewCombo(true);
break;
}
};
// bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates();
}
/// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
}
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
#endregion
@ -293,6 +462,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
{
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
}
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
@ -300,12 +474,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
new OsuMenuItem("Sound")
{
Items = new[]
{
createHitSampleMenuItem("Whistle", HitSampleInfo.HIT_WHISTLE),
createHitSampleMenuItem("Clap", HitSampleInfo.HIT_CLAP),
createHitSampleMenuItem("Finish", HitSampleInfo.HIT_FINISH)
}
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
},
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),
});
@ -322,41 +492,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
=> Enumerable.Empty<MenuItem>();
private MenuItem createHitSampleMenuItem(string name, string sampleName)
{
return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState)
{
State = { Value = getHitSampleState() }
};
void setHitSampleState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
}
TernaryState getHitSampleState()
{
int countExisting = SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName));
if (countExisting == 0)
return TernaryState.False;
if (countExisting < SelectedHitObjects.Count())
return TernaryState.Indeterminate;
return TernaryState.True;
}
}
#endregion
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class DifficultyPointPiece : CompositeDrawable
{
private readonly DifficultyControlPoint difficultyPoint;
private OsuSpriteText speedMultiplierText;
private readonly BindableNumber<double> speedMultiplier;
public DifficultyPointPiece(DifficultyControlPoint difficultyPoint)
{
this.difficultyPoint = difficultyPoint;
speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
Color4 colour = difficultyPoint.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Box
{
Colour = colour,
Width = 2,
RelativeSizeAxes = Axes.Y,
},
new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colour,
RelativeSizeAxes = Axes.Both,
},
speedMultiplierText = new OsuSpriteText
{
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Colour = Color4.White,
}
}
},
};
speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true);
}
}
}

View File

@ -0,0 +1,85 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class SamplePointPiece : CompositeDrawable
{
private readonly SampleControlPoint samplePoint;
private readonly Bindable<string> bank;
private readonly BindableNumber<int> volume;
private OsuSpriteText text;
private Box volumeBox;
public SamplePointPiece(SampleControlPoint samplePoint)
{
this.samplePoint = samplePoint;
volume = samplePoint.SampleVolumeBindable.GetBoundCopy();
bank = samplePoint.SampleBankBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Origin = Anchor.TopLeft;
Anchor = Anchor.TopLeft;
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
Color4 colour = samplePoint.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Y,
Width = 20,
Children = new Drawable[]
{
volumeBox = new Box
{
X = 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Colour = ColourInfo.GradientVertical(colour, Color4.Black),
RelativeSizeAxes = Axes.Both,
},
new Box
{
Colour = colour.Lighten(0.2f),
Width = 2,
RelativeSizeAxes = Axes.Y,
},
}
},
text = new OsuSpriteText
{
X = 2,
Y = -5,
Anchor = Anchor.BottomLeft,
Alpha = 0.9f,
Rotation = -90,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold)
}
};
volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true);
bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -21,55 +22,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
public readonly Bindable<bool> ControlPointsVisible = new Bindable<bool>();
public readonly Bindable<bool> TicksVisible = new Bindable<bool>();
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
[Resolved]
private EditorClock editorClock { get; set; }
public Timeline()
{
ZoomDuration = 200;
ZoomEasing = Easing.OutQuint;
ScrollbarVisible = false;
}
private WaveformGraph waveform;
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
{
Add(waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Blue.Opacity(0.2f),
LowColour = colours.BlueLighter,
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
Depth = float.MaxValue
});
// We don't want the centre marker to scroll
AddInternal(new CentreMarker { Depth = float.MaxValue });
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b =>
{
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
if (track.Length > 0)
{
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}
}, true);
}
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds);
/// <summary>
/// The timeline's scroll position in the last frame.
/// </summary>
@ -92,6 +54,69 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Track track;
public Timeline()
{
ZoomDuration = 200;
ZoomEasing = Easing.OutQuint;
ScrollbarVisible = false;
}
private WaveformGraph waveform;
private TimelineTickDisplay ticks;
private TimelineControlPointDisplay controlPoints;
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
{
AddRange(new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
Children = new Drawable[]
{
waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
BaseColour = colours.Blue.Opacity(0.2f),
LowColour = colours.BlueLighter,
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
},
ticks = new TimelineTickDisplay(),
controlPoints = new TimelineControlPointDisplay(),
}
},
});
// We don't want the centre marker to scroll
AddInternal(new CentreMarker { Depth = float.MaxValue });
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b =>
{
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
// todo: i don't think this is safe, the track may not be loaded yet.
if (track.Length > 0)
{
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}
}, true);
}
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds));
protected override void Update()
{
base.Update();

View File

@ -14,9 +14,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineArea : Container
{
private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both };
public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both };
protected override Container<Drawable> Content => timeline;
protected override Container<Drawable> Content => Timeline;
[BackgroundDependencyLoader]
private void load()
@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
CornerRadius = 5;
OsuCheckbox waveformCheckbox;
OsuCheckbox controlPointsCheckbox;
OsuCheckbox ticksCheckbox;
InternalChildren = new Drawable[]
{
@ -57,12 +59,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
Width = 160,
Padding = new MarginPadding { Horizontal = 15 },
Padding = new MarginPadding { Horizontal = 10 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new[]
{
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
waveformCheckbox = new OsuCheckbox
{
LabelText = "Waveform",
Current = { Value = true },
},
controlPointsCheckbox = new OsuCheckbox
{
LabelText = "Control Points",
Current = { Value = true },
},
ticksCheckbox = new OsuCheckbox
{
LabelText = "Ticks",
Current = { Value = true },
}
}
}
}
@ -107,7 +123,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
},
timeline
Timeline
},
},
ColumnDimensions = new[]
@ -119,11 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
waveformCheckbox.Current.Value = true;
timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
Timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current);
Timeline.TicksVisible.BindTo(ticksCheckbox.Current);
}
private void changeZoom(float change) => timeline.Zoom += change;
private void changeZoom(float change) => Timeline.Zoom += change;
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
/// <summary>
/// The part of the timeline that displays the control points.
/// </summary>
public class TimelineControlPointDisplay : TimelinePart<TimelineControlPointGroup>
{
private IBindableList<ControlPointGroup> controlPointGroups;
public TimelineControlPointDisplay()
{
RelativeSizeAxes = Axes.Both;
}
protected override void LoadBeatmap(WorkingBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
controlPointGroups.BindCollectionChanged((sender, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Reset:
Clear();
break;
case NotifyCollectionChangedAction.Add:
foreach (var group in args.NewItems.OfType<ControlPointGroup>())
Add(new TimelineControlPointGroup(group));
break;
case NotifyCollectionChangedAction.Remove:
foreach (var group in args.OldItems.OfType<ControlPointGroup>())
{
var matching = Children.SingleOrDefault(gv => gv.Group == group);
matching?.Expire();
}
break;
}
}, true);
}
}
}

View File

@ -0,0 +1,62 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineControlPointGroup : CompositeDrawable
{
public readonly ControlPointGroup Group;
private BindableList<ControlPoint> controlPoints;
[Resolved]
private OsuColour colours { get; set; }
public TimelineControlPointGroup(ControlPointGroup group)
{
Group = group;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
X = (float)group.Time;
}
protected override void LoadComplete()
{
base.LoadComplete();
controlPoints = (BindableList<ControlPoint>)Group.ControlPoints.GetBoundCopy();
controlPoints.BindCollectionChanged((_, __) =>
{
ClearInternal();
foreach (var point in controlPoints)
{
switch (point)
{
case DifficultyControlPoint difficultyPoint:
AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 });
break;
case TimingControlPoint timingPoint:
AddInternal(new TimingPointPiece(timingPoint));
break;
case SampleControlPoint samplePoint:
AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 });
break;
}
}
}, true);
}
}
}

View File

@ -3,18 +3,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@ -34,11 +39,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly List<Container> shadowComponents = new List<Container>();
private DrawableHitObject drawableHitObject;
private Bindable<Color4> comboColour;
private readonly Container mainComponents;
private readonly OsuSpriteText comboIndexText;
private Bindable<int> comboIndex;
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 16;
private const float circle_size = 24;
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
@ -54,14 +69,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AddRangeInternal(new Drawable[]
{
mainComponents = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
comboIndexText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
},
});
circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
@ -77,7 +106,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
DragBar dragBarUnderlay;
Container extensionBar;
AddRangeInternal(new Drawable[]
mainComponents.AddRange(new Drawable[]
{
extensionBar = new Container
{
@ -117,18 +146,93 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
AddInternal(circle);
mainComponents.Add(circle);
}
updateShadows();
}
[BackgroundDependencyLoader(true)]
private void load(HitObjectComposer composer)
{
if (composer != null)
{
// best effort to get the drawable representation for grabbing colour and what not.
drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (HitObject is IHasComboInformation comboInfo)
{
comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
comboIndex.BindValueChanged(combo =>
{
comboIndexText.Text = (combo.NewValue + 1).ToString();
}, true);
}
if (drawableHitObject != null)
{
comboColour = drawableHitObject.AccentColour.GetBoundCopy();
comboColour.BindValueChanged(colour =>
{
if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White);
else
mainComponents.Colour = drawableHitObject.AccentColour.Value;
var col = mainComponents.Colour.TopLeft.Linear;
float brightness = col.R + col.G + col.B;
// decide the combo index colour based on brightness?
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
}, true);
}
}
protected override void Update()
{
base.Update();
// no bindable so we perform this every update
Width = (float)(HitObject.GetEndTime() - HitObject.StartTime);
float duration = (float)(HitObject.GetEndTime() - HitObject.StartTime);
if (Width != duration)
{
Width = duration;
// kind of haphazard but yeah, no bindables.
if (HitObject is IHasRepeats repeats)
updateRepeats(repeats);
}
}
private Container repeatsContainer;
private void updateRepeats(IHasRepeats repeats)
{
repeatsContainer?.Expire();
mainComponents.Add(repeatsContainer = new Container
{
RelativeSizeAxes = Axes.Both,
});
for (int i = 0; i < repeats.RepeatCount; i++)
{
repeatsContainer.Add(new Circle
{
Size = new Vector2(circle_size / 2),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = (float)(i + 1) / (repeats.RepeatCount + 1),
});
}
}
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
@ -288,6 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
repeatHitObject.RepeatCount = proposedCount;
beatmap.Update(hitObject);
break;
case IHasDuration endTimeHitObject:
@ -297,10 +402,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
beatmap.Update(hitObject);
break;
}
beatmap.UpdateHitObject(hitObject);
}
}

View File

@ -1,9 +1,11 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -12,7 +14,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineTickDisplay : TimelinePart
public class TimelineTickDisplay : TimelinePart<PointVisualisation>
{
[Resolved]
private EditorBeatmap beatmap { get; set; }
@ -31,15 +33,63 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Both;
}
private readonly Cached tickCache = new Cached();
[BackgroundDependencyLoader]
private void load()
{
beatDivisor.BindValueChanged(_ => createLines(), true);
beatDivisor.BindValueChanged(_ => tickCache.Invalidate());
}
private void createLines()
/// <summary>
/// The visible time/position range of the timeline.
/// </summary>
private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
/// <summary>
/// The next time/position value to the left of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMinTick;
/// <summary>
/// The next time/position value to the right of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMaxTick;
[Resolved(canBeNull: true)]
private Timeline timeline { get; set; }
protected override void Update()
{
Clear();
base.Update();
if (timeline != null)
{
var newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
visibleRange = newRange;
// actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
tickCache.Invalidate();
}
}
if (!tickCache.IsValid)
createTicks();
}
private void createTicks()
{
int drawableIndex = 0;
int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
nextMinTick = null;
nextMaxTick = null;
for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++)
{
@ -50,41 +100,70 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value)
{
var indexInBeat = beat % beatDivisor.Value;
float xPos = (float)t;
if (indexInBeat == 0)
{
Add(new PointVisualisation(t)
{
Colour = BindableBeatDivisor.GetColourFor(1, colours),
Origin = Anchor.TopCentre,
});
}
if (t < visibleRange.min)
nextMinTick = xPos;
else if (t > visibleRange.max)
nextMaxTick ??= xPos;
else
{
// if this is the first beat in the beatmap, there is no next min tick
if (beat == 0 && i == 0)
nextMinTick = float.MinValue;
var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f;
Add(new PointVisualisation(t)
{
Colour = colour,
Height = height,
Origin = Anchor.TopCentre,
});
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
Add(new PointVisualisation(t)
{
Colour = colour,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
Height = height,
});
var topPoint = getNextUsablePoint();
topPoint.X = xPos;
topPoint.Colour = colour;
topPoint.Height = height;
topPoint.Anchor = Anchor.TopLeft;
topPoint.Origin = Anchor.TopCentre;
var bottomPoint = getNextUsablePoint();
bottomPoint.X = xPos;
bottomPoint.Colour = colour;
bottomPoint.Anchor = Anchor.BottomLeft;
bottomPoint.Origin = Anchor.BottomCentre;
bottomPoint.Height = height;
}
beat++;
}
}
int usedDrawables = drawableIndex;
// save a few drawables beyond the currently used for edge cases.
while (drawableIndex < Math.Min(usedDrawables + 16, Count))
Children[drawableIndex++].Hide();
// expire any excess
while (drawableIndex < Count)
Children[drawableIndex++].Expire();
tickCache.Validate();
Drawable getNextUsablePoint()
{
PointVisualisation point;
if (drawableIndex >= Count)
Add(point = new PointVisualisation());
else
point = Children[drawableIndex];
drawableIndex++;
point.Show();
return point;
}
}
}
}

View File

@ -0,0 +1,63 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimingPointPiece : CompositeDrawable
{
private readonly TimingControlPoint point;
private readonly BindableNumber<double> beatLength;
private OsuSpriteText bpmText;
public TimingPointPiece(TimingControlPoint point)
{
this.point = point;
beatLength = point.BeatLengthBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Origin = Anchor.CentreLeft;
Anchor = Anchor.CentreLeft;
AutoSizeAxes = Axes.Both;
Color4 colour = point.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Box
{
Alpha = 0.9f,
Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)),
RelativeSizeAxes = Axes.Both,
},
bpmText = new OsuSpriteText
{
Alpha = 0.9f,
Padding = new MarginPadding(3),
Font = OsuFont.Default.With(size: 40)
}
};
beatLength.BindValueChanged(beatLength =>
{
bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM";
}, true);
}
}
}

View File

@ -1,8 +1,12 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning;
@ -13,11 +17,28 @@ namespace osu.Game.Screens.Edit.Compose
{
private HitObjectComposer composer;
protected override Drawable CreateMainContent()
public ComposeScreen()
: base(EditorScreenMode.Compose)
{
var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance();
}
private Ruleset ruleset;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
ruleset = parent.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo.Ruleset?.CreateInstance();
composer = ruleset?.CreateHitObjectComposer();
// make the composer available to the timeline and other components in this screen.
dependencies.CacheAs(composer);
return dependencies;
}
protected override Drawable CreateMainContent()
{
if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");

View File

@ -6,6 +6,7 @@ namespace osu.Game.Screens.Edit.Design
public class DesignScreen : EditorScreen
{
public DesignScreen()
: base(EditorScreenMode.Design)
{
Child = new ScreenWhiteBox.UnderConstructionMessage("Design mode");
}

View File

@ -2,42 +2,48 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK.Graphics;
using osu.Framework.Screens;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Timing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Design;
using osuTK.Input;
using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.IO.Serialization;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
[Cached]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider
{
public override float BackgroundParallaxAmount => 0.1f;
@ -50,11 +56,20 @@ namespace osu.Game.Screens.Edit
public override bool AllowRateAdjustments => false;
protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
private bool exitConfirmed;
private string lastSavedHash;
private Box bottomBackground;
private Container screenContainer;
private Container<EditorScreen> screenContainer;
private EditorScreen currentScreen;
@ -65,13 +80,23 @@ namespace osu.Game.Screens.Edit
private EditorBeatmap editorBeatmap;
private EditorChangeHandler changeHandler;
private EditorMenuBar menuBar;
private DependencyContainer dependencies;
private bool isNewBeatmap;
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private MusicController music { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours, GameHost host)
{
@ -79,16 +104,23 @@ namespace osu.Game.Screens.Edit
beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
UpdateClockSource();
dependencies.CacheAs(clock);
dependencies.CacheAs<ISamplePlaybackDisabler>(clock);
AddInternal(clock);
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
if (Beatmap.Value is DummyWorkingBeatmap)
{
isNewBeatmap = true;
Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
}
try
{
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
@ -101,19 +133,23 @@ namespace osu.Game.Screens.Edit
return;
}
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap));
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, Beatmap.Value.Skin));
dependencies.CacheAs(editorBeatmap);
changeHandler = new EditorChangeHandler(editorBeatmap);
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
EditorMenuBar menuBar;
updateLastSavedHash();
OsuMenuItem undoMenuItem;
OsuMenuItem redoMenuItem;
EditorMenuItem cutMenuItem;
EditorMenuItem copyMenuItem;
EditorMenuItem pasteMenuItem;
var fileMenuItems = new List<MenuItem>
{
new EditorMenuItem("Save", MenuItemType.Standard, saveBeatmap)
new EditorMenuItem("Save", MenuItemType.Standard, Save)
};
if (RuntimeInfo.IsDesktop)
@ -132,7 +168,7 @@ namespace osu.Game.Screens.Edit
Name = "Screen container",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 40, Bottom = 60 },
Child = screenContainer = new Container
Child = screenContainer = new Container<EditorScreen>
{
RelativeSizeAxes = Axes.Both,
Masking = true
@ -148,6 +184,7 @@ namespace osu.Game.Screens.Edit
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose },
Items = new[]
{
new MenuItem("File")
@ -159,7 +196,11 @@ namespace osu.Game.Screens.Edit
Items = new[]
{
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo)
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
}
}
}
@ -220,11 +261,44 @@ namespace osu.Game.Screens.Edit
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) =>
{
var hasObjects = editorBeatmap.SelectedHitObjects.Count > 0;
cutMenuItem.Action.Disabled = !hasObjects;
copyMenuItem.Action.Disabled = !hasObjects;
}, true);
clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue));
menuBar.Mode.ValueChanged += onModeChanged;
bottomBackground.Colour = colours.Gray2;
}
/// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary>
public void UpdateClockSource()
{
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock.ChangeSource(sourceClock);
}
protected void Save()
{
// no longer new after first user-triggered save.
isNewBeatmap = false;
// apply any set-level metadata changes.
beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet);
// save the loaded beatmap's data stream.
beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin);
updateLastSavedHash();
}
protected override void Update()
{
base.Update();
@ -235,6 +309,18 @@ namespace osu.Game.Screens.Edit
{
switch (action.ActionType)
{
case PlatformActionType.Cut:
Cut();
return true;
case PlatformActionType.Copy:
Copy();
return true;
case PlatformActionType.Paste:
Paste();
return true;
case PlatformActionType.Undo:
Undo();
return true;
@ -244,7 +330,7 @@ namespace osu.Game.Screens.Edit
return true;
case PlatformActionType.Save:
saveBeatmap();
Save();
return true;
}
@ -284,7 +370,7 @@ namespace osu.Game.Screens.Edit
// this is a special case to handle the "pivot" scenario.
// if we are precise scrolling in one direction then change our mind and scroll backwards,
// the existing accumulation should be applied in the inverse direction to maintain responsiveness.
if (Math.Sign(scrollAccumulation) != scrollDirection)
if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection)
scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation));
scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1);
@ -305,14 +391,32 @@ namespace osu.Game.Screens.Edit
public bool OnPressed(GlobalAction action)
{
if (action == GlobalAction.Back)
switch (action)
{
// as we don't want to display the back button, manual handling of exit action is required.
this.Exit();
return true;
}
case GlobalAction.Back:
// as we don't want to display the back button, manual handling of exit action is required.
this.Exit();
return true;
return false;
case GlobalAction.EditorComposeMode:
menuBar.Mode.Value = EditorScreenMode.Compose;
return true;
case GlobalAction.EditorDesignMode:
menuBar.Mode.Value = EditorScreenMode.Design;
return true;
case GlobalAction.EditorTimingMode:
menuBar.Mode.Value = EditorScreenMode.Timing;
return true;
case GlobalAction.EditorSetupMode:
menuBar.Mode.Value = EditorScreenMode.SongSetup;
return true;
default:
return false;
}
}
public void OnReleased(GlobalAction action)
@ -334,12 +438,94 @@ namespace osu.Game.Screens.Edit
public override bool OnExiting(IScreen next)
{
if (!exitConfirmed)
{
// if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save.
if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog)
{
confirmExit();
return true;
}
if (isNewBeatmap || HasUnsavedChanges)
{
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
return true;
}
}
Background.FadeColour(Color4.White, 500);
resetTrack();
return base.OnExiting(next);
}
private void confirmExitWithSave()
{
exitConfirmed = true;
Save();
this.Exit();
}
private void confirmExit()
{
// stop the track if playing to allow the parent screen to choose a suitable playback mode.
Beatmap.Value.Track.Stop();
if (isNewBeatmap)
{
// confirming exit without save means we should delete the new beatmap completely.
beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
// in theory this shouldn't be required but due to EF core not sharing instance states 100%
// MusicController is unaware of the changed DeletePending state.
Beatmap.SetDefault();
}
exitConfirmed = true;
this.Exit();
}
private readonly Bindable<string> clipboard = new Bindable<string>();
protected void Cut()
{
Copy();
editorBeatmap.RemoveRange(editorBeatmap.SelectedHitObjects.ToArray());
}
protected void Copy()
{
if (editorBeatmap.SelectedHitObjects.Count == 0)
return;
clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
}
protected void Paste()
{
if (string.IsNullOrEmpty(clipboard.Value))
return;
var objects = clipboard.Value.Deserialize<ClipboardContent>().HitObjects;
Debug.Assert(objects.Any());
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime);
foreach (var h in objects)
h.StartTime += timeOffset;
editorBeatmap.BeginChange();
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.AddRange(objects);
editorBeatmap.SelectedHitObjects.AddRange(objects);
editorBeatmap.EndChange();
}
protected void Undo() => changeHandler.RestoreState(-1);
protected void Redo() => changeHandler.RestoreState(1);
@ -365,7 +551,21 @@ namespace osu.Game.Screens.Edit
private void onModeChanged(ValueChangedEvent<EditorScreenMode> e)
{
currentScreen?.Exit();
var lastScreen = currentScreen;
lastScreen?
.ScaleTo(0.98f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null)
{
screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);
currentScreen
.ScaleTo(1, 200, Easing.OutQuint)
.FadeIn(200, Easing.OutQuint);
return;
}
switch (e.NewValue)
{
@ -386,27 +586,44 @@ namespace osu.Game.Screens.Edit
break;
}
LoadComponentAsync(currentScreen, screenContainer.Add);
LoadComponentAsync(currentScreen, newScreen =>
{
if (newScreen == currentScreen)
screenContainer.Add(newScreen);
});
}
private void seek(UIEvent e, int direction)
{
double amount = e.ShiftPressed ? 2 : 1;
double amount = e.ShiftPressed ? 4 : 1;
bool trackPlaying = clock.IsRunning;
if (trackPlaying)
{
// generally users are not looking to perform tiny seeks when the track is playing,
// so seeks should always be by one full beat, bypassing the beatDivisor.
// this multiplication undoes the division that will be applied in the underlying seek operation.
amount *= beatDivisor.Value;
}
if (direction < 1)
clock.SeekBackward(!clock.IsRunning, amount);
clock.SeekBackward(!trackPlaying, amount);
else
clock.SeekForward(!clock.IsRunning, amount);
clock.SeekForward(!trackPlaying, amount);
}
private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap);
private void exportBeatmap()
{
saveBeatmap();
Save();
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
}
private void updateLastSavedHash()
{
lastSavedHash = changeHandler.CurrentStateHash;
}
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);

View File

@ -8,17 +8,16 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
namespace osu.Game.Screens.Edit
{
public class EditorBeatmap : Component, IBeatmap, IBeatSnapProvider
public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider
{
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="EditorBeatmap"/>.
@ -47,6 +46,8 @@ namespace osu.Game.Screens.Edit
public readonly IBeatmap PlayableBeatmap;
public readonly ISkin BeatmapSkin;
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
@ -54,9 +55,10 @@ namespace osu.Game.Screens.Edit
private readonly Dictionary<HitObject, Bindable<double>> startTimeBindables = new Dictionary<HitObject, Bindable<double>>();
public EditorBeatmap(IBeatmap playableBeatmap)
public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null)
{
PlayableBeatmap = playableBeatmap;
BeatmapSkin = beatmapSkin;
beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap);
@ -64,41 +66,6 @@ namespace osu.Game.Screens.Edit
trackStartTime(obj);
}
private readonly HashSet<HitObject> pendingUpdates = new HashSet<HitObject>();
private ScheduledDelegate scheduledUpdate;
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false);
private void updateHitObject([CanBeNull] HitObject hitObject, bool silent)
{
scheduledUpdate?.Cancel();
if (hitObject != null)
pendingUpdates.Add(hitObject);
scheduledUpdate = Schedule(() =>
{
beatmapProcessor?.PreProcess();
foreach (var obj in pendingUpdates)
obj.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
if (!silent)
{
foreach (var obj in pendingUpdates)
HitObjectUpdated?.Invoke(obj);
}
pendingUpdates.Clear();
});
}
public BeatmapInfo BeatmapInfo
{
get => PlayableBeatmap.BeatmapInfo;
@ -121,14 +88,22 @@ namespace osu.Game.Screens.Edit
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
private readonly List<HitObject> batchPendingInserts = new List<HitObject>();
private readonly List<HitObject> batchPendingDeletes = new List<HitObject>();
private readonly HashSet<HitObject> batchPendingUpdates = new HashSet<HitObject>();
/// <summary>
/// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObjects">The <see cref="HitObject"/>s to add.</param>
public void AddRange(IEnumerable<HitObject> hitObjects)
{
BeginChange();
foreach (var h in hitObjects)
Add(h);
EndChange();
}
/// <summary>
@ -156,14 +131,34 @@ namespace osu.Game.Screens.Edit
mutableHitObjects.Insert(index, hitObject);
HitObjectAdded?.Invoke(hitObject);
updateHitObject(hitObject, true);
BeginChange();
batchPendingInserts.Add(hitObject);
EndChange();
}
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public void Update([NotNull] HitObject hitObject)
{
// updates are debounced regardless of whether a batch is active.
batchPendingUpdates.Add(hitObject);
}
/// <summary>
/// Update all hit objects with potentially changed difficulty or control point data.
/// </summary>
public void UpdateAllHitObjects()
{
foreach (var h in HitObjects)
batchPendingUpdates.Add(h);
}
/// <summary>
/// Removes a <see cref="HitObject"/> from this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
/// <param name="hitObject">The <see cref="HitObject"/> to remove.</param>
/// <returns>True if the <see cref="HitObject"/> has been removed, false otherwise.</returns>
public bool Remove(HitObject hitObject)
{
@ -176,6 +171,18 @@ namespace osu.Game.Screens.Edit
return true;
}
/// <summary>
/// Removes a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObjects">The <see cref="HitObject"/>s to remove.</param>
public void RemoveRange(IEnumerable<HitObject> hitObjects)
{
BeginChange();
foreach (var h in hitObjects)
Remove(h);
EndChange();
}
/// <summary>
/// Finds the index of a <see cref="HitObject"/> in this <see cref="EditorBeatmap"/>.
/// </summary>
@ -195,31 +202,55 @@ namespace osu.Game.Screens.Edit
var bindable = startTimeBindables[hitObject];
bindable.UnbindAll();
startTimeBindables.Remove(hitObject);
HitObjectRemoved?.Invoke(hitObject);
updateHitObject(null, true);
BeginChange();
batchPendingDeletes.Add(hitObject);
EndChange();
}
protected override void Update()
{
base.Update();
if (batchPendingUpdates.Count > 0)
UpdateState();
}
protected override void UpdateState()
{
if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0)
return;
beatmapProcessor?.PreProcess();
foreach (var h in batchPendingDeletes) processHitObject(h);
foreach (var h in batchPendingInserts) processHitObject(h);
foreach (var h in batchPendingUpdates) processHitObject(h);
beatmapProcessor?.PostProcess();
// callbacks may modify the lists so let's be safe about it
var deletes = batchPendingDeletes.ToArray();
batchPendingDeletes.Clear();
var inserts = batchPendingInserts.ToArray();
batchPendingInserts.Clear();
var updates = batchPendingUpdates.ToArray();
batchPendingUpdates.Clear();
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
}
/// <summary>
/// Clears all <see cref="HitObjects"/> from this <see cref="EditorBeatmap"/>.
/// </summary>
public void Clear()
{
var removed = HitObjects.ToList();
public void Clear() => RemoveRange(HitObjects.ToArray());
mutableHitObjects.Clear();
foreach (var b in startTimeBindables)
b.Value.UnbindAll();
startTimeBindables.Clear();
foreach (var h in removed)
HitObjectRemoved?.Invoke(h);
updateHitObject(null, true);
}
private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
private void trackStartTime(HitObject hitObject)
{
@ -232,7 +263,7 @@ namespace osu.Game.Screens.Edit
var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
mutableHitObjects.Insert(insertionIndex + 1, hitObject);
UpdateHitObject(hitObject);
Update(hitObject);
};
}

View File

@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Objects;
@ -14,18 +16,31 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// Tracks changes to the <see cref="Editor"/>.
/// </summary>
public class EditorChangeHandler : IEditorChangeHandler
public class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
{
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
public event Action OnStateChange;
private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List<byte[]> savedStates = new List<byte[]>();
private int currentState = -1;
/// <summary>
/// A SHA-2 hash representing the current visible editor state.
/// </summary>
public string CurrentStateHash
{
get
{
using (var stream = new MemoryStream(savedStates[currentState]))
return stream.ComputeSHA2Hash();
}
}
private readonly EditorBeatmap editorBeatmap;
private int bulkChangesStarted;
private bool isRestoring;
public const int MAX_SAVED_STATES = 50;
@ -38,9 +53,9 @@ namespace osu.Game.Screens.Edit
{
this.editorBeatmap = editorBeatmap;
editorBeatmap.HitObjectAdded += hitObjectAdded;
editorBeatmap.HitObjectRemoved += hitObjectRemoved;
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
editorBeatmap.TransactionBegan += BeginChange;
editorBeatmap.TransactionEnded += EndChange;
editorBeatmap.SaveStateTriggered += SaveState;
patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
@ -48,51 +63,34 @@ namespace osu.Game.Screens.Edit
SaveState();
}
private void hitObjectAdded(HitObject obj) => SaveState();
private void hitObjectRemoved(HitObject obj) => SaveState();
private void hitObjectUpdated(HitObject obj) => SaveState();
public void BeginChange() => bulkChangesStarted++;
public void EndChange()
protected override void UpdateState()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
SaveState();
}
/// <summary>
/// Saves the current <see cref="Editor"/> state.
/// </summary>
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
if (isRestoring)
return;
if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
if (savedStates.Count > MAX_SAVED_STATES)
savedStates.RemoveAt(0);
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(editorBeatmap).Encode(sw);
new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
savedStates.Add(stream.ToArray());
var newState = stream.ToArray();
// if the previous state is binary equal we don't need to push a new one, unless this is the initial state.
if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return;
if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
if (savedStates.Count > MAX_SAVED_STATES)
savedStates.RemoveAt(0);
savedStates.Add(newState);
currentState = savedStates.Count - 1;
OnStateChange?.Invoke();
updateBindables();
}
currentState = savedStates.Count - 1;
updateBindables();
}
/// <summary>
@ -101,7 +99,7 @@ namespace osu.Game.Screens.Edit
/// <param name="direction">The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used.</param>
public void RestoreState(int direction)
{
if (bulkChangesStarted > 0)
if (TransactionActive)
return;
if (savedStates.Count == 0)
@ -118,6 +116,7 @@ namespace osu.Game.Screens.Edit
isRestoring = false;
OnStateChange?.Invoke();
updateBindables();
}

View File

@ -3,21 +3,28 @@
using System;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
/// </summary>
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler
{
public readonly double TrackLength;
public IBindable<Track> Track => track;
private readonly Bindable<Track> track = new Bindable<Track>();
public double TrackLength => track.Value?.Length ?? 60000;
public ControlPointInfo ControlPointInfo;
@ -25,6 +32,10 @@ namespace osu.Game.Screens.Edit
private readonly DecoupleableInterpolatingFramedClock underlyingClock;
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
: this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{
@ -35,7 +46,6 @@ namespace osu.Game.Screens.Edit
this.beatDivisor = beatDivisor;
ControlPointInfo = controlPointInfo;
TrackLength = trackLength;
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
@ -76,7 +86,7 @@ namespace osu.Game.Screens.Edit
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
/// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount);
public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount + (IsRunning ? 1.5 : 0));
/// <summary>
/// Seeks forwards by one beat length.
@ -161,11 +171,14 @@ namespace osu.Game.Screens.Edit
public void Stop()
{
samplePlaybackDisabled.Value = true;
underlyingClock.Stop();
}
public bool Seek(double position)
{
samplePlaybackDisabled.Value = true;
ClearTransforms();
return underlyingClock.Seek(position);
}
@ -190,7 +203,11 @@ namespace osu.Game.Screens.Edit
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source);
public void ChangeSource(IClock source)
{
track.Value = source as Track;
underlyingClock.ChangeSource(source);
}
public IClock Source => underlyingClock.Source;
@ -202,8 +219,35 @@ namespace osu.Game.Screens.Edit
private const double transform_time = 300;
protected override void Update()
{
base.Update();
updateSeekingState();
}
private void updateSeekingState()
{
if (samplePlaybackDisabled.Value)
{
if (track.Value?.IsRunning != true)
{
// seeking in the editor can happen while the track isn't running.
// in this case we always want to expose ourselves as seeking (to avoid sample playback).
return;
}
// we are either running a seek tween or doing an immediate seek.
// in the case of an immediate seek the seeking bool will be set to false after one update.
// this allows for silencing hit sounds and the likes.
samplePlaybackDisabled.Value = Transforms.Any();
}
}
public void SeekTo(double seekDestination)
{
samplePlaybackDisabled.Value = true;
if (IsRunning)
Seek(seekDestination);
else

View File

@ -23,8 +23,12 @@ namespace osu.Game.Screens.Edit
protected override Container<Drawable> Content => content;
private readonly Container content;
protected EditorScreen()
public readonly EditorScreenMode Type;
protected EditorScreen(EditorScreenMode type)
{
Type = type;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
@ -40,10 +44,5 @@ namespace osu.Game.Screens.Edit
.Then()
.FadeTo(1f, 250, Easing.OutQuint);
}
public void Exit()
{
this.FadeOut(250).Expire();
}
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK.Graphics;
@ -24,6 +25,11 @@ namespace osu.Game.Screens.Edit
private Container timelineContainer;
protected EditorScreenWithTimeline(EditorScreenMode type)
: base(type)
{
}
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{
@ -32,6 +38,8 @@ namespace osu.Game.Screens.Edit
Container mainContent;
LoadingSpinner spinner;
Children = new Drawable[]
{
mainContent = new Container
@ -44,6 +52,10 @@ namespace osu.Game.Screens.Edit
Top = vertical_margins + timeline_height,
Bottom = vertical_margins
},
Child = spinner = new LoadingSpinner(true)
{
State = { Value = Visibility.Visible },
},
},
new Container
{
@ -90,6 +102,8 @@ namespace osu.Game.Screens.Edit
LoadComponentAsync(CreateMainContent(), content =>
{
spinner.State.Value = Visibility.Hidden;
mainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint);
@ -98,13 +112,20 @@ namespace osu.Game.Screens.Edit
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new TimelineTickDisplay(),
CreateTimelineContent(),
}
}, timelineContainer.Add);
}, t =>
{
timelineContainer.Add(t);
OnTimelineLoaded(t);
});
});
}
protected virtual void OnTimelineLoaded(TimelineArea timelineArea)
{
}
protected abstract Drawable CreateMainContent();
protected virtual Drawable CreateTimelineContent() => new Container();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit
/// </summary>
public interface IEditorChangeHandler
{
/// <summary>
/// Fired whenever a state change occurs.
/// </summary>
event Action OnStateChange;
/// <summary>
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
/// </summary>
@ -29,5 +35,11 @@ namespace osu.Game.Screens.Edit
/// This should be invoked as soon as possible after <see cref="BeginChange"/> to cause a state change.
/// </remarks>
void EndChange();
/// <summary>
/// Immediately saves the current <see cref="Editor"/> state.
/// Note that this will be a no-op if there is a change in progress via <see cref="BeginChange"/>.
/// </summary>
void SaveState();
}
}

View File

@ -68,6 +68,8 @@ namespace osu.Game.Screens.Edit
toRemove.Sort();
toAdd.Sort();
editorBeatmap.BeginChange();
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
editorBeatmap.RemoveAt(toRemove[i]);
@ -78,6 +80,8 @@ namespace osu.Game.Screens.Edit
foreach (var i in toAdd)
editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
}
editorBeatmap.EndChange();
}
private string readString(byte[] state) => Encoding.UTF8.GetString(state);
@ -107,7 +111,7 @@ namespace osu.Game.Screens.Edit
protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetTrack() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
{
public class PromptForSaveDialog : PopupDialog
{
public PromptForSaveDialog(Action exit, Action saveAndExit)
{
HeaderText = "Did you want to save your changes?";
Icon = FontAwesome.Regular.Save;
Buttons = new PopupDialogButton[]
{
new PopupDialogCancelButton
{
Text = @"Save my masterpiece!",
Action = saveAndExit
},
new PopupDialogOkButton
{
Text = @"Forget all changes",
Action = exit
},
new PopupDialogCancelButton
{
Text = @"Oops, continue editing",
},
};
}
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
internal class DifficultySection : SetupSection
{
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
private LabelledSliderBar<float> circleSizeSlider;
private LabelledSliderBar<float> healthDrainSlider;
private LabelledSliderBar<float> approachRateSlider;
private LabelledSliderBar<float> overallDifficultySlider;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Difficulty settings"
},
circleSizeSlider = new LabelledSliderBar<float>
{
Label = "Object Size",
Description = "The size of all hit objects",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 2,
MaxValue = 7,
Precision = 0.1f,
}
},
healthDrainSlider = new LabelledSliderBar<float>
{
Label = "Health Drain",
Description = "The rate of passive health drain throughout playable time",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
approachRateSlider = new LabelledSliderBar<float>
{
Label = "Approach Rate",
Description = "The speed at which objects are presented to the player",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
overallDifficultySlider = new LabelledSliderBar<float>
{
Label = "Overall Difficulty",
Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
};
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
item.Current.ValueChanged += onValueChanged;
}
private void onValueChanged(ValueChangedEvent<float> args)
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
editorBeatmap.UpdateAllHitObjects();
}
}
}

View File

@ -0,0 +1,73 @@
// 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.IO;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
/// <summary>
/// A labelled textbox which reveals an inline file chooser when clicked.
/// </summary>
internal class FileChooserLabelledTextBox : LabelledTextBox
{
public Container Target;
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
public FileChooserLabelledTextBox()
{
currentFile.BindValueChanged(onFileSelected);
}
private void onFileSelected(ValueChangedEvent<FileInfo> file)
{
if (file.NewValue == null)
return;
Target.Clear();
Current.Value = file.NewValue.FullName;
}
protected override OsuTextBox CreateTextBox() =>
new FileChooserOsuTextBox
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS,
OnFocused = DisplayFileChooser
};
public void DisplayFileChooser()
{
Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions)
{
RelativeSizeAxes = Axes.X,
Height = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
CurrentFile = { BindTarget = currentFile }
};
}
internal class FileChooserOsuTextBox : OsuTextBox
{
public Action OnFocused;
protected override void OnFocus(FocusEvent e)
{
OnFocused?.Invoke();
base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this);
}
}
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
internal class MetadataSection : SetupSection
{
private LabelledTextBox artistTextBox;
private LabelledTextBox titleTextBox;
private LabelledTextBox creatorTextBox;
private LabelledTextBox difficultyTextBox;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Beatmap metadata"
},
artistTextBox = new LabelledTextBox
{
Label = "Artist",
Current = { Value = Beatmap.Value.Metadata.Artist },
TabbableContentContainer = this
},
titleTextBox = new LabelledTextBox
{
Label = "Title",
Current = { Value = Beatmap.Value.Metadata.Title },
TabbableContentContainer = this
},
creatorTextBox = new LabelledTextBox
{
Label = "Creator",
Current = { Value = Beatmap.Value.Metadata.AuthorString },
TabbableContentContainer = this
},
difficultyTextBox = new LabelledTextBox
{
Label = "Difficulty Name",
Current = { Value = Beatmap.Value.BeatmapInfo.Version },
TabbableContentContainer = this
},
};
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
}
private void onCommit(TextBox sender, bool newText)
{
if (!newText) return;
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value;
Beatmap.Value.Metadata.Title = titleTextBox.Current.Value;
Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value;
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value;
}
}
}

View File

@ -0,0 +1,211 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Setup
{
internal class ResourcesSection : SetupSection, ICanAcceptFiles
{
private LabelledTextBox audioTrackTextBox;
private Container backgroundSpriteContainer;
public IEnumerable<string> HandledExtensions => ImageExtensions.Concat(AudioExtensions);
public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" };
public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" };
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private MusicController music { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Editor editor { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Container audioTrackFileChooserContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
Children = new Drawable[]
{
backgroundSpriteContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = 250,
Masking = true,
CornerRadius = 10,
},
new OsuSpriteText
{
Text = "Resources"
},
audioTrackTextBox = new FileChooserLabelledTextBox
{
Label = "Audio Track",
Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" },
Target = audioTrackFileChooserContainer,
TabbableContentContainer = this
},
audioTrackFileChooserContainer,
};
updateBackgroundSprite();
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
}
Task ICanAcceptFiles.Import(params string[] paths)
{
Schedule(() =>
{
var firstFile = new FileInfo(paths.First());
if (ImageExtensions.Contains(firstFile.Extension))
{
ChangeBackgroundImage(firstFile.FullName);
}
else if (AudioExtensions.Contains(firstFile.Extension))
{
audioTrackTextBox.Text = firstFile.FullName;
}
});
return Task.CompletedTask;
}
protected override void LoadComplete()
{
base.LoadComplete();
game.RegisterImportHandler(this);
}
public bool ChangeBackgroundImage(string path)
{
var info = new FileInfo(path);
if (!info.Exists)
return false;
var set = Beatmap.Value.BeatmapSetInfo;
// remove the previous background for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile);
using (var stream = info.OpenRead())
{
if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
else
beatmaps.AddFile(set, stream, info.Name);
}
Beatmap.Value.Metadata.BackgroundFile = info.Name;
updateBackgroundSprite();
return true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
game?.UnregisterImportHandler(this);
}
public bool ChangeAudioTrack(string path)
{
var info = new FileInfo(path);
if (!info.Exists)
return false;
var set = Beatmap.Value.BeatmapSetInfo;
// remove the previous audio track for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile);
using (var stream = info.OpenRead())
{
if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
else
beatmaps.AddFile(set, stream, info.Name);
}
Beatmap.Value.Metadata.AudioFile = info.Name;
music.ReloadCurrentTrack();
editor?.UpdateClockSource();
return true;
}
private void audioTrackChanged(ValueChangedEvent<string> filePath)
{
if (!ChangeAudioTrack(filePath.NewValue))
audioTrackTextBox.Current.Value = filePath.OldValue;
}
private void updateBackgroundSprite()
{
LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
}, background =>
{
if (background.Texture != null)
backgroundSpriteContainer.Child = background;
else
{
backgroundSpriteContainer.Children = new Drawable[]
{
new Box
{
Colour = Colours.GreySeafoamDarker,
RelativeSizeAxes = Axes.Both,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24))
{
Text = "Drag image here to set beatmap background!",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
}
};
}
background.FadeInFromZero(500);
});
}
}
}

View File

@ -1,13 +1,78 @@
// 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.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Setup
{
public class SetupScreen : EditorScreen
{
[Resolved]
private OsuColour colours { get; set; }
[Cached]
protected readonly OverlayColourProvider ColourProvider;
public SetupScreen()
: base(EditorScreenMode.SongSetup)
{
Child = new ScreenWhiteBox.UnderConstructionMessage("Setup mode");
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
}
[BackgroundDependencyLoader]
private void load()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(50),
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
new SectionsContainer<SetupSection>
{
FixedHeader = new SetupScreenHeader(),
RelativeSizeAxes = Axes.Both,
Children = new SetupSection[]
{
new ResourcesSection(),
new MetadataSection(),
new DifficultySection(),
}
},
}
}
};
}
}
internal class SetupScreenHeader : OverlayHeader
{
protected override OverlayTitle CreateTitle() => new SetupScreenTitle();
private class SetupScreenTitle : OverlayTitle
{
public SetupScreenTitle()
{
Title = "beatmap setup";
Description = "change general settings of your beatmap";
IconTexture = "Icons/Hexacons/social";
}
}
}
}

View File

@ -0,0 +1,42 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Setup
{
internal class SetupSection : Container
{
private readonly FillFlowContainer flow;
[Resolved]
protected OsuColour Colours { get; private set; }
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
protected override Container<Drawable> Content => flow;
public SetupSection()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding(10);
InternalChild = flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Direction = FillDirection.Vertical,
};
}
}
}

View File

@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing
private IReadOnlyList<Drawable> createSections() => new Drawable[]
{
new GroupSection(),
new TimingSection(),
new DifficultySection(),
new SampleSection(),

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Timing
},
new OsuSpriteText
{
Text = $"{group.Time:n0}ms",
Text = group.Time.ToEditorFormattedString(),
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
},
new ControlGroupAttributes(group),
@ -112,12 +113,23 @@ namespace osu.Game.Screens.Edit.Timing
};
controlPoints = group.ControlPoints.GetBoundCopy();
controlPoints.ItemsAdded += _ => createChildren();
controlPoints.ItemsRemoved += _ => createChildren();
}
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
createChildren();
}
protected override void LoadComplete()
{
base.LoadComplete();
controlPoints.CollectionChanged += (_, __) => createChildren();
}
private void createChildren()
{
fill.ChildrenEnumerable = controlPoints.Select(createAttribute).Where(c => c != null);
@ -125,20 +137,22 @@ namespace osu.Game.Screens.Edit.Timing
private Drawable createAttribute(ControlPoint controlPoint)
{
Color4 colour = controlPoint.GetRepresentingColour(colours);
switch (controlPoint)
{
case TimingControlPoint timing:
return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}", colour);
case DifficultyControlPoint difficulty:
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x");
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour);
case EffectControlPoint effect:
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}", colour);
case SampleControlPoint sample:
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%");
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour);
}
return null;

View File

@ -2,27 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class DifficultySection : Section<DifficultyControlPoint>
{
private SettingsSlider<double> multiplier;
private SliderWithTextBoxInput<double> multiplierSlider;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
multiplier = new SettingsSlider<double>
multiplierSlider = new SliderWithTextBoxInput<double>("Speed Multiplier")
{
LabelText = "Speed Multiplier",
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
RelativeSizeAxes = Axes.X,
Current = new DifficultyControlPoint().SpeedMultiplierBindable
}
});
}
@ -31,7 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
{
if (point.NewValue != null)
{
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}

View File

@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null)
{
kiai.Current = point.NewValue.KiaiModeBindable;
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}

View File

@ -0,0 +1,120 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
internal class GroupSection : CompositeDrawable
{
private LabelledTextBox textBox;
private TriangleButton button;
[Resolved]
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved]
private EditorClock clock { get; set; }
[Resolved(canBeNull: true)]
private IEditorChangeHandler changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding(10);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = "Time"
},
button = new TriangleButton
{
Text = "Use current time",
RelativeSizeAxes = Axes.X,
Action = () => changeSelectedGroupTime(clock.CurrentTime)
}
}
},
};
textBox.OnCommit += (sender, isNew) =>
{
if (!isNew)
return;
if (double.TryParse(sender.Text, out var newTime))
{
changeSelectedGroupTime(newTime);
}
else
{
SelectedGroup.TriggerChange();
}
};
SelectedGroup.BindValueChanged(group =>
{
if (group.NewValue == null)
{
textBox.Text = string.Empty;
textBox.Current.Disabled = true;
button.Enabled.Value = false;
return;
}
textBox.Current.Disabled = false;
button.Enabled.Value = true;
textBox.Text = $"{group.NewValue.Time:n0}";
}, true);
}
private void changeSelectedGroupTime(in double time)
{
if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time)
return;
changeHandler?.BeginChange();
var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray();
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
foreach (var cp in currentGroupItems)
Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp);
// the control point might not necessarily exist yet, if currentGroupItems was empty.
SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time, true);
changeHandler?.EndChange();
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Timing
{
@ -16,11 +17,13 @@ namespace osu.Game.Screens.Edit.Timing
{
private readonly string header;
private readonly Func<string> content;
private readonly Color4 colour;
public RowAttribute(string header, Func<string> content)
public RowAttribute(string header, Func<string> content, Color4 colour)
{
this.header = header;
this.content = content;
this.colour = colour;
}
[BackgroundDependencyLoader]
@ -40,7 +43,7 @@ namespace osu.Game.Screens.Edit.Timing
{
new Box
{
Colour = colours.Yellow,
Colour = colour,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
@ -50,7 +53,7 @@ namespace osu.Game.Screens.Edit.Timing
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12),
Text = header,
Colour = colours.Gray3
Colour = colours.Gray0
},
};
}

View File

@ -2,18 +2,17 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SampleSection : Section<SampleControlPoint>
{
private LabelledTextBox bank;
private SettingsSlider<int> volume;
private SliderWithTextBoxInput<int> volume;
[BackgroundDependencyLoader]
private void load()
@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing
{
Label = "Bank Name",
},
volume = new SettingsSlider<int>
volume = new SliderWithTextBoxInput<int>("Volume")
{
Bindable = new SampleControlPoint().SampleVolumeBindable,
LabelText = "Volume",
Current = new SampleControlPoint().SampleVolumeBindable,
}
});
}
@ -37,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null)
{
bank.Current = point.NewValue.SampleBankBindable;
volume.Bindable = point.NewValue.SampleVolumeBindable;
bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
volume.Current = point.NewValue.SampleVolumeBindable;
volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}

View File

@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
[Resolved(canBeNull: true)]
protected IEditorChangeHandler ChangeHandler { get; private set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{

View File

@ -0,0 +1,78 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
public class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
private readonly SettingsSlider<T> slider;
public SliderWithTextBoxInput(string labelText)
{
LabelledTextBox textbox;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textbox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
}
}
},
};
textbox.OnCommit += (t, isNew) =>
{
if (!isNew) return;
try
{
slider.Current.Parse(t.Text);
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
Current.TriggerChange();
};
Current.BindValueChanged(val =>
{
textbox.Text = val.NewValue.ToString();
}, true);
}
public Bindable<T> Current
{
get => slider.Current;
set => slider.Current = value;
}
}
}

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
@ -24,6 +25,11 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private EditorClock clock { get; set; }
public TimingScreen()
: base(EditorScreenMode.Timing)
{
}
protected override Drawable CreateMainContent() => new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -53,6 +59,12 @@ namespace osu.Game.Screens.Edit.Timing
});
}
protected override void OnTimelineLoaded(TimelineArea timelineArea)
{
base.OnTimelineLoaded(timelineArea);
timelineArea.Timeline.Zoom = timelineArea.Timeline.MinZoom;
}
public class ControlPointList : CompositeDrawable
{
private OsuButton deleteButton;
@ -69,6 +81,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; }
[Resolved(canBeNull: true)]
private IEditorChangeHandler changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -124,12 +139,13 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
controlGroups.ItemsAdded += _ => createContent();
controlGroups.ItemsRemoved += _ => createContent();
createContent();
}
private void createContent() => table.ControlGroups = controlGroups;
controlGroups.BindCollectionChanged((sender, args) =>
{
table.ControlGroups = controlGroups;
changeHandler.SaveState();
}, true);
}
private void delete()
{

View File

@ -1,30 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class TimingSection : Section<TimingControlPoint>
{
private SettingsSlider<double> bpm;
private SettingsSlider<double> bpmSlider;
private SettingsEnumDropdown<TimeSignatures> timeSignature;
private BPMTextBox bpmTextEntry;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
bpm = new BPMSlider
{
Bindable = new TimingControlPoint().BeatLengthBindable,
LabelText = "BPM",
},
bpmTextEntry = new BPMTextBox(),
bpmSlider = new BPMSlider(),
timeSignature = new SettingsEnumDropdown<TimeSignatures>
{
LabelText = "Time Signature"
@ -36,8 +36,14 @@ namespace osu.Game.Screens.Edit.Timing
{
if (point.NewValue != null)
{
bpm.Bindable = point.NewValue.BeatLengthBindable;
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
bpmSlider.Current = point.NewValue.BeatLengthBindable;
bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
// no need to hook change handler here as it's the same bindable as above
timeSignature.Current = point.NewValue.TimeSignatureBindable;
timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
@ -52,34 +58,96 @@ namespace osu.Game.Screens.Edit.Timing
};
}
private class BPMSlider : SettingsSlider<double>
private class BPMTextBox : LabelledTextBox
{
private readonly BindableDouble beatLengthBindable = new BindableDouble();
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
private BindableDouble bpmBindable;
public override Bindable<double> Bindable
public BPMTextBox()
{
get => base.Bindable;
Label = "BPM";
OnCommit += (val, isNew) =>
{
if (!isNew) return;
try
{
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
beatLengthBindable.Value = beatLengthToBpm(doubleVal);
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
beatLengthBindable.TriggerChange();
};
beatLengthBindable.BindValueChanged(val =>
{
Current.Value = beatLengthToBpm(val.NewValue).ToString("N2");
}, true);
}
public Bindable<double> Bindable
{
get => beatLengthBindable;
set
{
// incoming will be beatlength
// incoming will be beat length, not bpm
beatLengthBindable.UnbindBindings();
beatLengthBindable.BindTo(value);
}
}
}
base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value))
{
MinValue = beatLengthToBpm(beatLengthBindable.MaxValue),
MaxValue = beatLengthToBpm(beatLengthBindable.MinValue),
Default = beatLengthToBpm(beatLengthBindable.Default),
};
private class BPMSlider : SettingsSlider<double>
{
private const double sane_minimum = 60;
private const double sane_maximum = 240;
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH)
{
MinValue = sane_minimum,
MaxValue = sane_maximum,
};
public BPMSlider()
{
beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true);
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
base.Current = bpmBindable;
TransferValueOnCommit = true;
}
public override Bindable<double> Current
{
get => base.Current;
set
{
// incoming will be beat length, not bpm
beatLengthBindable.UnbindBindings();
beatLengthBindable.BindTo(value);
}
}
private double beatLengthToBpm(double beatLength) => 60000 / beatLength;
private void updateCurrent(double newValue)
{
// we use a more sane range for the slider display unless overridden by the user.
// if a value comes in outside our range, we should expand temporarily.
bpmBindable.MinValue = Math.Min(newValue, sane_minimum);
bpmBindable.MaxValue = Math.Max(newValue, sane_maximum);
bpmBindable.Value = newValue;
}
}
private static double beatLengthToBpm(double beatLength) => 60000 / beatLength;
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A component that tracks a batch change, only applying after all active changes are completed.
/// </summary>
public abstract class TransactionalCommitComponent : Component
{
/// <summary>
/// Fires whenever a transaction begins. Will not fire on nested transactions.
/// </summary>
public event Action TransactionBegan;
/// <summary>
/// Fires when the last transaction completes.
/// </summary>
public event Action TransactionEnded;
/// <summary>
/// Fires when <see cref="SaveState"/> is called and results in a non-transactional state save.
/// </summary>
public event Action SaveStateTriggered;
public bool TransactionActive => bulkChangesStarted > 0;
private int bulkChangesStarted;
/// <summary>
/// Signal the beginning of a change.
/// </summary>
public void BeginChange()
{
if (bulkChangesStarted++ == 0)
TransactionBegan?.Invoke();
}
/// <summary>
/// Signal the end of a change.
/// </summary>
/// <exception cref="InvalidOperationException">Throws if <see cref="BeginChange"/> was not first called.</exception>
public void EndChange()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
{
UpdateState();
TransactionEnded?.Invoke();
}
}
/// <summary>
/// Force an update of the state with no attached transaction.
/// This is a no-op if a transaction is already active. Should generally be used as a safety measure to ensure granular changes are not left outside a transaction.
/// </summary>
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
SaveStateTriggered?.Invoke();
UpdateState();
}
protected abstract void UpdateState();
}
}

View File

@ -39,9 +39,9 @@ namespace osu.Game.Screens
bool HideOverlaysOnEnter { get; }
/// <summary>
/// Whether overlays should be able to be opened once this screen is entered or resumed.
/// Whether overlays should be able to be opened when this screen is current.
/// </summary>
OverlayActivation InitialOverlayActivationMode { get; }
IBindable<OverlayActivation> OverlayActivationMode { get; }
/// <summary>
/// The amount of parallax to be applied while this screen is displayed.

View File

@ -270,9 +270,6 @@ namespace osu.Game.Screens.Menu
ButtonSystemState lastState = state;
state = value;
if (game != null)
game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All;
updateLogoState(lastState);
Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}");
@ -324,10 +321,9 @@ namespace osu.Game.Screens.Menu
bool impact = logo.Scale.X > 0.6f;
if (lastState == ButtonSystemState.Initial)
logo.ScaleTo(0.5f, 200, Easing.In);
logo.ScaleTo(0.5f, 200, Easing.In);
logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.EnteringMode ? 0 : 200, Easing.In);
logoTrackingContainer.StartTracking(logo, 200, Easing.In);
logoDelayedAction?.Cancel();
logoDelayedAction = Scheduler.AddDelayed(() =>

View File

@ -42,8 +42,11 @@ namespace osu.Game.Screens.Menu
ValidForResume = false;
}
[Resolved]
private IAPIProvider api { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours, IAPIProvider api)
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
@ -104,7 +107,9 @@ namespace osu.Game.Screens.Menu
iconColour = colours.Yellow;
currentUser.BindTo(api.LocalUser);
// manually transfer the user once, but only do the final bind in LoadComplete to avoid thread woes (API scheduler could run while this screen is still loading).
// the manual transfer is here to ensure all text content is loaded ahead of time as this is very early in the game load process and we want to avoid stutters.
currentUser.Value = api.LocalUser.Value;
currentUser.BindValueChanged(e =>
{
supportFlow.Children.ForEach(d => d.FadeOut().Expire());
@ -141,6 +146,8 @@ namespace osu.Game.Screens.Menu
base.LoadComplete();
if (nextScreen != null)
LoadComponentAsync(nextScreen);
currentUser.BindTo(api.LocalUser);
}
public override void OnEntering(IScreen last)
@ -190,7 +197,7 @@ namespace osu.Game.Screens.Menu
{
"You can press Ctrl-T anywhere in the game to toggle the toolbar!",
"You can press Ctrl-O anywhere in the game to access options!",
"All settings are dynamic and take effect in real-time. Try changing the skin while playing!",
"All settings are dynamic and take effect in real-time. Try pausing and changing the skin while playing!",
"New features are coming online every update. Make sure to stay up-to-date!",
"If you find the UI too large or small, try adjusting UI scale in settings!",
"Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!",

View File

@ -12,6 +12,7 @@ using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.IO.Archives;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osuTK;
@ -43,9 +44,7 @@ namespace osu.Game.Screens.Menu
private WorkingBeatmap initialBeatmap;
protected Track Track => initialBeatmap?.Track;
private readonly BindableDouble exitingVolumeFade = new BindableDouble(1);
protected ITrack Track { get; private set; }
private const int exit_delay = 3000;
@ -60,8 +59,12 @@ namespace osu.Game.Screens.Menu
[Resolved]
private AudioManager audio { get; set; }
[Resolved]
private MusicController musicController { get; set; }
/// <summary>
/// Whether the <see cref="Track"/> is provided by osu! resources, rather than a user beatmap.
/// Only valid during or after <see cref="LogoArriving"/>.
/// </summary>
protected bool UsingThemedIntro { get; private set; }
@ -111,7 +114,6 @@ namespace osu.Game.Screens.Menu
if (setInfo != null)
{
initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]);
UsingThemedIntro = !(Track is TrackVirtual);
}
return UsingThemedIntro;
@ -123,17 +125,35 @@ namespace osu.Game.Screens.Menu
this.FadeIn(300);
double fadeOutTime = exit_delay;
var track = musicController.CurrentTrack;
// ensure the track doesn't change or loop as we are exiting.
track.Looping = false;
Beatmap.Disabled = true;
// we also handle the exit transition.
if (MenuVoice.Value)
{
seeya.Play();
// if playing the outro voice, we have more time to have fun with the background track.
// initially fade to almost silent then ramp out over the remaining time.
const double initial_fade = 200;
track
.VolumeTo(0.03f, initial_fade).Then()
.VolumeTo(0, fadeOutTime - initial_fade, Easing.In);
}
else
{
fadeOutTime = 500;
audio.AddAdjustment(AdjustableProperty.Volume, exitingVolumeFade);
this.TransformBindableTo(exitingVolumeFade, 0, fadeOutTime).OnComplete(_ => this.Exit());
// if outro voice is turned off, just do a simple fade out.
track.VolumeTo(0, fadeOutTime, Easing.Out);
}
//don't want to fade out completely else we will stop running updates.
Game.FadeTo(0.01f, fadeOutTime);
Game.FadeTo(0.01f, fadeOutTime).OnComplete(_ => this.Exit());
base.OnResuming(last);
}
@ -164,6 +184,11 @@ namespace osu.Game.Screens.Menu
if (!resuming)
{
beatmap.Value = initialBeatmap;
Track = initialBeatmap.Track;
UsingThemedIntro = !initialBeatmap.Track.IsDummyDevice;
// ensure the track starts at maximum volume
musicController.CurrentTrack.FinishTransforms();
logo.MoveTo(new Vector2(0.5f));
logo.ScaleTo(Vector2.One);

View File

@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu
const int line_end_offset = 120;
smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint);
smallRing.Delay(400).FadeColour(Color4.Black, 300);
lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint);
lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint);

View File

@ -44,7 +44,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load()
{
if (MenuVoice.Value && !UsingThemedIntro)
if (MenuVoice.Value)
welcome = audio.Samples.Get(@"Intro/welcome");
}
@ -64,7 +64,8 @@ namespace osu.Game.Screens.Menu
}, t =>
{
AddInternal(t);
welcome?.Play();
if (!UsingThemedIntro)
welcome?.Play();
StartTrack();
});
@ -260,7 +261,7 @@ namespace osu.Game.Screens.Menu
private class LazerLogo : CompositeDrawable
{
private HueAnimation highlight, background;
private LogoAnimation highlight, background;
public float Progress
{
@ -282,13 +283,13 @@ namespace osu.Game.Screens.Menu
{
InternalChildren = new Drawable[]
{
highlight = new HueAnimation
highlight = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-highlight"),
Colour = Color4.White,
},
background = new HueAnimation
background = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-background"),

View File

@ -39,8 +39,6 @@ namespace osu.Game.Screens.Menu
welcome = audio.Samples.Get(@"Intro/Welcome/welcome");
pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano");
Track.Looping = true;
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
@ -49,6 +47,8 @@ namespace osu.Game.Screens.Menu
if (!resuming)
{
Track.Looping = true;
LoadComponentAsync(new WelcomeIntroSequence
{
RelativeSizeAxes = Axes.Both

View File

@ -5,7 +5,6 @@ using System.Linq;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
@ -46,8 +45,8 @@ namespace osu.Game.Screens.Menu
[Resolved]
private GameHost host { get; set; }
[Resolved(canBeNull: true)]
private MusicController music { get; set; }
[Resolved]
private MusicController musicController { get; set; }
[Resolved(canBeNull: true)]
private LoginOverlay login { get; set; }
@ -62,8 +61,6 @@ namespace osu.Game.Screens.Menu
protected override BackgroundScreen CreateBackground() => background;
internal Track Track { get; private set; }
private Bindable<float> holdDelay;
private Bindable<bool> loginDisplayed;
@ -101,7 +98,11 @@ namespace osu.Game.Screens.Menu
{
buttons = new ButtonSystem
{
OnEdit = delegate { this.Push(new Editor()); },
OnEdit = delegate
{
Beatmap.SetDefault();
this.Push(new Editor());
},
OnSolo = onSolo,
OnMulti = delegate { this.Push(new Multiplayer()); },
OnExit = confirmAndExit,
@ -177,15 +178,14 @@ namespace osu.Game.Screens.Menu
base.OnEntering(last);
buttons.FadeInFromZero(500);
Track = Beatmap.Value.Track;
var metadata = Beatmap.Value.Metadata;
if (last is IntroScreen && Track != null)
if (last is IntroScreen && musicController.TrackLoaded)
{
if (!Track.IsRunning)
if (!musicController.CurrentTrack.IsRunning)
{
Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length);
Track.Start();
musicController.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.CurrentTrack.Length);
musicController.CurrentTrack.Start();
}
}
@ -260,7 +260,7 @@ namespace osu.Game.Screens.Menu
// we may have consumed our preloaded instance, so let's make another.
preloadSongSelect();
music.EnsurePlayingSomething();
musicController.EnsurePlayingSomething();
}
public override bool OnExiting(IScreen next)
@ -280,6 +280,7 @@ namespace osu.Game.Screens.Menu
}
buttons.State = ButtonSystemState.Exit;
OverlayActivationMode.Value = OverlayActivation.Disabled;
songTicker.Hide();

View File

@ -17,6 +17,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@ -46,7 +47,6 @@ namespace osu.Game.Screens.Menu
private SampleChannel sampleBeat;
private readonly Container colourAndTriangles;
private readonly Triangles triangles;
/// <summary>
@ -319,6 +319,9 @@ namespace osu.Game.Screens.Menu
intro.Delay(length + fade).FadeOut();
}
[Resolved]
private MusicController musicController { get; set; }
protected override void Update()
{
base.Update();
@ -327,10 +330,10 @@ namespace osu.Game.Screens.Menu
const float velocity_adjust_cutoff = 0.98f;
const float paused_velocity = 0.5f;
if (Beatmap.Value.Track.IsRunning)
if (musicController.CurrentTrack.IsRunning)
{
var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0;
logoAmplitudeContainer.ScaleTo(1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 75, Easing.OutQuint);
var maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0;
logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed));
if (maxAmplitude > velocity_adjust_cutoff)
triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50;

View File

@ -60,7 +60,7 @@ namespace osu.Game.Screens.Multi.Components
if (item?.Beatmap != null)
{
drawableRuleset.FadeIn(transition_duration);
drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value) { Size = new Vector2(height) };
drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) };
}
else
drawableRuleset.FadeOut(transition_duration);

View File

@ -37,8 +37,6 @@ namespace osu.Game.Screens.Multi
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
protected override bool ShowDragHandle => allowEdit;
private Container maskingContainer;
private Container difficultyIconContainer;
private LinkFlowContainer beatmapText;
@ -63,12 +61,13 @@ namespace osu.Game.Screens.Multi
// TODO: edit support should be moved out into a derived class
this.allowEdit = allowEdit;
this.allowSelection = allowSelection;
beatmap.BindTo(item.Beatmap);
ruleset.BindTo(item.Ruleset);
requiredMods.BindTo(item.RequiredMods);
ShowDragHandle.Value = allowEdit;
}
[BackgroundDependencyLoader]
@ -104,7 +103,7 @@ namespace osu.Game.Screens.Multi
private void refresh()
{
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) };
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) };
beatmapText.Clear();
beatmapText.AddLink(Item.Beatmap.ToString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString());

View File

@ -11,8 +11,8 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.SearchableList;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@ -42,7 +42,7 @@ namespace osu.Game.Screens.Multi
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = SearchableListOverlay.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Children = new Drawable[]
{
title = new MultiHeaderTitle

View File

@ -21,10 +21,12 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Screens.Multi.Lounge.Components
{
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
@ -39,6 +41,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components
private readonly Box selectionBox;
private CachedModelDependencyContainer<Room> dependencies;
[Resolved(canBeNull: true)]
private Multiplayer multiplayer { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -232,5 +237,13 @@ namespace osu.Game.Screens.Multi.Lounge.Components
Current = name;
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
multiplayer?.CreateRoom(Room.CreateCopy());
})
};
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online.Multiplayer;
using osuTK;
using osu.Game.Graphics.Cursor;
namespace osu.Game.Screens.Multi.Lounge.Components
{
@ -37,24 +39,31 @@ namespace osu.Game.Screens.Multi.Lounge.Components
[Resolved]
private IRoomManager roomManager { get; set; }
[Resolved(CanBeNull = true)]
private LoungeSubScreen loungeSubScreen { get; set; }
public RoomsContainer()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = roomFlow = new FillFlowContainer<DrawableRoom>
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Child = roomFlow = new FillFlowContainer<DrawableRoom>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
}
};
}
protected override void LoadComplete()
{
rooms.ItemsAdded += addRooms;
rooms.ItemsRemoved += removeRooms;
rooms.CollectionChanged += roomsChanged;
roomManager.RoomsUpdated += updateSorting;
rooms.BindTo(roomManager.Rooms);
@ -82,6 +91,20 @@ namespace osu.Game.Screens.Multi.Lounge.Components
});
}
private void roomsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
addRooms(args.NewItems.Cast<Room>());
break;
case NotifyCollectionChangedAction.Remove:
removeRooms(args.OldItems.Cast<Room>());
break;
}
}
private void addRooms(IEnumerable<Room> rooms)
{
foreach (var room in rooms)

View File

@ -12,12 +12,12 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.SearchableList;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.Match;
namespace osu.Game.Screens.Multi.Lounge
{
[Cached]
public class LoungeSubScreen : MultiplayerSubScreen
{
public override string Title => "Lounge";
@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Lounge
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
[Resolved(canBeNull: true)]
[Resolved]
private MusicController music { get; set; }
private bool joiningRoom;
@ -102,8 +102,8 @@ namespace osu.Game.Screens.Multi.Lounge
content.Padding = new MarginPadding
{
Top = Filter.DrawHeight,
Left = SearchableListOverlay.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = SearchableListOverlay.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING,
Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING,
};
}
@ -126,7 +126,7 @@ namespace osu.Game.Screens.Multi.Lounge
if (selectedRoom.Value?.RoomID.Value == null)
selectedRoom.Value = new Room();
music.EnsurePlayingSomething();
music?.EnsurePlayingSomething();
onReturning();
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Multiplayer;
@ -15,8 +14,6 @@ namespace osu.Game.Screens.Multi.Match.Components
{
public class MatchLeaderboard : Leaderboard<MatchLeaderboardScope, APIUserScoreAggregate>
{
public Action<IEnumerable<APIUserScoreAggregate>> ScoresLoaded;
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<int?> roomId { get; set; }
@ -40,18 +37,20 @@ namespace osu.Game.Screens.Multi.Match.Components
if (roomId.Value == null)
return null;
var req = new GetRoomScoresRequest(roomId.Value ?? 0);
var req = new GetRoomLeaderboardRequest(roomId.Value ?? 0);
req.Success += r =>
{
scoresCallback?.Invoke(r);
ScoresLoaded?.Invoke(r);
scoresCallback?.Invoke(r.Leaderboard);
TopScore = r.UserScore;
};
return req;
}
protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index);
protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position, false);
}
public enum MatchLeaderboardScope

View File

@ -14,8 +14,8 @@ namespace osu.Game.Screens.Multi.Match.Components
{
private readonly APIUserScoreAggregate score;
public MatchLeaderboardScore(APIUserScoreAggregate score, int rank)
: base(score.CreateScoreInfo(), rank)
public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool allowHighlight = true)
: base(score.CreateScoreInfo(), rank, allowHighlight)
{
this.score = score;
}

View File

@ -14,7 +14,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.SearchableList;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@ -117,7 +117,7 @@ namespace osu.Game.Screens.Multi.Match.Components
{
new Container
{
Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING },
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]

View File

@ -172,7 +172,7 @@ namespace osu.Game.Screens.Multi.Match
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 240),
new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120),
}
},
null

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge;
@ -50,6 +51,9 @@ namespace osu.Game.Screens.Multi
[Cached]
private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria());
[Resolved(CanBeNull = true)]
private MusicController music { get; set; }
[Cached(Type = typeof(IRoomManager))]
private RoomManager roomManager;
@ -130,7 +134,7 @@ namespace osu.Game.Screens.Multi
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Action = createRoom
Action = () => CreateRoom()
},
roomManager = new RoomManager()
}
@ -285,10 +289,11 @@ namespace osu.Game.Screens.Multi
logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut();
}
private void createRoom()
{
loungeSubScreen.Open(new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } });
}
/// <summary>
/// Create a new room.
/// </summary>
/// <param name="room">An optional template to use when creating the room.</param>
public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } });
private void beginHandlingTrack()
{
@ -346,8 +351,7 @@ namespace osu.Game.Screens.Multi
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
track.Looping = true;
if (!track.IsRunning)
track.Restart();
music?.EnsurePlayingSomething();
}
}
else

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@ -59,13 +58,13 @@ namespace osu.Game.Screens.Multi.Play
if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID);
var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash);
req.Success += r => token = r.ID;
req.Failure += e =>
{
failed = true;
Logger.Error(e, "Failed to retrieve a score submission token.");
Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are running an old or non-official release of osu! (ie. you are self-compiling).");
Schedule(() =>
{

View File

@ -3,13 +3,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
@ -21,7 +22,15 @@ namespace osu.Game.Screens.Multi.Ranking
private readonly int roomId;
private readonly PlaylistItem playlistItem;
private LoadingSpinner loadingLayer;
protected LoadingSpinner LeftSpinner { get; private set; }
protected LoadingSpinner CentreSpinner { get; private set; }
protected LoadingSpinner RightSpinner { get; private set; }
private MultiplayerScores higherScores;
private MultiplayerScores lowerScores;
[Resolved]
private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, allowRetry)
@ -33,29 +42,209 @@ namespace osu.Game.Screens.Multi.Ranking
[BackgroundDependencyLoader]
private void load()
{
AddInternal(loadingLayer = new LoadingLayer
AddInternal(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
X = -10,
State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y }
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y },
Children = new Drawable[]
{
LeftSpinner = new PanelListLoadingSpinner(ScorePanelList)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
},
CentreSpinner = new PanelListLoadingSpinner(ScorePanelList)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
},
RightSpinner = new PanelListLoadingSpinner(ScorePanelList)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
},
}
});
}
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID);
// This performs two requests:
// 1. A request to show the user's score (and scores around).
// 2. If that fails, a request to index the room starting from the highest score.
req.Success += r =>
var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id);
userScoreReq.Success += userScore =>
{
scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem)));
loadingLayer.Hide();
var allScores = new List<MultiplayerScore> { userScore };
if (userScore.ScoresAround?.Higher != null)
{
allScores.AddRange(userScore.ScoresAround.Higher.Scores);
higherScores = userScore.ScoresAround.Higher;
Debug.Assert(userScore.Position != null);
setPositions(higherScores, userScore.Position.Value, -1);
}
if (userScore.ScoresAround?.Lower != null)
{
allScores.AddRange(userScore.ScoresAround.Lower.Scores);
lowerScores = userScore.ScoresAround.Lower;
Debug.Assert(userScore.Position != null);
setPositions(lowerScores, userScore.Position.Value, 1);
}
performSuccessCallback(scoresCallback, allScores);
};
req.Failure += _ => loadingLayer.Hide();
// On failure, fallback to a normal index.
userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback));
return req;
return userScoreReq;
}
protected override APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback)
{
Debug.Assert(direction == 1 || direction == -1);
MultiplayerScores pivot = direction == -1 ? higherScores : lowerScores;
if (pivot?.Cursor == null)
return null;
if (pivot == higherScores)
LeftSpinner.Show();
else
RightSpinner.Show();
return createIndexRequest(scoresCallback, pivot);
}
/// <summary>
/// Creates a <see cref="IndexPlaylistScoresRequest"/> with an optional score pivot.
/// </summary>
/// <remarks>Does not queue the request.</remarks>
/// <param name="scoresCallback">The callback to perform with the resulting scores.</param>
/// <param name="pivot">An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.</param>
/// <returns>The indexing <see cref="APIRequest"/>.</returns>
private APIRequest createIndexRequest(Action<IEnumerable<ScoreInfo>> scoresCallback, [CanBeNull] MultiplayerScores pivot = null)
{
var indexReq = pivot != null
? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params)
: new IndexPlaylistScoresRequest(roomId, playlistItem.ID);
indexReq.Success += r =>
{
if (pivot == lowerScores)
{
lowerScores = r;
setPositions(r, pivot, 1);
}
else
{
higherScores = r;
setPositions(r, pivot, -1);
}
performSuccessCallback(scoresCallback, r.Scores, r);
};
indexReq.Failure += _ => hideLoadingSpinners(pivot);
return indexReq;
}
/// <summary>
/// Transforms returned <see cref="MultiplayerScores"/> into <see cref="ScoreInfo"/>s, ensure the <see cref="ScorePanelList"/> is put into a sane state, and invokes a given success callback.
/// </summary>
/// <param name="callback">The callback to invoke with the final <see cref="ScoreInfo"/>s.</param>
/// <param name="scores">The <see cref="MultiplayerScore"/>s that were retrieved from <see cref="APIRequest"/>s.</param>
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
{
var scoreInfos = new List<ScoreInfo>(scores.Select(s => s.CreateScoreInfo(playlistItem)));
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
{
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
hideLoadingSpinners(pivot);
}
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
{
CentreSpinner.Hide();
if (pivot == lowerScores)
RightSpinner.Hide();
else if (pivot == higherScores)
LeftSpinner.Hide();
}
/// <summary>
/// Applies positions to all <see cref="MultiplayerScore"/>s referenced to a given pivot.
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScores"/> to set positions on.</param>
/// <param name="pivot">The pivot.</param>
/// <param name="increment">The amount to increment the pivot position by for each <see cref="MultiplayerScore"/> in <paramref name="scores"/>.</param>
private void setPositions([NotNull] MultiplayerScores scores, [CanBeNull] MultiplayerScores pivot, int increment)
=> setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment);
/// <summary>
/// Applies positions to all <see cref="MultiplayerScore"/>s referenced to a given pivot.
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScores"/> to set positions on.</param>
/// <param name="pivotPosition">The pivot position.</param>
/// <param name="increment">The amount to increment the pivot position by for each <see cref="MultiplayerScore"/> in <paramref name="scores"/>.</param>
private void setPositions([NotNull] MultiplayerScores scores, int pivotPosition, int increment)
{
foreach (var s in scores.Scores)
{
pivotPosition += increment;
s.Position = pivotPosition;
}
}
private class PanelListLoadingSpinner : LoadingSpinner
{
private readonly ScorePanelList list;
/// <summary>
/// Creates a new <see cref="PanelListLoadingSpinner"/>.
/// </summary>
/// <param name="list">The list to track.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
public PanelListLoadingSpinner(ScorePanelList list, bool withBox = true)
: base(withBox)
{
this.list = list;
}
protected override void Update()
{
base.Update();
float panelOffset = list.DrawWidth / 2 - ScorePanel.EXPANDED_WIDTH;
if ((Anchor & Anchor.x0) > 0)
X = (float)(panelOffset - list.Current);
else if ((Anchor & Anchor.x2) > 0)
X = (float)(list.ScrollableExtent - list.Current - panelOffset);
}
}
}
}

View File

@ -14,7 +14,6 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Lounge.Components;

View File

@ -44,9 +44,13 @@ namespace osu.Game.Screens
public virtual bool HideOverlaysOnEnter => false;
/// <summary>
/// Whether overlays should be able to be opened once this screen is entered or resumed.
/// The initial overlay activation mode to use when this screen is entered for the first time.
/// </summary>
public virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All;
protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All;
protected readonly Bindable<OverlayActivation> OverlayActivationMode;
IBindable<OverlayActivation> IOsuScreen.OverlayActivationMode => OverlayActivationMode;
public virtual bool CursorVisible => true;
@ -138,6 +142,8 @@ namespace osu.Game.Screens
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
OverlayActivationMode = new Bindable<OverlayActivation>(InitialOverlayActivationMode);
}
[BackgroundDependencyLoader(true)]

View File

@ -89,6 +89,8 @@ namespace osu.Game.Screens.Play
private void applyToPlayfield(Playfield playfield)
{
double failTime = playfield.Time.Current;
foreach (var nested in playfield.NestedPlayfields)
applyToPlayfield(nested);
@ -97,13 +99,29 @@ namespace osu.Game.Screens.Play
if (appliedObjects.Contains(obj))
continue;
obj.RotateTo(RNG.NextSingle(-90, 90), duration);
obj.ScaleTo(obj.Scale * 0.5f, duration);
obj.MoveToOffset(new Vector2(0, 400), duration);
float rotation = RNG.NextSingle(-90, 90);
Vector2 originalPosition = obj.Position;
Vector2 originalScale = obj.Scale;
dropOffScreen(obj, failTime, rotation, originalScale, originalPosition);
// need to reapply the fail drop after judgement state changes
obj.ApplyCustomUpdateState += (o, _) => dropOffScreen(obj, failTime, rotation, originalScale, originalPosition);
appliedObjects.Add(obj);
}
}
private void dropOffScreen(DrawableHitObject obj, double failTime, float randomRotation, Vector2 originalScale, Vector2 originalPosition)
{
using (obj.BeginAbsoluteSequence(failTime))
{
obj.RotateTo(randomRotation, duration);
obj.ScaleTo(originalScale * 0.5f, duration);
obj.MoveTo(originalPosition + new Vector2(0, 400), duration);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Framework.Utils;
namespace osu.Game.Screens.Play
{
@ -20,6 +23,11 @@ namespace osu.Game.Screens.Play
public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// All adjustments applied to this clock which don't come from gameplay or mods.
/// </summary>
public virtual IEnumerable<Bindable<double>> NonGameplayAdjustments => Enumerable.Empty<Bindable<double>>();
public GameplayClock(IFrameBasedClock underlyingClock)
{
this.underlyingClock = underlyingClock;
@ -29,16 +37,38 @@ namespace osu.Game.Screens.Play
public double Rate => underlyingClock.Rate;
/// <summary>
/// The rate of gameplay when playback is at 100%.
/// This excludes any seeking / user adjustments.
/// </summary>
public double TrueGameplayRate
{
get
{
double baseRate = Rate;
foreach (var adjustment in NonGameplayAdjustments)
{
if (Precision.AlmostEquals(adjustment.Value, 0))
return 0;
baseRate /= adjustment.Value;
}
return baseRate;
}
}
public bool IsRunning => underlyingClock.IsRunning;
/// <summary>
/// Whether an ongoing seek operation is active.
/// Whether nested samples supporting the <see cref="ISamplePlaybackDisabler"/> interface should be paused.
/// </summary>
public virtual bool IsSeeking => false;
public virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
public void ProcessFrame()
{
// we do not want to process the underlying clock.
// intentionally not updating the underlying clock (handled externally).
}
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework;
@ -15,7 +16,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.Play
{
@ -25,12 +25,9 @@ namespace osu.Game.Screens.Play
public class GameplayClockContainer : Container
{
private readonly WorkingBeatmap beatmap;
private readonly IReadOnlyList<Mod> mods;
/// <summary>
/// The <see cref="WorkingBeatmap"/>'s track.
/// </summary>
private Track track;
[NotNull]
private ITrack track;
public readonly BindableBool IsPaused = new BindableBool();
@ -54,8 +51,10 @@ namespace osu.Game.Screens.Play
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
[Cached]
public readonly GameplayClock GameplayClock;
public GameplayClock GameplayClock => localGameplayClock;
[Cached(typeof(GameplayClock))]
private readonly LocalGameplayClock localGameplayClock;
private Bindable<double> userAudioOffset;
@ -63,17 +62,16 @@ namespace osu.Game.Screens.Play
private readonly FramedOffsetClock platformOffsetClock;
public GameplayClockContainer(WorkingBeatmap beatmap, IReadOnlyList<Mod> mods, double gameplayStartTime)
public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime)
{
this.beatmap = beatmap;
this.mods = mods;
this.gameplayStartTime = gameplayStartTime;
track = beatmap.Track;
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
RelativeSizeAxes = Axes.Both;
track = beatmap.Track;
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
@ -84,7 +82,7 @@ namespace osu.Game.Screens.Play
userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock);
// the clock to be exposed via DI to children.
GameplayClock = new GameplayClock(userOffsetClock);
localGameplayClock = new LocalGameplayClock(userOffsetClock);
GameplayClock.IsPaused.BindTo(IsPaused);
}
@ -123,13 +121,10 @@ namespace osu.Game.Screens.Play
public void Restart()
{
// The Reset() call below causes speed adjustments to be reset in an async context, leading to deadlocks.
// The deadlock can be prevented by resetting the track synchronously before entering the async context.
track.ResetSpeedAdjustments();
Task.Run(() =>
{
track.Reset();
track.Seek(0);
track.Stop();
Schedule(() =>
{
@ -195,23 +190,22 @@ namespace osu.Game.Screens.Play
}
/// <summary>
/// Changes the backing clock to avoid using the originally provided beatmap's track.
/// Changes the backing clock to avoid using the originally provided track.
/// </summary>
public void StopUsingBeatmapClock()
{
if (track != beatmap.Track)
return;
removeSourceClockAdjustments();
track = new TrackVirtual(beatmap.Track.Length);
track = new TrackVirtual(track.Length);
adjustableClock.ChangeSource(track);
}
protected override void Update()
{
if (!IsPaused.Value)
{
userOffsetClock.ProcessFrame();
}
base.Update();
}
@ -220,32 +214,46 @@ namespace osu.Game.Screens.Play
private void updateRate()
{
if (track == null) return;
speedAdjustmentsApplied = true;
track.ResetSpeedAdjustments();
if (speedAdjustmentsApplied)
return;
track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
foreach (var mod in mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(track);
localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust);
localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate);
speedAdjustmentsApplied = true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
removeSourceClockAdjustments();
track = null;
}
private void removeSourceClockAdjustments()
{
if (speedAdjustmentsApplied)
if (!speedAdjustmentsApplied) return;
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust);
localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate);
speedAdjustmentsApplied = false;
}
private class LocalGameplayClock : GameplayClock
{
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
public LocalGameplayClock(FramedOffsetClock underlyingClock)
: base(underlyingClock)
{
track.ResetSpeedAdjustments();
speedAdjustmentsApplied = false;
}
}

View File

@ -24,7 +24,8 @@ namespace osu.Game.Screens.Play
{
public abstract class GameplayMenuOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
{
private const int transition_duration = 200;
protected const int TRANSITION_DURATION = 200;
private const int button_height = 70;
private const float background_alpha = 0.75f;
@ -49,7 +50,7 @@ namespace osu.Game.Screens.Play
public abstract string Description { get; }
protected internal FillFlowContainer<DialogButton> InternalButtons;
protected ButtonContainer InternalButtons;
public IReadOnlyList<DialogButton> Buttons => InternalButtons;
private FillFlowContainer retryCounterContainer;
@ -58,7 +59,7 @@ namespace osu.Game.Screens.Play
{
RelativeSizeAxes = Axes.Both;
State.ValueChanged += s => selectionIndex = -1;
State.ValueChanged += s => InternalButtons.Deselect();
}
[BackgroundDependencyLoader]
@ -113,7 +114,7 @@ namespace osu.Game.Screens.Play
}
}
},
InternalButtons = new FillFlowContainer<DialogButton>
InternalButtons = new ButtonContainer
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
@ -156,8 +157,8 @@ namespace osu.Game.Screens.Play
}
}
protected override void PopIn() => this.FadeIn(transition_duration, Easing.In);
protected override void PopOut() => this.FadeOut(transition_duration, Easing.In);
protected override void PopIn() => this.FadeIn(TRANSITION_DURATION, Easing.In);
protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In);
// Don't let mouse down events through the overlay or people can click circles while paused.
protected override bool OnMouseDown(MouseDownEvent e) => true;
@ -185,40 +186,16 @@ namespace osu.Game.Screens.Play
InternalButtons.Add(button);
}
private int selectionIndex = -1;
private void setSelected(int value)
{
if (selectionIndex == value)
return;
// Deselect the previously-selected button
if (selectionIndex != -1)
InternalButtons[selectionIndex].Selected.Value = false;
selectionIndex = value;
// Select the newly-selected button
if (selectionIndex != -1)
InternalButtons[selectionIndex].Selected.Value = true;
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.SelectPrevious:
if (selectionIndex == -1 || selectionIndex == 0)
setSelected(InternalButtons.Count - 1);
else
setSelected(selectionIndex - 1);
InternalButtons.SelectPrevious();
return true;
case GlobalAction.SelectNext:
if (selectionIndex == -1 || selectionIndex == InternalButtons.Count - 1)
setSelected(0);
else
setSelected(selectionIndex + 1);
InternalButtons.SelectNext();
return true;
case GlobalAction.Back:
@ -240,9 +217,9 @@ namespace osu.Game.Screens.Play
private void buttonSelectionChanged(DialogButton button, bool isSelected)
{
if (!isSelected)
setSelected(-1);
InternalButtons.Deselect();
else
setSelected(InternalButtons.IndexOf(button));
InternalButtons.Select(button);
}
private void updateRetryCount()
@ -276,6 +253,46 @@ namespace osu.Game.Screens.Play
};
}
protected class ButtonContainer : FillFlowContainer<DialogButton>
{
private int selectedIndex = -1;
private void setSelected(int value)
{
if (selectedIndex == value)
return;
// Deselect the previously-selected button
if (selectedIndex != -1)
this[selectedIndex].Selected.Value = false;
selectedIndex = value;
// Select the newly-selected button
if (selectedIndex != -1)
this[selectedIndex].Selected.Value = true;
}
public void SelectNext()
{
if (selectedIndex == -1 || selectedIndex == Count - 1)
setSelected(0);
else
setSelected(selectedIndex + 1);
}
public void SelectPrevious()
{
if (selectedIndex == -1 || selectedIndex == 0)
setSelected(Count - 1);
else
setSelected(selectedIndex - 1);
}
public void Deselect() => setSelected(-1);
public void Select(DialogButton button) => setSelected(IndexOf(button));
}
private class Button : DialogButton
{
// required to ensure keyboard navigation always starts from an extremity (unless the cursor is moved)

View File

@ -1,200 +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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Play.HUD
{
public abstract class ComboCounter : Container
{
public BindableInt Current = new BindableInt
{
MinValue = 0,
};
public bool IsRolling { get; protected set; }
protected SpriteText PopOutCount;
protected virtual double PopOutDuration => 150;
protected virtual float PopOutScale => 2.0f;
protected virtual Easing PopOutEasing => Easing.None;
protected virtual float PopOutInitialAlpha => 0.75f;
protected virtual double FadeOutDuration => 100;
/// <summary>
/// Duration in milliseconds for the counter roll-up animation for each element.
/// </summary>
protected virtual double RollingDuration => 20;
/// <summary>
/// Easing for the counter rollover animation.
/// </summary>
protected Easing RollingEasing => Easing.None;
protected SpriteText DisplayedCountSpriteText;
private int previousValue;
/// <summary>
/// Base of all combo counters.
/// </summary>
protected ComboCounter()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
DisplayedCountSpriteText = new OsuSpriteText
{
Alpha = 0,
},
PopOutCount = new OsuSpriteText
{
Alpha = 0,
Margin = new MarginPadding(0.05f),
}
};
TextSize = 80;
Current.ValueChanged += combo => updateCount(combo.NewValue == 0);
}
protected override void LoadComplete()
{
base.LoadComplete();
DisplayedCountSpriteText.Text = FormatCount(Current.Value);
DisplayedCountSpriteText.Anchor = Anchor;
DisplayedCountSpriteText.Origin = Origin;
StopRolling();
}
private int displayedCount;
/// <summary>
/// Value shown at the current moment.
/// </summary>
public virtual int DisplayedCount
{
get => displayedCount;
protected set
{
if (displayedCount.Equals(value))
return;
updateDisplayedCount(displayedCount, value, IsRolling);
}
}
private float textSize;
public float TextSize
{
get => textSize;
set
{
textSize = value;
DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(size: TextSize);
PopOutCount.Font = PopOutCount.Font.With(size: TextSize);
}
}
/// <summary>
/// Increments the combo by an amount.
/// </summary>
/// <param name="amount"></param>
public void Increment(int amount = 1)
{
Current.Value += amount;
}
/// <summary>
/// Stops rollover animation, forcing the displayed count to be the actual count.
/// </summary>
public void StopRolling()
{
updateCount(false);
}
protected virtual string FormatCount(int count)
{
return count.ToString();
}
protected virtual void OnCountRolling(int currentValue, int newValue)
{
transformRoll(currentValue, newValue);
}
protected virtual void OnCountIncrement(int currentValue, int newValue)
{
DisplayedCount = newValue;
}
protected virtual void OnCountChange(int currentValue, int newValue)
{
DisplayedCount = newValue;
}
private double getProportionalDuration(int currentValue, int newValue)
{
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
return difference * RollingDuration;
}
private void updateDisplayedCount(int currentValue, int newValue, bool rolling)
{
displayedCount = newValue;
if (rolling)
OnDisplayedCountRolling(currentValue, newValue);
else if (currentValue + 1 == newValue)
OnDisplayedCountIncrement(newValue);
else
OnDisplayedCountChange(newValue);
}
private void updateCount(bool rolling)
{
int prev = previousValue;
previousValue = Current.Value;
if (!IsLoaded)
return;
if (!rolling)
{
FinishTransforms(false, nameof(DisplayedCount));
IsRolling = false;
DisplayedCount = prev;
if (prev + 1 == Current.Value)
OnCountIncrement(prev, Current.Value);
else
OnCountChange(prev, Current.Value);
}
else
{
OnCountRolling(displayedCount, Current.Value);
IsRolling = true;
}
}
private void transformRoll(int currentValue, int newValue)
{
this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), RollingEasing);
}
protected abstract void OnDisplayedCountRolling(int currentValue, int newValue);
protected abstract void OnDisplayedCountIncrement(int newValue);
protected abstract void OnDisplayedCountChange(int newValue);
}
}

View File

@ -1,32 +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 osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// Used to display combo with a roll-up animation in results screen.
/// </summary>
public class ComboResultCounter : RollingCounter<long>
{
protected override double RollingDuration => 500;
protected override Easing RollingEasing => Easing.Out;
protected override double GetProportionalDuration(long currentValue, long newValue)
{
return currentValue > newValue ? currentValue - newValue : newValue - currentValue;
}
protected override string FormatCount(long count)
{
return $@"{count}x";
}
public override void Increment(long amount)
{
Current.Value += amount;
}
}
}

View File

@ -0,0 +1,43 @@
// 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.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter
{
private readonly Vector2 offset = new Vector2(-20, 5);
public DefaultAccuracyCounter()
{
Origin = Anchor.TopRight;
Anchor = Anchor.TopRight;
}
[Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
}
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Anchor = Anchor.TopLeft;
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset;
}
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultComboCounter : RollingCounter<int>, IComboCounter
{
private readonly Vector2 offset = new Vector2(20, 5);
[Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }
public DefaultComboCounter()
{
Current.Value = DisplayedCount = 0;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset;
}
}
protected override string FormatCount(int count)
{
return $@"{count}x";
}
protected override double GetProportionalDuration(int currentValue, int newValue)
{
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
}
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
}
}

View File

@ -13,11 +13,10 @@ using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD
{
public class StandardHealthDisplay : HealthDisplay, IHasAccentColour
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour
{
/// <summary>
/// The base opacity of the glow.
@ -72,8 +71,12 @@ namespace osu.Game.Screens.Play.HUD
}
}
public StandardHealthDisplay()
public DefaultHealthDisplay()
{
Size = new Vector2(1, 5);
RelativeSizeAxes = Axes.X;
Margin = new MarginPadding { Top = 20 };
Children = new Drawable[]
{
new Box
@ -104,13 +107,7 @@ namespace osu.Game.Screens.Play.HUD
GlowColour = colours.BlueDarker;
}
public void Flash(JudgementResult result)
{
if (result.Type == HitResult.Miss)
return;
Scheduler.AddOnce(flash);
}
public override void Flash(JudgementResult result) => Scheduler.AddOnce(flash);
private void flash()
{

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultScoreCounter : ScoreCounter
{
public DefaultScoreCounter()
: base(6)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
}
[Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
// todo: check if default once health display is skinnable
hud?.ShowHealthbar.BindValueChanged(healthBar =>
{
this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING);
}, true);
}
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD
private const float max_alpha = 0.4f;
private const int fade_time = 400;
private const float gradient_size = 0.3f;
private const float gradient_size = 0.2f;
/// <summary>
/// The threshold under which the current player life should be considered low and the layer should start fading in.
@ -56,16 +56,16 @@ namespace osu.Game.Screens.Play.HUD
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)),
Height = gradient_size,
Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.White.Opacity(0)),
Width = gradient_size,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Height = gradient_size,
Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Width = gradient_size,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
}
},

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@ -12,14 +13,18 @@ namespace osu.Game.Screens.Play.HUD
/// A container for components displaying the current player health.
/// Gets bound automatically to the <see cref="HealthProcessor"/> when inserted to <see cref="DrawableRuleset.Overlays"/> hierarchy.
/// </summary>
public abstract class HealthDisplay : Container
public abstract class HealthDisplay : Container, IHealthDisplay
{
public readonly BindableDouble Current = new BindableDouble(1)
public Bindable<double> Current { get; } = new BindableDouble(1)
{
MinValue = 0,
MaxValue = 1
};
public virtual void Flash(JudgementResult result)
{
}
/// <summary>
/// Bind the tracked fields of <see cref="HealthProcessor"/> to this health display.
/// </summary>

View File

@ -66,54 +66,69 @@ namespace osu.Game.Screens.Play.HUD
switch (type.NewValue)
{
case ScoreMeterType.HitErrorBoth:
createBar(false);
createBar(true);
createBar(Anchor.CentreLeft);
createBar(Anchor.CentreRight);
break;
case ScoreMeterType.HitErrorLeft:
createBar(false);
createBar(Anchor.CentreLeft);
break;
case ScoreMeterType.HitErrorRight:
createBar(true);
createBar(Anchor.CentreRight);
break;
case ScoreMeterType.HitErrorBottom:
createBar(Anchor.BottomCentre);
break;
case ScoreMeterType.ColourBoth:
createColour(false);
createColour(true);
createColour(Anchor.CentreLeft);
createColour(Anchor.CentreRight);
break;
case ScoreMeterType.ColourLeft:
createColour(false);
createColour(Anchor.CentreLeft);
break;
case ScoreMeterType.ColourRight:
createColour(true);
createColour(Anchor.CentreRight);
break;
case ScoreMeterType.ColourBottom:
createColour(Anchor.BottomCentre);
break;
}
}
private void createBar(bool rightAligned)
private void createBar(Anchor anchor)
{
bool rightAligned = (anchor & Anchor.x2) > 0;
bool bottomAligned = (anchor & Anchor.y2) > 0;
var display = new BarHitErrorMeter(hitWindows, rightAligned)
{
Margin = new MarginPadding(margin),
Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
Anchor = anchor,
Origin = bottomAligned ? Anchor.CentreLeft : anchor,
Alpha = 0,
Rotation = bottomAligned ? 270 : 0
};
completeDisplayLoading(display);
}
private void createColour(bool rightAligned)
private void createColour(Anchor anchor)
{
bool bottomAligned = (anchor & Anchor.y2) > 0;
var display = new ColourHitErrorMeter(hitWindows)
{
Margin = new MarginPadding(margin),
Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
Anchor = anchor,
Origin = bottomAligned ? Anchor.CentreLeft : anchor,
Alpha = 0,
Rotation = bottomAligned ? 270 : 0
};
completeDisplayLoading(display);

View File

@ -99,7 +99,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Size = new Vector2(10),
Icon = FontAwesome.Solid.ShippingFast,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Origin = Anchor.Centre,
// undo any layout rotation to display the icon the correct orientation
Rotation = -Rotation,
},
new SpriteIcon
{
@ -107,7 +109,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Size = new Vector2(10),
Icon = FontAwesome.Solid.Bicycle,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Origin = Anchor.Centre,
// undo any layout rotation to display the icon the correct orientation
Rotation = -Rotation,
}
}
},

View File

@ -0,0 +1,19 @@
// 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.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a accuracy counter.
/// </summary>
public interface IAccuracyCounter : IDrawable
{
/// <summary>
/// The current accuracy to be displayed.
/// </summary>
Bindable<double> Current { get; }
}
}

View File

@ -0,0 +1,19 @@
// 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.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a combo counter.
/// </summary>
public interface IComboCounter : IDrawable
{
/// <summary>
/// The current combo to be displayed.
/// </summary>
Bindable<int> Current { get; }
}
}

View File

@ -0,0 +1,26 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a health display.
/// </summary>
public interface IHealthDisplay : IDrawable
{
/// <summary>
/// The current health to be displayed.
/// </summary>
Bindable<double> Current { get; }
/// <summary>
/// Flash the display for a specified result type.
/// </summary>
/// <param name="result">The result type.</param>
void Flash(JudgementResult result);
}
}

View File

@ -0,0 +1,25 @@
// 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.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a score counter.
/// </summary>
public interface IScoreCounter : IDrawable
{
/// <summary>
/// The current score to be displayed.
/// </summary>
Bindable<double> Current { get; }
/// <summary>
/// The number of digits required to display most sane scores.
/// This may be exceeded in very rare cases, but is useful to pad or space the display to avoid it jumping around.
/// </summary>
Bindable<int> RequiredDisplayDigits { get; }
}
}

Some files were not shown because too many files have changed in this diff Show More