Merge branch 'master' into refactor-combo-colour-retrieval

This commit is contained in:
Salman Ahmed
2021-07-20 10:08:25 +03:00
1214 changed files with 36441 additions and 11055 deletions

View File

@ -5,8 +5,8 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
@ -31,6 +31,8 @@ namespace osu.Game.Screens.Backgrounds
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
protected virtual bool AllowStoryboardBackground => true;
public BackgroundScreenDefault(bool animateOnEnter = true)
: base(animateOnEnter)
{
@ -51,14 +53,41 @@ namespace osu.Game.Screens.Backgrounds
mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next();
introSequence.ValueChanged += _ => Next();
seasonalBackgroundLoader.SeasonalBackgroundChanged += Next;
seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next();
currentDisplay = RNG.Next(0, background_count);
Next();
}
private void display(Background newBackground)
private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;
/// <summary>
/// Request loading the next background.
/// </summary>
/// <returns>Whether a new background was queued for load. May return false if the current background is still valid.</returns>
public bool Next()
{
var nextBackground = createBackground();
// in the case that the background hasn't changed, we want to avoid cancelling any tasks that could still be loading.
if (nextBackground == background)
return false;
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
nextTask?.Cancel();
nextTask = Scheduler.AddDelayed(() =>
{
LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token);
}, 100);
return true;
}
private void displayNext(Background newBackground)
{
background?.FadeOut(800, Easing.InOutSine);
background?.Expire();
@ -67,76 +96,55 @@ namespace osu.Game.Screens.Backgrounds
currentDisplay++;
}
private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;
public void Next()
{
nextTask?.Cancel();
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
nextTask = Scheduler.AddDelayed(() => LoadComponentAsync(createBackground(), display, cancellationTokenSource.Token), 100);
}
private Background createBackground()
{
Background newBackground;
string backgroundName;
// seasonal background loading gets highest priority.
Background newBackground = seasonalBackgroundLoader.LoadNextBackground();
var seasonalBackground = seasonalBackgroundLoader.LoadNextBackground();
if (seasonalBackground != null)
{
seasonalBackground.Depth = currentDisplay;
return seasonalBackground;
}
switch (introSequence.Value)
{
case IntroSequence.Welcome:
backgroundName = "Intro/Welcome/menu-background";
break;
default:
backgroundName = $@"Menu/menu-background-{currentDisplay % background_count + 1}";
break;
}
if (user.Value?.IsSupporter ?? false)
if (newBackground == null && user.Value?.IsSupporter == true)
{
switch (mode.Value)
{
case BackgroundSource.Beatmap:
newBackground = new BeatmapBackground(beatmap.Value, backgroundName);
break;
case BackgroundSource.BeatmapWithStoryboard:
{
if (mode.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
newBackground = new BeatmapBackgroundWithStoryboard(beatmap.Value, getBackgroundTextureName());
newBackground ??= new BeatmapBackground(beatmap.Value, getBackgroundTextureName());
default:
newBackground = new SkinnedBackground(skin.Value, backgroundName);
break;
}
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;
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
break;
}
}
else
newBackground = new Background(backgroundName);
// this method is called in many cases where the background might not necessarily need to change.
// if an equivalent background is currently being shown, we don't want to load it again.
if (newBackground?.Equals(background) == true)
return background;
newBackground ??= new Background(getBackgroundTextureName());
newBackground.Depth = currentDisplay;
return newBackground;
}
private class SkinnedBackground : Background
private string getBackgroundTextureName()
{
private readonly Skin skin;
public SkinnedBackground(Skin skin, string fallbackTextureName)
: base(fallbackTextureName)
switch (introSequence.Value)
{
this.skin = skin;
}
case IntroSequence.Welcome:
return @"Intro/Welcome/menu-background";
[BackgroundDependencyLoader]
private void load()
{
Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture;
default:
return $@"Menu/menu-background-{currentDisplay % background_count + 1}";
}
}
}

View File

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

View File

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

View File

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

View File

@ -3,9 +3,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
@ -24,6 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
/// </summary>
public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
where T : class
{
protected DragBox DragBox { get; private set; }
@ -39,6 +43,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
protected readonly BindableList<T> SelectedItems = new BindableList<T>();
protected BlueprintContainer()
{
RelativeSizeAxes = Axes.Both;
@ -47,6 +53,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
SelectedItems.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var o in args.NewItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
break;
case NotifyCollectionChangedAction.Remove:
foreach (var o in args.OldItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
break;
}
};
SelectionHandler = CreateSelectionHandler();
SelectionHandler.DeselectAll = deselectAll;
@ -71,6 +95,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Creates a <see cref="SelectionBlueprint{T}"/> for a specific item.
/// </summary>
/// <param name="item">The item to create the overlay for.</param>
[CanBeNull]
protected virtual SelectionBlueprint<T> CreateBlueprintFor(T item) => null;
protected virtual DragBox CreateDragBox(Action<RectangleF> performSelect) => new DragBox(performSelect);
@ -276,6 +301,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
}
/// <summary>
/// Retrieves an item's blueprint.
/// </summary>
/// <param name="item">The item to retrieve the blueprint of.</param>
/// <returns>The blueprint.</returns>
protected SelectionBlueprint<T> GetBlueprintFor(T item) => blueprintMap[item];
#endregion
#region Selection

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -61,6 +62,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
inputManager = GetContainingInputManager();
Beatmap.HitObjectAdded += hitObjectAdded;
// updates to selected are handled for us by SelectionHandler.
NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
@ -74,6 +77,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
{
base.TransferBlueprintFor(hitObject, drawableObject);
var blueprint = (HitObjectSelectionBlueprint)GetBlueprintFor(hitObject);
blueprint.DrawableObject = drawableObject;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed)
@ -246,15 +257,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (drawable == null)
return null;
return CreateBlueprintFor(drawable);
return CreateHitObjectBlueprintFor(item)?.With(b => b.DrawableObject = drawable);
}
public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null;
[CanBeNull]
public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null;
protected override void OnBlueprintAdded(HitObject item)
private void hitObjectAdded(HitObject obj)
{
base.OnBlueprintAdded(item);
// refresh the tool to handle the case of placement completing.
refreshTool();
// on successful placement, the new combo button should be reset as this is the most common user interaction.

View File

@ -2,15 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Screens.Edit.Compose.Components
{
@ -24,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly HitObjectComposer Composer;
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
private HitObjectUsageEventBuffer usageEventBuffer;
protected EditorBlueprintContainer(HitObjectComposer composer)
{
@ -34,23 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var o in args.NewItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
break;
case NotifyCollectionChangedAction.Remove:
foreach (var o in args.OldItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
break;
}
};
SelectedItems.BindTo(Beatmap.SelectedHitObjects);
}
protected override void LoadComplete()
@ -65,11 +48,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
Composer.Playfield.HitObjectUsageBegan += AddBlueprintFor;
Composer.Playfield.HitObjectUsageFinished += RemoveBlueprintFor;
usageEventBuffer = new HitObjectUsageEventBuffer(Composer.Playfield);
usageEventBuffer.HitObjectUsageBegan += AddBlueprintFor;
usageEventBuffer.HitObjectUsageFinished += RemoveBlueprintFor;
usageEventBuffer.HitObjectUsageTransferred += TransferBlueprintFor;
}
}
protected override void Update()
{
base.Update();
usageEventBuffer?.Update();
}
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
=> blueprints.OrderBy(b => b.Item.StartTime);
@ -86,7 +77,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
double offset = result.Time.Value - blueprints.First().Item.StartTime;
if (offset != 0)
Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
{
Beatmap.PerformOnSelection(obj =>
{
obj.StartTime += offset;
Beatmap.Update(obj);
});
}
}
return true;
@ -100,6 +97,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.AddBlueprintFor(item);
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> has been transferred to another <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The hit object which has been assigned to a new drawable.</param>
/// <param name="drawableObject">The new drawable that is representing the hit object.</param>
protected virtual void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
{
}
protected override void DragOperationCompleted()
{
base.DragOperationCompleted();
@ -153,11 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Beatmap.HitObjectRemoved -= RemoveBlueprintFor;
}
if (Composer != null)
{
Composer.Playfield.HitObjectUsageBegan -= AddBlueprintFor;
Composer.Playfield.HitObjectUsageFinished -= RemoveBlueprintFor;
}
usageEventBuffer?.Dispose();
}
}
}

View File

@ -108,17 +108,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
#endregion
#region Ternary state changes
@ -136,6 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
h.Samples.Add(new HitSampleInfo(sampleName));
EditorBeatmap.Update(h);
});
}
@ -145,7 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName));
EditorBeatmap.PerformOnSelection(h =>
{
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
EditorBeatmap.Update(h);
});
}
/// <summary>
@ -179,13 +173,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
if (SelectedBlueprints.All(b => b.Item is IHasComboInformation))
{
yield return new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
}
yield return new OsuMenuItem("Sound")
{
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
};
}

View File

@ -7,7 +7,7 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// An event which occurs when a <see cref="OverlaySelectionBlueprint"/> is moved.
/// An event which occurs when a <see cref="SelectionBlueprint{T}"/> is moved.
/// </summary>
public class MoveSelectionEvent<T>
{

View File

@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
private Container dragHandles;
private SelectionBoxDragHandleContainer dragHandles;
private FillFlowContainer buttons;
private OsuSpriteText selectionDetailsText;
@ -195,7 +195,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
},
}
},
dragHandles = new Container
dragHandles = new SelectionBoxDragHandleContainer
{
RelativeSizeAxes = Axes.Both,
// ensures that the centres of all drag handles line up with the middle of the selection box border.
@ -220,75 +220,76 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addRotationComponents()
{
const float separation = 40;
addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90));
addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90));
AddRangeInternal(new Drawable[]
{
new Box
{
Depth = float.MaxValue,
Colour = colours.YellowLight,
Blending = BlendingParameters.Additive,
Alpha = 0.3f,
Size = new Vector2(BORDER_RADIUS, separation),
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate")
{
Anchor = Anchor.TopCentre,
Y = -separation,
HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e)),
OperationStarted = operationStarted,
OperationEnded = operationEnded
}
});
addRotateHandle(Anchor.TopLeft);
addRotateHandle(Anchor.TopRight);
addRotateHandle(Anchor.BottomLeft);
addRotateHandle(Anchor.BottomRight);
}
private void addYScaleComponents()
{
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical));
addDragHandle(Anchor.TopCentre);
addDragHandle(Anchor.BottomCentre);
addScaleHandle(Anchor.TopCentre);
addScaleHandle(Anchor.BottomCentre);
}
private void addFullScaleComponents()
{
addDragHandle(Anchor.TopLeft);
addDragHandle(Anchor.TopRight);
addDragHandle(Anchor.BottomLeft);
addDragHandle(Anchor.BottomRight);
addScaleHandle(Anchor.TopLeft);
addScaleHandle(Anchor.TopRight);
addScaleHandle(Anchor.BottomLeft);
addScaleHandle(Anchor.BottomRight);
}
private void addXScaleComponents()
{
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal));
addDragHandle(Anchor.CentreLeft);
addDragHandle(Anchor.CentreRight);
addScaleHandle(Anchor.CentreLeft);
addScaleHandle(Anchor.CentreRight);
}
private void addButton(IconUsage icon, string tooltip, Action action)
{
buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip)
var button = new SelectionBoxButton(icon, tooltip)
{
OperationStarted = operationStarted,
OperationEnded = operationEnded,
Action = action
});
};
button.OperationStarted += operationStarted;
button.OperationEnded += operationEnded;
buttons.Add(button);
}
private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle
private void addScaleHandle(Anchor anchor)
{
Anchor = anchor,
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
OperationStarted = operationStarted,
OperationEnded = operationEnded
});
var handle = new SelectionBoxScaleHandle
{
Anchor = anchor,
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor)
};
handle.OperationStarted += operationStarted;
handle.OperationEnded += operationEnded;
dragHandles.AddScaleHandle(handle);
}
private void addRotateHandle(Anchor anchor)
{
var handle = new SelectionBoxRotationHandle
{
Anchor = anchor,
HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e))
};
handle.OperationStarted += operationStarted;
handle.OperationEnded += operationEnded;
dragHandles.AddRotationHandle(handle);
}
private int activeOperations;

