mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 15:44:04 +09:00
Merge branch 'master' of https://github.com/ppy/osu into carousel-perform-selection
Conflicts: osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
This commit is contained in:
@ -129,11 +129,19 @@ namespace osu.Game.Screens.Backgrounds
|
||||
}
|
||||
|
||||
case BackgroundSource.Skin:
|
||||
// default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
|
||||
if (skin.Value is DefaultSkin || skin.Value is DefaultLegacySkin)
|
||||
break;
|
||||
switch (skin.Value)
|
||||
{
|
||||
case TrianglesSkin:
|
||||
case ArgonSkin:
|
||||
case DefaultLegacySkin:
|
||||
// default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
|
||||
break;
|
||||
|
||||
default:
|
||||
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
|
||||
break;
|
||||
}
|
||||
|
||||
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -8,13 +8,12 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -30,9 +29,9 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
public readonly RadioButton Button;
|
||||
|
||||
private Color4 defaultBackgroundColour;
|
||||
private Color4 defaultBubbleColour;
|
||||
private Color4 defaultIconColour;
|
||||
private Color4 selectedBackgroundColour;
|
||||
private Color4 selectedBubbleColour;
|
||||
private Color4 selectedIconColour;
|
||||
|
||||
private Drawable icon;
|
||||
|
||||
@ -50,20 +49,13 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
defaultBackgroundColour = colours.Gray3;
|
||||
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
|
||||
selectedBackgroundColour = colours.BlueDark;
|
||||
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
|
||||
defaultBackgroundColour = colourProvider.Background3;
|
||||
selectedBackgroundColour = colourProvider.Background1;
|
||||
|
||||
Content.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 2,
|
||||
Offset = new Vector2(0, 1),
|
||||
Colour = Color4.Black.Opacity(0.5f)
|
||||
};
|
||||
defaultIconColour = defaultBackgroundColour.Darken(0.5f);
|
||||
selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
|
||||
|
||||
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
|
||||
{
|
||||
@ -98,7 +90,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
return;
|
||||
|
||||
BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
|
||||
icon.Colour = Button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
|
||||
icon.Colour = Button.Selected.Value ? selectedIconColour : defaultIconColour;
|
||||
}
|
||||
|
||||
protected override SpriteText CreateText() => new OsuSpriteText
|
||||
|
@ -6,12 +6,11 @@
|
||||
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 osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -20,9 +19,9 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
internal class DrawableTernaryButton : OsuButton
|
||||
{
|
||||
private Color4 defaultBackgroundColour;
|
||||
private Color4 defaultBubbleColour;
|
||||
private Color4 defaultIconColour;
|
||||
private Color4 selectedBackgroundColour;
|
||||
private Color4 selectedBubbleColour;
|
||||
private Color4 selectedIconColour;
|
||||
|
||||
private Drawable icon;
|
||||
|
||||
@ -38,20 +37,13 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
defaultBackgroundColour = colours.Gray3;
|
||||
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
|
||||
selectedBackgroundColour = colours.BlueDark;
|
||||
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
|
||||
defaultBackgroundColour = colourProvider.Background3;
|
||||
selectedBackgroundColour = colourProvider.Background1;
|
||||
|
||||
Content.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 2,
|
||||
Offset = new Vector2(0, 1),
|
||||
Colour = Color4.Black.Opacity(0.5f)
|
||||
};
|
||||
defaultIconColour = defaultBackgroundColour.Darken(0.5f);
|
||||
selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
|
||||
|
||||
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
|
||||
{
|
||||
@ -85,17 +77,17 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
switch (Button.Bindable.Value)
|
||||
{
|
||||
case TernaryState.Indeterminate:
|
||||
icon.Colour = selectedBubbleColour.Darken(0.5f);
|
||||
icon.Colour = selectedIconColour.Darken(0.5f);
|
||||
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
|
||||
break;
|
||||
|
||||
case TernaryState.False:
|
||||
icon.Colour = defaultBubbleColour;
|
||||
icon.Colour = defaultIconColour;
|
||||
BackgroundColour = defaultBackgroundColour;
|
||||
break;
|
||||
|
||||
case TernaryState.True:
|
||||
icon.Colour = selectedBubbleColour;
|
||||
icon.Colour = selectedIconColour;
|
||||
BackgroundColour = selectedBackgroundColour;
|
||||
break;
|
||||
}
|
||||
|
@ -123,16 +123,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = 15 },
|
||||
Text = "beat snap",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TextAnchor = Anchor.TopCentre
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
@ -173,6 +163,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = 15, Vertical = 8 },
|
||||
Text = "beat snap",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
},
|
||||
},
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
@ -13,7 +12,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -61,25 +59,31 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (object o in args.NewItems)
|
||||
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
|
||||
{
|
||||
if (blueprintMap.TryGetValue((T)o, out var blueprint))
|
||||
blueprint.Select();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (object o in args.OldItems)
|
||||
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
|
||||
{
|
||||
if (blueprintMap.TryGetValue((T)o, out var blueprint))
|
||||
blueprint.Deselect();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SelectionHandler = CreateSelectionHandler();
|
||||
SelectionHandler.DeselectAll = deselectAll;
|
||||
SelectionHandler.DeselectAll = DeselectAll;
|
||||
SelectionHandler.SelectedItems.BindTo(SelectedItems);
|
||||
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
DragBox = CreateDragBox(selectBlueprintsFromDragRectangle),
|
||||
DragBox = CreateDragBox(),
|
||||
SelectionHandler,
|
||||
SelectionBlueprints = CreateSelectionBlueprintContainer(),
|
||||
SelectionHandler.CreateProxy(),
|
||||
@ -101,12 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[CanBeNull]
|
||||
protected virtual SelectionBlueprint<T> CreateBlueprintFor(T item) => null;
|
||||
|
||||
protected virtual DragBox CreateDragBox(Action<RectangleF> performSelect) => new DragBox(performSelect);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to.
|
||||
/// </summary>
|
||||
protected virtual bool AllowDeselectionDuringDrag => true;
|
||||
protected virtual DragBox CreateDragBox() => new DragBox();
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
@ -142,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (endClickSelection(e) || ClickedBlueprint != null)
|
||||
return true;
|
||||
|
||||
deselectAll();
|
||||
DeselectAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -171,11 +170,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
finishSelectionMovement();
|
||||
}
|
||||
|
||||
private MouseButtonEvent lastDragEvent;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Right)
|
||||
return false;
|
||||
|
||||
lastDragEvent = e;
|
||||
|
||||
if (movementBlueprints != null)
|
||||
{
|
||||
isDraggingBlueprint = true;
|
||||
@ -183,30 +186,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DragBox.HandleDrag(e))
|
||||
{
|
||||
DragBox.Show();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
DragBox.HandleDrag(e);
|
||||
DragBox.Show();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Right)
|
||||
return;
|
||||
|
||||
if (DragBox.State == Visibility.Visible)
|
||||
DragBox.HandleDrag(e);
|
||||
lastDragEvent = e;
|
||||
|
||||
moveCurrentSelection(e);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Right)
|
||||
return;
|
||||
lastDragEvent = null;
|
||||
|
||||
if (isDraggingBlueprint)
|
||||
{
|
||||
@ -214,8 +208,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
if (DragBox.State == Visibility.Visible)
|
||||
DragBox.Hide();
|
||||
DragBox.Hide();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (lastDragEvent != null && DragBox.State == Visibility.Visible)
|
||||
{
|
||||
lastDragEvent.Target = this;
|
||||
DragBox.HandleDrag(lastDragEvent);
|
||||
UpdateSelectionFromDragBox();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -233,7 +238,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (!SelectionHandler.SelectedBlueprints.Any())
|
||||
return false;
|
||||
|
||||
deselectAll();
|
||||
DeselectAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -292,7 +297,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
blueprint.Selected -= OnBlueprintSelected;
|
||||
blueprint.Deselected -= OnBlueprintDeselected;
|
||||
|
||||
SelectionBlueprints.Remove(blueprint);
|
||||
SelectionBlueprints.Remove(blueprint, true);
|
||||
|
||||
if (movementBlueprints?.Contains(blueprint) == true)
|
||||
finishSelectionMovement();
|
||||
@ -380,44 +385,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select all masks in a given rectangle selection area.
|
||||
/// Select all blueprints in a selection area specified by <see cref="DragBox"/>.
|
||||
/// </summary>
|
||||
/// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param>
|
||||
private void selectBlueprintsFromDragRectangle(RectangleF rect)
|
||||
protected virtual void UpdateSelectionFromDragBox()
|
||||
{
|
||||
var quad = DragBox.Box.ScreenSpaceDrawQuad;
|
||||
|
||||
foreach (var blueprint in SelectionBlueprints)
|
||||
{
|
||||
// only run when utmost necessary to avoid unnecessary rect computations.
|
||||
bool isValidForSelection() => blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint);
|
||||
|
||||
switch (blueprint.State)
|
||||
{
|
||||
case SelectionState.NotSelected:
|
||||
if (isValidForSelection())
|
||||
blueprint.Select();
|
||||
case SelectionState.Selected:
|
||||
// Selection is preserved even after blueprint becomes dead.
|
||||
if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint))
|
||||
blueprint.Deselect();
|
||||
break;
|
||||
|
||||
case SelectionState.Selected:
|
||||
if (AllowDeselectionDuringDrag && !isValidForSelection())
|
||||
blueprint.Deselect();
|
||||
case SelectionState.NotSelected:
|
||||
if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint))
|
||||
blueprint.Select();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects all <see cref="SelectionBlueprint{T}"/>s.
|
||||
/// Select all currently-present items.
|
||||
/// </summary>
|
||||
protected virtual void SelectAll()
|
||||
{
|
||||
// Scheduled to allow the change in lifetime to take place.
|
||||
Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select()));
|
||||
}
|
||||
protected abstract void SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// Deselects all selected <see cref="SelectionBlueprint{T}"/>s.
|
||||
/// Deselect all selected items.
|
||||
/// </summary>
|
||||
private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
|
||||
protected void DeselectAll() => SelectedItems.Clear();
|
||||
|
||||
protected virtual void OnBlueprintSelected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
|
@ -12,7 +12,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -37,7 +36,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
|
||||
|
||||
private PlacementBlueprint currentPlacement;
|
||||
private InputManager inputManager;
|
||||
|
||||
/// <remarks>
|
||||
/// Positional input must be received outside the container's bounds,
|
||||
@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
Beatmap.HitObjectAdded += hitObjectAdded;
|
||||
|
||||
// updates to selected are handled for us by SelectionHandler.
|
||||
@ -220,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private void updatePlacementPosition()
|
||||
{
|
||||
var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||
var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position);
|
||||
|
||||
// if no time was found from positional snapping, we should still quantize to the beat.
|
||||
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Layout;
|
||||
@ -21,18 +20,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public class DragBox : CompositeDrawable, IStateful<Visibility>
|
||||
{
|
||||
protected readonly Action<RectangleF> PerformSelection;
|
||||
|
||||
protected Drawable Box;
|
||||
public Drawable Box { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DragBox"/>.
|
||||
/// </summary>
|
||||
/// <param name="performSelection">A delegate that performs drag selection.</param>
|
||||
public DragBox(Action<RectangleF> performSelection)
|
||||
public DragBox()
|
||||
{
|
||||
PerformSelection = performSelection;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AlwaysPresent = true;
|
||||
Alpha = 0;
|
||||
@ -46,30 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected virtual Drawable CreateBox() => new BoxWithBorders();
|
||||
|
||||
private RectangleF? dragRectangle;
|
||||
|
||||
/// <summary>
|
||||
/// Handle a forwarded mouse event.
|
||||
/// </summary>
|
||||
/// <param name="e">The mouse event.</param>
|
||||
/// <returns>Whether the event should be handled and blocking.</returns>
|
||||
public virtual bool HandleDrag(MouseButtonEvent e)
|
||||
public virtual void HandleDrag(MouseButtonEvent e)
|
||||
{
|
||||
var dragPosition = e.ScreenSpaceMousePosition;
|
||||
var dragStartPosition = e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
|
||||
|
||||
// We use AABBFloat instead of RectangleF since it handles negative sizes for us
|
||||
var rec = dragQuad.AABBFloat;
|
||||
dragRectangle = rec;
|
||||
|
||||
var topLeft = ToLocalSpace(rec.TopLeft);
|
||||
var bottomRight = ToLocalSpace(rec.BottomRight);
|
||||
|
||||
Box.Position = topLeft;
|
||||
Box.Size = bottomRight - topLeft;
|
||||
return true;
|
||||
Box.Position = Vector2.ComponentMin(e.MouseDownPosition, e.MousePosition);
|
||||
Box.Size = Vector2.ComponentMax(e.MouseDownPosition, e.MousePosition) - Box.Position;
|
||||
}
|
||||
|
||||
private Visibility state;
|
||||
@ -87,19 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (dragRectangle != null)
|
||||
PerformSelection?.Invoke(dragRectangle.Value);
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
State = Visibility.Hidden;
|
||||
dragRectangle = null;
|
||||
}
|
||||
public override void Hide() => State = Visibility.Hidden;
|
||||
|
||||
public override void Show() => State = Visibility.Visible;
|
||||
|
||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -27,6 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private HitObjectUsageEventBuffer usageEventBuffer;
|
||||
|
||||
protected InputManager InputManager { get; private set; }
|
||||
|
||||
protected EditorBlueprintContainer(HitObjectComposer composer)
|
||||
{
|
||||
Composer = composer;
|
||||
@ -42,6 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
InputManager = GetContainingInputManager();
|
||||
|
||||
Beatmap.HitObjectAdded += AddBlueprintFor;
|
||||
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
|
||||
|
||||
@ -66,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
|
||||
=> blueprints.OrderBy(b => b.Item.StartTime);
|
||||
|
||||
protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning;
|
||||
|
||||
protected override bool ApplySnapResult(SelectionBlueprint<HitObject>[] blueprints, SnapResult result)
|
||||
{
|
||||
if (!base.ApplySnapResult(blueprints, result))
|
||||
@ -133,8 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected override void SelectAll()
|
||||
{
|
||||
Composer.Playfield.KeepAllAlive();
|
||||
|
||||
base.SelectAll();
|
||||
SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray());
|
||||
}
|
||||
|
||||
protected override void OnBlueprintSelected(SelectionBlueprint<HitObject> blueprint)
|
||||
|
@ -24,21 +24,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
|
||||
editorBeatmap.BeatmapReprocessed += SortInternal;
|
||||
}
|
||||
|
||||
private void hitObjectUpdated(HitObject _) => SortInternal();
|
||||
|
||||
public override void Add(SelectionBlueprint<HitObject> drawable)
|
||||
{
|
||||
SortInternal();
|
||||
base.Add(drawable);
|
||||
}
|
||||
|
||||
public override bool Remove(SelectionBlueprint<HitObject> drawable)
|
||||
public override bool Remove(SelectionBlueprint<HitObject> drawable, bool disposeImmediately)
|
||||
{
|
||||
SortInternal();
|
||||
return base.Remove(drawable);
|
||||
return base.Remove(drawable, disposeImmediately);
|
||||
}
|
||||
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
@ -64,7 +62,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (result != 0) return result;
|
||||
}
|
||||
|
||||
return CompareReverseChildID(y, x);
|
||||
return CompareReverseChildID(x, y);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
@ -72,7 +70,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (editorBeatmap != null)
|
||||
editorBeatmap.HitObjectUpdated -= hitObjectUpdated;
|
||||
editorBeatmap.BeatmapReprocessed -= SortInternal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
64
osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs
Normal file
64
osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="DragBox"/> that scrolls along with the scrolling playfield.
|
||||
/// </summary>
|
||||
public class ScrollingDragBox : DragBox
|
||||
{
|
||||
public double MinTime { get; private set; }
|
||||
|
||||
public double MaxTime { get; private set; }
|
||||
|
||||
private double? startTime;
|
||||
|
||||
private readonly ScrollingPlayfield playfield;
|
||||
|
||||
public ScrollingDragBox(Playfield playfield)
|
||||
{
|
||||
this.playfield = playfield as ScrollingPlayfield ?? throw new ArgumentException("Playfield must be of type {nameof(ScrollingPlayfield)} to use this class.", nameof(playfield));
|
||||
}
|
||||
|
||||
public override void HandleDrag(MouseButtonEvent e)
|
||||
{
|
||||
base.HandleDrag(e);
|
||||
|
||||
startTime ??= playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMouseDownPosition);
|
||||
double endTime = playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMousePosition);
|
||||
|
||||
MinTime = Math.Min(startTime.Value, endTime);
|
||||
MaxTime = Math.Max(startTime.Value, endTime);
|
||||
|
||||
var startPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(startTime.Value));
|
||||
var endPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(endTime));
|
||||
|
||||
switch (playfield.ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
Box.Y = Math.Min(startPos.Y, endPos.Y);
|
||||
Box.Height = Math.Max(startPos.Y, endPos.Y) - Box.Y;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
Box.X = Math.Min(startPos.X, endPos.X);
|
||||
Box.Width = Math.Max(startPos.X, endPos.X) - Box.X;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
base.Hide();
|
||||
startTime = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -305,7 +305,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected void DeleteSelected()
|
||||
{
|
||||
DeleteItems(selectedBlueprints.Select(b => b.Item));
|
||||
DeleteItems(SelectedItems.ToArray());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -5,7 +5,6 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -64,8 +63,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
/// </summary>
|
||||
private bool trackWasPlaying;
|
||||
|
||||
private Track track;
|
||||
|
||||
/// <summary>
|
||||
/// The timeline zoom level at a 1x zoom scale.
|
||||
/// </summary>
|
||||
@ -93,6 +90,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private Bindable<float> waveformOpacity;
|
||||
|
||||
private double trackLengthForZoom;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
|
||||
{
|
||||
@ -144,9 +143,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Beatmap.BindValueChanged(b =>
|
||||
{
|
||||
waveform.Waveform = b.NewValue.Waveform;
|
||||
track = b.NewValue.Track;
|
||||
|
||||
setupTimelineZoom();
|
||||
}, true);
|
||||
|
||||
Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
|
||||
@ -185,8 +181,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
private void updateWaveformOpacity() =>
|
||||
waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint);
|
||||
|
||||
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds));
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -197,20 +191,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
// This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren
|
||||
if (editorClock.IsRunning)
|
||||
scrollToTrackTime();
|
||||
}
|
||||
|
||||
private void setupTimelineZoom()
|
||||
{
|
||||
if (!track.IsLoaded)
|
||||
if (editorClock.TrackLength != trackLengthForZoom)
|
||||
{
|
||||
Scheduler.AddOnce(setupTimelineZoom);
|
||||
return;
|
||||
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
|
||||
|
||||
float initialZoom = (float)(defaultTimelineZoom * (editorBeatmap.BeatmapInfo.TimelineZoom == 0 ? 1 : editorBeatmap.BeatmapInfo.TimelineZoom));
|
||||
float minimumZoom = getZoomLevelForVisibleMilliseconds(10000);
|
||||
float maximumZoom = getZoomLevelForVisibleMilliseconds(500);
|
||||
|
||||
SetupZoom(initialZoom, minimumZoom, maximumZoom);
|
||||
|
||||
float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(editorClock.TrackLength / milliseconds));
|
||||
|
||||
trackLengthForZoom = editorClock.TrackLength;
|
||||
}
|
||||
|
||||
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
|
||||
|
||||
float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
|
||||
SetupZoom(initialZoom, getZoomLevelForVisibleMilliseconds(10000), getZoomLevelForVisibleMilliseconds(500));
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
@ -255,16 +250,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private void seekTrackToCurrent()
|
||||
{
|
||||
if (!track.IsLoaded)
|
||||
return;
|
||||
|
||||
double target = Current / Content.DrawWidth * track.Length;
|
||||
editorClock.Seek(Math.Min(track.Length, target));
|
||||
double target = Current / Content.DrawWidth * editorClock.TrackLength;
|
||||
editorClock.Seek(Math.Min(editorClock.TrackLength, target));
|
||||
}
|
||||
|
||||
private void scrollToTrackTime()
|
||||
{
|
||||
if (!track.IsLoaded || track.Length == 0)
|
||||
if (editorClock.TrackLength == 0)
|
||||
return;
|
||||
|
||||
// covers the case where the user starts playback after a drag is in progress.
|
||||
@ -272,7 +264,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
if (handlingDragInput)
|
||||
editorClock.Stop();
|
||||
|
||||
ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false);
|
||||
ScrollTo((float)(editorClock.CurrentTime / editorClock.TrackLength) * Content.DrawWidth, false);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
@ -310,12 +302,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
/// <summary>
|
||||
/// The total amount of time visible on the timeline.
|
||||
/// </summary>
|
||||
public double VisibleRange => track.Length / Zoom;
|
||||
public double VisibleRange => editorClock.TrackLength / Zoom;
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
|
||||
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
||||
public double TimeAtPosition(float x)
|
||||
{
|
||||
return x / Content.DrawWidth * editorClock.TrackLength;
|
||||
}
|
||||
|
||||
private double getTimeFromPosition(Vector2 localPosition) =>
|
||||
(localPosition.X / Content.DrawWidth) * track.Length;
|
||||
public float PositionAtTime(double time)
|
||||
{
|
||||
return (float)(time / editorClock.TrackLength * Content.DrawWidth);
|
||||
}
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
|
||||
return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,16 +78,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
LabelText = "Waveform",
|
||||
Current = { Value = true },
|
||||
},
|
||||
controlPointsCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "Control Points",
|
||||
Current = { Value = true },
|
||||
},
|
||||
ticksCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "Ticks",
|
||||
Current = { Value = true },
|
||||
}
|
||||
},
|
||||
controlPointsCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "BPM",
|
||||
Current = { Value = true },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,7 +12,6 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
@ -31,10 +29,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
[Resolved(CanBeNull = true)]
|
||||
private Timeline timeline { get; set; }
|
||||
|
||||
private DragEvent lastDragEvent;
|
||||
private Bindable<HitObject> placement;
|
||||
private SelectionBlueprint<HitObject> placementBlueprint;
|
||||
|
||||
private bool hitObjectDragged;
|
||||
|
||||
/// <remarks>
|
||||
/// Positional input must be received outside the container's bounds,
|
||||
/// in order to handle timeline blueprints which are stacked offscreen.
|
||||
@ -65,7 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
DragBox.Alpha = 0;
|
||||
|
||||
placement = Beatmap.PlacementObject.GetBoundCopy();
|
||||
placement.ValueChanged += placementChanged;
|
||||
@ -77,7 +75,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
if (placementBlueprint != null)
|
||||
{
|
||||
SelectionBlueprints.Remove(placementBlueprint);
|
||||
SelectionBlueprints.Remove(placementBlueprint, true);
|
||||
placementBlueprint = null;
|
||||
}
|
||||
}
|
||||
@ -93,24 +91,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
handleScrollViaDrag(e);
|
||||
if (!base.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition))
|
||||
return false;
|
||||
|
||||
base.OnDrag(e);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
base.OnDragEnd(e);
|
||||
lastDragEvent = null;
|
||||
return base.OnDragStart(e);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
// trigger every frame so drags continue to update selection while playback is scrolling the timeline.
|
||||
if (lastDragEvent != null)
|
||||
OnDrag(lastDragEvent);
|
||||
if (IsDragged || hitObjectDragged)
|
||||
handleScrollViaDrag();
|
||||
|
||||
if (Composer != null && timeline != null)
|
||||
{
|
||||
@ -165,30 +157,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
return new TimelineHitObjectBlueprint(item)
|
||||
{
|
||||
OnDragHandled = handleScrollViaDrag,
|
||||
OnDragHandled = e => hitObjectDragged = e != null,
|
||||
};
|
||||
}
|
||||
|
||||
protected override DragBox CreateDragBox(Action<RectangleF> performSelect) => new TimelineDragBox(performSelect);
|
||||
protected sealed override DragBox CreateDragBox() => new TimelineDragBox();
|
||||
|
||||
private void handleScrollViaDrag(DragEvent e)
|
||||
protected override void UpdateSelectionFromDragBox()
|
||||
{
|
||||
lastDragEvent = e;
|
||||
var dragBox = (TimelineDragBox)DragBox;
|
||||
double minTime = dragBox.MinTime;
|
||||
double maxTime = dragBox.MaxTime;
|
||||
|
||||
if (lastDragEvent == null)
|
||||
return;
|
||||
SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject));
|
||||
|
||||
if (timeline != null)
|
||||
foreach (var hitObject in Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected))
|
||||
{
|
||||
var timelineQuad = timeline.ScreenSpaceDrawQuad;
|
||||
float mouseX = e.ScreenSpaceMousePosition.X;
|
||||
|
||||
// scroll if in a drag and dragging outside visible extents
|
||||
if (mouseX > timelineQuad.TopRight.X)
|
||||
timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
|
||||
else if (mouseX < timelineQuad.TopLeft.X)
|
||||
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
|
||||
Composer.Playfield.SetKeepAlive(hitObject, true);
|
||||
SelectedItems.Add(hitObject);
|
||||
}
|
||||
|
||||
bool shouldBeSelected(HitObject hitObject)
|
||||
{
|
||||
double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2;
|
||||
return minTime <= midTime && midTime <= maxTime;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleScrollViaDrag()
|
||||
{
|
||||
if (timeline == null) return;
|
||||
|
||||
var timelineQuad = timeline.ScreenSpaceDrawQuad;
|
||||
float mouseX = InputManager.CurrentState.Mouse.Position.X;
|
||||
|
||||
// scroll if in a drag and dragging outside visible extents
|
||||
if (mouseX > timelineQuad.TopRight.X)
|
||||
timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
|
||||
else if (mouseX < timelineQuad.TopLeft.X)
|
||||
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
|
||||
}
|
||||
|
||||
private class SelectableAreaBackground : CompositeDrawable
|
||||
|
@ -6,76 +6,44 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimelineDragBox : DragBox
|
||||
{
|
||||
// the following values hold the start and end X positions of the drag box in the timeline's local space,
|
||||
// but with zoom unapplied in order to be able to compensate for positional changes
|
||||
// while the timeline is being zoomed in/out.
|
||||
private float? selectionStart;
|
||||
private float selectionEnd;
|
||||
public double MinTime { get; private set; }
|
||||
|
||||
public double MaxTime { get; private set; }
|
||||
|
||||
private double? startTime;
|
||||
|
||||
[Resolved]
|
||||
private Timeline timeline { get; set; }
|
||||
|
||||
public TimelineDragBox(Action<RectangleF> performSelect)
|
||||
: base(performSelect)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Drawable CreateBox() => new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Alpha = 0.3f
|
||||
};
|
||||
|
||||
public override bool HandleDrag(MouseButtonEvent e)
|
||||
public override void HandleDrag(MouseButtonEvent e)
|
||||
{
|
||||
// The dragbox should only be active if the mouseDownPosition.Y is within this drawable's bounds.
|
||||
float localY = ToLocalSpace(e.ScreenSpaceMouseDownPosition).Y;
|
||||
if (DrawRectangle.Top > localY || DrawRectangle.Bottom < localY)
|
||||
return false;
|
||||
startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X);
|
||||
double endTime = timeline.TimeAtPosition(e.MousePosition.X);
|
||||
|
||||
selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom;
|
||||
MinTime = Math.Min(startTime.Value, endTime);
|
||||
MaxTime = Math.Max(startTime.Value, endTime);
|
||||
|
||||
// only calculate end when a transition is not in progress to avoid bouncing.
|
||||
if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom))
|
||||
selectionEnd = e.MousePosition.X / timeline.CurrentZoom;
|
||||
|
||||
updateDragBoxPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDragBoxPosition()
|
||||
{
|
||||
if (selectionStart == null)
|
||||
return;
|
||||
|
||||
float rescaledStart = selectionStart.Value * timeline.CurrentZoom;
|
||||
float rescaledEnd = selectionEnd * timeline.CurrentZoom;
|
||||
|
||||
Box.X = Math.Min(rescaledStart, rescaledEnd);
|
||||
Box.Width = Math.Abs(rescaledStart - rescaledEnd);
|
||||
|
||||
var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
// we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment.
|
||||
boxScreenRect.Y -= boxScreenRect.Height;
|
||||
boxScreenRect.Height *= 2;
|
||||
|
||||
PerformSelection?.Invoke(boxScreenRect);
|
||||
Box.X = timeline.PositionAtTime(MinTime);
|
||||
Box.Width = timeline.PositionAtTime(MaxTime) - Box.X;
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
base.Hide();
|
||||
selectionStart = null;
|
||||
startTime = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
@ -33,19 +31,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
private const float circle_size = 38;
|
||||
|
||||
private Container repeatsContainer;
|
||||
private Container? repeatsContainer;
|
||||
|
||||
public Action<DragEvent> OnDragHandled;
|
||||
public Action<DragEvent?>? OnDragHandled = null!;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly Bindable<double> startTime;
|
||||
|
||||
private Bindable<int> indexInCurrentComboBindable;
|
||||
private Bindable<int>? indexInCurrentComboBindable;
|
||||
|
||||
private Bindable<int> comboIndexBindable;
|
||||
private Bindable<int> comboIndexWithOffsetsBindable;
|
||||
private Bindable<int>? comboIndexBindable;
|
||||
private Bindable<int>? comboIndexWithOffsetsBindable;
|
||||
|
||||
private Bindable<Color4> displayColourBindable;
|
||||
private Bindable<Color4> displayColourBindable = null!;
|
||||
|
||||
private readonly ExtendableCircle circle;
|
||||
private readonly Border border;
|
||||
@ -54,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
private readonly OsuSpriteText comboIndexText;
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
public TimelineHitObjectBlueprint(HitObject item)
|
||||
: base(item)
|
||||
@ -124,7 +122,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
case IHasComboInformation comboInfo:
|
||||
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
|
||||
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
|
||||
indexInCurrentComboBindable.BindValueChanged(_ =>
|
||||
{
|
||||
comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
|
||||
}, true);
|
||||
|
||||
comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
|
||||
comboIndexWithOffsetsBindable = comboInfo.ComboIndexWithOffsetsBindable.GetBoundCopy();
|
||||
@ -149,8 +150,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
updateColour();
|
||||
}
|
||||
|
||||
private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
Color4 colour;
|
||||
@ -183,11 +182,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour);
|
||||
}
|
||||
|
||||
private SamplePointPiece sampleOverrideDisplay;
|
||||
private DifficultyPointPiece difficultyOverrideDisplay;
|
||||
private SamplePointPiece? sampleOverrideDisplay;
|
||||
private DifficultyPointPiece? difficultyOverrideDisplay;
|
||||
|
||||
private DifficultyControlPoint difficultyControlPoint;
|
||||
private SampleControlPoint sampleControlPoint;
|
||||
private DifficultyControlPoint difficultyControlPoint = null!;
|
||||
private SampleControlPoint sampleControlPoint = null!;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@ -276,16 +275,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
public class DragArea : Circle
|
||||
{
|
||||
private readonly HitObject hitObject;
|
||||
private readonly HitObject? hitObject;
|
||||
|
||||
[Resolved]
|
||||
private Timeline timeline { get; set; }
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
public Action<DragEvent> OnDragHandled;
|
||||
[Resolved]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Timeline timeline { get; set; } = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private ScheduledDelegate? dragOperation;
|
||||
|
||||
public Action<DragEvent?>? OnDragHandled;
|
||||
|
||||
public override bool HandlePositionalInput => hitObject != null;
|
||||
|
||||
public DragArea(HitObject hitObject)
|
||||
public DragArea(HitObject? hitObject)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
|
||||
@ -356,23 +366,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
this.FadeTo(IsHovered || hasMouseDown ? 1f : 0.9f, 200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
private ScheduledDelegate dragOperation;
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
base.OnDrag(e);
|
||||
|
@ -4,11 +4,12 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||
@ -34,8 +35,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private static readonly int highest_divisor = BindableBeatDivisor.PREDEFINED_DIVISORS.Last();
|
||||
|
||||
public TimelineTickDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -80,20 +79,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (timeline != null)
|
||||
if (timeline == null || DrawWidth <= 0) return;
|
||||
|
||||
(float, float) newRange = (
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
|
||||
|
||||
if (visibleRange != newRange)
|
||||
{
|
||||
var newRange = (
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
|
||||
visibleRange = newRange;
|
||||
|
||||
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();
|
||||
}
|
||||
// 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)
|
||||
@ -151,6 +149,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
}
|
||||
|
||||
if (Children.Count > 512)
|
||||
{
|
||||
// There should always be a sanely small number of ticks rendered.
|
||||
// If this assertion triggers, either the zoom logic is broken or a beatmap is
|
||||
// probably doing weird things...
|
||||
//
|
||||
// Let's hope the latter never happens.
|
||||
// If it does, we can choose to either fix it or ignore it as an outlier.
|
||||
string message = $"Timeline is rendering many ticks ({Children.Count})";
|
||||
|
||||
Logger.Log(message);
|
||||
Debug.Fail(message);
|
||||
}
|
||||
|
||||
int usedDrawables = drawableIndex;
|
||||
|
||||
// save a few drawables beyond the currently used for edge cases.
|
||||
|
@ -56,7 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
protected ZoomableScrollContainer()
|
||||
: base(Direction.Horizontal)
|
||||
{
|
||||
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
|
||||
base.Content.Add(zoomedContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
// We must hide content until SetupZoom is called.
|
||||
// If not, a child component that relies on its DrawWidth (via RelativeSizeAxes) may see a very incorrect value
|
||||
// momentarily, as noticed in the TimelineTickDisplay, which would render thousands of ticks incorrectly.
|
||||
Alpha = 0,
|
||||
});
|
||||
|
||||
AddLayout(zoomedContentWidthCache);
|
||||
}
|
||||
@ -87,10 +94,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
if (minimum > maximum)
|
||||
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
|
||||
|
||||
if (initial < minimum || initial > maximum)
|
||||
throw new ArgumentException($"{nameof(initial)} ({initial}) must be between {nameof(minimum)} ({minimum}) and {nameof(maximum)} ({maximum})");
|
||||
|
||||
minZoom = minimum;
|
||||
maxZoom = maximum;
|
||||
CurrentZoom = zoomTarget = initial;
|
||||
isZoomSetUp = true;
|
||||
|
||||
zoomedContent.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -115,9 +127,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
CurrentZoom = zoomTarget = newZoom;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.Update();
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!zoomedContentWidthCache.IsValid)
|
||||
updateZoomedContentWidth();
|
||||
|
18
osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs
Normal file
18
osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public class DeleteDifficultyConfirmationDialog : DeleteConfirmationDialog
|
||||
{
|
||||
public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction)
|
||||
{
|
||||
BodyText = $"\"{beatmapInfo.DifficultyName}\" difficulty";
|
||||
DeleteAction = deleteAction;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework;
|
||||
@ -220,7 +219,7 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
|
||||
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
|
||||
clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false };
|
||||
clock = new EditorClock(playableBeatmap, beatDivisor);
|
||||
clock.ChangeSource(loadableBeatmap.Track);
|
||||
|
||||
dependencies.CacheAs(clock);
|
||||
@ -879,35 +878,61 @@ namespace osu.Game.Screens.Edit
|
||||
clock.SeekForward(!trackPlaying, amount);
|
||||
}
|
||||
|
||||
private void updateLastSavedHash()
|
||||
{
|
||||
lastSavedHash = changeHandler?.CurrentStateHash;
|
||||
}
|
||||
|
||||
private List<MenuItem> createFileMenuItems() => new List<MenuItem>
|
||||
{
|
||||
new EditorMenuItem("Save", MenuItemType.Standard, () => Save()),
|
||||
new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new EditorMenuItemSpacer(),
|
||||
createDifficultyCreationMenu(),
|
||||
createDifficultySwitchMenu(),
|
||||
new EditorMenuItemSpacer(),
|
||||
new EditorMenuItem("Delete difficulty", MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } },
|
||||
new EditorMenuItemSpacer(),
|
||||
new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)
|
||||
};
|
||||
|
||||
private void exportBeatmap()
|
||||
{
|
||||
Save();
|
||||
new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo);
|
||||
}
|
||||
|
||||
private void updateLastSavedHash()
|
||||
{
|
||||
lastSavedHash = changeHandler?.CurrentStateHash;
|
||||
}
|
||||
/// <summary>
|
||||
/// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty.
|
||||
/// </summary>
|
||||
private IOrderedEnumerable<IGrouping<RulesetInfo, BeatmapInfo>> groupedOrderedBeatmaps => Beatmap.Value.BeatmapSetInfo.Beatmaps
|
||||
.OrderBy(b => b.StarRating)
|
||||
.GroupBy(b => b.Ruleset)
|
||||
.OrderBy(group => group.Key);
|
||||
|
||||
private List<MenuItem> createFileMenuItems()
|
||||
private void deleteDifficulty()
|
||||
{
|
||||
var fileMenuItems = new List<MenuItem>
|
||||
if (dialogOverlay == null)
|
||||
delete();
|
||||
else
|
||||
dialogOverlay.Push(new DeleteDifficultyConfirmationDialog(Beatmap.Value.BeatmapInfo, delete));
|
||||
|
||||
void delete()
|
||||
{
|
||||
new EditorMenuItem("Save", MenuItemType.Standard, () => Save())
|
||||
};
|
||||
BeatmapInfo difficultyToDelete = playableBeatmap.BeatmapInfo;
|
||||
|
||||
if (RuntimeInfo.IsDesktop)
|
||||
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
|
||||
var difficultiesBeforeDeletion = groupedOrderedBeatmaps.SelectMany(g => g).ToList();
|
||||
|
||||
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||
beatmapManager.DeleteDifficultyImmediately(difficultyToDelete);
|
||||
|
||||
fileMenuItems.Add(createDifficultyCreationMenu());
|
||||
fileMenuItems.Add(createDifficultySwitchMenu());
|
||||
int deletedIndex = difficultiesBeforeDeletion.IndexOf(difficultyToDelete);
|
||||
// of note, we're still working with the cloned version, so indices are all prior to deletion.
|
||||
BeatmapInfo nextToShow = difficultiesBeforeDeletion[deletedIndex == 0 ? 1 : deletedIndex - 1];
|
||||
|
||||
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
||||
return fileMenuItems;
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow);
|
||||
|
||||
SwitchToDifficulty(nextToShow);
|
||||
}
|
||||
}
|
||||
|
||||
private EditorMenuItem createDifficultyCreationMenu()
|
||||
@ -939,18 +964,14 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private EditorMenuItem createDifficultySwitchMenu()
|
||||
{
|
||||
var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet;
|
||||
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
var difficultyItems = new List<MenuItem>();
|
||||
|
||||
foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key))
|
||||
foreach (var rulesetBeatmaps in groupedOrderedBeatmaps)
|
||||
{
|
||||
if (difficultyItems.Count > 0)
|
||||
difficultyItems.Add(new EditorMenuItemSpacer());
|
||||
|
||||
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
|
||||
foreach (var beatmap in rulesetBeatmaps)
|
||||
{
|
||||
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap);
|
||||
difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty));
|
||||
|
@ -48,6 +48,15 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
public event Action<HitObject> HitObjectUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after any state changes occurred which triggered a beatmap reprocess via an <see cref="IBeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Beatmap processing may change the order of hitobjects. This event gives external components a chance to handle any changes
|
||||
/// not covered by the <see cref="HitObjectAdded"/> / <see cref="HitObjectUpdated"/> / <see cref="HitObjectRemoved"/> events.
|
||||
/// </remarks>
|
||||
public event Action BeatmapReprocessed;
|
||||
|
||||
/// <summary>
|
||||
/// All currently selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
@ -331,6 +340,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
beatmapProcessor?.PostProcess();
|
||||
|
||||
BeatmapReprocessed?.Invoke();
|
||||
|
||||
// callbacks may modify the lists so let's be safe about it
|
||||
var deletes = batchPendingDeletes.ToArray();
|
||||
batchPendingDeletes.Clear();
|
||||
@ -341,6 +352,8 @@ namespace osu.Game.Screens.Edit
|
||||
var updates = batchPendingUpdates.ToArray();
|
||||
batchPendingUpdates.Clear();
|
||||
|
||||
foreach (var h in deletes) SelectedHitObjects.Remove(h);
|
||||
|
||||
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
|
||||
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
|
||||
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
|
||||
|
@ -4,10 +4,12 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
@ -19,7 +21,7 @@ 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 : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
|
||||
{
|
||||
public IBindable<Track> Track => track;
|
||||
|
||||
@ -33,7 +35,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private readonly BindableBeatDivisor beatDivisor;
|
||||
|
||||
private readonly DecoupleableInterpolatingFramedClock underlyingClock;
|
||||
private readonly FramedBeatmapClock underlyingClock;
|
||||
|
||||
private bool playbackFinished;
|
||||
|
||||
@ -52,7 +54,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
|
||||
|
||||
underlyingClock = new DecoupleableInterpolatingFramedClock();
|
||||
underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false };
|
||||
AddInternal(underlyingClock);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -155,6 +158,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public double CurrentTime => underlyingClock.CurrentTime;
|
||||
|
||||
public double TotalAppliedOffset => underlyingClock.TotalAppliedOffset;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ClearTransforms();
|
||||
@ -219,18 +224,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public void ProcessFrame()
|
||||
{
|
||||
underlyingClock.ProcessFrame();
|
||||
|
||||
playbackFinished = CurrentTime >= TrackLength;
|
||||
|
||||
if (playbackFinished)
|
||||
{
|
||||
if (IsRunning)
|
||||
underlyingClock.Stop();
|
||||
|
||||
if (CurrentTime > TrackLength)
|
||||
underlyingClock.Seek(TrackLength);
|
||||
}
|
||||
// Noop to ensure an external consumer doesn't process the internal clock an extra time.
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
|
||||
@ -247,18 +241,26 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public IClock Source => underlyingClock.Source;
|
||||
|
||||
public bool IsCoupled
|
||||
{
|
||||
get => underlyingClock.IsCoupled;
|
||||
set => underlyingClock.IsCoupled = value;
|
||||
}
|
||||
|
||||
private const double transform_time = 300;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// EditorClock wasn't being added in many places. This gives us more certainty that it is.
|
||||
Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded);
|
||||
|
||||
playbackFinished = CurrentTime >= TrackLength;
|
||||
|
||||
if (playbackFinished)
|
||||
{
|
||||
if (IsRunning)
|
||||
underlyingClock.Stop();
|
||||
|
||||
if (CurrentTime > TrackLength)
|
||||
underlyingClock.Seek(TrackLength);
|
||||
}
|
||||
|
||||
updateSeekingState();
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,11 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
||||
}
|
||||
|
||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||
=> new MasterGameplayClockContainer(beatmap, gameplayStart) { StartTime = editorState.Time };
|
||||
{
|
||||
var masterGameplayClockContainer = new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
masterGameplayClockContainer.Reset(editorState.Time);
|
||||
return masterGameplayClockContainer;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
@ -41,6 +41,11 @@ namespace osu.Game.Screens
|
||||
/// </summary>
|
||||
bool HideOverlaysOnEnter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the menu cursor should be hidden when non-mouse input is received.
|
||||
/// </summary>
|
||||
bool HideMenuCursorOnNonMouseInput { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether overlays should be able to be opened when this screen is current.
|
||||
/// </summary>
|
||||
|
@ -14,7 +14,6 @@ using osu.Game.Screens.Menu;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using IntroSequence = osu.Game.Configuration.IntroSequence;
|
||||
|
||||
@ -66,32 +65,13 @@ namespace osu.Game.Screens
|
||||
|
||||
protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DatabaseContextFactory efContextFactory { get; set; }
|
||||
|
||||
private EFToRealmMigrator realmMigrator;
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
|
||||
|
||||
// A non-null context factory means there's still content to migrate.
|
||||
if (efContextFactory != null)
|
||||
{
|
||||
LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal);
|
||||
realmMigrator.MigrationCompleted.ContinueWith(_ => Schedule(() =>
|
||||
{
|
||||
// Delay initial screen loading to ensure that the migration is in a complete and sane state
|
||||
// before the intro screen may import the game intro beatmap.
|
||||
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||
}
|
||||
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||
|
||||
LoadComponentAsync(spinner = new LoadingSpinner(true, true)
|
||||
{
|
||||
|
@ -278,11 +278,11 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
if (!UsingThemedIntro)
|
||||
{
|
||||
initialBeatmap?.PrepareTrackForPreview(false);
|
||||
initialBeatmap?.PrepareTrackForPreview(false, -2600);
|
||||
|
||||
drawableTrack.VolumeTo(0);
|
||||
drawableTrack.Restart();
|
||||
drawableTrack.VolumeTo(1, 2200, Easing.InCubic);
|
||||
drawableTrack.VolumeTo(1, 2600, Easing.InCubic);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -255,8 +255,7 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
lazerLogo.FadeOut().OnComplete(_ =>
|
||||
{
|
||||
logoContainerSecondary.Remove(lazerLogo);
|
||||
lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run.
|
||||
logoContainerSecondary.Remove(lazerLogo, true);
|
||||
|
||||
logo.FadeIn();
|
||||
|
||||
|
@ -78,13 +78,17 @@ namespace osu.Game.Screens.Menu
|
||||
if (reverbChannel != null)
|
||||
intro.LogoVisualisation.AddAmplitudeSource(reverbChannel);
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (!UsingThemedIntro)
|
||||
StartTrack();
|
||||
|
||||
// this classic intro loops forever.
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (UsingThemedIntro)
|
||||
{
|
||||
StartTrack();
|
||||
// this classic intro loops forever.
|
||||
Track.Looping = true;
|
||||
}
|
||||
|
||||
const float fade_in_time = 200;
|
||||
|
||||
|
@ -561,6 +561,10 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case DownloadState.Unknown:
|
||||
// Ignore initial state to ensure the button doesn't briefly appear.
|
||||
break;
|
||||
|
||||
case DownloadState.LocallyAvailable:
|
||||
// Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching.
|
||||
if (beatmapManager.QueryBeatmap(b => b.MD5Hash == beatmap.MD5Hash) == null)
|
||||
|
@ -232,7 +232,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
|
||||
private void removeUser(APIUser user)
|
||||
{
|
||||
avatarFlow.RemoveAll(a => a.User == user);
|
||||
avatarFlow.RemoveAll(a => a.User == user, true);
|
||||
}
|
||||
|
||||
private void clearUsers()
|
||||
@ -250,7 +250,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
hiddenUsers.Count = hiddenCount;
|
||||
|
||||
if (displayedCircles > NumberOfCircles)
|
||||
avatarFlow.Remove(avatarFlow.Last());
|
||||
avatarFlow.Remove(avatarFlow.Last(), true);
|
||||
else if (displayedCircles < NumberOfCircles)
|
||||
{
|
||||
var nextUser = RecentParticipants.FirstOrDefault(u => avatarFlow.All(a => a.User != u));
|
||||
|
@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
foreach (var r in rooms)
|
||||
{
|
||||
roomFlow.RemoveAll(d => d.Room == r);
|
||||
roomFlow.RemoveAll(d => d.Room == r, true);
|
||||
|
||||
// selection may have a lease due to being in a sub screen.
|
||||
if (!SelectedRoom.Disabled)
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -16,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
|
||||
public class MatchLeaderboard : Leaderboard<MatchLeaderboardScope, APIUserScoreAggregate>
|
||||
{
|
||||
[Resolved(typeof(Room), nameof(Room.RoomID))]
|
||||
private Bindable<long?> roomId { get; set; }
|
||||
private Bindable<long?> roomId { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -33,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
|
||||
|
||||
protected override bool IsOnlineScope => true;
|
||||
|
||||
protected override APIRequest FetchScores(CancellationToken cancellationToken)
|
||||
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
|
||||
{
|
||||
if (roomId.Value == null)
|
||||
return null;
|
||||
|
@ -433,6 +433,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
|
||||
private void updateWorkingBeatmap()
|
||||
{
|
||||
if (SelectedItem.Value == null || !this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
var beatmap = SelectedItem.Value?.Beatmap;
|
||||
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
|
@ -109,7 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
if (isReady() && Client.IsHost && Room.Countdown == null)
|
||||
if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
startMatch();
|
||||
else
|
||||
toggleReady();
|
||||
@ -140,10 +140,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
private void cancelCountdown()
|
||||
{
|
||||
if (Client.Room == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
|
||||
MultiplayerCountdown countdown = Client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
|
||||
Client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
|
||||
}
|
||||
|
||||
private void endOperation()
|
||||
@ -192,7 +196,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
|
||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && Room.Countdown == null;
|
||||
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
|
||||
|
||||
if (newCountReady == countReady)
|
||||
return;
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
@ -79,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
bool countdownActive = multiplayerClient.Room?.Countdown is MatchStartCountdown;
|
||||
bool countdownActive = multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true;
|
||||
|
||||
if (countdownActive)
|
||||
{
|
||||
@ -121,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
});
|
||||
}
|
||||
|
||||
if (multiplayerClient.Room?.Countdown != null && multiplayerClient.IsHost)
|
||||
if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && multiplayerClient.IsHost)
|
||||
{
|
||||
flow.Add(new OsuButton
|
||||
{
|
||||
|
@ -1,12 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@ -30,12 +27,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay
|
||||
{
|
||||
private MatchSettings settings;
|
||||
private MatchSettings settings = null!;
|
||||
|
||||
protected override OsuButton SubmitButton => settings.ApplyButton;
|
||||
|
||||
[Resolved]
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
|
||||
|
||||
protected override bool IsLoading => ongoingOperationTracker.InProgress.Value;
|
||||
|
||||
@ -57,19 +54,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
private const float disabled_alpha = 0.2f;
|
||||
|
||||
public Action SettingsApplied;
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
public OsuTextBox NameField, MaxParticipantsField;
|
||||
public MatchTypePicker TypePicker;
|
||||
public OsuEnumDropdown<QueueMode> QueueModeDropdown;
|
||||
public OsuTextBox PasswordTextBox;
|
||||
public TriangleButton ApplyButton;
|
||||
public Action? SettingsApplied;
|
||||
|
||||
public OsuSpriteText ErrorText;
|
||||
public OsuTextBox NameField = null!;
|
||||
public OsuTextBox MaxParticipantsField = null!;
|
||||
public MatchTypePicker TypePicker = null!;
|
||||
public OsuEnumDropdown<QueueMode> QueueModeDropdown = null!;
|
||||
public OsuTextBox PasswordTextBox = null!;
|
||||
public OsuCheckbox AutoSkipCheckbox = null!;
|
||||
public TriangleButton ApplyButton = null!;
|
||||
|
||||
private OsuEnumDropdown<StartMode> startModeDropdown;
|
||||
private OsuSpriteText typeLabel;
|
||||
private LoadingLayer loadingLayer;
|
||||
public OsuSpriteText ErrorText = null!;
|
||||
|
||||
private OsuEnumDropdown<StartMode> startModeDropdown = null!;
|
||||
private OsuSpriteText typeLabel = null!;
|
||||
private LoadingLayer loadingLayer = null!;
|
||||
|
||||
public void SelectBeatmap()
|
||||
{
|
||||
@ -78,26 +79,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerMatchSubScreen matchSubScreen { get; set; }
|
||||
private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IRoomManager manager { get; set; }
|
||||
private IRoomManager manager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; }
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<bool> operationInProgress = new BindableBool();
|
||||
|
||||
[CanBeNull]
|
||||
private IDisposable applyingSettingsOperation;
|
||||
|
||||
private readonly Room room;
|
||||
|
||||
private Drawable playlistContainer;
|
||||
private DrawableRoomPlaylist drawablePlaylist;
|
||||
private IDisposable? applyingSettingsOperation;
|
||||
private Drawable playlistContainer = null!;
|
||||
private DrawableRoomPlaylist drawablePlaylist = null!;
|
||||
|
||||
public MatchSettings(Room room)
|
||||
{
|
||||
@ -249,6 +247,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
LengthLimit = 255,
|
||||
},
|
||||
},
|
||||
new Section("Other")
|
||||
{
|
||||
Child = AutoSkipCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "Automatically skip the beatmap intro"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -343,6 +348,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true);
|
||||
QueueMode.BindValueChanged(mode => QueueModeDropdown.Current.Value = mode.NewValue, true);
|
||||
AutoStartDuration.BindValueChanged(duration => startModeDropdown.Current.Value = (StartMode)(int)duration.NewValue.TotalSeconds, true);
|
||||
AutoSkip.BindValueChanged(autoSkip => AutoSkipCheckbox.Current.Value = autoSkip.NewValue, true);
|
||||
|
||||
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
|
||||
operationInProgress.BindValueChanged(v =>
|
||||
@ -390,7 +396,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
password: PasswordTextBox.Text,
|
||||
matchType: TypePicker.Current.Value,
|
||||
queueMode: QueueModeDropdown.Current.Value,
|
||||
autoStartDuration: autoStartDuration)
|
||||
autoStartDuration: autoStartDuration,
|
||||
autoSkip: AutoSkipCheckbox.Current.Value)
|
||||
.ContinueWith(t => Schedule(() =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
@ -406,19 +413,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
room.Password.Value = PasswordTextBox.Current.Value;
|
||||
room.QueueMode.Value = QueueModeDropdown.Current.Value;
|
||||
room.AutoStartDuration.Value = autoStartDuration;
|
||||
room.AutoSkip.Value = AutoSkipCheckbox.Current.Value;
|
||||
|
||||
if (int.TryParse(MaxParticipantsField.Text, out int max))
|
||||
room.MaxParticipants.Value = max;
|
||||
else
|
||||
room.MaxParticipants.Value = null;
|
||||
|
||||
manager?.CreateRoom(room, onSuccess, onError);
|
||||
manager.CreateRoom(room, onSuccess, onError);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideError() => ErrorText.FadeOut(50);
|
||||
|
||||
private void onSuccess(Room room)
|
||||
private void onSuccess(Room room) => Schedule(() =>
|
||||
{
|
||||
Debug.Assert(applyingSettingsOperation != null);
|
||||
|
||||
@ -426,9 +434,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
applyingSettingsOperation.Dispose();
|
||||
applyingSettingsOperation = null;
|
||||
}
|
||||
});
|
||||
|
||||
private void onError(string text)
|
||||
private void onError(string text) => Schedule(() =>
|
||||
{
|
||||
Debug.Assert(applyingSettingsOperation != null);
|
||||
|
||||
@ -449,13 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
applyingSettingsOperation.Dispose();
|
||||
applyingSettingsOperation = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public class CreateOrUpdateButton : TriangleButton
|
||||
{
|
||||
[Resolved(typeof(Room), nameof(Room.RoomID))]
|
||||
private Bindable<long?> roomId { get; set; }
|
||||
private Bindable<long?> roomId { get; set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
@ -57,23 +57,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
MultiplayerCountdown newCountdown;
|
||||
|
||||
switch (room?.Countdown)
|
||||
{
|
||||
case MatchStartCountdown:
|
||||
newCountdown = room.Countdown;
|
||||
break;
|
||||
|
||||
// Clear the countdown with any other (including non-null) countdown values.
|
||||
default:
|
||||
newCountdown = null;
|
||||
break;
|
||||
}
|
||||
MultiplayerCountdown newCountdown = room?.ActiveCountdowns.SingleOrDefault(c => c is MatchStartCountdown);
|
||||
|
||||
if (newCountdown != countdown)
|
||||
{
|
||||
countdown = room?.Countdown;
|
||||
countdown = newCountdown;
|
||||
countdownChangeTime = Time.Current;
|
||||
}
|
||||
|
||||
@ -213,7 +201,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
if (room?.Host?.Equals(localUser) == true && room.Countdown == null)
|
||||
if (room?.Host?.Equals(localUser) == true && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
setGreen();
|
||||
else
|
||||
setYellow();
|
||||
@ -248,8 +236,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
get
|
||||
{
|
||||
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && !room.Settings.AutoStartEnabled)
|
||||
if (room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true
|
||||
&& multiplayerClient.IsHost
|
||||
&& multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready
|
||||
&& !room.Settings.AutoStartEnabled)
|
||||
{
|
||||
return "Cancel countdown";
|
||||
}
|
||||
|
||||
return base.TooltipText;
|
||||
}
|
||||
|
@ -78,9 +78,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
return;
|
||||
|
||||
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
|
||||
bool isValidItem = isItemOwner && !Item.Expired;
|
||||
|
||||
AllowDeletion = isItemOwner && !Item.Expired && Item.ID != multiplayerClient.Room.Settings.PlaylistItemId;
|
||||
AllowEditing = isItemOwner && !Item.Expired;
|
||||
AllowDeletion = isValidItem
|
||||
&& (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check.
|
||||
|| multiplayerClient.Room.Playlist.Count(i => !i.Expired) > 1);
|
||||
|
||||
AllowEditing = isValidItem;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
roomAccessTypeDropdown.Current.BindValueChanged(_ => UpdateFilter());
|
||||
|
||||
return base.CreateFilterControls().Prepend(roomAccessTypeDropdown);
|
||||
return base.CreateFilterControls().Append(roomAccessTypeDropdown);
|
||||
}
|
||||
|
||||
protected override FilterCriteria CreateFilterCriteria()
|
||||
|
@ -8,11 +8,9 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
@ -27,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private OngoingOperationTracker operationTracker { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
|
||||
private readonly long? itemToEdit;
|
||||
private readonly PlaylistItem? itemToEdit;
|
||||
|
||||
private LoadingLayer loadingLayer = null!;
|
||||
private IDisposable? selectionOperation;
|
||||
@ -37,21 +35,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
/// </summary>
|
||||
/// <param name="room">The room.</param>
|
||||
/// <param name="itemToEdit">The item to be edited. May be null, in which case a new item will be added to the playlist.</param>
|
||||
/// <param name="beatmap">An optional initial beatmap selection to perform.</param>
|
||||
/// <param name="ruleset">An optional initial ruleset selection to perform.</param>
|
||||
public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap? beatmap = null, RulesetInfo? ruleset = null)
|
||||
: base(room)
|
||||
public MultiplayerMatchSongSelect(Room room, PlaylistItem? itemToEdit = null)
|
||||
: base(room, itemToEdit)
|
||||
{
|
||||
this.itemToEdit = itemToEdit;
|
||||
|
||||
if (beatmap != null || ruleset != null)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (beatmap != null) Beatmap.Value = beatmap;
|
||||
if (ruleset != null) Ruleset.Value = ruleset;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -80,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
if (operationInProgress.Value)
|
||||
{
|
||||
Logger.Log($"{nameof(SelectedItem)} aborted due to {nameof(operationInProgress)}");
|
||||
Logger.Log($"{nameof(SelectItem)} aborted due to {nameof(operationInProgress)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -92,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
var multiplayerItem = new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = itemToEdit ?? 0,
|
||||
ID = itemToEdit?.ID ?? 0,
|
||||
BeatmapID = item.Beatmap.OnlineID,
|
||||
BeatmapChecksum = item.Beatmap.MD5Hash,
|
||||
RulesetID = item.RulesetID,
|
||||
|
@ -49,11 +49,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
private readonly IBindable<bool> isConnected = new Bindable<bool>();
|
||||
|
||||
private AddItemButton addItemButton;
|
||||
|
||||
public MultiplayerMatchSubScreen(Room room)
|
||||
@ -227,12 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
int id = itemToEdit?.Beatmap.OnlineID ?? Room.Playlist.Last().Beatmap.OnlineID;
|
||||
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id);
|
||||
|
||||
var workingBeatmap = localBeatmap == null ? null : beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
|
||||
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit?.ID, workingBeatmap));
|
||||
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
|
||||
}
|
||||
|
||||
protected override Drawable CreateFooter() => new MultiplayerMatchFooter();
|
||||
@ -424,7 +414,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
return;
|
||||
}
|
||||
|
||||
this.Push(new MultiplayerMatchSongSelect(Room, client.Room.Settings.PlaylistItemId, beatmap, ruleset));
|
||||
this.Push(new MultiplayerMatchSongSelect(Room, Room.Playlist.Single(item => item.ID == client.Room.Settings.PlaylistItemId)));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -9,8 +9,6 @@ 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.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -21,7 +19,6 @@ using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
@ -41,14 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
|
||||
|
||||
private MultiplayerGameplayLeaderboard leaderboard;
|
||||
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
private readonly Bindable<bool> leaderboardExpanded = new BindableBool();
|
||||
|
||||
private LoadingLayer loadingDisplay;
|
||||
private FillFlowContainer leaderboardFlow;
|
||||
|
||||
private MultiplayerGameplayLeaderboard multiplayerLeaderboard;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a multiplayer player.
|
||||
@ -61,7 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
AllowPause = false,
|
||||
AllowRestart = false,
|
||||
AllowSkipping = false,
|
||||
AllowSkipping = room.AutoSkip.Value,
|
||||
AutomaticallySkipIntro = room.AutoSkip.Value,
|
||||
AlwaysShowLeaderboard = true,
|
||||
})
|
||||
{
|
||||
this.users = users;
|
||||
@ -73,45 +69,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5)
|
||||
});
|
||||
|
||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
|
||||
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
leaderboard.Expanded.BindTo(leaderboardExpanded);
|
||||
|
||||
leaderboardFlow.Insert(0, l);
|
||||
|
||||
if (leaderboard.TeamScores.Count >= 2)
|
||||
{
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
}, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay));
|
||||
}
|
||||
});
|
||||
|
||||
LoadComponentAsync(new GameplayChatDisplay(Room)
|
||||
{
|
||||
Expanded = { BindTarget = leaderboardExpanded },
|
||||
}, chat => leaderboardFlow.Insert(2, chat));
|
||||
Expanded = { BindTarget = LeaderboardExpandedState },
|
||||
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
||||
|
||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users);
|
||||
|
||||
protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard)
|
||||
{
|
||||
Debug.Assert(leaderboard == multiplayerLeaderboard);
|
||||
|
||||
HUDOverlay.LeaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
if (multiplayerLeaderboard.TeamScores.Count >= 2)
|
||||
{
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
@ -166,9 +150,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLeaderboardExpandedState() =>
|
||||
leaderboardExpanded.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
||||
|
||||
private void failAndBail(string message = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
@ -177,23 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Schedule(() => PerformExit(false));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
adjustLeaderboardPosition();
|
||||
}
|
||||
|
||||
private void adjustLeaderboardPosition()
|
||||
{
|
||||
const float padding = 44; // enough margin to avoid the hit error display.
|
||||
|
||||
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
|
||||
}
|
||||
|
||||
private void onGameplayStarted() => Scheduler.Add(() =>
|
||||
{
|
||||
if (!this.IsCurrentScreen())
|
||||
@ -231,8 +195,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
Debug.Assert(Room.RoomID.Value != null);
|
||||
|
||||
return leaderboard.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, leaderboard.TeamScores)
|
||||
return multiplayerLeaderboard.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
|
||||
: new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
|
||||
}
|
||||
|
||||
|
@ -110,6 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (Client != null)
|
||||
{
|
||||
Client.RoomUpdated -= invokeOnRoomUpdated;
|
||||
Client.LoadRequested -= invokeOnRoomLoadRequested;
|
||||
Client.UserLeft -= invokeUserLeft;
|
||||
Client.UserKicked -= invokeUserKicked;
|
||||
Client.UserJoined -= invokeUserJoined;
|
||||
|
@ -1,10 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -25,9 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
private const double fade_time = 50;
|
||||
|
||||
private SpriteIcon icon;
|
||||
private OsuSpriteText text;
|
||||
private ProgressBar progressBar;
|
||||
private SpriteIcon icon = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
private ProgressBar progressBar = null!;
|
||||
|
||||
public StateDisplay()
|
||||
{
|
||||
@ -86,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
};
|
||||
}
|
||||
|
||||
private OsuColour colours;
|
||||
private OsuColour colours = null!;
|
||||
|
||||
public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
|
||||
{
|
||||
@ -164,10 +161,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
break;
|
||||
|
||||
case DownloadState.Downloading:
|
||||
Debug.Assert(availability.DownloadProgress != null);
|
||||
|
||||
progressBar.FadeIn(fade_time);
|
||||
progressBar.CurrentTime = availability.DownloadProgress.Value;
|
||||
progressBar.CurrentTime = availability.DownloadProgress ?? 0;
|
||||
|
||||
text.Text = "downloading map";
|
||||
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
|
@ -1,7 +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 System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -13,6 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public class MultiSpectatorPlayer : SpectatorPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// All adjustments applied to the clock of this <see cref="MultiSpectatorPlayer"/> which come from mods.
|
||||
/// </summary>
|
||||
public IAggregateAudioAdjustment ClockAdjustmentsFromMods => clockAdjustmentsFromMods;
|
||||
|
||||
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
|
||||
private readonly SpectatorPlayerClock spectatorPlayerClock;
|
||||
|
||||
/// <summary>
|
||||
@ -27,8 +35,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(CancellationToken cancellationToken)
|
||||
{
|
||||
// HUD overlay may not be loaded if load has been cancelled early.
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
HUDOverlay.PlayerSettingsOverlay.Expire();
|
||||
HUDOverlay.HoldToQuit.Expire();
|
||||
}
|
||||
@ -53,6 +65,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
|
||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||
=> new GameplayClockContainer(spectatorPlayerClock);
|
||||
{
|
||||
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock);
|
||||
clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods);
|
||||
return gameplayClockContainer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -43,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
private IAggregateAudioAdjustment? boundAdjustments;
|
||||
|
||||
private readonly PlayerArea[] instances;
|
||||
private MasterGameplayClockContainer masterClockContainer = null!;
|
||||
private SpectatorSyncManager syncManager = null!;
|
||||
@ -132,7 +135,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}, _ =>
|
||||
{
|
||||
foreach (var instance in instances)
|
||||
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
|
||||
leaderboard.AddClock(instance.UserId, instance.SpectatorPlayerClock);
|
||||
|
||||
leaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
@ -157,23 +160,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
base.LoadComplete();
|
||||
|
||||
masterClockContainer.Reset();
|
||||
|
||||
// Start with adjustments from the first player to keep a sane state.
|
||||
bindAudioAdjustments(instances.First());
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
|
||||
if (!isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock))
|
||||
{
|
||||
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock))
|
||||
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime))
|
||||
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock))
|
||||
.OrderBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime))
|
||||
.FirstOrDefault();
|
||||
|
||||
// Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio.
|
||||
if (currentAudioSource != null)
|
||||
bindAudioAdjustments(currentAudioSource);
|
||||
|
||||
foreach (var instance in instances)
|
||||
instance.Mute = instance != currentAudioSource;
|
||||
}
|
||||
}
|
||||
|
||||
private void bindAudioAdjustments(PlayerArea first)
|
||||
{
|
||||
if (boundAdjustments != null)
|
||||
masterClockContainer.AdjustmentsFromMods.UnbindAdjustments(boundAdjustments);
|
||||
|
||||
boundAdjustments = first.ClockAdjustmentsFromMods;
|
||||
masterClockContainer.AdjustmentsFromMods.BindAdjustments(boundAdjustments);
|
||||
}
|
||||
|
||||
private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
|
||||
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;
|
||||
|
||||
@ -187,8 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
.DefaultIfEmpty(0)
|
||||
.Min();
|
||||
|
||||
masterClockContainer.StartTime = startTime;
|
||||
masterClockContainer.Reset(true);
|
||||
masterClockContainer.Reset(startTime, true);
|
||||
}
|
||||
|
||||
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
||||
@ -198,25 +216,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
|
||||
=> instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score);
|
||||
|
||||
protected override void EndGameplay(int userId, SpectatorState state)
|
||||
protected override void QuitGameplay(int userId)
|
||||
{
|
||||
// Allowed passed/failed users to complete their remaining replay frames.
|
||||
// The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used.
|
||||
if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed)
|
||||
return;
|
||||
|
||||
// we could also potentially receive EndGameplay with "Playing" state, at which point we can only early-return and hope it's a passing player.
|
||||
// todo: this shouldn't exist, but it's here as a hotfix for an issue with multi-spectator screen not proceeding to results screen.
|
||||
// see: https://github.com/ppy/osu/issues/19593
|
||||
if (state.State == SpectatedUserState.Playing)
|
||||
return;
|
||||
|
||||
RemoveUser(userId);
|
||||
|
||||
var instance = instances.Single(i => i.UserId == userId);
|
||||
|
||||
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
|
||||
syncManager.RemoveManagedClock(instance.GameplayClock);
|
||||
syncManager.RemoveManagedClock(instance.SpectatorPlayerClock);
|
||||
}
|
||||
|
||||
public override bool OnBackButton()
|
||||
|
@ -38,9 +38,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public readonly int UserId;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||
/// The <see cref="Spectate.SpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||
/// </summary>
|
||||
public readonly SpectatorPlayerClock GameplayClock;
|
||||
public readonly SpectatorPlayerClock SpectatorPlayerClock;
|
||||
|
||||
/// <summary>
|
||||
/// The clock adjustments applied by the <see cref="Player"/> loaded in this area.
|
||||
/// </summary>
|
||||
public IAggregateAudioAdjustment ClockAdjustmentsFromMods => clockAdjustmentsFromMods;
|
||||
|
||||
/// <summary>
|
||||
/// The currently-loaded score.
|
||||
@ -50,6 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
|
||||
private readonly BindableDouble volumeAdjustment = new BindableDouble();
|
||||
private readonly Container gameplayContent;
|
||||
private readonly LoadingLayer loadingLayer;
|
||||
@ -58,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public PlayerArea(int userId, SpectatorPlayerClock clock)
|
||||
{
|
||||
UserId = userId;
|
||||
GameplayClock = clock;
|
||||
SpectatorPlayerClock = clock;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
@ -95,8 +101,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
stack.Push(new MultiSpectatorPlayerLoader(Score, () =>
|
||||
{
|
||||
var player = new MultiSpectatorPlayer(Score, GameplayClock);
|
||||
var player = new MultiSpectatorPlayer(Score, SpectatorPlayerClock);
|
||||
player.OnGameplayStarted += () => OnGameplayStarted?.Invoke();
|
||||
|
||||
clockAdjustmentsFromMods.BindAdjustments(player.ClockAdjustmentsFromMods);
|
||||
|
||||
return player;
|
||||
}));
|
||||
|
||||
|
@ -77,7 +77,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
if (IsRunning)
|
||||
{
|
||||
double elapsedSource = masterClock.ElapsedFrameTime;
|
||||
// When in catch-up mode, the source is usually not running.
|
||||
// In such a case, its elapsed time may be zero, which would cause catch-up to get stuck.
|
||||
// To avoid this, use a constant 16ms elapsed time for now. Probably not too correct, but this whole logic isn't too correct anyway.
|
||||
// Clamping is required to ensure that player clocks don't get too far ahead if ProcessFrame is run multiple times.
|
||||
double elapsedSource = masterClock.ElapsedFrameTime != 0 ? masterClock.ElapsedFrameTime : Math.Clamp(masterClock.CurrentTime - CurrentTime, 0, 16);
|
||||
double elapsed = elapsedSource * Rate;
|
||||
|
||||
CurrentTime += elapsed;
|
||||
|
@ -86,6 +86,9 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
[Resolved(typeof(Room))]
|
||||
protected Bindable<TimeSpan> AutoStartDuration { get; private set; }
|
||||
|
||||
[Resolved(typeof(Room))]
|
||||
protected Bindable<bool> AutoSkip { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }
|
||||
|
||||
|
@ -105,7 +105,8 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
while (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
}
|
||||
else
|
||||
// Also handle the case where a child screen is current (ie. gameplay).
|
||||
else if (this.GetChildScreen() != null)
|
||||
{
|
||||
this.MakeCurrent();
|
||||
Schedule(forcefullyExit);
|
||||
|
@ -1,13 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -35,32 +33,34 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
public override bool AllowEditing => false;
|
||||
|
||||
[Resolved(typeof(Room), nameof(Room.Playlist))]
|
||||
protected BindableList<PlaylistItem> Playlist { get; private set; }
|
||||
|
||||
[CanBeNull]
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected IBindable<PlaylistItem> SelectedItem { get; private set; }
|
||||
protected BindableList<PlaylistItem> Playlist { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
|
||||
|
||||
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
private readonly Room room;
|
||||
|
||||
private WorkingBeatmap initialBeatmap;
|
||||
private RulesetInfo initialRuleset;
|
||||
private IReadOnlyList<Mod> initialMods;
|
||||
private bool itemSelected;
|
||||
|
||||
private readonly PlaylistItem? initialItem;
|
||||
private readonly FreeModSelectOverlay freeModSelectOverlay;
|
||||
private IDisposable freeModSelectOverlayRegistration;
|
||||
|
||||
protected OnlinePlaySongSelect(Room room)
|
||||
private IDisposable? freeModSelectOverlayRegistration;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="OnlinePlaySongSelect"/>.
|
||||
/// </summary>
|
||||
/// <param name="room">The room.</param>
|
||||
/// <param name="initialItem">An optional initial <see cref="PlaylistItem"/> to use for the initial beatmap/ruleset/mods.
|
||||
/// If <c>null</c>, the last <see cref="PlaylistItem"/> in the room will be used.</param>
|
||||
protected OnlinePlaySongSelect(Room room, PlaylistItem? initialItem = null)
|
||||
{
|
||||
this.room = room;
|
||||
this.initialItem = initialItem ?? room.Playlist.LastOrDefault();
|
||||
|
||||
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
|
||||
|
||||
@ -75,11 +75,6 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
private void load()
|
||||
{
|
||||
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
|
||||
|
||||
initialBeatmap = Beatmap.Value;
|
||||
initialRuleset = Ruleset.Value;
|
||||
initialMods = Mods.Value.ToList();
|
||||
|
||||
LoadComponent(freeModSelectOverlay);
|
||||
}
|
||||
|
||||
@ -87,14 +82,35 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
|
||||
|
||||
if (rulesetInstance != null)
|
||||
if (initialItem != null)
|
||||
{
|
||||
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
|
||||
// Similarly, freeMods is currently empty but should only contain the allowed mods.
|
||||
Mods.Value = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
// Prefer using a local databased beatmap lookup since OnlineId may be -1 for an invalid beatmap selection.
|
||||
BeatmapInfo? beatmapInfo = initialItem.Beatmap as BeatmapInfo;
|
||||
|
||||
// And in the case that this isn't a local databased beatmap, query by online ID.
|
||||
if (beatmapInfo == null)
|
||||
{
|
||||
int onlineId = initialItem.Beatmap.OnlineID;
|
||||
beatmapInfo = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId);
|
||||
}
|
||||
|
||||
if (beatmapInfo != null)
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
|
||||
RulesetInfo? ruleset = rulesets.GetRuleset(initialItem.RulesetID);
|
||||
|
||||
if (ruleset != null)
|
||||
{
|
||||
Ruleset.Value = ruleset;
|
||||
|
||||
var rulesetInstance = ruleset.CreateInstance();
|
||||
Debug.Assert(rulesetInstance != null);
|
||||
|
||||
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
|
||||
// Similarly, freeMods is currently empty but should only contain the allowed mods.
|
||||
Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
Mods.BindValueChanged(onModsChanged);
|
||||
@ -125,13 +141,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
|
||||
};
|
||||
|
||||
if (SelectItem(item))
|
||||
{
|
||||
itemSelected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return SelectItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -154,15 +164,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (!itemSelected)
|
||||
{
|
||||
Beatmap.Value = initialBeatmap;
|
||||
Ruleset.Value = initialRuleset;
|
||||
Mods.Value = initialMods;
|
||||
}
|
||||
|
||||
freeModSelectOverlay.Hide();
|
||||
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
|
||||
@ -199,7 +201,6 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
freeModSelectOverlayRegistration?.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
|
||||
|
||||
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo));
|
||||
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -180,31 +180,26 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
/// <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)
|
||||
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() =>
|
||||
{
|
||||
var scoreInfos = scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).ToArray();
|
||||
var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
|
||||
|
||||
// Score panels calculate total score before displaying, which can take some time. In order to count that calculation as part of the loading spinner display duration,
|
||||
// calculate the total scores locally before invoking the success callback.
|
||||
scoreManager.OrderByTotalScoreAsync(scoreInfos).ContinueWith(_ => Schedule(() =>
|
||||
// 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)
|
||||
{
|
||||
// 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(() =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
// Prefer selecting the local user's score, or otherwise default to the first visible score.
|
||||
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
// Prefer selecting the local user's score, or otherwise default to the first visible score.
|
||||
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == 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.OnlineID != Score?.OnlineID));
|
||||
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
|
||||
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
|
||||
|
||||
hideLoadingSpinners(pivot);
|
||||
}));
|
||||
}
|
||||
hideLoadingSpinners(pivot);
|
||||
});
|
||||
|
||||
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
|
||||
{
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.API;
|
||||
|
@ -40,11 +40,10 @@ namespace osu.Game.Screens
|
||||
|
||||
public virtual bool AllowExternalScreenChange => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether all overlays should be hidden when this screen is entered or resumed.
|
||||
/// </summary>
|
||||
public virtual bool HideOverlaysOnEnter => false;
|
||||
|
||||
public virtual bool HideMenuCursorOnNonMouseInput => false;
|
||||
|
||||
/// <summary>
|
||||
/// The initial overlay activation mode to use when this screen is entered for the first time.
|
||||
/// </summary>
|
||||
|
@ -1,14 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using ManagedBass.Fx;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -34,27 +31,27 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public class FailAnimation : Container
|
||||
{
|
||||
public Action OnComplete;
|
||||
public Action? OnComplete;
|
||||
|
||||
private readonly DrawableRuleset drawableRuleset;
|
||||
private readonly BindableDouble trackFreq = new BindableDouble(1);
|
||||
private readonly BindableDouble volumeAdjustment = new BindableDouble(0.5);
|
||||
|
||||
private Container filters;
|
||||
private Container filters = null!;
|
||||
|
||||
private Box redFlashLayer;
|
||||
private Box redFlashLayer = null!;
|
||||
|
||||
private Track track;
|
||||
private Track track = null!;
|
||||
|
||||
private AudioFilter failLowPassFilter;
|
||||
private AudioFilter failHighPassFilter;
|
||||
private AudioFilter failLowPassFilter = null!;
|
||||
private AudioFilter failHighPassFilter = null!;
|
||||
|
||||
private const float duration = 2500;
|
||||
|
||||
private Sample failSample;
|
||||
private Sample? failSample;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container
|
||||
{
|
||||
@ -66,8 +63,7 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// The player screen background, used to adjust appearance on failing.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public BackgroundScreen Background { private get; set; }
|
||||
public BackgroundScreen? Background { private get; set; }
|
||||
|
||||
public FailAnimation(DrawableRuleset drawableRuleset)
|
||||
{
|
||||
@ -105,6 +101,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
private bool started;
|
||||
private bool filtersRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Start the fail animation playing.
|
||||
@ -113,6 +110,7 @@ namespace osu.Game.Screens.Play
|
||||
public void Start()
|
||||
{
|
||||
if (started) throw new InvalidOperationException("Animation cannot be started more than once.");
|
||||
if (filtersRemoved) throw new InvalidOperationException("Animation cannot be started after filters have been removed.");
|
||||
|
||||
started = true;
|
||||
|
||||
@ -125,7 +123,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
failHighPassFilter.CutoffTo(300);
|
||||
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
|
||||
failSample.Play();
|
||||
failSample?.Play();
|
||||
|
||||
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
@ -155,16 +153,20 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public void RemoveFilters(bool resetTrackFrequency = true)
|
||||
{
|
||||
if (resetTrackFrequency)
|
||||
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
filtersRemoved = true;
|
||||
|
||||
track?.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
if (!started)
|
||||
return;
|
||||
|
||||
if (resetTrackFrequency)
|
||||
track.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
|
||||
track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
|
||||
if (filters.Parent == null)
|
||||
return;
|
||||
|
||||
RemoveInternal(filters);
|
||||
filters.Dispose();
|
||||
RemoveInternal(filters, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -2,21 +2,21 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates gameplay timing logic and provides a <see cref="IGameplayClock"/> via DI for gameplay components to use.
|
||||
/// </summary>
|
||||
[Cached(typeof(IGameplayClock))]
|
||||
public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock
|
||||
{
|
||||
/// <summary>
|
||||
@ -36,72 +36,83 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
/// <summary>
|
||||
/// The time from which the clock should start. Will be seeked to on calling <see cref="Reset"/>.
|
||||
/// Can be adjusted by calling <see cref="Reset"/> with a time value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If not set, a value of zero will be used.
|
||||
/// Importantly, the value will be inferred from the current ruleset in <see cref="MasterGameplayClockContainer"/> unless specified.
|
||||
/// By default, a value of zero will be used.
|
||||
/// Importantly, the value will be inferred from the current beatmap in <see cref="MasterGameplayClockContainer"/> by default.
|
||||
/// </remarks>
|
||||
public double? StartTime { get; set; }
|
||||
public double StartTime { get; protected set; }
|
||||
|
||||
public virtual IEnumerable<double> NonGameplayAdjustments => Enumerable.Empty<double>();
|
||||
|
||||
/// <summary>
|
||||
/// The final clock which is exposed to gameplay components.
|
||||
/// </summary>
|
||||
protected IFrameBasedClock FramedClock { get; private set; }
|
||||
public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments();
|
||||
|
||||
private readonly BindableBool isPaused = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// The adjustable source clock used for gameplay. Should be used for seeks and clock control.
|
||||
/// This is the final source exposed to gameplay components <see cref="IGameplayClock"/> via delegation in this class.
|
||||
/// </summary>
|
||||
private readonly DecoupleableInterpolatingFramedClock decoupledClock;
|
||||
protected readonly FramedBeatmapClock GameplayClock;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameplayClockContainer"/>.
|
||||
/// </summary>
|
||||
/// <param name="sourceClock">The source <see cref="IClock"/> used for timing.</param>
|
||||
public GameplayClockContainer(IClock sourceClock)
|
||||
/// <param name="applyOffsets">Whether to apply platform, user and beatmap offsets to the mix.</param>
|
||||
public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false)
|
||||
{
|
||||
SourceClock = sourceClock;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
|
||||
IsPaused.BindValueChanged(OnIsPausedChanged);
|
||||
|
||||
// this will be replaced during load, but non-null for tests which don't add this component to the hierarchy.
|
||||
FramedClock = new FramedClock();
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
FramedClock = CreateGameplayClock(decoupledClock);
|
||||
|
||||
dependencies.CacheAs<IGameplayClock>(this);
|
||||
|
||||
return dependencies;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
GameplayClock = new FramedBeatmapClock(applyOffsets) { IsCoupled = false },
|
||||
Content
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts gameplay.
|
||||
/// Starts gameplay and marks un-paused state.
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
public void Start()
|
||||
{
|
||||
ensureSourceClockSet();
|
||||
|
||||
if (!decoupledClock.IsRunning)
|
||||
{
|
||||
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
|
||||
// This accounts for the clock source potentially taking time to enter a completely stopped state
|
||||
Seek(FramedClock.CurrentTime);
|
||||
|
||||
decoupledClock.Start();
|
||||
}
|
||||
if (!isPaused.Value)
|
||||
return;
|
||||
|
||||
isPaused.Value = false;
|
||||
|
||||
ensureSourceClockSet();
|
||||
|
||||
PrepareStart();
|
||||
|
||||
// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
|
||||
// Because we generally update our own current time quicker than children can query it (via Start/Seek/Update),
|
||||
// this means that the first frame ever exposed to children may have a non-zero current time.
|
||||
//
|
||||
// If the child component is not aware of the parent ElapsedFrameTime (which is the case for FrameStabilityContainer)
|
||||
// they will take on the new CurrentTime with a zero elapsed time. This can in turn cause components to behave incorrectly
|
||||
// if they are intending to trigger events at the precise StartTime (ie. DrawableStoryboardSample).
|
||||
//
|
||||
// By scheduling the start call, children are guaranteed to receive one frame at the original start time, allowing
|
||||
// then to progress with a correct locally calculated elapsed time.
|
||||
SchedulerAfterChildren.Add(() =>
|
||||
{
|
||||
if (isPaused.Value)
|
||||
return;
|
||||
|
||||
StartGameplayClock();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="Start"/> is called, this will be run to give an opportunity to prepare the clock at the correct
|
||||
/// start location.
|
||||
/// </summary>
|
||||
protected virtual void PrepareStart()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -112,43 +123,56 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}");
|
||||
|
||||
decoupledClock.Seek(time);
|
||||
|
||||
// Manually process to make sure the gameplay clock is correctly updated after a seek.
|
||||
FramedClock.ProcessFrame();
|
||||
GameplayClock.Seek(time);
|
||||
|
||||
OnSeek?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops gameplay.
|
||||
/// Stops gameplay and marks paused state.
|
||||
/// </summary>
|
||||
public void Stop() => isPaused.Value = true;
|
||||
public void Stop()
|
||||
{
|
||||
if (isPaused.Value)
|
||||
return;
|
||||
|
||||
isPaused.Value = true;
|
||||
StopGameplayClock();
|
||||
}
|
||||
|
||||
protected virtual void StartGameplayClock() => GameplayClock.Start();
|
||||
protected virtual void StopGameplayClock() => GameplayClock.Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to seek to on resetting. If <c>null</c>, the existing <see cref="StartTime"/> will be used.</param>
|
||||
/// <param name="startClock">Whether to start the clock immediately, if not already started.</param>
|
||||
public void Reset(bool startClock = false)
|
||||
public void Reset(double? time = null, bool startClock = false)
|
||||
{
|
||||
// Manually stop the source in order to not affect the IsPaused state.
|
||||
decoupledClock.Stop();
|
||||
bool wasPaused = isPaused.Value;
|
||||
|
||||
if (!IsPaused.Value || startClock)
|
||||
Start();
|
||||
Stop();
|
||||
|
||||
ensureSourceClockSet();
|
||||
Seek(StartTime ?? 0);
|
||||
|
||||
if (time != null)
|
||||
StartTime = time.Value;
|
||||
|
||||
Seek(StartTime);
|
||||
|
||||
if (!wasPaused || startClock)
|
||||
Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the source clock.
|
||||
/// </summary>
|
||||
/// <param name="sourceClock">The new source.</param>
|
||||
protected void ChangeSource(IClock sourceClock) => decoupledClock.ChangeSource(SourceClock = sourceClock);
|
||||
protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the <see cref="decoupledClock"/> is set to <see cref="SourceClock"/>, if it hasn't been given a source yet.
|
||||
/// Ensures that the <see cref="GameplayClock"/> is set to <see cref="SourceClock"/>, if it hasn't been given a source yet.
|
||||
/// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode,
|
||||
/// but not the actual source clock.
|
||||
/// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor,
|
||||
@ -156,40 +180,10 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
private void ensureSourceClockSet()
|
||||
{
|
||||
if (decoupledClock.Source == null)
|
||||
if (GameplayClock.Source == null)
|
||||
ChangeSource(SourceClock);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (!IsPaused.Value)
|
||||
FramedClock.ProcessFrame();
|
||||
|
||||
base.Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the value of <see cref="IsPaused"/> is changed to start or stop the <see cref="decoupledClock"/> clock.
|
||||
/// </summary>
|
||||
/// <param name="isPaused">Whether the clock should now be paused.</param>
|
||||
protected virtual void OnIsPausedChanged(ValueChangedEvent<bool> isPaused)
|
||||
{
|
||||
if (isPaused.NewValue)
|
||||
decoupledClock.Stop();
|
||||
else
|
||||
decoupledClock.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the final <see cref="FramedClock"/> which is exposed via DI to be used by gameplay components.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Any intermediate clocks such as platform offsets should be applied here.
|
||||
/// </remarks>
|
||||
/// <param name="source">The <see cref="IFrameBasedClock"/> providing the source time.</param>
|
||||
/// <returns>The final <see cref="FramedClock"/>.</returns>
|
||||
protected virtual IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) => source;
|
||||
|
||||
#region IAdjustableClock
|
||||
|
||||
bool IAdjustableClock.Seek(double position)
|
||||
@ -200,19 +194,21 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
void IAdjustableClock.Reset() => Reset();
|
||||
|
||||
public void ResetSpeedAdjustments() => throw new NotImplementedException();
|
||||
public virtual void ResetSpeedAdjustments()
|
||||
{
|
||||
}
|
||||
|
||||
double IAdjustableClock.Rate
|
||||
{
|
||||
get => FramedClock.Rate;
|
||||
get => GameplayClock.Rate;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public double Rate => FramedClock.Rate;
|
||||
public double Rate => GameplayClock.Rate;
|
||||
|
||||
public double CurrentTime => FramedClock.CurrentTime;
|
||||
public double CurrentTime => GameplayClock.CurrentTime;
|
||||
|
||||
public bool IsRunning => FramedClock.IsRunning;
|
||||
public bool IsRunning => GameplayClock.IsRunning;
|
||||
|
||||
#endregion
|
||||
|
||||
@ -221,28 +217,10 @@ namespace osu.Game.Screens.Play
|
||||
// Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times.
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime => FramedClock.ElapsedFrameTime;
|
||||
public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime;
|
||||
|
||||
public double FramesPerSecond => FramedClock.FramesPerSecond;
|
||||
public double FramesPerSecond => GameplayClock.FramesPerSecond;
|
||||
|
||||
public FrameTimeInfo TimeInfo => FramedClock.TimeInfo;
|
||||
|
||||
public double TrueGameplayRate
|
||||
{
|
||||
get
|
||||
{
|
||||
double baseRate = Rate;
|
||||
|
||||
foreach (double adjustment in NonGameplayAdjustments)
|
||||
{
|
||||
if (Precision.AlmostEquals(adjustment, 0))
|
||||
return 0;
|
||||
|
||||
baseRate /= adjustment;
|
||||
}
|
||||
|
||||
return baseRate;
|
||||
}
|
||||
}
|
||||
public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo;
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Screens/Play/GameplayClockExtensions.cs
Normal file
24
osu.Game/Screens/Play/GameplayClockExtensions.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public static class GameplayClockExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The rate of gameplay when playback is at 100%.
|
||||
/// This excludes any seeking / user adjustments.
|
||||
/// </summary>
|
||||
public static double GetTrueGameplayRate(this IGameplayClock clock)
|
||||
{
|
||||
// To handle rewind, we still want to maintain the same direction as the underlying clock.
|
||||
double rate = clock.Rate == 0 ? 1 : Math.Sign(clock.Rate);
|
||||
|
||||
return rate
|
||||
* clock.AdjustmentsFromMods.AggregateFrequency.Value
|
||||
* clock.AdjustmentsFromMods.AggregateTempo.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
|
||||
{
|
||||
public class ClicksPerSecondCalculator : Component
|
||||
{
|
||||
private readonly List<double> timestamps = new List<double>();
|
||||
|
||||
[Resolved]
|
||||
private IGameplayClock gameplayClock { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableRuleset? drawableRuleset { get; set; }
|
||||
|
||||
public int Value { get; private set; }
|
||||
|
||||
// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset`
|
||||
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
|
||||
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;
|
||||
|
||||
public ClicksPerSecondCalculator()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public void AddInputTimestamp() => timestamps.Add(clock.CurrentTime);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double latestValidTime = clock.CurrentTime;
|
||||
double earliestTimeValid = latestValidTime - 1000 * gameplayClock.GetTrueGameplayRate();
|
||||
|
||||
int count = 0;
|
||||
|
||||
for (int i = timestamps.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// handle rewinding by removing future timestamps as we go
|
||||
if (timestamps[i] > latestValidTime)
|
||||
{
|
||||
timestamps.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestamps[i] >= earliestTimeValid)
|
||||
count++;
|
||||
}
|
||||
|
||||
Value = count;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
// 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.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
|
||||
{
|
||||
public class ClicksPerSecondCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private ClicksPerSecondCalculator calculator { get; set; } = null!;
|
||||
|
||||
protected override double RollingDuration => 350;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public ClicksPerSecondCounter()
|
||||
{
|
||||
Current.Value = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = colours.BlueLighter;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Current.Value = calculator.Value;
|
||||
}
|
||||
|
||||
protected override IHasText CreateText() => new TextComponent();
|
||||
|
||||
private class TextComponent : CompositeDrawable, IHasText
|
||||
{
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => text.Text;
|
||||
set => text.Text = value;
|
||||
}
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
public TextComponent()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Font = OsuFont.Numeric.With(size: 16, fixedWidth: true)
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Numeric.With(size: 6, fixedWidth: false),
|
||||
Text = @"clicks",
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Numeric.With(size: 6, fixedWidth: false),
|
||||
Text = @"/sec",
|
||||
Padding = new MarginPadding { Bottom = 3f }, // align baseline better
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
osu.Game/Screens/Play/HUD/ComboCounter.cs
Normal file
24
osu.Game/Screens/Play/HUD/ComboCounter.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public abstract class ComboCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
protected ComboCounter()
|
||||
{
|
||||
Current.Value = DisplayedCount = 0;
|
||||
}
|
||||
|
||||
protected override double GetProportionalDuration(int currentValue, int newValue)
|
||||
{
|
||||
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class DefaultComboCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
public class DefaultComboCounter : ComboCounter
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public DefaultComboCounter()
|
||||
{
|
||||
Current.Value = DisplayedCount = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, ScoreProcessor scoreProcessor)
|
||||
{
|
||||
@ -31,17 +19,12 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
}
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText()
|
||||
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
|
||||
|
||||
protected override LocalisableString 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));
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class DefaultSongProgress : SongProgress
|
||||
{
|
||||
private const float info_height = 20;
|
||||
private const float bottom_bar_height = 5;
|
||||
private const float graph_height = SquareGraph.Column.WIDTH * 6;
|
||||
private const float handle_height = 18;
|
||||
@ -65,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = info_height,
|
||||
},
|
||||
graph = new SongProgressGraph
|
||||
{
|
||||
@ -178,7 +176,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
Height = bottom_bar_height + graph_height + handle_size.Y + info_height - graph.Y;
|
||||
Height = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y;
|
||||
}
|
||||
|
||||
private void updateBarVisibility()
|
||||
|
@ -1,11 +1,8 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -13,15 +10,14 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class GameplayLeaderboard : CompositeDrawable
|
||||
public abstract class GameplayLeaderboard : CompositeDrawable
|
||||
{
|
||||
private readonly int maxPanels;
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
@ -31,22 +27,22 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private bool requiresScroll;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
private GameplayLeaderboardScore trackedScore;
|
||||
public GameplayLeaderboardScore? TrackedScore { get; private set; }
|
||||
|
||||
private const int max_panels = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
|
||||
public GameplayLeaderboard(int maxPanels = 8)
|
||||
protected GameplayLeaderboard()
|
||||
{
|
||||
this.maxPanels = maxPanels;
|
||||
|
||||
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
scroll = new InputDisabledScrollContainer
|
||||
{
|
||||
ClampExtension = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
|
||||
{
|
||||
@ -77,24 +73,25 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// Whether the player should be tracked on the leaderboard.
|
||||
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
||||
/// </param>
|
||||
public ILeaderboardScore Add([CanBeNull] APIUser user, bool isTracked)
|
||||
public ILeaderboardScore Add(IUser? user, bool isTracked)
|
||||
{
|
||||
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
|
||||
if (isTracked)
|
||||
{
|
||||
if (trackedScore != null)
|
||||
if (TrackedScore != null)
|
||||
throw new InvalidOperationException("Cannot track more than one score.");
|
||||
|
||||
trackedScore = drawable;
|
||||
TrackedScore = drawable;
|
||||
}
|
||||
|
||||
drawable.Expanded.BindTo(Expanded);
|
||||
|
||||
Flow.Add(drawable);
|
||||
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, maxPanels);
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
requiresScroll = displayCount != Flow.Count;
|
||||
|
||||
@ -104,21 +101,22 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public void Clear()
|
||||
{
|
||||
Flow.Clear();
|
||||
trackedScore = null;
|
||||
TrackedScore = null;
|
||||
scroll.ScrollToStart(false);
|
||||
}
|
||||
|
||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) =>
|
||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
|
||||
new GameplayLeaderboardScore(user, isTracked);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (requiresScroll && trackedScore != null)
|
||||
if (requiresScroll && TrackedScore != null)
|
||||
{
|
||||
float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
|
||||
scroll.ScrollTo(scrollTarget, false);
|
||||
float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
|
||||
|
||||
scroll.ScrollTo(scrollTarget);
|
||||
}
|
||||
|
||||
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
@ -126,7 +124,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
float fadeBottom = scroll.Current + scroll.DrawHeight;
|
||||
float fadeTop = scroll.Current + panel_height;
|
||||
|
||||
if (scroll.Current <= 0) fadeTop -= panel_height;
|
||||
if (scroll.IsScrolledToStart()) fadeTop -= panel_height;
|
||||
if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;
|
||||
|
||||
// logic is mostly shared with Leaderboard, copied here for simplicity.
|
||||
@ -165,7 +163,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
|
||||
var orderedByScore = Flow
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.DisplayOrder.Value)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < Flow.Count; i++)
|
||||
{
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private const float rank_text_width = 35f;
|
||||
|
||||
private const float score_components_width = 85f;
|
||||
|
||||
private const float avatar_size = 25f;
|
||||
|
||||
private const double panel_transition_duration = 500;
|
||||
@ -55,6 +54,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public BindableDouble Accuracy { get; } = new BindableDouble(1);
|
||||
public BindableInt Combo { get; } = new BindableInt();
|
||||
public BindableBool HasQuit { get; } = new BindableBool();
|
||||
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
|
||||
|
||||
public Color4? BackgroundColour { get; set; }
|
||||
|
||||
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public APIUser User { get; }
|
||||
public IUser User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this score is the local user or a replay player (and should be focused / always visible).
|
||||
@ -103,7 +103,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// </summary>
|
||||
/// <param name="user">The score's player.</param>
|
||||
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
|
||||
public GameplayLeaderboardScore([CanBeNull] APIUser user, bool tracked)
|
||||
public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
@ -160,7 +160,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
new Dimension(GridSizeMode.Absolute, rank_text_width),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
@ -285,8 +285,19 @@ namespace osu.Game.Screens.Play.HUD
|
||||
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
|
||||
|
||||
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
|
||||
Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true);
|
||||
Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true);
|
||||
|
||||
Accuracy.BindValueChanged(v =>
|
||||
{
|
||||
accuracyText.Text = v.NewValue.FormatAccuracy();
|
||||
updateDetailsWidth();
|
||||
}, true);
|
||||
|
||||
Combo.BindValueChanged(v =>
|
||||
{
|
||||
comboText.Text = $"{v.NewValue}x";
|
||||
updateDetailsWidth();
|
||||
}, true);
|
||||
|
||||
HasQuit.BindValueChanged(_ => updateState());
|
||||
}
|
||||
|
||||
@ -302,13 +313,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void changeExpandedState(ValueChangedEvent<bool> expanded)
|
||||
{
|
||||
scoreComponents.ClearTransforms();
|
||||
|
||||
if (expanded.NewValue)
|
||||
{
|
||||
gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint);
|
||||
|
||||
scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint);
|
||||
scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint);
|
||||
|
||||
usernameText.FadeIn(panel_transition_duration, Easing.OutQuint);
|
||||
@ -317,11 +325,29 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint);
|
||||
|
||||
scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
|
||||
scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint);
|
||||
|
||||
usernameText.FadeOut(text_transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
updateDetailsWidth();
|
||||
}
|
||||
|
||||
private float? scoreComponentsTargetWidth;
|
||||
|
||||
private void updateDetailsWidth()
|
||||
{
|
||||
const float score_components_min_width = 88f;
|
||||
|
||||
float newWidth = Expanded.Value
|
||||
? Math.Max(score_components_min_width, comboText.DrawWidth + accuracyText.DrawWidth + 25)
|
||||
: 0;
|
||||
|
||||
if (scoreComponentsTargetWidth == newWidth)
|
||||
return;
|
||||
|
||||
scoreComponentsTargetWidth = newWidth;
|
||||
scoreComponents.ResizeWidthTo(newWidth, panel_transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
|
@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -44,8 +45,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
public Bindable<LabelStyles> LabelStyle { get; } = new Bindable<LabelStyles>(LabelStyles.Icons);
|
||||
|
||||
private SpriteIcon arrow;
|
||||
private Drawable labelEarly;
|
||||
private Drawable labelLate;
|
||||
private UprightAspectMaintainingContainer labelEarly;
|
||||
private UprightAspectMaintainingContainer labelLate;
|
||||
|
||||
private Container colourBarsEarly;
|
||||
private Container colourBarsLate;
|
||||
@ -122,6 +123,20 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = judgement_line_width,
|
||||
},
|
||||
labelEarly = new UprightAspectMaintainingContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -10,
|
||||
},
|
||||
labelLate = new UprightAspectMaintainingContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = 10,
|
||||
},
|
||||
}
|
||||
},
|
||||
arrowContainer = new Container
|
||||
@ -261,57 +276,39 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
{
|
||||
const float icon_size = 14;
|
||||
|
||||
labelEarly?.Expire();
|
||||
labelEarly = null;
|
||||
|
||||
labelLate?.Expire();
|
||||
labelLate = null;
|
||||
|
||||
switch (style)
|
||||
{
|
||||
case LabelStyles.None:
|
||||
break;
|
||||
|
||||
case LabelStyles.Icons:
|
||||
labelEarly = new SpriteIcon
|
||||
labelEarly.Child = new SpriteIcon
|
||||
{
|
||||
Y = -10,
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.ShippingFast,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
labelLate = new SpriteIcon
|
||||
labelLate.Child = new SpriteIcon
|
||||
{
|
||||
Y = 10,
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.Bicycle,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case LabelStyles.Text:
|
||||
labelEarly = new OsuSpriteText
|
||||
labelEarly.Child = new OsuSpriteText
|
||||
{
|
||||
Y = -10,
|
||||
Text = "Early",
|
||||
Font = OsuFont.Default.With(size: 10),
|
||||
Height = 12,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
labelLate = new OsuSpriteText
|
||||
labelLate.Child = new OsuSpriteText
|
||||
{
|
||||
Y = 10,
|
||||
Text = "Late",
|
||||
Font = OsuFont.Default.With(size: 10),
|
||||
Height = 12,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
break;
|
||||
@ -320,26 +317,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
throw new ArgumentOutOfRangeException(nameof(style), style, null);
|
||||
}
|
||||
|
||||
if (labelEarly != null)
|
||||
{
|
||||
colourBars.Add(labelEarly);
|
||||
labelEarly.FadeInFromZero(500);
|
||||
}
|
||||
|
||||
if (labelLate != null)
|
||||
{
|
||||
colourBars.Add(labelLate);
|
||||
labelLate.FadeInFromZero(500);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// undo any layout rotation to display icons in the correct orientation
|
||||
if (labelEarly != null) labelEarly.Rotation = -Rotation;
|
||||
if (labelLate != null) labelLate.Rotation = -Rotation;
|
||||
labelEarly.FadeInFromZero(500);
|
||||
labelLate.FadeInFromZero(500);
|
||||
}
|
||||
|
||||
private void createColourBars((HitResult result, double length)[] windows)
|
||||
|
@ -1,13 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
@ -17,18 +17,37 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
{
|
||||
public class ColourHitErrorMeter : HitErrorMeter
|
||||
{
|
||||
internal const int MAX_DISPLAYED_JUDGEMENTS = 20;
|
||||
|
||||
private const int animation_duration = 200;
|
||||
private const int drawable_judgement_size = 8;
|
||||
private const int spacing = 2;
|
||||
|
||||
[SettingSource("Judgement count", "The number of displayed judgements")]
|
||||
public BindableNumber<int> JudgementCount { get; } = new BindableNumber<int>(20)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 50,
|
||||
};
|
||||
|
||||
[SettingSource("Judgement spacing", "The space between each displayed judgement")]
|
||||
public BindableNumber<float> JudgementSpacing { get; } = new BindableNumber<float>(2)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
[SettingSource("Judgement shape", "The shape of each displayed judgement")]
|
||||
public Bindable<ShapeStyle> JudgementShape { get; } = new Bindable<ShapeStyle>();
|
||||
|
||||
private readonly JudgementFlow judgementsFlow;
|
||||
|
||||
public ColourHitErrorMeter()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
InternalChild = judgementsFlow = new JudgementFlow();
|
||||
InternalChild = judgementsFlow = new JudgementFlow
|
||||
{
|
||||
JudgementShape = { BindTarget = JudgementShape },
|
||||
JudgementSpacing = { BindTarget = JudgementSpacing },
|
||||
JudgementCount = { BindTarget = JudgementCount }
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnNewJudgement(JudgementResult judgement)
|
||||
@ -41,53 +60,105 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
|
||||
public override void Clear() => judgementsFlow.Clear();
|
||||
|
||||
private class JudgementFlow : FillFlowContainer<HitErrorCircle>
|
||||
private class JudgementFlow : FillFlowContainer<HitErrorShape>
|
||||
{
|
||||
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.Reverse();
|
||||
|
||||
public readonly Bindable<ShapeStyle> JudgementShape = new Bindable<ShapeStyle>();
|
||||
|
||||
public readonly Bindable<float> JudgementSpacing = new Bindable<float>();
|
||||
|
||||
public readonly Bindable<int> JudgementCount = new Bindable<int>();
|
||||
|
||||
public JudgementFlow()
|
||||
{
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = MAX_DISPLAYED_JUDGEMENTS * (drawable_judgement_size + spacing) - spacing;
|
||||
Spacing = new Vector2(0, spacing);
|
||||
Width = drawable_judgement_size;
|
||||
Direction = FillDirection.Vertical;
|
||||
LayoutDuration = animation_duration;
|
||||
LayoutEasing = Easing.OutQuint;
|
||||
}
|
||||
|
||||
public void Push(Color4 colour)
|
||||
{
|
||||
Add(new HitErrorCircle(colour, drawable_judgement_size));
|
||||
|
||||
if (Children.Count > MAX_DISPLAYED_JUDGEMENTS)
|
||||
Children.FirstOrDefault(c => !c.IsRemoved)?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
internal class HitErrorCircle : Container
|
||||
{
|
||||
public bool IsRemoved { get; private set; }
|
||||
|
||||
private readonly Circle circle;
|
||||
|
||||
public HitErrorCircle(Color4 colour, int size)
|
||||
{
|
||||
Size = new Vector2(size);
|
||||
Child = circle = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Colour = colour
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
circle.FadeInFromZero(animation_duration, Easing.OutQuint);
|
||||
circle.MoveToY(-DrawSize.Y);
|
||||
circle.MoveToY(0, animation_duration, Easing.OutQuint);
|
||||
JudgementCount.BindValueChanged(count =>
|
||||
{
|
||||
removeExtraJudgements();
|
||||
updateMetrics();
|
||||
});
|
||||
|
||||
JudgementSpacing.BindValueChanged(_ => updateMetrics(), true);
|
||||
}
|
||||
|
||||
public void Push(Color4 colour)
|
||||
{
|
||||
Add(new HitErrorShape(colour, drawable_judgement_size)
|
||||
{
|
||||
Shape = { BindTarget = JudgementShape },
|
||||
});
|
||||
|
||||
removeExtraJudgements();
|
||||
}
|
||||
|
||||
private void removeExtraJudgements()
|
||||
{
|
||||
var remainingChildren = Children.Where(c => !c.IsRemoved);
|
||||
|
||||
while (remainingChildren.Count() > JudgementCount.Value)
|
||||
remainingChildren.First().Remove();
|
||||
}
|
||||
|
||||
private void updateMetrics()
|
||||
{
|
||||
Height = JudgementCount.Value * (drawable_judgement_size + JudgementSpacing.Value) - JudgementSpacing.Value;
|
||||
Spacing = new Vector2(0, JudgementSpacing.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public class HitErrorShape : Container
|
||||
{
|
||||
public bool IsRemoved { get; private set; }
|
||||
|
||||
public readonly Bindable<ShapeStyle> Shape = new Bindable<ShapeStyle>();
|
||||
|
||||
private readonly Color4 colour;
|
||||
|
||||
private Container content = null!;
|
||||
|
||||
public HitErrorShape(Color4 colour, int size)
|
||||
{
|
||||
this.colour = colour;
|
||||
Size = new Vector2(size);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Child = content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colour
|
||||
};
|
||||
|
||||
Shape.BindValueChanged(shape =>
|
||||
{
|
||||
switch (shape.NewValue)
|
||||
{
|
||||
case ShapeStyle.Circle:
|
||||
content.Child = new Circle { RelativeSizeAxes = Axes.Both };
|
||||
break;
|
||||
|
||||
case ShapeStyle.Square:
|
||||
content.Child = new Box { RelativeSizeAxes = Axes.Both };
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
content.FadeInFromZero(animation_duration, Easing.OutQuint);
|
||||
content.MoveToY(-DrawSize.Y);
|
||||
content.MoveToY(0, animation_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
@ -97,5 +168,11 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
this.FadeOut(animation_duration, Easing.OutQuint).Expire();
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShapeStyle
|
||||
{
|
||||
Circle,
|
||||
Square
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,30 +59,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
|
||||
protected Color4 GetColourForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Miss:
|
||||
return colours.Red;
|
||||
|
||||
case HitResult.Meh:
|
||||
return colours.Yellow;
|
||||
|
||||
case HitResult.Ok:
|
||||
return colours.Green;
|
||||
|
||||
case HitResult.Good:
|
||||
return colours.GreenLight;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.Great:
|
||||
return colours.Blue;
|
||||
|
||||
default:
|
||||
return colours.BlueLight;
|
||||
}
|
||||
return colours.ForHitResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -14,5 +14,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
BindableInt Combo { get; }
|
||||
|
||||
BindableBool HasQuit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
Bindable<long> DisplayOrder { get; }
|
||||
}
|
||||
}
|
||||
|
83
osu.Game/Screens/Play/HUD/LongestComboCounter.cs
Normal file
83
osu.Game/Screens/Play/HUD/LongestComboCounter.cs
Normal file
@ -0,0 +1,83 @@
|
||||
// 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.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class LongestComboCounter : ComboCounter
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, ScoreProcessor scoreProcessor)
|
||||
{
|
||||
Colour = colours.YellowLighter;
|
||||
Current.BindTo(scoreProcessor.HighestCombo);
|
||||
}
|
||||
|
||||
protected override IHasText CreateText() => new TextComponent();
|
||||
|
||||
private class TextComponent : CompositeDrawable, IHasText
|
||||
{
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => text.Text;
|
||||
set => text.Text = $"{value}x";
|
||||
}
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
public TextComponent()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Font = OsuFont.Numeric.With(size: 20)
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Numeric.With(size: 8),
|
||||
Text = @"longest",
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Numeric.With(size: 8),
|
||||
Text = @"combo",
|
||||
Padding = new MarginPadding { Bottom = 3f }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@ -12,6 +10,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
@ -21,6 +20,7 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
@ -33,19 +33,20 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public readonly SortedDictionary<int, BindableLong> TeamScores = new SortedDictionary<int, BindableLong>();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; }
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; }
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; }
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
private Bindable<ScoringMode> scoringMode = null!;
|
||||
|
||||
private readonly MultiplayerRoomUser[] playingUsers;
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
|
||||
|
||||
@ -125,14 +126,17 @@ namespace osu.Game.Screens.Play.HUD
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked)
|
||||
{
|
||||
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
|
||||
if (UserScores[user.Id].Team is int team)
|
||||
if (user != null)
|
||||
{
|
||||
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
||||
leaderboardScore.TextColour = Color4.White;
|
||||
if (UserScores[user.OnlineID].Team is int team)
|
||||
{
|
||||
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
||||
leaderboardScore.TextColour = Color4.White;
|
||||
}
|
||||
}
|
||||
|
||||
return leaderboardScore;
|
||||
@ -188,7 +192,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (spectatorClient != null)
|
||||
if (spectatorClient.IsNotNull())
|
||||
{
|
||||
foreach (var user in playingUsers)
|
||||
spectatorClient.StopWatchingUser(user.UserID);
|
||||
|
99
osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
Normal file
99
osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
Normal 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class SoloGameplayLeaderboard : GameplayLeaderboard
|
||||
{
|
||||
private const int duration = 100;
|
||||
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
private readonly IUser trackingUser;
|
||||
|
||||
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
|
||||
|
||||
// hold references to ensure bindables are updated.
|
||||
private readonly List<Bindable<long>> scoreBindables = new List<Bindable<long>>();
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor scoreProcessor { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the leaderboard should be visible regardless of the configuration value.
|
||||
/// This is true by default, but can be changed.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> AlwaysVisible = new Bindable<bool>(true);
|
||||
|
||||
public SoloGameplayLeaderboard(IUser trackingUser)
|
||||
{
|
||||
this.trackingUser = trackingUser;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
|
||||
|
||||
// Alpha will be updated via `updateVisibility` below.
|
||||
Alpha = 0;
|
||||
|
||||
AlwaysVisible.BindValueChanged(_ => updateVisibility());
|
||||
configVisibility.BindValueChanged(_ => updateVisibility(), true);
|
||||
}
|
||||
|
||||
private void showScores()
|
||||
{
|
||||
Clear();
|
||||
scoreBindables.Clear();
|
||||
|
||||
if (!Scores.Any())
|
||||
return;
|
||||
|
||||
foreach (var s in Scores)
|
||||
{
|
||||
var score = Add(s.User, false);
|
||||
|
||||
var bindableTotal = scoreManager.GetBindableTotalScore(s);
|
||||
|
||||
// Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
|
||||
bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
|
||||
scoreBindables.Add(bindableTotal);
|
||||
|
||||
score.Accuracy.Value = s.Accuracy;
|
||||
score.Combo.Value = s.MaxCombo;
|
||||
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
ILeaderboardScore local = Add(trackingUser, true);
|
||||
|
||||
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
local.Combo.BindTarget = scoreProcessor.HighestCombo;
|
||||
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
local.DisplayOrder.Value = long.MaxValue;
|
||||
}
|
||||
|
||||
private void updateVisibility() =>
|
||||
this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
|
||||
}
|
||||
}
|
@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
if (isInIntro)
|
||||
{
|
||||
double introStartTime = GameplayClock.StartTime ?? 0;
|
||||
double introStartTime = GameplayClock.StartTime;
|
||||
|
||||
double introOffsetCurrent = currentTime - introStartTime;
|
||||
double introDuration = FirstHitTime - introStartTime;
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using System;
|
||||
|
||||
@ -14,9 +15,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class SongProgressInfo : Container
|
||||
{
|
||||
private OsuSpriteText timeCurrent;
|
||||
private OsuSpriteText timeLeft;
|
||||
private OsuSpriteText progress;
|
||||
private SizePreservingSpriteText timeCurrent;
|
||||
private SizePreservingSpriteText timeLeft;
|
||||
private SizePreservingSpriteText progress;
|
||||
|
||||
private double startTime;
|
||||
private double endTime;
|
||||
@ -46,36 +47,71 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (clock != null)
|
||||
gameplayClock = clock;
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
timeCurrent = new OsuSpriteText
|
||||
new Container
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
Margin = new MarginPadding
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new UprightAspectMaintainingContainer
|
||||
{
|
||||
Left = margin,
|
||||
},
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scaling = ScaleMode.Vertical,
|
||||
ScalingFactor = 0.5f,
|
||||
Child = timeCurrent = new SizePreservingSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
}
|
||||
}
|
||||
},
|
||||
progress = new OsuSpriteText
|
||||
new Container
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
},
|
||||
timeLeft = new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.BottomRight,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
Margin = new MarginPadding
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new UprightAspectMaintainingContainer
|
||||
{
|
||||
Right = margin,
|
||||
},
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scaling = ScaleMode.Vertical,
|
||||
ScalingFactor = 0.5f,
|
||||
Child = progress = new SizePreservingSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Origin = Anchor.CentreRight,
|
||||
Anchor = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new UprightAspectMaintainingContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scaling = ScaleMode.Vertical,
|
||||
ScalingFactor = 0.5f,
|
||||
Child = timeLeft = new SizePreservingSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@ -22,6 +21,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -34,21 +34,23 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public const Easing FADE_EASING = Easing.OutQuint;
|
||||
|
||||
/// <summary>
|
||||
/// The total height of all the top of screen scoring elements.
|
||||
/// </summary>
|
||||
public float TopScoringElementsHeight { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total height of all the bottom of screen scoring elements.
|
||||
/// </summary>
|
||||
public float BottomScoringElementsHeight { get; private set; }
|
||||
|
||||
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
|
||||
// Without blocking input, this would also allow them to be interacted with in such a state.
|
||||
public override bool PropagatePositionalInputSubTree => ShowHud.Value;
|
||||
|
||||
public readonly KeyCounterDisplay KeyCounter;
|
||||
public readonly ModDisplay ModDisplay;
|
||||
public readonly HoldForMenuButton HoldToQuit;
|
||||
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
|
||||
|
||||
[Cached]
|
||||
private readonly ClicksPerSecondCalculator clicksPerSecondCalculator;
|
||||
|
||||
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
|
||||
|
||||
private readonly DrawableRuleset drawableRuleset;
|
||||
@ -76,9 +78,15 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly SkinnableTargetContainer mainComponents;
|
||||
|
||||
private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
|
||||
/// <summary>
|
||||
/// A flow which sits at the left side of the screen to house leaderboard (and related) components.
|
||||
/// Will automatically be positioned to avoid colliding with top scoring elements.
|
||||
/// </summary>
|
||||
public readonly FillFlowContainer LeaderboardFlow;
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
|
||||
private readonly List<Drawable> hideTargets;
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
{
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
@ -122,8 +130,21 @@ namespace osu.Game.Screens.Play
|
||||
KeyCounter = CreateKeyCounter(),
|
||||
HoldToQuit = CreateHoldForMenuButton(),
|
||||
}
|
||||
}
|
||||
},
|
||||
LeaderboardFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding(44), // enough margin to avoid the hit error display
|
||||
Spacing = new Vector2(5)
|
||||
},
|
||||
clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, KeyCounter, topRightElements };
|
||||
|
||||
if (!alwaysShowLeaderboard)
|
||||
hideTargets.Add(LeaderboardFlow);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
@ -172,22 +193,36 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Vector2? lowestTopScreenSpace = null;
|
||||
float? lowestTopScreenSpaceLeft = null;
|
||||
float? lowestTopScreenSpaceRight = null;
|
||||
|
||||
Vector2? highestBottomScreenSpace = null;
|
||||
|
||||
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
|
||||
foreach (var element in mainComponents.Components.Cast<Drawable>())
|
||||
{
|
||||
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
|
||||
if (element.Anchor.HasFlagFast(Anchor.TopRight) || (element.Anchor.HasFlagFast(Anchor.y0) && element.RelativeSizeAxes == Axes.X))
|
||||
// for now align some top components with the bottom-edge of the lowest top-anchored hud element.
|
||||
if (element.Anchor.HasFlagFast(Anchor.y0))
|
||||
{
|
||||
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
|
||||
if (element is LegacyHealthDisplay)
|
||||
continue;
|
||||
|
||||
var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
|
||||
if (lowestTopScreenSpace == null || bottomRight.Y > lowestTopScreenSpace.Value.Y)
|
||||
lowestTopScreenSpace = bottomRight;
|
||||
float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y;
|
||||
|
||||
bool isRelativeX = element.RelativeSizeAxes == Axes.X;
|
||||
|
||||
if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
|
||||
{
|
||||
if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
|
||||
lowestTopScreenSpaceRight = bottom;
|
||||
}
|
||||
|
||||
if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
|
||||
{
|
||||
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
|
||||
lowestTopScreenSpaceLeft = bottom;
|
||||
}
|
||||
}
|
||||
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
|
||||
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
|
||||
@ -198,11 +233,16 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestTopScreenSpace.HasValue)
|
||||
topRightElements.Y = TopScoringElementsHeight = MathHelper.Clamp(ToLocalSpace(lowestTopScreenSpace.Value).Y, 0, DrawHeight - topRightElements.DrawHeight);
|
||||
if (lowestTopScreenSpaceRight.HasValue)
|
||||
topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight);
|
||||
else
|
||||
topRightElements.Y = 0;
|
||||
|
||||
if (lowestTopScreenSpaceLeft.HasValue)
|
||||
LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
|
||||
else
|
||||
LeaderboardFlow.Y = 0;
|
||||
|
||||
if (highestBottomScreenSpace.HasValue)
|
||||
bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
|
||||
else
|
||||
@ -259,7 +299,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset)
|
||||
{
|
||||
(drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter);
|
||||
if (drawableRuleset is ICanAttachHUDPieces attachTarget)
|
||||
{
|
||||
attachTarget.Attach(KeyCounter);
|
||||
attachTarget.Attach(clicksPerSecondCalculator);
|
||||
}
|
||||
|
||||
replayLoaded.BindTo(drawableRuleset.HasReplayLoaded);
|
||||
}
|
||||
|
@ -1,7 +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.Collections.Generic;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
@ -9,25 +9,19 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
public interface IGameplayClock : IFrameBasedClock
|
||||
{
|
||||
/// <summary>
|
||||
/// The rate of gameplay when playback is at 100%.
|
||||
/// This excludes any seeking / user adjustments.
|
||||
/// </summary>
|
||||
double TrueGameplayRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time from which the clock should start. Will be seeked to on calling <see cref="GameplayClockContainer.Reset"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If not set, a value of zero will be used.
|
||||
/// Importantly, the value will be inferred from the current ruleset in <see cref="MasterGameplayClockContainer"/> unless specified.
|
||||
/// By default, a value of zero will be used.
|
||||
/// Importantly, the value will be inferred from the current beatmap in <see cref="MasterGameplayClockContainer"/> by default.
|
||||
/// </remarks>
|
||||
double? StartTime { get; }
|
||||
double StartTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All adjustments applied to this clock which don't come from gameplay or mods.
|
||||
/// All adjustments applied to this clock which come from mods.
|
||||
/// </summary>
|
||||
IEnumerable<double> NonGameplayAdjustments { get; }
|
||||
IAdjustableAudioComponent AdjustmentsFromMods { get; }
|
||||
|
||||
IBindable<bool> IsPaused { get; }
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,19 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public class KeyCounterState
|
||||
{
|
||||
public KeyCounterState(double time, int count)
|
||||
{
|
||||
Time = time;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public readonly double Time;
|
||||
public readonly int Count;
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
@ -13,8 +11,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -37,37 +34,30 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public readonly BindableNumber<double> UserPlaybackRate = new BindableDouble(1)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Precision = 0.1,
|
||||
};
|
||||
|
||||
private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
|
||||
|
||||
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(); // Important that this starts at zero, matching the paused state of the clock.
|
||||
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
|
||||
private OffsetCorrectionClock userGlobalOffsetClock = null!;
|
||||
private OffsetCorrectionClock userBeatmapOffsetClock = null!;
|
||||
private OffsetCorrectionClock platformOffsetClock = null!;
|
||||
|
||||
private Bindable<double> userAudioOffset = null!;
|
||||
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
private readonly Track track;
|
||||
|
||||
private readonly double skipTargetTime;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Stores the time at which the last <see cref="StopGameplayClock"/> call was triggered.
|
||||
/// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp.
|
||||
///
|
||||
/// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled
|
||||
/// to avoid fails occurring after the pause screen has been shown.
|
||||
///
|
||||
/// In the future I want to change this.
|
||||
/// </summary>
|
||||
private double? actualStopTime;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private readonly List<Bindable<double>> nonGameplayAdjustments = new List<Bindable<double>>();
|
||||
|
||||
public override IEnumerable<double> NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value);
|
||||
private MusicController musicController { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new master gameplay clock container.
|
||||
@ -75,32 +65,14 @@ namespace osu.Game.Screens.Play
|
||||
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
|
||||
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
|
||||
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime)
|
||||
: base(beatmap.Track)
|
||||
: base(beatmap.Track, true)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.skipTargetTime = skipTargetTime;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
track = beatmap.Track;
|
||||
|
||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
||||
|
||||
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||
r => r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings,
|
||||
settings => settings.Offset,
|
||||
val => userBeatmapOffsetClock.Offset = val);
|
||||
|
||||
// Reset may have been called externally before LoadComplete.
|
||||
// If it was, and the clock is in a playing state, we want to ensure that it isn't stopped here.
|
||||
bool isStarted = !IsPaused.Value;
|
||||
|
||||
// If a custom start time was not specified, calculate the best value to use.
|
||||
StartTime ??= findEarliestStartTime();
|
||||
|
||||
Reset(startClock: isStarted);
|
||||
StartTime = findEarliestStartTime();
|
||||
}
|
||||
|
||||
private double findEarliestStartTime()
|
||||
@ -126,54 +98,70 @@ namespace osu.Game.Screens.Play
|
||||
return time;
|
||||
}
|
||||
|
||||
protected override void OnIsPausedChanged(ValueChangedEvent<bool> isPaused)
|
||||
protected override void StopGameplayClock()
|
||||
{
|
||||
actualStopTime = GameplayClock.CurrentTime;
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
// During normal operation, the source is stopped after performing a frequency ramp.
|
||||
if (isPaused.NewValue)
|
||||
this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ =>
|
||||
{
|
||||
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
|
||||
{
|
||||
if (IsPaused.Value == isPaused.NewValue)
|
||||
base.OnIsPausedChanged(isPaused);
|
||||
});
|
||||
}
|
||||
else
|
||||
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
|
||||
if (IsPaused.Value)
|
||||
base.StopGameplayClock();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isPaused.NewValue)
|
||||
base.OnIsPausedChanged(isPaused);
|
||||
base.StopGameplayClock();
|
||||
|
||||
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
|
||||
pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1;
|
||||
GameplayClock.ExternalPauseFrequencyAdjust.Value = 0;
|
||||
|
||||
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
|
||||
// Without doing this, an initial seek may be performed with the wrong offset.
|
||||
FramedClock.ProcessFrame();
|
||||
GameplayClock.ProcessFrame();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
addSourceClockAdjustments();
|
||||
base.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seek to a specific time in gameplay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track).
|
||||
/// </remarks>
|
||||
/// <param name="time">The destination time to seek to.</param>
|
||||
public override void Seek(double time)
|
||||
{
|
||||
// remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track.
|
||||
// we may want to consider reversing the application of offsets in the future as it may feel more correct.
|
||||
base.Seek(time - totalAppliedOffset);
|
||||
// Safety in case the clock is seeked while stopped.
|
||||
actualStopTime = null;
|
||||
|
||||
base.Seek(time);
|
||||
}
|
||||
|
||||
protected override void PrepareStart()
|
||||
{
|
||||
if (actualStopTime != null)
|
||||
{
|
||||
Seek(actualStopTime.Value);
|
||||
actualStopTime = null;
|
||||
}
|
||||
else
|
||||
base.PrepareStart();
|
||||
}
|
||||
|
||||
protected override void StartGameplayClock()
|
||||
{
|
||||
addSourceClockAdjustments();
|
||||
|
||||
base.StartGameplayClock();
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
|
||||
GameplayClock.ExternalPauseFrequencyAdjust.Value = 1;
|
||||
|
||||
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
|
||||
// Without doing this, an initial seek may be performed with the wrong offset.
|
||||
GameplayClock.ProcessFrame();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -181,29 +169,18 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public void Skip()
|
||||
{
|
||||
if (FramedClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
|
||||
if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
|
||||
return;
|
||||
|
||||
double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME;
|
||||
|
||||
if (FramedClock.CurrentTime < 0 && skipTarget > 6000)
|
||||
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
|
||||
// double skip exception for storyboards with very long intros
|
||||
skipTarget = 0;
|
||||
|
||||
Seek(skipTarget);
|
||||
}
|
||||
|
||||
protected override IFrameBasedClock CreateGameplayClock(IFrameBasedClock source)
|
||||
{
|
||||
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
|
||||
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||
platformOffsetClock = new OffsetCorrectionClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
|
||||
|
||||
// the final usable gameplay clock with user-set offsets applied.
|
||||
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust);
|
||||
return userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the backing clock to avoid using the originally provided track.
|
||||
/// </summary>
|
||||
@ -221,15 +198,12 @@ namespace osu.Game.Screens.Play
|
||||
if (speedAdjustmentsApplied)
|
||||
return;
|
||||
|
||||
if (SourceClock is not Track track)
|
||||
return;
|
||||
musicController.ResetTrackAdjustments();
|
||||
|
||||
track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
|
||||
track.BindAdjustments(AdjustmentsFromMods);
|
||||
track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust);
|
||||
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
|
||||
|
||||
nonGameplayAdjustments.Add(pauseFreqAdjust);
|
||||
nonGameplayAdjustments.Add(UserPlaybackRate);
|
||||
|
||||
speedAdjustmentsApplied = true;
|
||||
}
|
||||
|
||||
@ -238,22 +212,16 @@ namespace osu.Game.Screens.Play
|
||||
if (!speedAdjustmentsApplied)
|
||||
return;
|
||||
|
||||
if (SourceClock is not Track track)
|
||||
return;
|
||||
|
||||
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
|
||||
track.UnbindAdjustments(AdjustmentsFromMods);
|
||||
track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust);
|
||||
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
|
||||
|
||||
nonGameplayAdjustments.Remove(pauseFreqAdjust);
|
||||
nonGameplayAdjustments.Remove(UserPlaybackRate);
|
||||
|
||||
speedAdjustmentsApplied = false;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
beatmapOffsetSubscription?.Dispose();
|
||||
removeSourceClockAdjustments();
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,12 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -33,6 +35,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users;
|
||||
@ -63,6 +66,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public override bool HideOverlaysOnEnter => true;
|
||||
|
||||
public override bool HideMenuCursorOnNonMouseInput => true;
|
||||
|
||||
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
|
||||
|
||||
// We are managing our own adjustments (see OnEntering/OnExiting).
|
||||
@ -91,6 +96,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public int RestartCount;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="HUDOverlay"/> is currently visible.
|
||||
/// </summary>
|
||||
public IBindable<bool> ShowingOverlayComponents = new Bindable<bool>();
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
@ -374,6 +384,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
if (Configuration.AutomaticallySkipIntro)
|
||||
skipIntroOverlay.SkipWhenReady();
|
||||
|
||||
loadLeaderboard();
|
||||
}
|
||||
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
@ -416,7 +428,7 @@ namespace osu.Game.Screens.Play
|
||||
// display the cursor above some HUD elements.
|
||||
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
|
||||
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods)
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
|
||||
{
|
||||
HoldToQuit =
|
||||
{
|
||||
@ -561,9 +573,6 @@ namespace osu.Game.Screens.Play
|
||||
/// </param>
|
||||
protected void PerformExit(bool showDialogFirst)
|
||||
{
|
||||
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
// there is a chance that an exit request occurs after the transition to results has already started.
|
||||
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
|
||||
if (!this.IsCurrentScreen())
|
||||
@ -598,6 +607,9 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
// The actual exit is performed if
|
||||
// - the pause / fail dialog was not requested
|
||||
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
|
||||
@ -640,8 +652,7 @@ namespace osu.Game.Screens.Play
|
||||
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
||||
DrawableRuleset.FrameStablePlayback = false;
|
||||
|
||||
GameplayClockContainer.StartTime = time;
|
||||
GameplayClockContainer.Reset();
|
||||
GameplayClockContainer.Reset(time);
|
||||
|
||||
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
|
||||
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
||||
@ -776,19 +787,11 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
///
|
||||
/// Calling this method multiple times will have no effect.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
if (resultsDisplayDelegate != null)
|
||||
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
|
||||
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
|
||||
// may take x00 more milliseconds than expected in the very rare edge case).
|
||||
//
|
||||
// If required we can handle this more correctly by rescheduling here.
|
||||
return;
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
@ -820,6 +823,41 @@ namespace osu.Game.Screens.Play
|
||||
return mouseWheelDisabled.Value && !e.AltPressed;
|
||||
}
|
||||
|
||||
#region Gameplay leaderboard
|
||||
|
||||
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
|
||||
|
||||
private void loadLeaderboard()
|
||||
{
|
||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
var gameplayLeaderboard = CreateGameplayLeaderboard();
|
||||
|
||||
if (gameplayLeaderboard != null)
|
||||
{
|
||||
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||
|
||||
AddLeaderboardToHUD(leaderboard);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
|
||||
|
||||
protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
|
||||
|
||||
private void updateLeaderboardExpandedState() =>
|
||||
LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fail Logic
|
||||
|
||||
protected FailOverlay FailOverlay { get; private set; }
|
||||
@ -828,9 +866,17 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private bool onFail()
|
||||
{
|
||||
// Failing after the quit sequence has started may cause weird side effects with the fail animation / effects.
|
||||
if (GameplayState.HasQuit)
|
||||
return false;
|
||||
|
||||
if (!CheckModsAllowFailure())
|
||||
return false;
|
||||
|
||||
Debug.Assert(!GameplayState.HasFailed);
|
||||
Debug.Assert(!GameplayState.HasPassed);
|
||||
Debug.Assert(!GameplayState.HasQuit);
|
||||
|
||||
GameplayState.HasFailed = true;
|
||||
|
||||
updateGameplayState();
|
||||
@ -976,6 +1022,8 @@ namespace osu.Game.Screens.Play
|
||||
});
|
||||
|
||||
HUDOverlay.IsPlaying.BindTo(localUserPlaying);
|
||||
ShowingOverlayComponents.BindTo(HUDOverlay.ShowHud);
|
||||
|
||||
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
|
||||
|
||||
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
|
||||
@ -988,12 +1036,8 @@ namespace osu.Game.Screens.Play
|
||||
foreach (var mod in GameplayState.Mods.OfType<IApplicableToHUD>())
|
||||
mod.ApplyToHUD(HUDOverlay);
|
||||
|
||||
// Our mods are local copies of the global mods so they need to be re-applied to the track.
|
||||
// This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack.
|
||||
// Todo: In the future, player will receive in a track and will probably not have to worry about this...
|
||||
musicController.ResetTrackAdjustments();
|
||||
foreach (var mod in GameplayState.Mods.OfType<IApplicableToTrack>())
|
||||
mod.ApplyToTrack(musicController.CurrentTrack);
|
||||
mod.ApplyToTrack(GameplayClockContainer.AdjustmentsFromMods);
|
||||
|
||||
updateGameplayState();
|
||||
|
||||
@ -1012,7 +1056,7 @@ namespace osu.Game.Screens.Play
|
||||
if (GameplayClockContainer.IsRunning)
|
||||
throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running");
|
||||
|
||||
GameplayClockContainer.Reset(true);
|
||||
GameplayClockContainer.Reset(startClock: true);
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
@ -1045,6 +1089,7 @@ namespace osu.Game.Screens.Play
|
||||
musicController.ResetTrackAdjustments();
|
||||
|
||||
fadeOut();
|
||||
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
|
||||
|
@ -36,5 +36,10 @@ namespace osu.Game.Screens.Play
|
||||
/// Whether the intro should be skipped by default.
|
||||
/// </summary>
|
||||
public bool AutomaticallySkipIntro { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the gameplay leaderboard should always be shown (usually in a contracted state).
|
||||
/// </summary>
|
||||
public bool AlwaysShowLeaderboard { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected Task? DisposalTask { get; private set; }
|
||||
|
||||
private OsuScrollContainer settingsScroll = null!;
|
||||
|
||||
private bool backgroundBrightnessReduction;
|
||||
|
||||
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
|
||||
@ -71,6 +73,9 @@ namespace osu.Game.Screens.Play
|
||||
private AudioFilter lowPassFilter = null!;
|
||||
private AudioFilter highPassFilter = null!;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
protected bool BackgroundBrightnessReduction
|
||||
{
|
||||
set
|
||||
@ -165,30 +170,30 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new OsuScrollContainer
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
|
||||
Padding = new MarginPadding { Vertical = padding },
|
||||
Masking = false,
|
||||
Child = PlayerSettings = new FillFlowContainer<PlayerSettingsGroup>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 20),
|
||||
Padding = new MarginPadding { Horizontal = padding },
|
||||
Children = new PlayerSettingsGroup[]
|
||||
{
|
||||
VisualSettings = new VisualSettings(),
|
||||
AudioSettings = new AudioSettings(),
|
||||
new InputSettings()
|
||||
}
|
||||
},
|
||||
},
|
||||
idleTracker = new IdleTracker(750),
|
||||
}),
|
||||
settingsScroll = new OsuScrollContainer
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
|
||||
Padding = new MarginPadding { Vertical = padding },
|
||||
Masking = false,
|
||||
Child = PlayerSettings = new FillFlowContainer<PlayerSettingsGroup>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 20),
|
||||
Padding = new MarginPadding { Horizontal = padding },
|
||||
Children = new PlayerSettingsGroup[]
|
||||
{
|
||||
VisualSettings = new VisualSettings(),
|
||||
AudioSettings = new AudioSettings(),
|
||||
new InputSettings()
|
||||
}
|
||||
},
|
||||
},
|
||||
idleTracker = new IdleTracker(750),
|
||||
lowPassFilter = new AudioFilter(audio.TrackMixer),
|
||||
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass)
|
||||
};
|
||||
@ -224,6 +229,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
|
||||
// Start off-screen.
|
||||
settingsScroll.MoveToX(settingsScroll.DrawWidth);
|
||||
|
||||
content.ScaleTo(0.7f);
|
||||
|
||||
contentIn();
|
||||
@ -313,6 +321,16 @@ namespace osu.Game.Screens.Play
|
||||
content.StopTracking();
|
||||
}
|
||||
|
||||
protected override void LogoSuspending(OsuLogo logo)
|
||||
{
|
||||
base.LogoSuspending(logo);
|
||||
content.StopTracking();
|
||||
|
||||
logo
|
||||
.FadeOut(CONTENT_OUT_DURATION / 2, Easing.OutQuint)
|
||||
.ScaleTo(logo.Scale * 0.8f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Update()
|
||||
@ -363,7 +381,7 @@ namespace osu.Game.Screens.Play
|
||||
return;
|
||||
|
||||
CurrentPlayer = createPlayer();
|
||||
CurrentPlayer.Configuration.AutomaticallySkipIntro = quickRestart;
|
||||
CurrentPlayer.Configuration.AutomaticallySkipIntro |= quickRestart;
|
||||
CurrentPlayer.RestartCount = restartCount++;
|
||||
CurrentPlayer.RestartRequested = restartRequested;
|
||||
|
||||
@ -391,6 +409,10 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
content.FadeInFromZero(400);
|
||||
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
|
||||
|
||||
settingsScroll.FadeInFromZero(500, Easing.Out)
|
||||
.MoveToX(0, 500, Easing.OutQuint);
|
||||
|
||||
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
|
||||
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
|
||||
|
||||
@ -404,6 +426,10 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
|
||||
content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
|
||||
|
||||
settingsScroll.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint)
|
||||
.MoveToX(settingsScroll.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
|
||||
|
||||
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
|
||||
highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION);
|
||||
}
|
||||
@ -432,7 +458,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
ContentOut();
|
||||
|
||||
TransformSequence<PlayerLoader> pushSequence = this.Delay(CONTENT_OUT_DURATION);
|
||||
TransformSequence<PlayerLoader> pushSequence = this.Delay(0);
|
||||
|
||||
// only show if the warning was created (i.e. the beatmap needs it)
|
||||
// and this is not a restart of the map (the warning expires after first load).
|
||||
@ -441,6 +467,7 @@ namespace osu.Game.Screens.Play
|
||||
const double epilepsy_display_length = 3000;
|
||||
|
||||
pushSequence
|
||||
.Delay(CONTENT_OUT_DURATION)
|
||||
.Schedule(() => epilepsyWarning.State.Value = Visibility.Visible)
|
||||
.TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint)
|
||||
.Delay(epilepsy_display_length)
|
||||
@ -502,7 +529,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private int restartCount;
|
||||
|
||||
private const double volume_requirement = 0.05;
|
||||
private const double volume_requirement = 0.01;
|
||||
|
||||
private void showMuteWarningIfNeeded()
|
||||
{
|
||||
@ -530,7 +557,7 @@ namespace osu.Game.Screens.Play
|
||||
private void load(OsuColour colours, AudioManager audioManager, INotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay)
|
||||
{
|
||||
Icon = FontAwesome.Solid.VolumeMute;
|
||||
IconBackground.Colour = colours.RedDark;
|
||||
IconContent.Colour = colours.RedDark;
|
||||
|
||||
Activated = delegate
|
||||
{
|
||||
@ -539,10 +566,11 @@ namespace osu.Game.Screens.Play
|
||||
volumeOverlay.IsMuted.Value = false;
|
||||
|
||||
// Check values before resetting, as the user may have only had mute enabled, in which case we might not need to adjust volumes.
|
||||
// Note that we only restore halfway to ensure the user isn't suddenly overloaded by unexpectedly high volume.
|
||||
if (audioManager.Volume.Value <= volume_requirement)
|
||||
audioManager.Volume.SetDefault();
|
||||
audioManager.Volume.Value = 0.5f;
|
||||
if (audioManager.VolumeTrack.Value <= volume_requirement)
|
||||
audioManager.VolumeTrack.SetDefault();
|
||||
audioManager.VolumeTrack.Value = 0.5f;
|
||||
|
||||
return true;
|
||||
};
|
||||
@ -584,7 +612,7 @@ namespace osu.Game.Screens.Play
|
||||
private void load(OsuColour colours, INotificationOverlay notificationOverlay)
|
||||
{
|
||||
Icon = FontAwesome.Solid.BatteryQuarter;
|
||||
IconBackground.Colour = colours.RedDark;
|
||||
IconContent.Colour = colours.RedDark;
|
||||
|
||||
Activated = delegate
|
||||
{
|
||||
|
@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
public BindableDouble Current { get; } = new BindableDouble
|
||||
{
|
||||
Default = 0,
|
||||
Value = 0,
|
||||
MinValue = -50,
|
||||
MaxValue = 50,
|
||||
Precision = 0.1,
|
||||
|
@ -17,7 +17,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Precision = 0.1,
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -14,6 +15,7 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@ -55,6 +57,15 @@ namespace osu.Game.Screens.Play
|
||||
// Don't re-import replay scores as they're already present in the database.
|
||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||
|
||||
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
AlwaysVisible = { Value = true },
|
||||
Scores = { BindTarget = LeaderboardScores }
|
||||
};
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
|
@ -63,8 +63,7 @@ namespace osu.Game.Screens.Play
|
||||
if (player != null)
|
||||
{
|
||||
importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.Detach());
|
||||
if (importedScore != null)
|
||||
state.Value = DownloadState.LocallyAvailable;
|
||||
state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded;
|
||||
}
|
||||
|
||||
state.BindValueChanged(state =>
|
||||
|
@ -114,16 +114,17 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
displayTime = gameplayClock.CurrentTime;
|
||||
|
||||
// skip is not required if there is no extra "empty" time to skip.
|
||||
// we may need to remove this if rewinding before the initial player load position becomes a thing.
|
||||
if (fadeOutBeginTime < gameplayClock.CurrentTime)
|
||||
if (fadeOutBeginTime <= displayTime)
|
||||
{
|
||||
Expire();
|
||||
return;
|
||||
}
|
||||
|
||||
button.Action = () => RequestSkip?.Invoke();
|
||||
displayTime = gameplayClock.CurrentTime;
|
||||
|
||||
fadeContainer.TriggerShow();
|
||||
|
||||
@ -146,7 +147,12 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime));
|
||||
// This case causes an immediate expire in `LoadComplete`, but `Update` may run once after that.
|
||||
// Avoid div-by-zero below.
|
||||
if (fadeOutBeginTime <= displayTime)
|
||||
return;
|
||||
|
||||
double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime));
|
||||
|
||||
remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1));
|
||||
|
||||
|
@ -5,12 +5,15 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -40,8 +43,27 @@ namespace osu.Game.Screens.Play
|
||||
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
||||
}
|
||||
|
||||
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
AlwaysVisible = { Value = false },
|
||||
Scores = { BindTarget = LeaderboardScores }
|
||||
};
|
||||
|
||||
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
|
||||
|
||||
protected override Task ImportScore(Score score)
|
||||
{
|
||||
// Before importing a score, stop binding the leaderboard with its score source.
|
||||
// This avoids a case where the imported score may cause a leaderboard refresh
|
||||
// (if the leaderboard's source is local).
|
||||
LeaderboardScores.UnbindBindings();
|
||||
|
||||
return base.ImportScore(score);
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
{
|
||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
|
||||
|
@ -182,7 +182,7 @@ namespace osu.Game.Screens.Play
|
||||
scheduleStart(spectatorGameplayState);
|
||||
}
|
||||
|
||||
protected override void EndGameplay(int userId, SpectatorState state)
|
||||
protected override void QuitGameplay(int userId)
|
||||
{
|
||||
scheduledStart?.Cancel();
|
||||
immediateSpectatorGameplayState = null;
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
@ -84,6 +85,7 @@ namespace osu.Game.Screens.Play
|
||||
foreach (var frame in bundle.Frames)
|
||||
{
|
||||
IConvertibleReplayFrame convertibleFrame = GameplayState.Ruleset.CreateConvertibleReplayFrame();
|
||||
Debug.Assert(convertibleFrame != null);
|
||||
convertibleFrame.FromLegacy(frame, GameplayState.Beatmap);
|
||||
|
||||
var convertedFrame = (ReplayFrame)convertibleFrame;
|
||||
|
@ -15,7 +15,6 @@ using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@ -24,11 +23,6 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
private BufferedContainer<Column> columns;
|
||||
|
||||
public SquareGraph()
|
||||
{
|
||||
AddLayout(layout);
|
||||
}
|
||||
|
||||
public int ColumnCount => columns?.Children.Count ?? 0;
|
||||
|
||||
private int progress;
|
||||
@ -57,7 +51,7 @@ namespace osu.Game.Screens.Play
|
||||
if (value == values) return;
|
||||
|
||||
values = value;
|
||||
layout.Invalidate();
|
||||
graphNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,21 +69,25 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize);
|
||||
private ScheduledDelegate scheduledCreate;
|
||||
|
||||
private bool graphNeedsUpdate;
|
||||
|
||||
private Vector2 previousDrawSize;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (values != null && !layout.IsValid)
|
||||
if (graphNeedsUpdate || (values != null && DrawSize != previousDrawSize))
|
||||
{
|
||||
columns?.FadeOut(500, Easing.OutQuint).Expire();
|
||||
|
||||
scheduledCreate?.Cancel();
|
||||
scheduledCreate = Scheduler.AddDelayed(RecreateGraph, 500);
|
||||
|
||||
layout.Validate();
|
||||
previousDrawSize = DrawSize;
|
||||
graphNeedsUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
req.Success += r =>
|
||||
{
|
||||
Logger.Log($"Score submission token retrieved ({r.ID})");
|
||||
token = r.ID;
|
||||
tcs.SetResult(true);
|
||||
};
|
||||
@ -85,12 +86,14 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
// Generally a timeout would not happen here as APIAccess will timeout first.
|
||||
if (!tcs.Task.Wait(60000))
|
||||
handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
|
||||
req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
|
||||
|
||||
return true;
|
||||
|
||||
void handleTokenFailure(Exception exception)
|
||||
{
|
||||
tcs.SetResult(false);
|
||||
|
||||
if (HandleTokenRetrievalFailure(exception))
|
||||
{
|
||||
if (string.IsNullOrEmpty(exception.Message))
|
||||
@ -104,8 +107,12 @@ namespace osu.Game.Screens.Play
|
||||
this.Exit();
|
||||
});
|
||||
}
|
||||
|
||||
tcs.SetResult(false);
|
||||
else
|
||||
{
|
||||
// Gameplay is allowed to continue, but we still should keep track of the error.
|
||||
// In the future, this should be visible to the user in some way.
|
||||
Logger.Log($"Score submission token retrieval failed ({exception.Message})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,12 +7,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -67,12 +69,10 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
|
||||
string creator = metadata.Author.Username;
|
||||
|
||||
int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely();
|
||||
|
||||
var topStatistics = new List<StatisticDisplay>
|
||||
{
|
||||
new AccuracyStatistic(score.Accuracy),
|
||||
new ComboStatistic(score.MaxCombo, beatmapMaxCombo),
|
||||
new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)),
|
||||
new PerformanceStatistic(score),
|
||||
};
|
||||
|
||||
@ -282,12 +282,34 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
|
||||
public class PlayedOnText : OsuSpriteText
|
||||
{
|
||||
private readonly DateTimeOffset time;
|
||||
private readonly Bindable<bool> prefer24HourTime = new Bindable<bool>();
|
||||
|
||||
public PlayedOnText(DateTimeOffset time)
|
||||
{
|
||||
this.time = time;
|
||||
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold);
|
||||
Text = $"Played on {time.ToLocalTime():d MMMM yyyy HH:mm}";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager configManager)
|
||||
{
|
||||
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
prefer24HourTime.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
Text = prefer24HourTime.Value ? $"Played on {time.ToLocalTime():d MMMM yyyy HH:mm}" : $"Played on {time.ToLocalTime():d MMMM yyyy h:mm tt}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -317,7 +317,7 @@ namespace osu.Game.Screens.Ranking
|
||||
var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft;
|
||||
|
||||
// Remove from the local container and re-attach.
|
||||
detachedPanelContainer.Remove(detachedPanel);
|
||||
detachedPanelContainer.Remove(detachedPanel, false);
|
||||
ScorePanelList.Attach(detachedPanel);
|
||||
|
||||
// Move into its original location in the attached container first, then to the final location.
|
||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Screens.Ranking
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
private DrawableAudioMixer mixer;
|
||||
private AudioContainer audioContent;
|
||||
|
||||
private bool displayWithFlair;
|
||||
|
||||
@ -130,7 +130,7 @@ namespace osu.Game.Screens.Ranking
|
||||
// Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale.
|
||||
const float vertical_fudge = 20;
|
||||
|
||||
InternalChild = mixer = new DrawableAudioMixer
|
||||
InternalChild = audioContent = new AudioContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -225,7 +225,7 @@ namespace osu.Game.Screens.Ranking
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
mixer.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1;
|
||||
audioContent.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1;
|
||||
}
|
||||
|
||||
private void playAppearSample()
|
||||
@ -274,7 +274,7 @@ namespace osu.Game.Screens.Ranking
|
||||
break;
|
||||
}
|
||||
|
||||
mixer.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
|
||||
audioContent.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
|
||||
|
||||
bool topLayerExpanded = topLayerContainer.Y < 0;
|
||||
|
||||
|
@ -8,11 +8,9 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -151,32 +149,27 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
var score = trackingContainer.Panel.Score;
|
||||
|
||||
// Calculating score can take a while in extreme scenarios, so only display scores after the process completes.
|
||||
scoreManager.GetTotalScoreAsync(score)
|
||||
.ContinueWith(task => Schedule(() =>
|
||||
{
|
||||
flow.SetLayoutPosition(trackingContainer, task.GetResultSafely());
|
||||
flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score));
|
||||
|
||||
trackingContainer.Show();
|
||||
trackingContainer.Show();
|
||||
|
||||
if (SelectedScore.Value?.Equals(score) == true)
|
||||
{
|
||||
SelectedScore.TriggerChange();
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
|
||||
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
|
||||
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
|
||||
{
|
||||
// A somewhat hacky property is used here because we need to:
|
||||
// 1) Scroll after the scroll container's visible range is updated.
|
||||
// 2) Scroll before the scroll container's scroll position is updated.
|
||||
// Without this, we would have a 1-frame positioning error which looks very jarring.
|
||||
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
|
||||
}
|
||||
}
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
if (SelectedScore.Value?.Equals(score) == true)
|
||||
{
|
||||
SelectedScore.TriggerChange();
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
|
||||
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
|
||||
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
|
||||
{
|
||||
// A somewhat hacky property is used here because we need to:
|
||||
// 1) Scroll after the scroll container's visible range is updated.
|
||||
// 2) Scroll before the scroll container's scroll position is updated.
|
||||
// Without this, we would have a 1-frame positioning error which looks very jarring.
|
||||
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Screens.Ranking
|
||||
if (InternalChildren.Count == 0)
|
||||
throw new InvalidOperationException("Score panel container is not attached.");
|
||||
|
||||
RemoveInternal(Panel);
|
||||
RemoveInternal(Panel, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,13 +1,10 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -48,6 +45,12 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
/// </summary>
|
||||
private readonly IReadOnlyList<HitEvent> hitEvents;
|
||||
|
||||
private readonly IDictionary<HitResult, int>[] bins;
|
||||
private double binSize;
|
||||
private double hitOffset;
|
||||
|
||||
private Bar[]? barDrawables;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HitEventTimingDistributionGraph"/>.
|
||||
/// </summary>
|
||||
@ -55,22 +58,15 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
public HitEventTimingDistributionGraph(IReadOnlyList<HitEvent> hitEvents)
|
||||
{
|
||||
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
|
||||
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
|
||||
}
|
||||
|
||||
private int[] bins;
|
||||
private double binSize;
|
||||
private double hitOffset;
|
||||
|
||||
private Bar[] barDrawables;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (hitEvents == null || hitEvents.Count == 0)
|
||||
if (hitEvents.Count == 0)
|
||||
return;
|
||||
|
||||
bins = new int[total_timing_distribution_bins];
|
||||
|
||||
binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins);
|
||||
|
||||
// Prevent div-by-0 by enforcing a minimum bin size
|
||||
@ -89,7 +85,8 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
{
|
||||
bool roundUp = true;
|
||||
|
||||
Array.Clear(bins, 0, bins.Length);
|
||||
foreach (var bin in bins)
|
||||
bin.Clear();
|
||||
|
||||
foreach (var e in hitEvents)
|
||||
{
|
||||
@ -110,23 +107,23 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
|
||||
// may be out of range when applying an offset. for such cases we can just drop the results.
|
||||
if (index >= 0 && index < bins.Length)
|
||||
bins[index]++;
|
||||
{
|
||||
bins[index].TryGetValue(e.Result, out int value);
|
||||
bins[index][e.Result] = ++value;
|
||||
}
|
||||
}
|
||||
|
||||
if (barDrawables != null)
|
||||
{
|
||||
for (int i = 0; i < barDrawables.Length; i++)
|
||||
{
|
||||
barDrawables[i].UpdateOffset(bins[i]);
|
||||
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int maxCount = bins.Max();
|
||||
barDrawables = new Bar[total_timing_distribution_bins];
|
||||
|
||||
for (int i = 0; i < barDrawables.Length; i++)
|
||||
barDrawables[i] = new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index);
|
||||
int maxCount = bins.Max(b => b.Values.Sum());
|
||||
barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
|
||||
|
||||
Container axisFlow;
|
||||
|
||||
@ -209,50 +206,102 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
|
||||
private class Bar : CompositeDrawable
|
||||
{
|
||||
private readonly float value;
|
||||
private readonly IReadOnlyList<KeyValuePair<HitResult, int>> values;
|
||||
private readonly float maxValue;
|
||||
private readonly bool isCentre;
|
||||
private readonly float totalValue;
|
||||
|
||||
private readonly Circle boxOriginal;
|
||||
private Circle boxAdjustment;
|
||||
private float basalHeight;
|
||||
private float offsetAdjustment;
|
||||
|
||||
private const float minimum_height = 0.05f;
|
||||
private Circle[] boxOriginals = null!;
|
||||
|
||||
public Bar(float value, float maxValue, bool isCentre)
|
||||
private Circle? boxAdjustment;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private const double duration = 300;
|
||||
|
||||
public Bar(IDictionary<HitResult, int> values, float maxValue, bool isCentre)
|
||||
{
|
||||
this.value = value;
|
||||
this.values = values.OrderBy(v => v.Key.GetIndexForOrderedDisplay()).ToList();
|
||||
this.maxValue = maxValue;
|
||||
this.isCentre = isCentre;
|
||||
totalValue = values.Sum(v => v.Value);
|
||||
offsetAdjustment = totalValue;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (values.Any())
|
||||
{
|
||||
boxOriginal = new Circle
|
||||
boxOriginals = values.Select((v, i) => new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = isCentre ? Color4.White : Color4Extensions.FromHex("#66FFCC"),
|
||||
Height = minimum_height,
|
||||
},
|
||||
};
|
||||
Colour = isCentre && i == 0 ? Color4.White : colours.ForHitResult(v.Key),
|
||||
Height = 0,
|
||||
}).ToArray();
|
||||
// The bars of the stacked bar graph will be processed (stacked) from the bottom, which is the base position,
|
||||
// to the top, and the bottom bar should be drawn more toward the front by design,
|
||||
// while the drawing order is from the back to the front, so the order passed to `InternalChildren` is the opposite.
|
||||
InternalChildren = boxOriginals.Reverse().ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// A bin with no value draws a grey dot instead.
|
||||
Circle dot = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = isCentre ? Color4.White : Color4.Gray,
|
||||
Height = 0,
|
||||
};
|
||||
InternalChildren = boxOriginals = new[] { dot };
|
||||
}
|
||||
}
|
||||
|
||||
private const double duration = 300;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
float height = Math.Clamp(value / maxValue, minimum_height, 1);
|
||||
if (!values.Any())
|
||||
return;
|
||||
|
||||
if (height > minimum_height)
|
||||
boxOriginal.ResizeHeightTo(height, duration, Easing.OutQuint);
|
||||
updateBasalHeight();
|
||||
|
||||
foreach (var boxOriginal in boxOriginals)
|
||||
{
|
||||
boxOriginal.Y = 0;
|
||||
boxOriginal.Height = basalHeight;
|
||||
}
|
||||
|
||||
float offsetValue = 0;
|
||||
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
|
||||
boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint);
|
||||
offsetValue -= values[i].Value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updateBasalHeight();
|
||||
}
|
||||
|
||||
public void UpdateOffset(float adjustment)
|
||||
{
|
||||
bool hasAdjustment = adjustment != value && adjustment / maxValue >= minimum_height;
|
||||
bool hasAdjustment = adjustment != totalValue;
|
||||
|
||||
if (boxAdjustment == null)
|
||||
{
|
||||
@ -271,7 +320,53 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
});
|
||||
}
|
||||
|
||||
boxAdjustment.ResizeHeightTo(Math.Clamp(adjustment / maxValue, minimum_height, 1), duration, Easing.OutQuint);
|
||||
offsetAdjustment = adjustment;
|
||||
drawAdjustmentBar();
|
||||
}
|
||||
|
||||
private void updateBasalHeight()
|
||||
{
|
||||
float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1;
|
||||
|
||||
if (newBasalHeight == basalHeight)
|
||||
return;
|
||||
|
||||
basalHeight = newBasalHeight;
|
||||
foreach (var dot in boxOriginals)
|
||||
dot.Height = basalHeight;
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue;
|
||||
|
||||
private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0);
|
||||
|
||||
private void draw()
|
||||
{
|
||||
resizeBars();
|
||||
|
||||
if (boxAdjustment != null)
|
||||
drawAdjustmentBar();
|
||||
}
|
||||
|
||||
private void resizeBars()
|
||||
{
|
||||
float offsetValue = 0;
|
||||
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight;
|
||||
boxOriginals[i].Height = heightForValue(values[i].Value);
|
||||
offsetValue -= values[i].Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAdjustmentBar()
|
||||
{
|
||||
bool hasAdjustment = offsetAdjustment != totalValue;
|
||||
|
||||
boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint);
|
||||
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@ -49,31 +47,31 @@ namespace osu.Game.Screens.Select
|
||||
/// <summary>
|
||||
/// Triggered when the <see cref="BeatmapSets"/> loaded change and are completely loaded.
|
||||
/// </summary>
|
||||
public Action BeatmapSetsChanged;
|
||||
public Action? BeatmapSetsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected beatmap.
|
||||
/// </summary>
|
||||
public BeatmapInfo SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo;
|
||||
public BeatmapInfo? SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo;
|
||||
|
||||
private CarouselBeatmap selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected);
|
||||
private CarouselBeatmap? selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected);
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected beatmap set.
|
||||
/// </summary>
|
||||
public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet;
|
||||
public BeatmapSetInfo? SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet;
|
||||
|
||||
/// <summary>
|
||||
/// A function to optionally decide on a recommended difficulty from a beatmap set.
|
||||
/// </summary>
|
||||
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo> GetRecommendedBeatmap;
|
||||
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo>? GetRecommendedBeatmap;
|
||||
|
||||
private CarouselBeatmapSet selectedBeatmapSet;
|
||||
private CarouselBeatmapSet? selectedBeatmapSet;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
|
||||
/// </summary>
|
||||
public Action<BeatmapInfo> SelectionChanged;
|
||||
public Action<BeatmapInfo?>? SelectionChanged;
|
||||
|
||||
public override bool HandleNonPositionalInput => AllowSelection;
|
||||
public override bool HandlePositionalInput => AllowSelection;
|
||||
@ -151,15 +149,15 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private CarouselRoot root;
|
||||
|
||||
private IDisposable subscriptionSets;
|
||||
private IDisposable subscriptionDeletedSets;
|
||||
private IDisposable subscriptionBeatmaps;
|
||||
private IDisposable subscriptionHiddenBeatmaps;
|
||||
private IDisposable? subscriptionSets;
|
||||
private IDisposable? subscriptionDeletedSets;
|
||||
private IDisposable? subscriptionBeatmaps;
|
||||
private IDisposable? subscriptionHiddenBeatmaps;
|
||||
|
||||
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
|
||||
|
||||
private Sample spinSample;
|
||||
private Sample randomSelectSample;
|
||||
private Sample? spinSample;
|
||||
private Sample? randomSelectSample;
|
||||
|
||||
private int visibleSetsCount;
|
||||
|
||||
@ -200,7 +198,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@ -215,7 +213,7 @@ namespace osu.Game.Screens.Select
|
||||
subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => b.Hidden), beatmapsChanged);
|
||||
}
|
||||
|
||||
private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
||||
private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception? error)
|
||||
{
|
||||
// If loading test beatmaps, avoid overwriting with realm subscription callbacks.
|
||||
if (loadedTestBeatmaps)
|
||||
@ -228,7 +226,7 @@ namespace osu.Game.Screens.Select
|
||||
removeBeatmapSet(sender[i].ID);
|
||||
}
|
||||
|
||||
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
||||
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception? error)
|
||||
{
|
||||
// If loading test beatmaps, avoid overwriting with realm subscription callbacks.
|
||||
if (loadedTestBeatmaps)
|
||||
@ -265,9 +263,49 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
UpdateBeatmapSet(sender[i].Detach());
|
||||
|
||||
if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null)
|
||||
{
|
||||
// If SelectedBeatmapInfo is non-null, the set should also be non-null.
|
||||
Debug.Assert(SelectedBeatmapSet != null);
|
||||
|
||||
// To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions.
|
||||
// When an update occurs, the previous beatmap set is either soft or hard deleted.
|
||||
// Check if the current selection was potentially deleted by re-querying its validity.
|
||||
bool selectedSetMarkedDeleted = realm.Run(r => r.Find<BeatmapSetInfo>(SelectedBeatmapSet.ID))?.DeletePending != false;
|
||||
|
||||
int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray();
|
||||
|
||||
if (selectedSetMarkedDeleted && modifiedAndInserted.Any())
|
||||
{
|
||||
// If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices.
|
||||
// This relies on the full update operation being in a single transaction, so please don't change that.
|
||||
foreach (int i in modifiedAndInserted)
|
||||
{
|
||||
var beatmapSetInfo = sender[i];
|
||||
|
||||
foreach (var beatmapInfo in beatmapSetInfo.Beatmaps)
|
||||
{
|
||||
if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata))
|
||||
continue;
|
||||
|
||||
// Best effort matching. We can't use ID because in the update flow a new version will get its own GUID.
|
||||
if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName)
|
||||
{
|
||||
SelectBeatmap(beatmapInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed.
|
||||
// Let's attempt to follow set-level selection anyway.
|
||||
SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
|
||||
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes, Exception? error)
|
||||
{
|
||||
// we only care about actual changes in hidden status.
|
||||
if (changes == null)
|
||||
@ -330,7 +368,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
// check if we can/need to maintain our current selection.
|
||||
if (previouslySelectedID != null)
|
||||
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
||||
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
||||
}
|
||||
|
||||
itemsCache.Invalidate();
|
||||
@ -347,7 +385,7 @@ namespace osu.Game.Screens.Select
|
||||
/// <param name="beatmapInfo">The beatmap to select.</param>
|
||||
/// <param name="bypassFilters">Whether to select the beatmap even if it is filtered (i.e., not visible on carousel).</param>
|
||||
/// <returns>True if a selection was made, False if it wasn't.</returns>
|
||||
public bool SelectBeatmap(BeatmapInfo beatmapInfo, bool bypassFilters = true)
|
||||
public bool SelectBeatmap(BeatmapInfo? beatmapInfo, bool bypassFilters = true)
|
||||
{
|
||||
// ensure that any pending events from BeatmapManager have been run before attempting a selection.
|
||||
Scheduler.Update();
|
||||
@ -405,6 +443,9 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void selectNextSet(int direction, bool skipDifficulties)
|
||||
{
|
||||
if (selectedBeatmap == null || selectedBeatmapSet == null)
|
||||
return;
|
||||
|
||||
var unfilteredSets = beatmapSets.Where(s => !s.Filtered.Value).ToList();
|
||||
|
||||
var nextSet = unfilteredSets[(unfilteredSets.IndexOf(selectedBeatmapSet) + direction + unfilteredSets.Count) % unfilteredSets.Count];
|
||||
@ -417,7 +458,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void selectNextDifficulty(int direction)
|
||||
{
|
||||
if (selectedBeatmap == null)
|
||||
if (selectedBeatmap == null || selectedBeatmapSet == null)
|
||||
return;
|
||||
|
||||
var unfilteredDifficulties = selectedBeatmapSet.Items.Where(s => !s.Filtered.Value).ToList();
|
||||
@ -446,7 +487,7 @@ namespace osu.Game.Screens.Select
|
||||
if (!visibleSets.Any())
|
||||
return false;
|
||||
|
||||
if (selectedBeatmap != null)
|
||||
if (selectedBeatmap != null && selectedBeatmapSet != null)
|
||||
{
|
||||
randomSelectedBeatmaps.Push(selectedBeatmap);
|
||||
|
||||
@ -489,11 +530,13 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (!beatmap.Filtered.Value)
|
||||
{
|
||||
if (RandomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation)
|
||||
previouslyVisitedRandomSets.Remove(selectedBeatmapSet);
|
||||
|
||||
if (selectedBeatmapSet != null)
|
||||
{
|
||||
if (RandomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation)
|
||||
previouslyVisitedRandomSets.Remove(selectedBeatmapSet);
|
||||
|
||||
playSpinSample(distanceBetween(beatmap, selectedBeatmapSet));
|
||||
}
|
||||
|
||||
select(beatmap);
|
||||
break;
|
||||
@ -505,14 +548,18 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void playSpinSample(double distance)
|
||||
{
|
||||
var chan = spinSample.GetChannel();
|
||||
chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount);
|
||||
chan.Play();
|
||||
var chan = spinSample?.GetChannel();
|
||||
|
||||
if (chan != null)
|
||||
{
|
||||
chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount);
|
||||
chan.Play();
|
||||
}
|
||||
|
||||
randomSelectSample?.Play();
|
||||
}
|
||||
|
||||
private void select(CarouselItem item)
|
||||
private void select(CarouselItem? item)
|
||||
{
|
||||
if (!AllowSelection)
|
||||
return;
|
||||
@ -524,7 +571,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private FilterCriteria activeCriteria = new FilterCriteria();
|
||||
|
||||
protected ScheduledDelegate PendingFilter;
|
||||
protected ScheduledDelegate? PendingFilter;
|
||||
|
||||
public bool AllowSelection = true;
|
||||
|
||||
@ -556,7 +603,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
public void Filter(FilterCriteria newCriteria, bool debounce = true)
|
||||
public void Filter(FilterCriteria? newCriteria, bool debounce = true)
|
||||
{
|
||||
if (newCriteria != null)
|
||||
activeCriteria = newCriteria;
|
||||
@ -759,7 +806,7 @@ namespace osu.Game.Screens.Select
|
||||
return (firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet)
|
||||
private CarouselBeatmapSet? createCarouselSet(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
// This can be moved to the realm query if required using:
|
||||
// .Filter("DeletePending == false && Protected == false && ANY Beatmaps.Hidden == false")
|
||||
@ -925,7 +972,7 @@ namespace osu.Game.Screens.Select
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be updated.</param>
|
||||
/// <param name="parent">For nested items, the parent of the item to be updated.</param>
|
||||
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
|
||||
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem? parent = null)
|
||||
{
|
||||
Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
|
||||
float itemDrawY = posInScroll.Y - visibleUpperBound;
|
||||
@ -953,13 +1000,13 @@ namespace osu.Game.Screens.Select
|
||||
/// </summary>
|
||||
private class CarouselBoundsItem : CarouselItem
|
||||
{
|
||||
public override DrawableCarouselItem CreateDrawableRepresentation() =>
|
||||
throw new NotImplementedException();
|
||||
public override DrawableCarouselItem CreateDrawableRepresentation() => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private class CarouselRoot : CarouselGroupEagerSelect
|
||||
{
|
||||
private readonly BeatmapCarousel carousel;
|
||||
// May only be null during construction (State.Value set causes PerformSelection to be triggered).
|
||||
private readonly BeatmapCarousel? carousel;
|
||||
|
||||
public readonly Dictionary<Guid, CarouselBeatmapSet> BeatmapSetsByID = new Dictionary<Guid, CarouselBeatmapSet>();
|
||||
|
||||
@ -980,7 +1027,7 @@ namespace osu.Game.Screens.Select
|
||||
base.AddItem(i);
|
||||
}
|
||||
|
||||
public CarouselBeatmapSet RemoveChild(Guid beatmapSetID)
|
||||
public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID)
|
||||
{
|
||||
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet))
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user