mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into refactor-combo-colour-retrieval
This commit is contained in:
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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!";
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
83
osu.Game/Screens/Edit/Compose/HitObjectUsageEventBuffer.cs
Normal file
83
osu.Game/Screens/Edit/Compose/HitObjectUsageEventBuffer.cs
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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()
|
||||
|
@ -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);
|
44
osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs
Normal file
44
osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs
Normal 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();
|
||||
}
|
||||
}
|
61
osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
Normal file
61
osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
comboColours = new LabelledColourPalette
|
||||
{
|
||||
Label = "Hitcircle / Slider Combos",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
ColourNamePrefix = "Combo"
|
||||
}
|
||||
};
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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,
|
||||
|
20
osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs
Normal file
20
osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>();
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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>();
|
||||
|
27
osu.Game/Screens/Edit/Verify/InterpretationSection.cs
Normal file
27
osu.Game/Screens/Edit/Verify/InterpretationSection.cs
Normal 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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
115
osu.Game/Screens/Edit/Verify/IssueList.cs
Normal file
115
osu.Game/Screens/Edit/Verify/IssueList.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
osu.Game/Screens/Edit/Verify/VisibilitySection.cs
Normal file
54
osu.Game/Screens/Edit/Verify/VisibilitySection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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!",
|
||||
|
@ -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);
|
||||
|
@ -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(_ =>
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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 += () =>
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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"/>.
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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(() =>
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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));
|
||||
|
@ -139,7 +139,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
Scale = new Vector2(0.5f),
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
DisplayUnrankedText = false,
|
||||
}
|
||||
},
|
||||
userStateDisplay = new StateDisplay
|
||||
|
@ -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")
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
144
osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
Normal file
144
osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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[]
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
});
|
||||
|
@ -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
Reference in New Issue
Block a user