View File

@ -7,15 +7,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A drag "handle" which shares the visual appearance but behaves more like a clickable button.
/// </summary>
public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip
public sealed class SelectionBoxButton : SelectionBoxControl, IHasTooltip
{
private SpriteIcon icon;
@ -23,7 +21,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Action Action;
public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip)
public SelectionBoxButton(IconUsage iconUsage, string tooltip)
{
this.iconUsage = iconUsage;
@ -36,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
Size *= 2;
Size = new Vector2(20);
AddInternal(icon = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
@ -49,18 +47,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnClick(ClickEvent e)
{
OperationStarted?.Invoke();
TriggerOperationStarted();
Action?.Invoke();
OperationEnded?.Invoke();
TriggerOperatoinEnded();
return true;
}
protected override void UpdateHoverState()
{
base.UpdateHoverState();
icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black;
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
}
}

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// Represents the base appearance for UI controls of the <see cref="SelectionBox"/>,
/// such as scale handles, rotation handles, buttons, etc...
/// </summary>
public abstract class SelectionBoxControl : CompositeDrawable
{
public const double TRANSFORM_DURATION = 100;
public event Action OperationStarted;
public event Action OperationEnded;
private Circle circle;
/// <summary>
/// Whether the user is currently holding the control with mouse.
/// </summary>
public bool IsHeld { get; private set; }
[Resolved]
protected OsuColour Colours { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UpdateHoverState();
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
UpdateHoverState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
UpdateHoverState();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
IsHeld = true;
UpdateHoverState();
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
IsHeld = false;
UpdateHoverState();
}
protected virtual void UpdateHoverState()
{
if (IsHeld)
circle.FadeColour(Colours.GrayF, TRANSFORM_DURATION, Easing.OutQuint);
else
circle.FadeColour(IsHovered ? Colours.Red : Colours.YellowDark, TRANSFORM_DURATION, Easing.OutQuint);
this.ScaleTo(IsHeld || IsHovered ? 1.5f : 1, TRANSFORM_DURATION, Easing.OutQuint);
}
protected void TriggerOperationStarted() => OperationStarted?.Invoke();
protected void TriggerOperatoinEnded() => OperationEnded?.Invoke();
}
}

View File

@ -2,75 +2,17 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBoxDragHandle : Container
public abstract class SelectionBoxDragHandle : SelectionBoxControl
{
public Action OperationStarted;
public Action OperationEnded;
public Action<DragEvent> HandleDrag { get; set; }
private Circle circle;
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(10);
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UpdateHoverState();
}
protected override bool OnHover(HoverEvent e)
{
UpdateHoverState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
UpdateHoverState();
}
protected bool HandlingMouse;
protected override bool OnMouseDown(MouseDownEvent e)
{
HandlingMouse = true;
UpdateHoverState();
return true;
}
protected override bool OnDragStart(DragStartEvent e)
{
OperationStarted?.Invoke();
TriggerOperationStarted();
return true;
}
@ -82,24 +24,45 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnDragEnd(DragEndEvent e)
{
HandlingMouse = false;
OperationEnded?.Invoke();
TriggerOperatoinEnded();
UpdateHoverState();
base.OnDragEnd(e);
}
protected override void OnMouseUp(MouseUpEvent e)
#region Internal events for SelectionBoxDragHandleContainer
internal event Action HoverGained;
internal event Action HoverLost;
internal event Action MouseDown;
internal event Action MouseUp;
protected override bool OnHover(HoverEvent e)
{
HandlingMouse = false;
UpdateHoverState();
base.OnMouseUp(e);
bool result = base.OnHover(e);
HoverGained?.Invoke();
return result;
}
protected virtual void UpdateHoverState()
protected override void OnHoverLost(HoverLostEvent e)
{
circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark);
this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint);
base.OnHoverLost(e);
HoverLost?.Invoke();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
bool result = base.OnMouseDown(e);
MouseDown?.Invoke();
return result;
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
MouseUp?.Invoke();
}
#endregion
}
}

View File

@ -0,0 +1,109 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// Represents a display composite containing and managing the visibility state of the selection box's drag handles.
/// </summary>
public class SelectionBoxDragHandleContainer : CompositeDrawable
{
private Container<SelectionBoxScaleHandle> scaleHandles;
private Container<SelectionBoxRotationHandle> rotationHandles;
private readonly List<SelectionBoxDragHandle> allDragHandles = new List<SelectionBoxDragHandle>();
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
scaleHandles = new Container<SelectionBoxScaleHandle>
{
RelativeSizeAxes = Axes.Both,
},
rotationHandles = new Container<SelectionBoxRotationHandle>
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(-12.5f),
},
};
}
public void AddScaleHandle(SelectionBoxScaleHandle handle)
{
bindDragHandle(handle);
scaleHandles.Add(handle);
}
public void AddRotationHandle(SelectionBoxRotationHandle handle)
{
handle.Alpha = 0;
handle.AlwaysPresent = true;
bindDragHandle(handle);
rotationHandles.Add(handle);
}
private void bindDragHandle(SelectionBoxDragHandle handle)
{
handle.HoverGained += updateRotationHandlesVisibility;
handle.HoverLost += updateRotationHandlesVisibility;
handle.MouseDown += updateRotationHandlesVisibility;
handle.MouseUp += updateRotationHandlesVisibility;
allDragHandles.Add(handle);
}
private SelectionBoxRotationHandle displayedRotationHandle;
private SelectionBoxDragHandle activeHandle;
private void updateRotationHandlesVisibility()
{
// if the active handle is a rotation handle and is held or hovered,
// then no need to perform any updates to the rotation handles visibility.
if (activeHandle is SelectionBoxRotationHandle && (activeHandle?.IsHeld == true || activeHandle?.IsHovered == true))
return;
displayedRotationHandle?.FadeOut(SelectionBoxControl.TRANSFORM_DURATION, Easing.OutQuint);
displayedRotationHandle = null;
// if the active handle is not a rotation handle but is held, then keep the rotation handle hidden.
if (activeHandle?.IsHeld == true)
return;
activeHandle = rotationHandles.FirstOrDefault(h => h.IsHeld || h.IsHovered);
activeHandle ??= allDragHandles.FirstOrDefault(h => h.IsHovered);
if (activeHandle != null)
{
displayedRotationHandle = getCorrespondingRotationHandle(activeHandle, rotationHandles);
displayedRotationHandle?.FadeIn(SelectionBoxControl.TRANSFORM_DURATION, Easing.OutQuint);
}
}
/// <summary>
/// Gets the rotation handle corresponding to the given handle.
/// </summary>
[CanBeNull]
private static SelectionBoxRotationHandle getCorrespondingRotationHandle(SelectionBoxDragHandle handle, IEnumerable<SelectionBoxRotationHandle> rotationHandles)
{
if (handle is SelectionBoxRotationHandle rotationHandle)
return rotationHandle;
return rotationHandles.SingleOrDefault(r => r.Anchor == handle.Anchor);
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBoxRotationHandle : SelectionBoxDragHandle
{
private SpriteIcon icon;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(15f);
AddInternal(icon = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Redo,
Scale = new Vector2
{
X = Anchor.HasFlagFast(Anchor.x0) ? 1f : -1f,
Y = Anchor.HasFlagFast(Anchor.y0) ? 1f : -1f
}
});
}
protected override void UpdateHoverState()
{
base.UpdateHoverState();
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +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.
using osu.Framework.Allocation;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBoxScaleHandle : SelectionBoxDragHandle
{
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(10);
}
}
}

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@ -236,6 +237,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
DeleteSelected();
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected static TernaryState GetStateFromSelection<TObject>(IEnumerable<TObject> selection, Func<TObject, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
/// <summary>
/// Called whenever the deletion of items has been requested.
/// </summary>
@ -274,8 +286,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
int count = SelectedItems.Count;
SelectionBox.Text = count > 0 ? count.ToString() : string.Empty;
SelectionBox.FadeTo(count > 0 ? 1 : 0);
OnSelectionChanged();
}
@ -339,5 +351,98 @@ namespace osu.Game.Screens.Edit.Compose.Components
=> Enumerable.Empty<MenuItem>();
#endregion
#region Helper Methods
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
/// <summary>
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
/// </summary>
protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
{
var centre = quad.Centre;
switch (direction)
{
case Direction.Horizontal:
position.X = centre.X - (position.X - centre.X);
break;
case Direction.Vertical:
position.Y = centre.Y - (position.Y - centre.Y);
break;
}
return position;
}
/// <summary>
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
/// will return the scaled position in screen space coordinates.
/// </summary>
protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
{
// adjust the direction of scale depending on which side the user is dragging.
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
// guard against no-ops and NaN.
if (scale.X != 0 && selectionQuad.Width > 0)
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
if (scale.Y != 0 && selectionQuad.Height > 0)
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
return position;
}
/// <summary>
/// Returns a quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
protected static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
{
if (!points.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
#endregion
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
placementBlueprint = CreateBlueprintFor(obj.NewValue);
placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull();
placementBlueprint.Colour = Color4.MediumPurple;
@ -276,7 +277,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime);
double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount;
EditorBeatmap.PerformOnSelection(h => h.StartTime += adjustment);
EditorBeatmap.PerformOnSelection(h =>
{
h.StartTime += adjustment;
EditorBeatmap.Update(h);
});
}
}

View File

@ -38,6 +38,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable<int> indexInCurrentComboBindable;
private Bindable<int> comboIndexBindable;
private Bindable<Color4> displayColourBindable;
private readonly ExtendableCircle circle;
private readonly Border border;
@ -107,43 +108,61 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
base.LoadComplete();
if (Item is IHasComboInformation comboInfo)
switch (Item)
{
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
case IHasDisplayColour displayColour:
displayColourBindable = displayColour.DisplayColour.GetBoundCopy();
displayColourBindable.BindValueChanged(_ => updateColour(), true);
break;
comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
case IHasComboInformation comboInfo:
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
skin.SourceChanged += updateComboColour;
comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateColour(), true);
skin.SourceChanged += updateColour;
break;
}
}
protected override void OnSelected()
{
// base logic hides selected blueprints when not selected, but timeline doesn't do that.
updateComboColour();
updateColour();
}
protected override void OnDeselected()
{
// base logic hides selected blueprints when not selected, but timeline doesn't do that.
updateComboColour();
updateColour();
}
private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
private void updateComboColour()
private void updateColour()
{
if (!(Item is IHasComboInformation combo))
return;
Color4 colour;
var comboColour = combo.GetComboColour(skin);
switch (Item)
{
case IHasDisplayColour displayColour:
colour = displayColour.DisplayColour.Value;
break;
case IHasComboInformation combo:
colour = combo.GetComboColour(skin);
break;
default:
return;
}
if (IsSelected)
{
border.Show();
comboColour = comboColour.Lighten(0.3f);
colour = colour.Lighten(0.3f);
}
else
{
@ -151,9 +170,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
if (Item is IHasDuration duration && duration.Duration > 0)
circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
circle.Colour = ColourInfo.GradientHorizontal(colour, colour.Lighten(0.4f));
else
circle.Colour = comboColour;
circle.Colour = colour;
var col = circle.Colour.TopLeft.Linear;
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col);

View File

@ -73,15 +73,7 @@ namespace osu.Game.Screens.Edit.Compose
{
Debug.Assert(ruleset != null);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmap.Value.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, EditorBeatmap.PlayableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
return new RulesetSkinProvidingContainer(ruleset, EditorBeatmap.PlayableBeatmap, beatmap.Value.Skin).WithChild(content);
}
#region Input Handling

View 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 System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Edit.Compose
{
/// <summary>
/// Buffers events from the many <see cref="HitObjectContainer"/>s in a nested <see cref="Playfield"/> hierarchy
/// to ensure correct ordering of events.
/// </summary>
internal class HitObjectUsageEventBuffer : IDisposable
{
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If the ruleset uses pooled objects, this represents the time when the <see cref="HitObject"/>s become alive.
/// </remarks>
public event Action<HitObject> HitObjectUsageBegan;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes unused by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If the ruleset uses pooled objects, this represents the time when the <see cref="HitObject"/>s become dead.
/// </remarks>
public event Action<HitObject> HitObjectUsageFinished;
/// <summary>
/// Invoked when a <see cref="HitObject"/> has been transferred to another <see cref="DrawableHitObject"/>.
/// </summary>
public event Action<HitObject, DrawableHitObject> HitObjectUsageTransferred;
private readonly Playfield playfield;
/// <summary>
/// Creates a new <see cref="HitObjectUsageEventBuffer"/>.
/// </summary>
/// <param name="playfield">The most top-level <see cref="Playfield"/>.</param>
public HitObjectUsageEventBuffer([NotNull] Playfield playfield)
{
this.playfield = playfield;
playfield.HitObjectUsageBegan += onHitObjectUsageBegan;
playfield.HitObjectUsageFinished += onHitObjectUsageFinished;
}
private readonly List<HitObject> usageFinishedHitObjects = new List<HitObject>();
private void onHitObjectUsageBegan(HitObject hitObject)
{
if (usageFinishedHitObjects.Remove(hitObject))
HitObjectUsageTransferred?.Invoke(hitObject, playfield.AllHitObjects.Single(d => d.HitObject == hitObject));
else
HitObjectUsageBegan?.Invoke(hitObject);
}
private void onHitObjectUsageFinished(HitObject hitObject) => usageFinishedHitObjects.Add(hitObject);
public void Update()
{
foreach (var hitObject in usageFinishedHitObjects)
HitObjectUsageFinished?.Invoke(hitObject);
usageFinishedHitObjects.Clear();
}
public void Dispose()
{
if (playfield != null)
{
playfield.HitObjectUsageBegan -= onHitObjectUsageBegan;
playfield.HitObjectUsageFinished -= onHitObjectUsageFinished;
}
}
}
}

View File

@ -17,7 +17,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
@ -129,7 +128,7 @@ namespace osu.Game.Screens.Edit
// clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages.
// eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases.
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.CreateCopy();
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.DeepClone();
}
catch (Exception e)
{
@ -144,8 +143,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 };
UpdateClockSource();
clock.ChangeSource(loadableBeatmap.Track);
dependencies.CacheAs(clock);
AddInternal(clock);
@ -308,11 +306,7 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary>
public void UpdateClockSource()
{
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock.ChangeSource(sourceClock);
}
public void UpdateClockSource() => clock.ChangeSource(Beatmap.Value.Track);
protected void Save()
{
@ -479,25 +473,23 @@ namespace osu.Game.Screens.Edit
{
if (!exitConfirmed)
{
// if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save.
if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog)
// dialog overlay may not be available in visual tests.
if (dialogOverlay == null)
{
confirmExit();
return base.OnExiting(next);
return true;
}
// if the dialog is already displayed, confirm exit with no save.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
{
saveDialog.PerformOkAction();
return true;
}
if (isNewBeatmap || HasUnsavedChanges)
{
dialogOverlay?.Push(new PromptForSaveDialog(() =>
{
confirmExit();
this.Exit();
}, () =>
{
confirmExitWithSave();
this.Exit();
}));
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
return true;
}
}
@ -505,15 +497,23 @@ namespace osu.Game.Screens.Edit
ApplyToBackground(b => b.FadeColour(Color4.White, 500));
resetTrack();
Beatmap.Value = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
// To update the game-wide beatmap with any changes, perform a re-fetch on exit.
// This is required as the editor makes its local changes via EditorBeatmap
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
Beatmap.Value = refetchedBeatmap;
return base.OnExiting(next);
}
private void confirmExitWithSave()
{
exitConfirmed = true;
Save();
exitConfirmed = true;
this.Exit();
}
private void confirmExit()
@ -535,6 +535,7 @@ namespace osu.Game.Screens.Edit
}
exitConfirmed = true;
this.Exit();
}
private readonly Bindable<string> clipboard = new Bindable<string>();
@ -583,7 +584,7 @@ namespace osu.Game.Screens.Edit
private void resetTrack(bool seekToStart = false)
{
Beatmap.Value.Track?.Stop();
Beatmap.Value.Track.Stop();
if (seekToStart)
{
@ -641,6 +642,9 @@ namespace osu.Game.Screens.Edit
case EditorScreenMode.Verify:
currentScreen = new VerifyScreen();
break;
default:
throw new InvalidOperationException("Editor menu bar switched to an unsupported mode");
}
LoadComponentAsync(currentScreen, newScreen =>

View File

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

View File

@ -10,7 +10,7 @@ using osu.Game.Overlays;
namespace osu.Game.Screens.Edit
{
public class RoundedContentEditorScreen : EditorScreen
public class EditorRoundedScreen : EditorScreen
{
public const int HORIZONTAL_PADDING = 100;
@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit
protected override Container<Drawable> Content => roundedContent;
public RoundedContentEditorScreen(EditorScreenMode mode)
public EditorRoundedScreen(EditorScreenMode mode)
: base(mode)
{
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit
{
public abstract class EditorRoundedScreenSettings : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background4,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = CreateSections()
},
}
};
}
protected abstract IReadOnlyList<Drawable> CreateSections();
}
}

View File

@ -0,0 +1,61 @@
// 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.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit
{
public abstract class EditorRoundedScreenSettingsSection : CompositeDrawable
{
private const int header_height = 50;
protected abstract string HeaderText { get; }
protected FillFlowContainer Flow { get; private set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
Height = header_height,
Padding = new MarginPadding { Horizontal = 20 },
Child = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = HeaderText,
Font = new FontUsage(size: 25, weight: "bold")
}
},
new Container
{
Y = header_height,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Flow = new FillFlowContainer
{
Padding = new MarginPadding { Horizontal = 20 },
Spacing = new Vector2(10),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
}
};
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Skinning;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Screens.Edit
@ -117,6 +118,8 @@ namespace osu.Game.Screens.Edit
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
}
}

View File

@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Setup
comboColours = new LabelledColourPalette
{
Label = "Hitcircle / Slider Combos",
FixedLabelWidth = LABEL_WIDTH,
ColourNamePrefix = "Combo"
}
};

View File

@ -28,6 +28,7 @@ namespace osu.Game.Screens.Edit.Setup
circleSizeSlider = new LabelledSliderBar<float>
{
Label = "Object Size",
FixedLabelWidth = LABEL_WIDTH,
Description = "The size of all hit objects",
Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize)
{
@ -40,6 +41,7 @@ namespace osu.Game.Screens.Edit.Setup
healthDrainSlider = new LabelledSliderBar<float>
{
Label = "Health Drain",
FixedLabelWidth = LABEL_WIDTH,
Description = "The rate of passive health drain throughout playable time",
Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.DrainRate)
{
@ -52,6 +54,7 @@ namespace osu.Game.Screens.Edit.Setup
approachRateSlider = new LabelledSliderBar<float>
{
Label = "Approach Rate",
FixedLabelWidth = LABEL_WIDTH,
Description = "The speed at which objects are presented to the player",
Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate)
{
@ -64,6 +67,7 @@ namespace osu.Game.Screens.Edit.Setup
overallDifficultySlider = new LabelledSliderBar<float>
{
Label = "Overall Difficulty",
FixedLabelWidth = LABEL_WIDTH,
Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)",
Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty)
{

View File

@ -56,9 +56,9 @@ namespace osu.Game.Screens.Edit.Setup
public void DisplayFileChooser()
{
FileSelector fileSelector;
OsuFileSelector fileSelector;
Target.Child = fileSelector = new FileSelector(currentFile.Value?.DirectoryName, handledExtensions)
Target.Child = fileSelector = new OsuFileSelector(currentFile.Value?.DirectoryName, handledExtensions)
{
RelativeSizeAxes = Axes.X,
Height = 400,

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
internal class LabelledRomanisedTextBox : LabelledTextBox
{
protected override OsuTextBox CreateTextBox() => new RomanisedTextBox();
private class RomanisedTextBox : OsuTextBox
{
protected override bool CanAddCharacter(char character)
=> MetadataUtils.IsRomanised(character);
}
}
}

View File

@ -3,75 +3,117 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
internal class MetadataSection : SetupSection
{
private LabelledTextBox artistTextBox;
private LabelledTextBox titleTextBox;
protected LabelledTextBox ArtistTextBox;
protected LabelledTextBox RomanisedArtistTextBox;
protected LabelledTextBox TitleTextBox;
protected LabelledTextBox RomanisedTitleTextBox;
private LabelledTextBox creatorTextBox;
private LabelledTextBox difficultyTextBox;
private LabelledTextBox sourceTextBox;
private LabelledTextBox tagsTextBox;
public override LocalisableString Title => "Metadata";
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
var metadata = Beatmap.Metadata;
Children = new[]
{
artistTextBox = new LabelledTextBox
{
Label = "Artist",
Current = { Value = Beatmap.Metadata.Artist },
TabbableContentContainer = this
},
titleTextBox = new LabelledTextBox
{
Label = "Title",
Current = { Value = Beatmap.Metadata.Title },
TabbableContentContainer = this
},
creatorTextBox = new LabelledTextBox
{
Label = "Creator",
Current = { Value = Beatmap.Metadata.AuthorString },
TabbableContentContainer = this
},
difficultyTextBox = new LabelledTextBox
{
Label = "Difficulty Name",
Current = { Value = Beatmap.BeatmapInfo.Version },
TabbableContentContainer = this
},
ArtistTextBox = createTextBox<LabelledTextBox>("Artist",
!string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist),
RomanisedArtistTextBox = createTextBox<LabelledRomanisedTextBox>("Romanised Artist",
!string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
Empty(),
TitleTextBox = createTextBox<LabelledTextBox>("Title",
!string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title),
RomanisedTitleTextBox = createTextBox<LabelledRomanisedTextBox>("Romanised Title",
!string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
Empty(),
creatorTextBox = createTextBox<LabelledTextBox>("Creator", metadata.AuthorString),
difficultyTextBox = createTextBox<LabelledTextBox>("Difficulty Name", Beatmap.BeatmapInfo.Version),
sourceTextBox = createTextBox<LabelledTextBox>("Source", metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>("Tags", metadata.Tags)
};
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
}
private TTextBox createTextBox<TTextBox>(string label, string initialValue)
where TTextBox : LabelledTextBox, new()
=> new TTextBox
{
Label = label,
FixedLabelWidth = LABEL_WIDTH,
Current = { Value = initialValue },
TabbableContentContainer = this
};
protected override void LoadComplete()
{
base.LoadComplete();
if (string.IsNullOrEmpty(artistTextBox.Current.Value))
GetContainingInputManager().ChangeFocus(artistTextBox);
if (string.IsNullOrEmpty(ArtistTextBox.Current.Value))
GetContainingInputManager().ChangeFocus(ArtistTextBox);
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));
updateReadOnlyState();
}
private void transferIfRomanised(string value, LabelledTextBox target)
{
if (MetadataUtils.IsRomanised(value))
target.Current.Value = value;
updateReadOnlyState();
updateMetadata();
}
private void updateReadOnlyState()
{
RomanisedArtistTextBox.ReadOnly = MetadataUtils.IsRomanised(ArtistTextBox.Current.Value);
RomanisedTitleTextBox.ReadOnly = MetadataUtils.IsRomanised(TitleTextBox.Current.Value);
}
private void onCommit(TextBox sender, bool newText)
{
if (!newText) return;
// for now, update these on commit rather than making BeatmapMetadata bindables.
// for now, update on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Metadata.Artist = artistTextBox.Current.Value;
Beatmap.Metadata.Title = titleTextBox.Current.Value;
updateMetadata();
}
private void updateMetadata()
{
Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value;
Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value;
Beatmap.Metadata.TitleUnicode = TitleTextBox.Current.Value;
Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value;
Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value;
Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value;
Beatmap.Metadata.Source = sourceTextBox.Current.Value;
Beatmap.Metadata.Tags = tagsTextBox.Current.Value;
}
}
}

View File

@ -54,6 +54,7 @@ namespace osu.Game.Screens.Edit.Setup
backgroundTextBox = new FileChooserLabelledTextBox(".jpg", ".jpeg", ".png")
{
Label = "Background",
FixedLabelWidth = LABEL_WIDTH,
PlaceholderText = "Click to select a background image",
Current = { Value = working.Value.Metadata.BackgroundFile },
Target = backgroundFileChooserContainer,
@ -72,6 +73,7 @@ namespace osu.Game.Screens.Edit.Setup
audioTrackTextBox = new FileChooserLabelledTextBox(".mp3", ".ogg")
{
Label = "Audio Track",
FixedLabelWidth = LABEL_WIDTH,
PlaceholderText = "Click to select a track",
Current = { Value = working.Value.Metadata.AudioFile },
Target = audioTrackFileChooserContainer,

View File

@ -7,7 +7,7 @@ using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Edit.Setup
{
public class SetupScreen : RoundedContentEditorScreen
public class SetupScreen : EditorRoundedScreen
{
[Cached]
private SectionsContainer<SetupSection> sections = new SectionsContainer<SetupSection>();

View File

@ -93,7 +93,7 @@ namespace osu.Game.Screens.Edit.Setup
public SetupScreenTabControl()
{
TabContainer.Margin = new MarginPadding { Horizontal = RoundedContentEditorScreen.HORIZONTAL_PADDING };
TabContainer.Margin = new MarginPadding { Horizontal = EditorRoundedScreen.HORIZONTAL_PADDING };
AddInternal(background = new Box
{

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Screens.Edit.Setup
@ -15,6 +16,11 @@ namespace osu.Game.Screens.Edit.Setup
{
private readonly FillFlowContainer flow;
/// <summary>
/// Used to align some of the child <see cref="LabelledDrawable{T}"/>s together to achieve a grid-like look.
/// </summary>
protected const float LABEL_WIDTH = 160;
[Resolved]
protected OsuColour Colours { get; private set; }
@ -33,7 +39,7 @@ namespace osu.Game.Screens.Edit.Setup
Padding = new MarginPadding
{
Vertical = 10,
Horizontal = RoundedContentEditorScreen.HORIZONTAL_PADDING
Horizontal = EditorRoundedScreen.HORIZONTAL_PADDING
};
InternalChild = new FillFlowContainer
@ -53,7 +59,7 @@ namespace osu.Game.Screens.Edit.Setup
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(10),
Direction = FillDirection.Vertical,
}
}

View File

@ -2,44 +2,13 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Timing
{
public class ControlPointSettings : CompositeDrawable
public class ControlPointSettings : EditorRoundedScreenSettings
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background4,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = createSections()
},
}
};
}
private IReadOnlyList<Drawable> createSections() => new Drawable[]
protected override IReadOnlyList<Drawable> CreateSections() => new Drawable[]
{
new GroupSection(),
new TimingSection(),

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private EditorClock clock { get; set; }
public const float TIMING_COLUMN_WIDTH = 220;
public const float TIMING_COLUMN_WIDTH = 230;
public IEnumerable<ControlPointGroup> ControlGroups
{
@ -91,7 +91,7 @@ namespace osu.Game.Screens.Edit.Timing
{
Text = group.Time.ToEditorFormattedString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Width = 60,
Width = 70,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},

View File

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
public class TimingScreen : RoundedContentEditorScreen
public class TimingScreen : EditorRoundedScreen
{
[Cached]
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Verify
{
internal class InterpretationSection : EditorRoundedScreenSettingsSection
{
protected override string HeaderText => "Interpretation";
[BackgroundDependencyLoader]
private void load(VerifyScreen verify)
{
Flow.Add(new SettingsEnumDropdown<DifficultyRating>
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
TooltipText = "Affects checks that depend on difficulty level",
Current = verify.InterpretedDifficulty.GetBoundCopy()
});
}
}
}

View File

@ -0,0 +1,115 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osuTK;
namespace osu.Game.Screens.Edit.Verify
{
[Cached]
public class IssueList : CompositeDrawable
{
private IssueTable table;
[Resolved]
private EditorClock clock { get; set; }
[Resolved]
private IBindable<WorkingBeatmap> workingBeatmap { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private VerifyScreen verify { get; set; }
private IBeatmapVerifier rulesetVerifier;
private BeatmapVerifier generalVerifier;
private BeatmapVerifierContext context;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
generalVerifier = new BeatmapVerifier();
rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
context = new BeatmapVerifierContext(beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value);
verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue);
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background2,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = table = new IssueTable(),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(20),
Children = new Drawable[]
{
new TriangleButton
{
Text = "Refresh",
Action = refresh,
Size = new Vector2(120, 40),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
verify.InterpretedDifficulty.BindValueChanged(_ => refresh());
verify.HiddenIssueTypes.BindCollectionChanged((_, __) => refresh());
refresh();
}
private void refresh()
{
var issues = generalVerifier.Run(context);
if (rulesetVerifier != null)
issues = issues.Concat(rulesetVerifier.Run(context));
issues = filter(issues);
table.Issues = issues
.OrderBy(issue => issue.Template.Type)
.ThenBy(issue => issue.Check.Metadata.Category);
}
private IEnumerable<Issue> filter(IEnumerable<Issue> issues)
{
return issues.Where(issue => !verify.HiddenIssueTypes.Contains(issue.Template.Type));
}
}
}

View File

@ -2,45 +2,16 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Edit.Verify
{
public class IssueSettings : CompositeDrawable
public class IssueSettings : EditorRoundedScreenSettings
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = createSections()
},
}
};
}
private IReadOnlyList<Drawable> createSections() => new Drawable[]
protected override IReadOnlyList<Drawable> CreateSections() => new Drawable[]
{
new InterpretationSection(),
new VisibilitySection()
};
}
}

View File

@ -18,7 +18,9 @@ namespace osu.Game.Screens.Edit.Verify
public class IssueTable : EditorTable
{
[Resolved]
private Bindable<Issue> selectedIssue { get; set; }
private VerifyScreen verify { get; set; }
private Bindable<Issue> selectedIssue;
[Resolved]
private EditorClock clock { get; set; }
@ -71,6 +73,7 @@ namespace osu.Game.Screens.Edit.Verify
{
base.LoadComplete();
selectedIssue = verify.SelectedIssue.GetBoundCopy();
selectedIssue.BindValueChanged(issue =>
{
foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue;

View File

@ -1,26 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osuTK;
namespace osu.Game.Screens.Edit.Verify
{
public class VerifyScreen : RoundedContentEditorScreen
[Cached]
public class VerifyScreen : EditorRoundedScreen
{
[Cached]
private Bindable<Issue> selectedIssue = new Bindable<Issue>();
public readonly Bindable<Issue> SelectedIssue = new Bindable<Issue>();
public readonly Bindable<DifficultyRating> InterpretedDifficulty = new Bindable<DifficultyRating>();
public readonly BindableList<IssueType> HiddenIssueTypes = new BindableList<IssueType> { IssueType.Negligible };
public IssueList IssueList { get; private set; }
public VerifyScreen()
: base(EditorScreenMode.Verify)
@ -30,6 +29,10 @@ namespace osu.Game.Screens.Edit.Verify
[BackgroundDependencyLoader]
private void load()
{
InterpretedDifficulty.Default = EditorBeatmap.BeatmapInfo.DifficultyRating;
InterpretedDifficulty.SetDefault();
IssueList = new IssueList();
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -45,92 +48,12 @@ namespace osu.Game.Screens.Edit.Verify
{
new Drawable[]
{
new IssueList(),
IssueList,
new IssueSettings(),
},
}
}
};
}
public class IssueList : CompositeDrawable
{
private IssueTable table;
[Resolved]
private EditorClock clock { get; set; }
[Resolved]
private IBindable<WorkingBeatmap> workingBeatmap { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private Bindable<Issue> selectedIssue { get; set; }
private IBeatmapVerifier rulesetVerifier;
private BeatmapVerifier generalVerifier;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
generalVerifier = new BeatmapVerifier();
rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background2,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = table = new IssueTable(),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(20),
Children = new Drawable[]
{
new TriangleButton
{
Text = "Refresh",
Action = refresh,
Size = new Vector2(120, 40),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
refresh();
}
private void refresh()
{
var issues = generalVerifier.Run(beatmap, workingBeatmap.Value);
if (rulesetVerifier != null)
issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value));
table.Issues = issues
.OrderBy(issue => issue.Template.Type)
.ThenBy(issue => issue.Check.Metadata.Category);
}
}
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Screens.Edit.Verify
{
internal class VisibilitySection : EditorRoundedScreenSettingsSection
{
private readonly IssueType[] configurableIssueTypes =
{
IssueType.Warning,
IssueType.Error,
IssueType.Negligible
};
private BindableList<IssueType> hiddenIssueTypes;
protected override string HeaderText => "Visibility";
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours, VerifyScreen verify)
{
hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy();
foreach (IssueType issueType in configurableIssueTypes)
{
var checkbox = new SettingsCheckbox
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
LabelText = issueType.ToString(),
Current = { Default = !hiddenIssueTypes.Contains(issueType) }
};
checkbox.Current.SetDefault();
checkbox.Current.BindValueChanged(state =>
{
if (!state.NewValue)
hiddenIssueTypes.Add(issueType);
else
hiddenIssueTypes.Remove(issueType);
});
Flow.Add(checkbox);
}
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Edit
{
private readonly Bindable<float> waveformOpacity;
private readonly Dictionary<float, ToggleMenuItem> menuItemLookup = new Dictionary<float, ToggleMenuItem>();
private readonly Dictionary<float, TernaryStateRadioMenuItem> menuItemLookup = new Dictionary<float, TernaryStateRadioMenuItem>();
public WaveformOpacityMenuItem(Bindable<float> waveformOpacity)
: base("Waveform opacity")
@ -29,13 +29,13 @@ namespace osu.Game.Screens.Edit
waveformOpacity.BindValueChanged(opacity =>
{
foreach (var kvp in menuItemLookup)
kvp.Value.State.Value = kvp.Key == opacity.NewValue;
kvp.Value.State.Value = kvp.Key == opacity.NewValue ? TernaryState.True : TernaryState.False;
}, true);
}
private ToggleMenuItem createMenuItem(float opacity)
private TernaryStateRadioMenuItem createMenuItem(float opacity)
{
var item = new ToggleMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity));
var item = new TernaryStateRadioMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity));
menuItemLookup[opacity] = item;
return item;
}

View File

@ -67,8 +67,11 @@ namespace osu.Game.Screens
/// Invoked when the back button has been pressed to close any overlays before exiting this <see cref="IOsuScreen"/>.
/// </summary>
/// <remarks>
/// If this <see cref="IOsuScreen"/> has not yet finished loading, the exit will occur immediately without this method being invoked.
/// <para>
/// Return <c>true</c> to block this <see cref="IOsuScreen"/> from being exited after closing an overlay.
/// Return <c>false</c> if this <see cref="IOsuScreen"/> should continue exiting.
/// </para>
/// </remarks>
bool OnBackButton();
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Screens.Import
{
public override bool HideOverlaysOnEnter => true;
private FileSelector fileSelector;
private OsuFileSelector fileSelector;
private Container contentContainer;
private TextFlowContainer currentFileText;
@ -57,7 +57,7 @@ namespace osu.Game.Screens.Import
Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
fileSelector = new FileSelector(validFileExtensions: game.HandledExtensions.ToArray())
fileSelector = new OsuFileSelector(validFileExtensions: game.HandledExtensions.ToArray())
{
RelativeSizeAxes = Axes.Both,
Width = 0.65f

View File

@ -19,6 +19,7 @@ using osu.Game.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Screens.Menu
@ -50,7 +51,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public Button(string text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
{
this.sampleName = sampleName;
this.clickAction = clickAction;

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
@ -22,6 +23,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@ -97,8 +99,8 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[]
{
new Button(@"settings", string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new Button(@"back", @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
new Button(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
{
VisibleState = ButtonSystemState.Play,
},
@ -121,19 +123,19 @@ namespace osu.Game.Screens.Menu
private LoginOverlay loginOverlay { get; set; }
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host, LocalisationManager strings)
{
buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new Button(@"playlists", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.Add(new Button(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new Button(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new Button(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit)
buttonsTopLevel.Add(new Button(@"exit", string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsTopLevel);
@ -259,7 +261,7 @@ namespace osu.Game.Screens.Menu
switch (state)
{
default:
return true;
return false;
case ButtonSystemState.Initial:
State = ButtonSystemState.TopLevel;
@ -295,7 +297,7 @@ namespace osu.Game.Screens.Menu
Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}");
using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0, true))
using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0))
{
buttonArea.ButtonSystemState = state;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Menu
private readonly Bindable<User> currentUser = new Bindable<User>();
private FillFlowContainer fill;
private readonly List<Drawable> expendableText = new List<Drawable>();
public Disclaimer(OsuScreen nextScreen = null)
{
this.nextScreen = nextScreen;
@ -54,7 +57,7 @@ namespace osu.Game.Screens.Menu
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Flask,
Icon = OsuIcon.Logo,
Size = new Vector2(icon_size),
Y = icon_y,
},
@ -70,37 +73,55 @@ namespace osu.Game.Screens.Menu
{
textFlow = new LinkFlowContainer
{
RelativeSizeAxes = Axes.X,
Width = 680,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Spacing = new Vector2(0, 2),
LayoutDuration = 2000,
LayoutEasing = Easing.OutQuint
},
supportFlow = new LinkFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Alpha = 0,
Spacing = new Vector2(0, 2),
},
}
}
},
supportFlow = new LinkFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Padding = new MarginPadding(20),
Alpha = 0,
Spacing = new Vector2(0, 2),
},
};
textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Light));
textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.SemiBold));
textFlow.AddText("this is osu!", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular));
expendableText.AddRange(textFlow.AddText("lazer", t =>
{
t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular);
t.Colour = colours.PinkLight;
}));
static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular);
static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold);
textFlow.NewParagraph();
static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold);
textFlow.AddText("the next ", formatRegular);
textFlow.AddText("major update", t =>
{
t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold);
t.Colour = colours.Pink;
});
expendableText.AddRange(textFlow.AddText(" coming to osu!", formatRegular));
textFlow.AddText(".", formatRegular);
textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold));
textFlow.NewParagraph();
textFlow.NewParagraph();
textFlow.AddParagraph("today's tip:", formatSemiBold);
textFlow.AddParagraph(getRandomTip(), formatRegular);
textFlow.NewParagraph();
textFlow.NewParagraph();
@ -116,19 +137,19 @@ namespace osu.Game.Screens.Menu
if (e.NewValue.IsSupporter)
{
supportFlow.AddText("Eternal thanks to you for supporting osu!", format);
supportFlow.AddText("Eternal thanks to you for supporting osu!", formatSemiBold);
}
else
{
supportFlow.AddText("Consider becoming an ", format);
supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", creationParameters: format);
supportFlow.AddText(" to help support the game", format);
supportFlow.AddText("Consider becoming an ", formatSemiBold);
supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", formatSemiBold);
supportFlow.AddText(" to help support osu!'s development", formatSemiBold);
}
heart = supportFlow.AddIcon(FontAwesome.Solid.Heart, t =>
{
t.Padding = new MarginPadding { Left = 5, Top = 3 };
t.Font = t.Font.With(size: 12);
t.Font = t.Font.With(size: 20);
t.Origin = Anchor.Centre;
t.Colour = colours.Pink;
}).First();
@ -160,7 +181,7 @@ namespace osu.Game.Screens.Menu
icon.Delay(500).FadeIn(500).ScaleTo(1, 500, Easing.OutQuint);
using (BeginDelayedSequence(3000, true))
using (BeginDelayedSequence(3000))
{
icon.FadeColour(iconColour, 200, Easing.OutQuint);
icon.MoveToY(icon_y * 1.3f, 500, Easing.OutCirc)
@ -169,7 +190,15 @@ namespace osu.Game.Screens.Menu
.MoveToY(icon_y, 160, Easing.InQuart)
.FadeColour(Color4.White, 160);
fill.Delay(520 + 160).MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart);
using (BeginDelayedSequence(520 + 160))
{
fill.MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart);
Schedule(() => expendableText.ForEach(t =>
{
t.FadeOut(100);
t.ScaleTo(new Vector2(0, 1), 100, Easing.OutQuart);
}));
}
}
supportFlow.FadeOut().Delay(2000).FadeIn(500);
@ -201,7 +230,7 @@ namespace osu.Game.Screens.Menu
"New features are coming online every update. Make sure to stay up-to-date!",
"If you find the UI too large or small, try adjusting UI scale in settings!",
"Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!",
"For now, what used to be \"osu!direct\" is available to all users on lazer. You can access it anywhere using Ctrl-D!",
"What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-D!",
"Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!",
"Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!",
"Try scrolling down in the mod select panel to find a bunch of new fun mods!",

View File

@ -189,7 +189,7 @@ namespace osu.Game.Screens.Menu
double remainingTime() => length - TransformDelay;
using (BeginDelayedSequence(250, true))
using (BeginDelayedSequence(250))
{
welcomeText.FadeIn(700);
welcomeText.TransformSpacingTo(new Vector2(20, 0), remainingTime(), Easing.Out);
@ -212,17 +212,17 @@ namespace osu.Game.Screens.Menu
lineBottomLeft.MoveTo(new Vector2(-line_end_offset, line_end_offset), line_duration, Easing.OutQuint);
lineBottomRight.MoveTo(new Vector2(line_end_offset, line_end_offset), line_duration, Easing.OutQuint);
using (BeginDelayedSequence(length * 0.56, true))
using (BeginDelayedSequence(length * 0.56))
{
bigRing.ResizeTo(logo_size, 500, Easing.InOutQuint);
bigRing.Foreground.Delay(250).ResizeTo(1, 850, Easing.OutQuint);
using (BeginDelayedSequence(250, true))
using (BeginDelayedSequence(250))
{
backgroundFill.ResizeHeightTo(1, remainingTime(), Easing.InOutQuart);
backgroundFill.RotateTo(-90, remainingTime(), Easing.InOutQuart);
using (BeginDelayedSequence(50, true))
using (BeginDelayedSequence(50))
{
foregroundFill.ResizeWidthTo(1, remainingTime(), Easing.InOutQuart);
foregroundFill.RotateTo(-90, remainingTime(), Easing.InOutQuart);
@ -239,19 +239,19 @@ namespace osu.Game.Screens.Menu
purpleCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart);
purpleCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart);
using (BeginDelayedSequence(appear_delay, true))
using (BeginDelayedSequence(appear_delay))
{
yellowCircle.MoveToY(-circle_size / 2, remainingTime(), Easing.InOutQuart);
yellowCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart);
yellowCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart);
using (BeginDelayedSequence(appear_delay, true))
using (BeginDelayedSequence(appear_delay))
{
blueCircle.MoveToX(-circle_size / 2, remainingTime(), Easing.InOutQuart);
blueCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart);
blueCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart);
using (BeginDelayedSequence(appear_delay, true))
using (BeginDelayedSequence(appear_delay))
{
pinkCircle.MoveToX(circle_size / 2, remainingTime(), Easing.InOutQuart);
pinkCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart);

View File

@ -172,27 +172,27 @@ namespace osu.Game.Screens.Menu
lazerLogo.Hide();
background.ApplyToBackground(b => b.Hide());
using (BeginAbsoluteSequence(0, true))
using (BeginAbsoluteSequence(0))
{
using (BeginDelayedSequence(text_1, true))
using (BeginDelayedSequence(text_1))
welcomeText.FadeIn().OnComplete(t => t.Text = "wel");
using (BeginDelayedSequence(text_2, true))
using (BeginDelayedSequence(text_2))
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome");
using (BeginDelayedSequence(text_3, true))
using (BeginDelayedSequence(text_3))
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to");
using (BeginDelayedSequence(text_4, true))
using (BeginDelayedSequence(text_4))
{
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!");
welcomeText.TransformTo(nameof(welcomeText.Spacing), new Vector2(50, 0), 5000);
}
using (BeginDelayedSequence(text_glitch, true))
using (BeginDelayedSequence(text_glitch))
triangles.FadeIn();
using (BeginDelayedSequence(rulesets_1, true))
using (BeginDelayedSequence(rulesets_1))
{
rulesetsScale.ScaleTo(0.8f, 1000);
rulesets.FadeIn().ScaleTo(1).TransformSpacingTo(new Vector2(200, 0));
@ -200,18 +200,18 @@ namespace osu.Game.Screens.Menu
triangles.FadeOut();
}
using (BeginDelayedSequence(rulesets_2, true))
using (BeginDelayedSequence(rulesets_2))
{
rulesets.ScaleTo(2).TransformSpacingTo(new Vector2(30, 0));
}
using (BeginDelayedSequence(rulesets_3, true))
using (BeginDelayedSequence(rulesets_3))
{
rulesets.ScaleTo(4).TransformSpacingTo(new Vector2(10, 0));
rulesetsScale.ScaleTo(1.3f, 1000);
}
using (BeginDelayedSequence(logo_1, true))
using (BeginDelayedSequence(logo_1))
{
rulesets.FadeOut();
@ -223,7 +223,7 @@ namespace osu.Game.Screens.Menu
logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad);
}
using (BeginDelayedSequence(logo_2, true))
using (BeginDelayedSequence(logo_2))
{
lazerLogo.FadeOut().OnComplete(_ =>
{

View File

@ -143,7 +143,7 @@ namespace osu.Game.Screens.Menu
{
base.LoadComplete();
using (BeginDelayedSequence(0, true))
using (BeginDelayedSequence(0))
{
scaleContainer.ScaleTo(0.9f).ScaleTo(1, delay_step_two).OnComplete(_ => Expire());
scaleContainer.FadeInFromZero(1800);

View File

@ -1,9 +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.
using System.Linq;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -22,6 +19,8 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
@ -120,7 +119,7 @@ namespace osu.Game.Screens.Menu
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 }
},
exitConfirmOverlay?.CreateProxy() ?? Drawable.Empty()
exitConfirmOverlay?.CreateProxy() ?? Empty()
});
buttons.StateChanged += state =>
@ -270,15 +269,11 @@ namespace osu.Game.Screens.Menu
if (!exitConfirmed && dialogOverlay != null)
{
if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog)
{
exitConfirmed = true;
exitDialog.Buttons.First().Click();
}
exitDialog.PerformOkAction();
else
{
dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort()));
return true;
}
return true;
}
buttons.State = ButtonSystemState.Exit;

View File

@ -72,8 +72,6 @@ namespace osu.Game.Screens.Menu
set => colourAndTriangles.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint);
}
public bool BeatMatching = true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => logoContainer.ReceivePositionalInputAt(screenSpacePos);
public bool Ripple
@ -272,8 +270,6 @@ namespace osu.Game.Screens.Menu
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!BeatMatching) return;
lastBeatIndex = beatIndex;
var beatLength = timingPoint.BeatLength;

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
@ -16,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
private readonly GameType type;
public string TooltipText => type.Name;
public LocalisableString TooltipText => type.Name;
public DrawableGameType(GameType type)
{

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Users;
@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
});
}
private class UserTile : CompositeDrawable, IHasTooltip
private class UserTile : CompositeDrawable
{
public User User
{
@ -99,8 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
set => avatar.User = value;
}
public string TooltipText => User?.Username ?? string.Empty;
private readonly UpdateableAvatar avatar;
public UserTile()
@ -116,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"27252d"),
},
avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both },
avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both },
};
}
}

View File

@ -84,10 +84,10 @@ namespace osu.Game.Screens.OnlinePlay.Components
private JoinRoomRequest currentJoinRoomRequest;
public virtual void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
public virtual void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = new JoinRoomRequest(room);
currentJoinRoomRequest = new JoinRoomRequest(room, password);
currentJoinRoomRequest.Success += () =>
{

View File

@ -0,0 +1,93 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Ranking.Expanded;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Components
{
public class StarRatingRangeDisplay : OnlinePlayComposite
{
[Resolved]
private OsuColour colours { get; set; }
private StarRatingDisplay minDisplay;
private Drawable minBackground;
private StarRatingDisplay maxDisplay;
private Drawable maxBackground;
public StarRatingRangeDisplay()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 1,
Children = new[]
{
minBackground = new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
},
maxBackground = new Box
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
minDisplay = new StarRatingDisplay(default),
maxDisplay = new StarRatingDisplay(default)
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged(updateRange, true);
}
private void updateRange(object sender, NotifyCollectionChangedEventArgs e)
{
var orderedDifficulties = Playlist.Select(p => p.Beatmap.Value).OrderBy(b => b.StarDifficulty).ToArray();
StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarDifficulty : 0, 0);
StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarDifficulty : 0, 0);
minDisplay.Current.Value = minDifficulty;
maxDisplay.Current.Value = maxDifficulty;
minBackground.Colour = colours.ForDifficultyRating(minDifficulty.DifficultyRating, true);
maxBackground.Colour = colours.ForDifficultyRating(maxDifficulty.DifficultyRating, true);
}
}
}

View File

@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) };
beatmapText.Clear();
beatmapText.AddLink(Item.Beatmap.ToString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString());
beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString());
authorText.Clear();
@ -202,7 +202,6 @@ namespace osu.Game.Screens.OnlinePlay
Child = modDisplay = new ModDisplay
{
Scale = new Vector2(0.4f),
DisplayUnrankedText = false,
ExpansionMode = ExpansionMode.AlwaysExpanded
}
}

View File

@ -31,7 +31,6 @@ namespace osu.Game.Screens.OnlinePlay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
DisplayUnrankedText = false,
Scale = new Vector2(0.8f),
ExpansionMode = ExpansionMode.AlwaysContracted,
});

View File

@ -25,14 +25,13 @@ namespace osu.Game.Screens.OnlinePlay
public new Func<Mod, bool> IsValidMod
{
get => base.IsValidMod;
set => base.IsValidMod = m => m.HasImplementation && !(m is ModAutoplay) && value(m);
set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value(m);
}
public FreeModSelectOverlay()
{
IsValidMod = m => true;
MultiplierSection.Alpha = 0;
DeselectAllButton.Alpha = 0;
Drawable selectAllButton;

View File

@ -6,6 +6,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
#nullable enable
namespace osu.Game.Screens.OnlinePlay
{
[Cached(typeof(IRoomManager))]
@ -32,15 +34,16 @@ namespace osu.Game.Screens.OnlinePlay
/// <param name="room">The <see cref="Room"/> to create.</param>
/// <param name="onSuccess">An action to be invoked if the creation succeeds.</param>
/// <param name="onError">An action to be invoked if an error occurred.</param>
void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null);
void CreateRoom(Room room, Action<Room>? onSuccess = null, Action<string>? onError = null);
/// <summary>
/// Joins a <see cref="Room"/>.
/// </summary>
/// <param name="room">The <see cref="Room"/> to join. <see cref="Room.RoomID"/> must be populated.</param>
/// <param name="password">An optional password to use for the join operation.</param>
/// <param name="onSuccess"></param>
/// <param name="onError"></param>
void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null);
void JoinRoom(Room room, string? password = null, Action<Room>? onSuccess = null, Action<string>? onError = null);
/// <summary>
/// Parts the currently-joined <see cref="Room"/>.

View File

@ -6,19 +6,25 @@ using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
@ -26,7 +32,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler<GlobalAction>
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
@ -39,7 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public event Action<SelectionState> StateChanged;
private readonly Box selectionBox;
private CachedModelDependencyContainer<Room> dependencies;
[Resolved(canBeNull: true)]
private OnlinePlayScreen parentScreen { get; set; }
@ -47,6 +52,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Bindable<Room> selectedRoom { get; set; }
[Resolved(canBeNull: true)]
private LoungeSubScreen lounge { get; set; }
public readonly Room Room;
private SelectionState state;
@ -92,6 +103,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public bool FilteringActive { get; set; }
private PasswordProtectedIcon passwordIcon;
private readonly Bindable<bool> hasPassword = new Bindable<bool>();
public DrawableRoom(Room room)
{
Room = room;
@ -201,6 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
},
},
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
},
},
@ -209,9 +225,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
dependencies = new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent));
dependencies.Model.Value = Room;
return dependencies;
return new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent))
{
Model = { Value = Room }
};
}
protected override void LoadComplete()
@ -222,10 +239,69 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
this.FadeInFromZero(transition_duration);
else
Alpha = 0;
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
}
public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join };
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
parentScreen?.OpenNewRoom(Room.DeepClone());
})
};
public bool OnPressed(GlobalAction action)
{
if (selectedRoom.Value != Room)
return false;
switch (action)
{
case GlobalAction.Select:
Click();
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (selectedRoom.Value != Room)
return true;
return base.OnMouseDown(e);
}
protected override bool OnClick(ClickEvent e)
{
if (Room != selectedRoom.Value)
{
selectedRoom.Value = Room;
return true;
}
if (Room.HasPassword.Value)
{
this.ShowPopover();
return true;
}
lounge?.Join(Room, null);
return base.OnClick(e);
}
private class RoomName : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
@ -238,12 +314,83 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
public class PasswordProtectedIcon : CompositeDrawable
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
parentScreen?.OpenNewRoom(Room.CreateCopy());
})
};
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
Size = new Vector2(32);
InternalChildren = new Drawable[]
{
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre,
Colour = colours.Gray5,
Rotation = 45,
RelativeSizeAxes = Axes.Both,
Width = 2,
},
new SpriteIcon
{
Icon = FontAwesome.Solid.Lock,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(6),
Size = new Vector2(14),
}
};
}
}
public class PasswordEntryPopover : OsuPopover
{
private readonly Room room;
public Action<Room, string> JoinRequested;
public PasswordEntryPopover(Room room)
{
this.room = room;
}
private OsuPasswordTextBox passwordTextbox;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Child = new FillFlowContainer
{
Margin = new MarginPadding(10),
Spacing = new Vector2(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
passwordTextbox = new OsuPasswordTextBox
{
Width = 200,
},
new TriangleButton
{
Width = 80,
Text = "Join Room",
Action = () => JoinRequested?.Invoke(room, passwordTextbox.Text)
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox));
}
}
}
}

View File

@ -24,8 +24,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomsContainer : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public Action<Room> JoinRequested;
private readonly IBindableList<Room> rooms = new BindableList<Room>();
private readonly FillFlowContainer<DrawableRoom> roomFlow;
@ -93,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
bool matchingFilter = true;
matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset));
matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset));
if (!string.IsNullOrEmpty(criteria.SearchString))
matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
@ -121,19 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
foreach (var room in rooms)
{
roomFlow.Add(new DrawableRoom(room)
{
Action = () =>
{
if (room == selectedRoom.Value)
{
joinSelected();
return;
}
selectRoom(room);
}
});
roomFlow.Add(new DrawableRoom(room));
}
Filter(filter?.Value);
@ -150,7 +136,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.Remove(toRemove);
selectRoom(null);
selectedRoom.Value = null;
}
}
@ -160,18 +146,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.SetLayoutPosition(room, room.Room.Position.Value);
}
private void selectRoom(Room room) => selectedRoom.Value = room;
private void joinSelected()
{
if (selectedRoom.Value == null) return;
JoinRequested?.Invoke(selectedRoom.Value);
}
protected override bool OnClick(ClickEvent e)
{
selectRoom(null);
selectedRoom.Value = null;
return base.OnClick(e);
}
@ -181,10 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
switch (action)
{
case GlobalAction.Select:
joinSelected();
return true;
case GlobalAction.SelectNext:
beginRepeatSelection(() => selectNext(1), action);
return true;
@ -253,7 +226,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
// we already have a valid selection only change selection if we still have a room to switch to.
if (room != null)
selectRoom(room);
selectedRoom.Value = room;
}
#endregion

View File

@ -6,6 +6,7 @@ using System.Linq;
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;
@ -46,10 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
private RoomsContainer roomsContainer;
[BackgroundDependencyLoader]
private void load()
{
RoomsContainer roomsContainer;
OsuScrollContainer scrollContainer;
InternalChildren = new Drawable[]
@ -70,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Padding = new MarginPadding(10),
Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested }
Child = roomsContainer = new RoomsContainer()
},
loadingLayer = new LoadingLayer(true),
}
@ -150,31 +152,39 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
onReturning();
}
private void onReturning()
{
filter.HoldFocus = true;
}
public override bool OnExiting(IScreen next)
{
filter.HoldFocus = false;
onLeaving();
return base.OnExiting(next);
}
public override void OnSuspending(IScreen next)
{
onLeaving();
base.OnSuspending(next);
filter.HoldFocus = false;
}
private void joinRequested(Room room)
private void onReturning()
{
filter.HoldFocus = true;
}
private void onLeaving()
{
filter.HoldFocus = false;
// ensure any password prompt is dismissed.
this.HidePopover();
}
public void Join(Room room, string password)
{
if (joiningRoomOperation != null)
return;
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
RoomManager?.JoinRoom(room, r =>
RoomManager?.JoinRoom(room, password, r =>
{
Open(room);
joiningRoomOperation?.Dispose();

View File

@ -25,8 +25,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
private void load()
{
Masking = true;
Add(Settings = CreateSettings());
}
protected abstract OnlinePlayComposite CreateSettings();
protected override void PopIn()
{
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);

View File

@ -17,7 +17,6 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Match
{
@ -73,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Depth = float.MinValue,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
@ -148,12 +147,22 @@ namespace osu.Game.Screens.OnlinePlay.Match
return base.OnExiting(next);
}
protected void StartPlay(Func<Player> player)
protected void StartPlay()
{
sampleStart?.Play();
ParentScreen?.Push(new PlayerLoader(player));
// fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
var targetScreen = (Screen)ParentScreen ?? this;
targetScreen.Push(CreateGameplayScreen());
}
/// <summary>
/// Creates the gameplay screen to be entered.
/// </summary>
/// <returns>The screen to enter.</returns>
protected abstract Screen CreateGameplayScreen();
private void selectedItemChanged()
{
updateWorkingBeatmap();

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private IBindable<bool> operationInProgress;
[Resolved]
private StatefulMultiplayerClient multiplayerClient { get; set; }
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }

View File

@ -27,16 +27,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay
{
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
SettingsApplied = Hide
};
}
protected class MatchSettings : OnlinePlayComposite
{
@ -47,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public OsuTextBox PasswordTextBox;
public TriangleButton ApplyButton;
public OsuSpriteText ErrorText;
@ -59,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private IRoomManager manager { get; set; }
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
@ -93,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Distributed),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
@ -193,12 +191,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
},
new Section("Password (optional)")
{
Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox
Child = PasswordTextBox = new SettingsPasswordTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
}
@ -275,6 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true);
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true);
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(v =>
@ -307,7 +304,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
{
client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() =>
client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value);
@ -320,6 +317,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
currentRoom.Value.Name.Value = NameField.Text;
currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value;
currentRoom.Value.Type.Value = TypePicker.Current.Value;
currentRoom.Value.Password.Value = PasswordTextBox.Current.Value;
if (int.TryParse(MaxParticipantsField.Text, out int max))
currentRoom.Value.MaxParticipants.Value = max;

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -72,25 +71,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
var localUser = Client.LocalUser;
if (localUser == null)
return;
int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0;
int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0;
Debug.Assert(Room != null);
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countText = $"({newCountReady} / {newCountTotal} ready)";
switch (localUser.State)
switch (localUser?.State)
{
case MultiplayerUserState.Idle:
default:
button.Text = "Ready";
updateButtonColour(true);
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
string countText = $"({newCountReady} / {newCountTotal} ready)";
if (Room?.Host?.Equals(localUser) == true)
{
button.Text = $"Start match {countText}";
@ -108,7 +102,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser.State == MultiplayerUserState.Spectating)
if (localUser?.State == MultiplayerUserState.Spectating)
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
button.Enabled.Value = enableButton;

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -57,14 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void updateState()
{
var localUser = Client.LocalUser;
if (localUser == null)
return;
Debug.Assert(Room != null);
switch (localUser.State)
switch (Client.LocalUser?.State)
{
default:
button.Text = "Spectate";
@ -81,7 +73,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
button.Enabled.Value = Client.Room != null
&& Client.Room.State != MultiplayerRoomState.Closed
&& !operationInProgress.Value;
}
private class ButtonWithTrianglesExposed : TriangleButton

View File

@ -15,13 +15,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public class Multiplayer : OnlinePlayScreen
{
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
if (client.Room != null)
if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating)
client.ChangeState(MultiplayerUserState.Idle);
}
@ -54,12 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})");
}
protected override Room CreateNewRoom()
{
var room = new Room { Name = { Value = $"{API.LocalUser}'s awesome room" } };
room.Category.Value = RoomCategory.Realtime;
return room;
}
protected override Room CreateNewRoom() =>
new Room
{
Name = { Value = $"{API.LocalUser}'s awesome room" },
Category = { Value = RoomCategory.Realtime }
};
protected override string ScreenTitle => "Multiplayer";

View File

@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
public override void Open(Room room)
{

View File

@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public class MultiplayerMatchSongSelect : OnlinePlaySongSelect
{
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
private LoadingLayer loadingLayer;

View File

@ -14,6 +14,7 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@ -25,6 +26,8 @@ using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osuTK;
@ -40,11 +43,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override string ShortTitle => "room";
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
private MultiplayerMatchSettingsOverlay settingsOverlay;
private readonly IBindable<bool> isConnected = new Bindable<bool>();
@ -182,7 +188,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DisplayUnrankedText = false,
Current = UserMods,
Scale = new Vector2(0.8f),
},
@ -271,6 +276,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!connected.NewValue)
Schedule(this.Exit);
}, true);
currentRoom.BindValueChanged(room =>
{
if (room.NewValue == null)
{
// the room has gone away.
// this could mean something happened during the join process, or an external connection issue occurred.
// one specific scenario is where the underlying room is created, but the signalr server returns an error during the join process. this triggers a PartRoom operation (see https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs#L97)
Schedule(this.Exit);
}
}, true);
}
protected override void UpdateMods()
@ -303,18 +319,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
return base.OnBackButton();
}
public override bool OnExiting(IScreen next)
{
// the room may not be left immediately after a disconnection due to async flow,
// so checking the IsConnected status is also required.
if (client.Room == null || !client.IsConnected.Value)
{
// room has not been created yet; exit immediately.
return base.OnExiting(next);
}
if (!exitConfirmed && dialogOverlay != null)
{
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
confirmDialog.PerformOkAction();
else
{
exitConfirmed = true;
this.Exit();
}));
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
{
exitConfirmed = true;
this.Exit();
}));
}
return true;
}
return base.OnBackButton();
return base.OnExiting(next);
}
private ModSettingChangeTracker modSettingChangeTracker;
@ -353,10 +387,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
client.ChangeBeatmapAvailability(availability.NewValue);
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable()
&& client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
if (availability.NewValue.State != DownloadState.LocallyAvailable)
{
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
if (client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
else
{
if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
onLoadRequested();
}
}
private void onReadyClick()
@ -407,22 +448,46 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onRoomUpdated()
{
// user mods may have changed.
Scheduler.AddOnce(UpdateMods);
}
private void onLoadRequested()
{
Debug.Assert(client.Room != null);
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
return;
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
// In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session.
// For now, we want to game to switch to the new game so need to request exiting from the play screen.
if (!ParentScreen.IsCurrentScreen())
{
ParentScreen.MakeCurrent();
StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
Schedule(onLoadRequested);
return;
}
StartPlay();
readyClickOperation?.Dispose();
readyClickOperation = null;
}
protected override Screen CreateGameplayScreen()
{
Debug.Assert(client.LocalUser != null);
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
return new MultiSpectatorScreen(userIds);
default:
return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override bool CheckModsAllowFailure() => false;
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
private IBindable<bool> isConnected;
@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
AllowPause = false,
AllowRestart = false,
AllowSkippingIntro = false,
AllowSkipping = false,
})
{
this.userIds = userIds;
@ -125,9 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
const float padding = 44; // enough margin to avoid the hit error display.
leaderboard.Position = new Vector2(
padding,
padding + HUDOverlay.TopScoringElementsHeight);
leaderboard.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
}
private void onMatchStarted() => Scheduler.Add(() =>

View File

@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected MultiplayerRoom Room => Client.Room;
[Resolved]
protected StatefulMultiplayerClient Client { get; private set; }
protected MultiplayerClient Client { get; private set; }
protected override void LoadComplete()
{

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public class MultiplayerRoomManager : RoomManager
{
[Resolved]
private StatefulMultiplayerClient multiplayerClient { get; set; }
private MultiplayerClient multiplayerClient { get; set; }
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password.Value, onSuccess, onError), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
if (!multiplayerClient.IsConnected.Value)
{
@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError);
}
public override void PartRoom()
@ -79,11 +79,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
});
}
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
private void joinMultiplayerRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null)
{
Debug.Assert(room.RoomID.Value != null);
multiplayerClient.JoinRoom(room).ContinueWith(t =>
multiplayerClient.JoinRoom(room, password).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully)
Schedule(() => onSuccess?.Invoke(room));

View File

@ -139,7 +139,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
DisplayUnrankedText = false,
}
},
userStateDisplay = new StateDisplay

View File

@ -10,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
public class ParticipantsListHeader : OverlinedHeader
{
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private MultiplayerClient client { get; set; }
public ParticipantsListHeader()
: base("Participants")

View File

@ -0,0 +1,88 @@
// 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 enable
using System;
using osu.Framework.Bindables;
using osu.Framework.Timing;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A <see cref="ISpectatorPlayerClock"/> which catches up using rate adjustment.
/// </summary>
public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock
{
/// <summary>
/// The catch up rate.
/// </summary>
public const double CATCHUP_RATE = 2;
/// <summary>
/// The source clock.
/// </summary>
public IFrameBasedClock? Source { get; set; }
public double CurrentTime { get; private set; }
public bool IsRunning { get; private set; }
public void Reset() => CurrentTime = 0;
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;
public bool Seek(double position)
{
CurrentTime = position;
return true;
}
public void ResetSpeedAdjustments()
{
}
public double Rate => IsCatchingUp ? CATCHUP_RATE : 1;
double IAdjustableClock.Rate
{
get => Rate;
set => throw new NotSupportedException();
}
double IClock.Rate => Rate;
public void ProcessFrame()
{
ElapsedFrameTime = 0;
FramesPerSecond = 0;
if (Source == null)
return;
Source.ProcessFrame();
if (IsRunning)
{
double elapsedSource = Source.ElapsedFrameTime;
double elapsed = elapsedSource * Rate;
CurrentTime += elapsed;
ElapsedFrameTime = elapsed;
FramesPerSecond = Source.FramesPerSecond;
}
}
public double ElapsedFrameTime { get; private set; }
public double FramesPerSecond { get; private set; }
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
public bool IsCatchingUp { get; set; }
}
}

View File

@ -0,0 +1,168 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Timing;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A <see cref="ISyncManager"/> which synchronises de-synced player clocks through catchup.
/// </summary>
public class CatchUpSyncManager : Component, ISyncManager
{
/// <summary>
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
/// </summary>
public const double SYNC_TARGET = 16;
/// <summary>
/// The offset from the master clock at which player clocks begin resynchronising.
/// </summary>
public const double MAX_SYNC_OFFSET = 50;
/// <summary>
/// The maximum delay to start gameplay, if any (but not all) player clocks are ready.
/// </summary>
public const double MAXIMUM_START_DELAY = 15000;
public event Action ReadyToStart;
/// <summary>
/// The master clock which is used to control the timing of all player clocks clocks.
/// </summary>
public IAdjustableClock MasterClock { get; }
public IBindable<MasterClockState> MasterState => masterState;
/// <summary>
/// The player clocks.
/// </summary>
private readonly List<ISpectatorPlayerClock> playerClocks = new List<ISpectatorPlayerClock>();
private readonly Bindable<MasterClockState> masterState = new Bindable<MasterClockState>();
private bool hasStarted;
private double? firstStartAttemptTime;
public CatchUpSyncManager(IAdjustableClock master)
{
MasterClock = master;
}
public void AddPlayerClock(ISpectatorPlayerClock clock)
{
Debug.Assert(!playerClocks.Contains(clock));
playerClocks.Add(clock);
}
public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock);
protected override void Update()
{
base.Update();
if (!attemptStart())
{
// Ensure all player clocks are stopped until the start succeeds.
foreach (var clock in playerClocks)
clock.Stop();
return;
}
updatePlayerCatchup();
updateMasterState();
}
/// <summary>
/// Attempts to start playback. Waits for all player clocks to have available frames for up to <see cref="MAXIMUM_START_DELAY"/> milliseconds.
/// </summary>
/// <returns>Whether playback was started and syncing should occur.</returns>
private bool attemptStart()
{
if (hasStarted)
return true;
if (playerClocks.Count == 0)
return false;
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
if (readyCount == playerClocks.Count)
return performStart();
if (readyCount > 0)
{
firstStartAttemptTime ??= Time.Current;
if (Time.Current - firstStartAttemptTime > MAXIMUM_START_DELAY)
return performStart();
}
bool performStart()
{
ReadyToStart?.Invoke();
return hasStarted = true;
}
return false;
}
/// <summary>
/// Updates the catchup states of all player clocks clocks.
/// </summary>
private void updatePlayerCatchup()
{
for (int i = 0; i < playerClocks.Count; i++)
{
var clock = playerClocks[i];
// How far this player's clock is out of sync, compared to the master clock.
// A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up).
double timeDelta = MasterClock.CurrentTime - clock.CurrentTime;
// Check that the player clock isn't too far ahead.
// This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock.
if (timeDelta < -SYNC_TARGET)
{
// Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock
// when it is required to be running (ie. if all players are ahead of the master).
clock.IsCatchingUp = false;
clock.Stop();
continue;
}
// Make sure the player clock is running if it can.
if (!clock.WaitingOnFrames.Value)
clock.Start();
if (clock.IsCatchingUp)
{
// Stop the player clock from catching up if it's within the sync target.
if (timeDelta <= SYNC_TARGET)
clock.IsCatchingUp = false;
}
else
{
// Make the player clock start catching up if it's exceeded the maximum allowable sync offset.
if (timeDelta > MAX_SYNC_OFFSET)
clock.IsCatchingUp = true;
}
}
}
/// <summary>
/// Updates the state of the master clock.
/// </summary>
private void updateMasterState()
{
bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Timing;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A clock which is used by <see cref="MultiSpectatorPlayer"/>s and managed by an <see cref="ISyncManager"/>.
/// </summary>
public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
{
/// <summary>
/// Whether this clock is waiting on frames to continue playback.
/// </summary>
Bindable<bool> WaitingOnFrames { get; }
/// <summary>
/// Whether this clock is resynchronising to the master clock.
/// </summary>
bool IsCatchingUp { get; set; }
/// <summary>
/// The source clock
/// </summary>
IFrameBasedClock Source { set; }
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Timing;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// Manages the synchronisation between one or more <see cref="ISpectatorPlayerClock"/>s in relation to a master clock.
/// </summary>
public interface ISyncManager
{
/// <summary>
/// An event which is invoked when gameplay is ready to start.
/// </summary>
event Action ReadyToStart;
/// <summary>
/// The master clock which player clocks should synchronise to.
/// </summary>
IAdjustableClock MasterClock { get; }
/// <summary>
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
/// </summary>
IBindable<MasterClockState> MasterState { get; }
/// <summary>
/// Adds an <see cref="ISpectatorPlayerClock"/> to manage.
/// </summary>
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to add.</param>
void AddPlayerClock(ISpectatorPlayerClock clock);
/// <summary>
/// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>.
/// </summary>
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param>
void RemovePlayerClock(ISpectatorPlayerClock clock);
}
}

View 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.
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public enum MasterClockState
{
/// <summary>
/// The master clock is synchronised with at least one player clock.
/// </summary>
Synchronised,
/// <summary>
/// The master clock is too far ahead of any player clock and needs to slow down.
/// </summary>
TooFarAhead
}
}

View File

@ -9,9 +9,9 @@ using osu.Game.Screens.Play.HUD;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
{
public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds)
: base(scoreProcessor, userIds)
{
}
@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public void AddClock(int userId, IClock clock)
{
if (!UserScores.TryGetValue(userId, out var data))
return;
throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId));
((SpectatingTrackedUserData)data).Clock = clock;
}
@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public void RemoveClock(int userId)
{
if (!UserScores.TryGetValue(userId, out var data))
return;
throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId));
((SpectatingTrackedUserData)data).Clock = null;
}

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A single spectated player within a <see cref="MultiSpectatorScreen"/>.
/// </summary>
public class MultiSpectatorPlayer : SpectatorPlayer
{
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
private readonly ISpectatorPlayerClock spectatorPlayerClock;
/// <summary>
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
/// </summary>
/// <param name="score">The score containing the player's replay.</param>
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param>
public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock)
: base(score)
{
this.spectatorPlayerClock = spectatorPlayerClock;
}
[BackgroundDependencyLoader]
private void load()
{
spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
=> new SpectatorGameplayClockContainer(spectatorPlayerClock);
private class SpectatorGameplayClockContainer : GameplayClockContainer
{
public SpectatorGameplayClockContainer([NotNull] IClock sourceClock)
: base(sourceClock)
{
}
protected override void Update()
{
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay.
if (SourceClock.IsRunning)
Start();
else
Stop();
base.Update();
}
protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source);
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// Used to load a single <see cref="MultiSpectatorPlayer"/> in a <see cref="MultiSpectatorScreen"/>.
/// </summary>
public class MultiSpectatorPlayerLoader : SpectatorPlayerLoader
{
public MultiSpectatorPlayerLoader([NotNull] Score score, [NotNull] Func<MultiSpectatorPlayer> createPlayer)
: base(score, createPlayer)
{
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
}
protected override void LogoExiting(OsuLogo logo)
{
}
}
}

View File

@ -0,0 +1,196 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Play;
using osu.Game.Screens.Spectate;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A <see cref="SpectatorScreen"/> that spectates multiple users in a match.
/// </summary>
public class MultiSpectatorScreen : SpectatorScreen
{
// Isolates beatmap/ruleset to this screen.
public override bool DisallowExternalBeatmapRulesetChanges => true;
// We are managing our own adjustments. For now, this happens inside the Player instances themselves.
public override bool AllowRateAdjustments => false;
/// <summary>
/// Whether all spectating players have finished loading.
/// </summary>
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
[Resolved]
private SpectatorClient spectatorClient { get; set; }
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer;
private ISyncManager syncManager;
private PlayerGrid grid;
private MultiSpectatorLeaderboard leaderboard;
private PlayerArea currentAudioSource;
private bool canStartMasterClock;
/// <summary>
/// Creates a new <see cref="MultiSpectatorScreen"/>.
/// </summary>
/// <param name="userIds">The players to spectate.</param>
public MultiSpectatorScreen(int[] userIds)
: base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray())
{
instances = new PlayerArea[UserIds.Count];
}
[BackgroundDependencyLoader]
private void load()
{
Container leaderboardContainer;
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0);
InternalChildren = new[]
{
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
masterClockContainer.WithChild(new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
leaderboardContainer = new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
}
}
})
};
for (int i = 0; i < UserIds.Count; i++)
{
grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock));
syncManager.AddPlayerClock(instances[i].GameplayClock);
}
// Todo: This is not quite correct - it should be per-user to adjust for other mod combinations.
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray())
{
Expanded = { Value = true },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}, l =>
{
foreach (var instance in instances)
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
leaderboardContainer.Add(leaderboard);
});
}
protected override void LoadComplete()
{
base.LoadComplete();
masterClockContainer.Reset();
masterClockContainer.Stop();
syncManager.ReadyToStart += onReadyToStart;
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
}
protected override void Update()
{
base.Update();
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
{
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock))
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime))
.FirstOrDefault();
foreach (var instance in instances)
instance.Mute = instance != currentAudioSource;
}
}
private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
private void onReadyToStart()
{
// Seek the master clock to the gameplay time.
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
var startTime = instances.Where(i => i.Score != null)
.SelectMany(i => i.Score.Replay.Frames)
.Select(f => f.Time)
.DefaultIfEmpty(0)
.Min();
masterClockContainer.Seek(startTime);
masterClockContainer.Start();
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
canStartMasterClock = true;
}
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
{
switch (state.NewValue)
{
case MasterClockState.Synchronised:
if (canStartMasterClock)
masterClockContainer.Start();
break;
case MasterClockState.TooFarAhead:
masterClockContainer.Stop();
break;
}
}
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
{
}
protected override void StartGameplay(int userId, GameplayState gameplayState)
=> instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score);
protected override void EndGameplay(int userId)
{
RemoveUser(userId);
leaderboard.RemoveClock(userId);
}
public override bool OnBackButton()
{
// On a manual exit, set the player state back to idle.
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
return base.OnBackButton();
}
}
}

View File

@ -0,0 +1,144 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// Provides an area for and manages the hierarchy of a spectated player within a <see cref="MultiSpectatorScreen"/>.
/// </summary>
public class PlayerArea : CompositeDrawable
{
/// <summary>
/// Whether a <see cref="Player"/> is loaded in the area.
/// </summary>
public bool PlayerLoaded => (stack?.CurrentScreen as Player)?.IsLoaded == true;
/// <summary>
/// The user id this <see cref="PlayerArea"/> corresponds to.
/// </summary>
public readonly int UserId;
/// <summary>
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
/// </summary>
[NotNull]
public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock();
/// <summary>
/// The currently-loaded score.
/// </summary>
[CanBeNull]
public Score Score { get; private set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer;
private OsuScreenStack stack;
public PlayerArea(int userId, IFrameBasedClock masterClock)
{
UserId = userId;
RelativeSizeAxes = Axes.Both;
Masking = true;
AudioContainer audioContainer;
InternalChildren = new Drawable[]
{
audioContainer = new AudioContainer
{
RelativeSizeAxes = Axes.Both,
Child = gameplayContent = new DrawSizePreservingFillContainer { RelativeSizeAxes = Axes.Both },
},
loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } }
};
audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
GameplayClock.Source = masterClock;
}
public void LoadScore([NotNull] Score score)
{
if (Score != null)
throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score.");
Score = score;
gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
{
RelativeSizeAxes = Axes.Both,
Child = stack = new OsuScreenStack()
};
stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock)));
loadingLayer.Hide();
}
private bool mute = true;
public bool Mute
{
get => mute;
set
{
mute = value;
volumeAdjustment.Value = value ? 0 : 1;
}
}
// Player interferes with global input, so disable input for now.
public override bool PropagatePositionalInputSubTree => false;
public override bool PropagateNonPositionalInputSubTree => false;
/// <summary>
/// Isolates each player instance from the game-wide ruleset/beatmap/mods (to allow for different players having different settings).
/// </summary>
private class PlayerIsolationContainer : Container
{
[Cached]
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
[Cached]
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
[Cached]
private readonly Bindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>();
public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList<Mod> mods)
{
this.beatmap.Value = beatmap;
this.ruleset.Value = ruleset;
this.mods.Value = mods;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(ruleset.BeginLease(false));
dependencies.CacheAs(beatmap.BeginLease(false));
dependencies.CacheAs(mods.BeginLease(false));
return dependencies;
}
}
}
}

View File

@ -15,6 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public partial class PlayerGrid : CompositeDrawable
{
/// <summary>
/// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen.
/// Todo: Can be removed in the future with scrolling support + performance improvements.
/// </summary>
public const int MAX_PLAYERS = 16;
private const float player_spacing = 5;
/// <summary>
@ -58,11 +64,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// Adds a new cell with content to this grid.
/// </summary>
/// <param name="content">The content the cell should contain.</param>
/// <exception cref="InvalidOperationException">If more than 16 cells are added.</exception>
/// <exception cref="InvalidOperationException">If more than <see cref="MAX_PLAYERS"/> cells are added.</exception>
public void Add(Drawable content)
{
if (cellContainer.Count == 16)
throw new InvalidOperationException("Only 16 cells are supported.");
if (cellContainer.Count == MAX_PLAYERS)
throw new InvalidOperationException($"Only {MAX_PLAYERS} cells are supported.");
int index = cellContainer.Count;

View File

@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<RoomAvailability> Availability { get; private set; }
[Resolved(typeof(Room), nameof(Room.Password))]
public Bindable<string> Password { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<TimeSpan?> Duration { get; private set; }

View File

@ -240,20 +240,25 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(IScreen next)
{
if (screenStack.CurrentScreen?.OnExiting(next) == true)
return true;
RoomManager.PartRoom();
waves.Hide();
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
screenStack.CurrentScreen?.OnExiting(next);
base.OnExiting(next);
return false;
}
public override bool OnBackButton()
{
if ((screenStack.CurrentScreen as IOnlinePlaySubScreen)?.OnBackButton() == true)
if (!(screenStack.CurrentScreen is IOnlinePlaySubScreen onlineSubScreen))
return false;
if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowBackButton && onlineSubScreen.OnBackButton())
return true;
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))

View File

@ -72,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay
// 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.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
Mods.BindValueChanged(onModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);
@ -96,16 +96,20 @@ namespace osu.Game.Screens.OnlinePlay
{
itemSelected = true;
var item = new PlaylistItem();
var item = new PlaylistItem
{
Beatmap =
{
Value = Beatmap.Value.BeatmapInfo
},
Ruleset =
{
Value = Ruleset.Value
}
};
item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone()));
SelectItem(item);
return true;
@ -157,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a valid mod for online play.</returns>
protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && !ModUtils.FlattenMod(mod).Any(m => m is ModAutoplay);
protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable);
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid for per-player free-mod selection.

View File

@ -26,16 +26,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
public Action EditPlaylist;
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
EditPlaylist = () => EditPlaylist?.Invoke()
};
}
protected class MatchSettings : OnlinePlayComposite
{
@ -75,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Distributed),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Screens;
@ -54,11 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
protected override Score CreateScore()
protected override async Task PrepareScoreForResultsAsync(Score score)
{
var score = base.CreateScore();
score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
return score;
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
}
protected override void Dispose(bool isDisposing)

View File

@ -13,6 +13,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osuTK;
@ -174,7 +175,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DisplayUnrankedText = false,
Current = UserMods,
Scale = new Vector2(0.8f),
},
@ -218,10 +218,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
new Drawable[]
{
new Footer
{
OnStart = onStart,
}
new Footer { OnStart = StartPlay }
}
},
RowDimensions = new[]
@ -274,7 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, true);
}
private void onStart() => StartPlay(() => new PlaylistsPlayer(SelectedItem.Value)
protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(SelectedItem.Value)
{
Exited = () => leaderboard.RefreshScores()
});

View File

@ -55,10 +55,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone()));
}
}
}

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