Merge branch 'master' into osu-distance-spacing

This commit is contained in:
Salman Ahmed
2022-04-24 05:23:30 +03:00
791 changed files with 21280 additions and 6782 deletions

View File

@ -48,7 +48,7 @@ namespace osu.Game.Screens
Scale = new Vector2(1 + x_movement_amount / DrawSize.X * 2);
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
if (animateOnEnter)
{
@ -59,16 +59,16 @@ namespace osu.Game.Screens
this.MoveToX(0, TRANSITION_LENGTH, Easing.InOutQuart);
}
base.OnEntering(last);
base.OnEntering(e);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
this.MoveToX(-x_movement_amount, TRANSITION_LENGTH, Easing.InOutQuart);
base.OnSuspending(next);
base.OnSuspending(e);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
if (IsLoaded)
{
@ -76,14 +76,14 @@ namespace osu.Game.Screens
this.MoveToX(x_movement_amount, TRANSITION_LENGTH, Easing.OutExpo);
}
return base.OnExiting(next);
return base.OnExiting(e);
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
if (IsLoaded)
this.MoveToX(0, TRANSITION_LENGTH, Easing.OutExpo);
base.OnResuming(last);
base.OnResuming(e);
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Backgrounds
};
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
Show();
}

View File

@ -1,46 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit
{
public class BindableBeatDivisor : BindableInt
{
public static readonly int[] VALID_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 };
public static readonly int[] PREDEFINED_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 };
public Bindable<BeatDivisorPresetCollection> ValidDivisors { get; } = new Bindable<BeatDivisorPresetCollection>(BeatDivisorPresetCollection.COMMON);
public BindableBeatDivisor(int value = 1)
: base(value)
{
ValidDivisors.BindValueChanged(_ => updateBindableProperties(), true);
BindValueChanged(_ => ensureValidDivisor());
}
public void Next() => Value = VALID_DIVISORS[Math.Min(VALID_DIVISORS.Length - 1, Array.IndexOf(VALID_DIVISORS, Value) + 1)];
public void Previous() => Value = VALID_DIVISORS[Math.Max(0, Array.IndexOf(VALID_DIVISORS, Value) - 1)];
public override int Value
private void updateBindableProperties()
{
get => base.Value;
set
{
if (!VALID_DIVISORS.Contains(value))
{
// If it doesn't match, value will be 0, but will be clamped to the valid range via DefaultMinValue
value = Array.FindLast(VALID_DIVISORS, d => d < value);
}
ensureValidDivisor();
base.Value = value;
}
MinValue = ValidDivisors.Value.Presets.Min();
MaxValue = ValidDivisors.Value.Presets.Max();
}
private void ensureValidDivisor()
{
if (!ValidDivisors.Value.Presets.Contains(Value))
Value = 1;
}
public void Next()
{
var presets = ValidDivisors.Value.Presets;
Value = presets.Cast<int?>().SkipWhile(preset => preset != Value).ElementAtOrDefault(1) ?? presets[0];
}
public void Previous()
{
var presets = ValidDivisors.Value.Presets;
Value = presets.Cast<int?>().TakeWhile(preset => preset != Value).LastOrDefault() ?? presets[^1];
}
protected override int DefaultMinValue => VALID_DIVISORS.First();
protected override int DefaultMaxValue => VALID_DIVISORS.Last();
protected override int DefaultPrecision => 1;
public override void BindTo(Bindable<int> them)
{
// bind to valid divisors first (if applicable) to ensure correct transfer of the actual divisor.
if (them is BindableBeatDivisor otherBeatDivisor)
ValidDivisors.BindTo(otherBeatDivisor.ValidDivisors);
base.BindTo(them);
}
protected override Bindable<int> CreateInstance() => new BindableBeatDivisor();
/// <summary>
@ -92,7 +110,7 @@ namespace osu.Game.Screens.Edit
{
int beat = index % beatDivisor;
foreach (int divisor in BindableBeatDivisor.VALID_DIVISORS)
foreach (int divisor in PREDEFINED_DIVISORS)
{
if ((beat * divisor) % beatDivisor == 0)
return divisor;

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Components
{
/// <summary>
/// A sidebar area that can be attached to the left or right edge of the screen.
/// Houses scrolling sectionised content.
/// </summary>
internal class EditorSidebar : Container<EditorSidebarSection>
{
private readonly Box background;
protected override Container<EditorSidebarSection> Content { get; }
public EditorSidebar()
{
Width = 250;
RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
Padding = new MarginPadding { Left = 20 },
ScrollbarOverlapsContent = false,
RelativeSizeAxes = Axes.Both,
Child = Content = new FillFlowContainer<EditorSidebarSection>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
},
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background5;
}
}
}

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Components
{
public class EditorSidebarSection : Container
{
protected override Container<Drawable> Content { get; }
public EditorSidebarSection(LocalisableString sectionName)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SectionHeader(sectionName),
Content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
}
};
}
public class SectionHeader : CompositeDrawable
{
private readonly LocalisableString text;
public SectionHeader(LocalisableString text)
{
this.text = text;
Margin = new MarginPadding { Vertical = 10, Horizontal = 5 };
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Children = new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold))
{
Text = text,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new Circle
{
Colour = colourProvider.Highlight1,
Size = new Vector2(28, 2),
}
}
};
}
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
{
public class EditorMenuBar : OsuMenu
{
public readonly Bindable<EditorScreenMode> Mode = new Bindable<EditorScreenMode>();
public EditorMenuBar()
: base(Direction.Horizontal, true)
{
@ -28,25 +25,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
MaskingContainer.CornerRadius = 0;
ItemsContainer.Padding = new MarginPadding { Left = 100 };
BackgroundColour = Color4Extensions.FromHex("111");
ScreenSelectionTabControl tabControl;
AddRangeInternal(new Drawable[]
{
tabControl = new ScreenSelectionTabControl
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -15
}
});
Mode.BindTo(tabControl.Current);
}
protected override void LoadComplete()
{
base.LoadComplete();
Mode.TriggerChange();
}
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu();

View File

@ -2,19 +2,26 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using Humanizer;
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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
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 osuTK;
using osuTK.Graphics;
using osuTK.Input;
@ -62,7 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
new TickSliderBar(beatDivisor, BindableBeatDivisor.VALID_DIVISORS)
new TickSliderBar(beatDivisor)
{
RelativeSizeAxes = Axes.Both,
}
@ -84,7 +91,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -92,13 +98,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
new Drawable[]
{
new DivisorButton
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronLeft,
Action = beatDivisor.Previous
},
new DivisorText(beatDivisor),
new DivisorButton
new DivisorDisplay { BeatDivisor = { BindTarget = beatDivisor } },
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronRight,
Action = beatDivisor.Next
@ -121,49 +127,233 @@ namespace osu.Game.Screens.Edit.Compose.Components
new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
{
Padding = new MarginPadding { Horizontal = 15 },
Text = "beat snap divisor",
Text = "beat snap",
RelativeSizeAxes = Axes.X,
TextAnchor = Anchor.TopCentre
},
}
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4
},
new Container
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronLeft,
Action = () => cycleDivisorType(-1)
},
new DivisorTypeText { BeatDivisor = { BindTarget = beatDivisor } },
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronRight,
Action = () => cycleDivisorType(1)
}
},
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20)
}
}
}
}
}
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 30),
new Dimension(GridSizeMode.Absolute, 25),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 15)
}
}
};
}
private class DivisorText : SpriteText
private void cycleDivisorType(int direction)
{
private readonly Bindable<int> beatDivisor = new Bindable<int>();
Debug.Assert(Math.Abs(direction) == 1);
int nextDivisorType = (int)beatDivisor.ValidDivisors.Value.Type + direction;
if (nextDivisorType > (int)BeatDivisorType.Triplets)
nextDivisorType = (int)BeatDivisorType.Common;
else if (nextDivisorType < (int)BeatDivisorType.Common)
nextDivisorType = (int)BeatDivisorType.Triplets;
public DivisorText(BindableBeatDivisor beatDivisor)
switch ((BeatDivisorType)nextDivisorType)
{
this.beatDivisor.BindTo(beatDivisor);
case BeatDivisorType.Common:
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
break;
case BeatDivisorType.Triplets:
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
break;
case BeatDivisorType.Custom:
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(beatDivisor.ValidDivisors.Value.Presets.Max());
break;
}
}
internal class DivisorDisplay : OsuAnimatedButton, IHasPopover
{
public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor();
private readonly OsuSpriteText divisorText;
public DivisorDisplay()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
Add(divisorText = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 20),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding
{
Horizontal = 5
}
});
Action = this.ShowPopover;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
divisorText.Colour = colours.BlueLighter;
}
protected override void LoadComplete()
{
base.LoadComplete();
beatDivisor.BindValueChanged(val => Text = $"1/{val.NewValue}", true);
updateState();
}
private void updateState()
{
BeatDivisor.BindValueChanged(val => divisorText.Text = $"1/{val.NewValue}", true);
}
public Popover GetPopover() => new CustomDivisorPopover
{
BeatDivisor = { BindTarget = BeatDivisor }
};
}
internal class CustomDivisorPopover : OsuPopover
{
public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor();
private readonly OsuNumberBox divisorTextBox;
public CustomDivisorPopover()
{
Child = new FillFlowContainer
{
Width = 150,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Children = new Drawable[]
{
divisorTextBox = new OsuNumberBox
{
RelativeSizeAxes = Axes.X,
PlaceholderText = "Beat divisor"
},
new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Related divisors will be added to the list of presets."
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
BeatDivisor.BindValueChanged(_ => updateState(), true);
divisorTextBox.OnCommit += (_, __) => setPresets();
Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox));
}
private void setPresets()
{
if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64)
{
updateState();
return;
}
if (!BeatDivisor.ValidDivisors.Value.Presets.Contains(divisor))
{
if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
else
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
}
BeatDivisor.Value = divisor;
this.HidePopover();
}
private void updateState()
{
divisorTextBox.Text = BeatDivisor.Value.ToString();
}
}
private class DivisorButton : IconButton
private class DivisorTypeText : OsuSpriteText
{
public DivisorButton()
public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor();
public DivisorTypeText()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Font = OsuFont.Default.With(size: 14);
}
protected override void LoadComplete()
{
base.LoadComplete();
BeatDivisor.ValidDivisors.BindValueChanged(val => Text = val.NewValue.Type.Humanize(LetterCasing.LowerCase), true);
}
}
internal class ChevronButton : IconButton
{
public ChevronButton()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -192,20 +382,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
private OsuColour colours { get; set; }
private readonly BindableBeatDivisor beatDivisor;
private readonly int[] availableDivisors;
public TickSliderBar(BindableBeatDivisor beatDivisor, params int[] divisors)
public TickSliderBar(BindableBeatDivisor beatDivisor)
{
CurrentNumber.BindTo(this.beatDivisor = beatDivisor);
availableDivisors = divisors;
Padding = new MarginPadding { Horizontal = 5 };
}
[BackgroundDependencyLoader]
private void load()
protected override void LoadComplete()
{
foreach (int t in availableDivisors)
base.LoadComplete();
beatDivisor.ValidDivisors.BindValueChanged(_ => updateDivisors(), true);
}
private void updateDivisors()
{
ClearInternal();
CurrentNumber.ValueChanged -= moveMarker;
foreach (int t in beatDivisor.ValidDivisors.Value.Presets)
{
AddInternal(new Tick
{
@ -218,17 +415,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
AddInternal(marker = new Marker());
CurrentNumber.ValueChanged += moveMarker;
CurrentNumber.TriggerChange();
}
protected override void LoadComplete()
private void moveMarker(ValueChangedEvent<int> divisor)
{
base.LoadComplete();
CurrentNumber.BindValueChanged(div =>
{
marker.MoveToX(getMappedPosition(div.NewValue), 100, Easing.OutQuint);
marker.Flash();
}, true);
marker.MoveToX(getMappedPosition(divisor.NewValue), 100, Easing.OutQuint);
marker.Flash();
}
protected override void UpdateValue(float value)
@ -289,11 +483,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
// copied from SliderBar so we can do custom spacing logic.
float xPosition = (ToLocalSpace(screenSpaceMousePosition).X - RangePadding) / UsableWidth;
CurrentNumber.Value = availableDivisors.OrderBy(d => Math.Abs(getMappedPosition(d) - xPosition)).First();
CurrentNumber.Value = beatDivisor.ValidDivisors.Value.Presets.OrderBy(d => Math.Abs(getMappedPosition(d) - xPosition)).First();
OnUserChange(Current.Value);
}
private float getMappedPosition(float divisor) => MathF.Pow((divisor - 1) / (availableDivisors.Last() - 1), 0.90f);
private float getMappedPosition(float divisor) => MathF.Pow((divisor - 1) / (beatDivisor.ValidDivisors.Value.Presets.Last() - 1), 0.90f);
private class Tick : CompositeDrawable
{

View File

@ -0,0 +1,41 @@
// 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;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class BeatDivisorPresetCollection
{
public BeatDivisorType Type { get; }
public IReadOnlyList<int> Presets { get; }
private BeatDivisorPresetCollection(BeatDivisorType type, IEnumerable<int> presets)
{
Type = type;
Presets = presets.ToArray();
}
public static readonly BeatDivisorPresetCollection COMMON = new BeatDivisorPresetCollection(BeatDivisorType.Common, new[] { 1, 2, 4, 8, 16 });
public static readonly BeatDivisorPresetCollection TRIPLETS = new BeatDivisorPresetCollection(BeatDivisorType.Triplets, new[] { 1, 3, 6, 12 });
public static BeatDivisorPresetCollection Custom(int maxDivisor)
{
var presets = new List<int>();
for (int candidate = 1; candidate <= Math.Sqrt(maxDivisor); ++candidate)
{
if (maxDivisor % candidate != 0)
continue;
presets.Add(candidate);
presets.Add(maxDivisor / candidate);
}
return new BeatDivisorPresetCollection(BeatDivisorType.Custom, presets.Distinct().OrderBy(d => d));
}
}
}

View File

@ -0,0 +1,23 @@
// 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.Edit.Compose.Components
{
public enum BeatDivisorType
{
/// <summary>
/// Most common divisors, all with denominators being powers of two.
/// </summary>
Common,
/// <summary>
/// Divisors with denominators divisible by 3.
/// </summary>
Triplets,
/// <summary>
/// Fully arbitrary/custom beat divisors.
/// </summary>
Custom,
}
}

View File

@ -73,6 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionHandler = CreateSelectionHandler();
SelectionHandler.DeselectAll = deselectAll;
SelectionHandler.SelectedItems.BindTo(SelectedItems);
AddRangeInternal(new[]
{

View File

@ -29,11 +29,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
SelectedItems.BindTo(EditorBeatmap.SelectedHitObjects);
SelectedItems.CollectionChanged += (sender, args) =>
{
Scheduler.AddOnce(UpdateTernaryStates);
};
SelectedItems.CollectionChanged += (sender, args) => Scheduler.AddOnce(UpdateTernaryStates);
}
protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items);

View File

@ -17,6 +17,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
@ -358,7 +359,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (SelectedBlueprints.Count == 1)
items.AddRange(SelectedBlueprints[0].ContextMenuItems);
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, DeleteSelected));
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected));
return items.ToArray();
}

View File

@ -75,9 +75,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader]
private void load()
{
FillFlowContainer flow;
Children = new Drawable[]
{
new FillFlowContainer
flow = new FillFlowContainer
{
Width = 200,
Direction = FillDirection.Vertical,
@ -94,6 +96,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
bank.TabbableContentContainer = flow;
volume.TabbableContentContainer = flow;
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();

View File

@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private OsuColour colours { get; set; }
private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last();
private static readonly int highest_divisor = BindableBeatDivisor.PREDEFINED_DIVISORS.Last();
public TimelineTickDisplay()
{

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
{
public class CreateNewDifficultyDialog : PopupDialog
{
/// <summary>
/// Delegate used to create new difficulties.
/// A value of <see langword="true"/> in the <c>createCopy</c> parameter
/// indicates that the new difficulty should be an exact copy of an existing one;
/// otherwise, the new difficulty should have its hitobjects and beatmap-level settings cleared.
/// </summary>
public delegate void CreateNewDifficulty(bool createCopy);
public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty)
{
HeaderText = "Would you like to create a blank difficulty?";
Icon = FontAwesome.Regular.Clone;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = "Yeah, let's start from scratch!",
Action = () => createNewDifficulty.Invoke(false)
},
new PopupDialogCancelButton
{
Text = "No, create an exact copy of this difficulty",
Action = () => createNewDifficulty.Invoke(true)
},
new PopupDialogCancelButton
{
Text = "I changed my mind, I want to keep editing this difficulty",
Action = () => { }
}
};
}
}
}

View File

@ -29,6 +29,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
@ -84,10 +85,12 @@ namespace osu.Game.Screens.Edit
private Storage storage { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
private IDialogOverlay dialogOverlay { get; set; }
[Resolved(canBeNull: true)]
private NotificationOverlay notifications { get; set; }
private INotificationOverlay notifications { get; set; }
public readonly Bindable<EditorScreenMode> Mode = new Bindable<EditorScreenMode>();
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
@ -95,7 +98,7 @@ namespace osu.Game.Screens.Edit
private bool canSave;
private bool exitConfirmed;
protected bool ExitConfirmed { get; private set; }
private string lastSavedHash;
@ -115,8 +118,6 @@ namespace osu.Game.Screens.Edit
[CanBeNull] // Should be non-null once it can support custom rulesets.
private EditorChangeHandler changeHandler;
private EditorMenuBar menuBar;
private DependencyContainer dependencies;
private TestGameplayButton testGameplayButton;
@ -239,40 +240,49 @@ namespace osu.Game.Screens.Edit
Name = "Top bar",
RelativeSizeAxes = Axes.X,
Height = 40,
Child = menuBar = new EditorMenuBar
Children = new Drawable[]
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose },
Items = new[]
new EditorMenuBar
{
new MenuItem("File")
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Items = new[]
{
Items = createFileMenuItems()
},
new MenuItem("Edit")
{
Items = new[]
new MenuItem("File")
{
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
}
},
new MenuItem("View")
{
Items = new MenuItem[]
Items = createFileMenuItems()
},
new MenuItem(CommonStrings.ButtonsEdit)
{
new WaveformOpacityMenuItem(config.GetBindable<float>(OsuSetting.EditorWaveformOpacity)),
new HitAnimationsMenuItem(config.GetBindable<bool>(OsuSetting.EditorHitAnimations))
Items = new[]
{
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
}
},
new MenuItem("View")
{
Items = new MenuItem[]
{
new WaveformOpacityMenuItem(config.GetBindable<float>(OsuSetting.EditorWaveformOpacity)),
new HitAnimationsMenuItem(config.GetBindable<bool>(OsuSetting.EditorHitAnimations))
}
}
}
}
}
},
new ScreenSelectionTabControl
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -15,
Current = Mode,
},
},
},
new Container
{
@ -340,14 +350,15 @@ namespace osu.Game.Screens.Edit
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
menuBar.Mode.ValueChanged += onModeChanged;
}
protected override void LoadComplete()
{
base.LoadComplete();
setUpClipboardActionAvailability();
Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose;
Mode.BindValueChanged(onModeChanged, true);
}
/// <summary>
@ -358,14 +369,14 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// Creates an <see cref="EditorState"/> instance representing the current state of the editor.
/// </summary>
/// <param name="nextBeatmap">
/// The next beatmap to be shown, in the case of difficulty switch.
/// <param name="nextRuleset">
/// The ruleset of the next beatmap to be shown, in the case of difficulty switch.
/// <see langword="null"/> indicates that the beatmap will not be changing.
/// </param>
public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState
public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState
{
Time = clock.CurrentTimeAccurate,
ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextBeatmap.Ruleset.ShortName ? Clipboard.Content.Value : string.Empty
ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty
};
/// <summary>
@ -517,23 +528,23 @@ namespace osu.Game.Screens.Edit
return true;
case GlobalAction.EditorComposeMode:
menuBar.Mode.Value = EditorScreenMode.Compose;
Mode.Value = EditorScreenMode.Compose;
return true;
case GlobalAction.EditorDesignMode:
menuBar.Mode.Value = EditorScreenMode.Design;
Mode.Value = EditorScreenMode.Design;
return true;
case GlobalAction.EditorTimingMode:
menuBar.Mode.Value = EditorScreenMode.Timing;
Mode.Value = EditorScreenMode.Timing;
return true;
case GlobalAction.EditorSetupMode:
menuBar.Mode.Value = EditorScreenMode.SongSetup;
Mode.Value = EditorScreenMode.SongSetup;
return true;
case GlobalAction.EditorVerifyMode:
menuBar.Mode.Value = EditorScreenMode.Verify;
Mode.Value = EditorScreenMode.Verify;
return true;
case GlobalAction.EditorTestGameplay:
@ -549,16 +560,16 @@ namespace osu.Game.Screens.Edit
{
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
dimBackground();
resetTrack(true);
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
dimBackground();
}
@ -574,9 +585,9 @@ namespace osu.Game.Screens.Edit
});
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
if (!exitConfirmed)
if (!ExitConfirmed)
{
// dialog overlay may not be available in visual tests.
if (dialogOverlay == null)
@ -585,12 +596,9 @@ namespace osu.Game.Screens.Edit
return true;
}
// if the dialog is already displayed, confirm exit with no save.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
{
saveDialog.PerformOkAction();
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog)
return true;
}
if (isNewBeatmap || HasUnsavedChanges)
{
@ -605,12 +613,12 @@ namespace osu.Game.Screens.Edit
refetchBeatmap();
return base.OnExiting(next);
return base.OnExiting(e);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
clock.Stop();
refetchBeatmap();
}
@ -635,7 +643,7 @@ namespace osu.Game.Screens.Edit
{
Save();
exitConfirmed = true;
ExitConfirmed = true;
this.Exit();
}
@ -658,7 +666,7 @@ namespace osu.Game.Screens.Edit
Beatmap.SetDefault();
}
exitConfirmed = true;
ExitConfirmed = true;
this.Exit();
}
@ -841,7 +849,18 @@ namespace osu.Game.Screens.Edit
}
protected void CreateNewDifficulty(RulesetInfo rulesetInfo)
=> loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState());
{
if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset))
{
switchToNewDifficulty(rulesetInfo, false);
return;
}
dialogOverlay.Push(new CreateNewDifficultyDialog(createCopy => switchToNewDifficulty(rulesetInfo, createCopy)));
}
private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy)
=> loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo));
private EditorMenuItem createDifficultySwitchMenu()
{
@ -866,7 +885,7 @@ namespace osu.Game.Screens.Edit
return new EditorMenuItem("Change difficulty") { Items = difficultyItems };
}
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap));
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
private void cancelExit()
{

View File

@ -21,21 +21,24 @@ namespace osu.Game.Screens.Edit
{
public event Action BeatmapSkinChanged;
/// <summary>
/// The underlying beatmap skin.
/// </summary>
protected internal readonly Skin Skin;
/// <summary>
/// The combo colours of this skin.
/// If empty, the default combo colours will be used.
/// </summary>
public readonly BindableList<Colour4> ComboColours;
private readonly Skin skin;
public BindableList<Colour4> ComboColours { get; }
public EditorBeatmapSkin(Skin skin)
{
this.skin = skin;
Skin = skin;
ComboColours = new BindableList<Colour4>();
if (skin.Configuration.ComboColours != null)
ComboColours.AddRange(skin.Configuration.ComboColours.Select(c => (Colour4)c));
if (Skin.Configuration.ComboColours != null)
ComboColours.AddRange(Skin.Configuration.ComboColours.Select(c => (Colour4)c));
ComboColours.BindCollectionChanged((_, __) => updateColours());
}
@ -43,16 +46,16 @@ namespace osu.Game.Screens.Edit
private void updateColours()
{
skin.Configuration.CustomComboColours = ComboColours.Select(c => (Color4)c).ToList();
Skin.Configuration.CustomComboColours = ComboColours.Select(c => (Color4)c).ToList();
invokeSkinChanged();
}
#region Delegated ISkin implementation
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
public Drawable GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Skin.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => Skin.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Skin.GetConfig<TLookup, TValue>(lookup);
#endregion
}

View File

@ -4,6 +4,7 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
@ -80,12 +81,18 @@ namespace osu.Game.Screens.Edit
}
}
public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState)
public void ScheduleSwitchToNewDifficulty(BeatmapInfo referenceBeatmapInfo, RulesetInfo rulesetInfo, bool createCopy, EditorState editorState)
=> scheduleDifficultySwitch(() =>
{
try
{
return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo);
// fetch a fresh detached reference from database to avoid polluting model instances attached to cached working beatmaps.
var targetBeatmapSet = beatmapManager.QueryBeatmap(b => b.ID == referenceBeatmapInfo.ID).AsNonNull().BeatmapSet.AsNonNull();
var referenceWorkingBeatmap = beatmapManager.GetWorkingBeatmap(referenceBeatmapInfo);
return createCopy
? beatmapManager.CopyExistingDifficulty(targetBeatmapSet, referenceWorkingBeatmap)
: beatmapManager.CreateNewDifficulty(targetBeatmapSet, referenceWorkingBeatmap, rulesetInfo);
}
catch (Exception ex)
{

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit
private readonly EditorBeatmapSkin? beatmapSkin;
public EditorSkinProvidingContainer(EditorBeatmap editorBeatmap)
: base(editorBeatmap.PlayableBeatmap.BeatmapInfo.Ruleset.CreateInstance(), editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin)
: base(editorBeatmap.PlayableBeatmap.BeatmapInfo.Ruleset.CreateInstance(), editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin?.Skin)
{
beatmapSkin = editorBeatmap.BeatmapSkin;
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
=> new MasterGameplayClockContainer(beatmap, editorState.Time, true);
=> new MasterGameplayClockContainer(beatmap, gameplayStart) { StartTime = editorState.Time };
protected override void LoadComplete()
{
@ -44,9 +44,9 @@ namespace osu.Game.Screens.Edit.GameplayTest
protected override bool CheckModsAllowFailure() => false; // never fail.
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
// finish alpha transforms on entering to avoid gameplay starting in a half-hidden state.
// the finish calls are purposefully not propagated to children to avoid messing up their state.
@ -54,13 +54,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
GameplayClockContainer.FinishTransforms(false, nameof(Alpha));
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
musicController.Stop();
editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
return base.OnExiting(next);
return base.OnExiting(e);
}
}
}

View File

@ -19,9 +19,9 @@ namespace osu.Game.Screens.Edit.GameplayTest
{
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
MetadataInfo.FinishTransforms(true);
}

View File

@ -17,12 +17,12 @@ namespace osu.Game.Screens.Edit
Buttons = new PopupDialogButton[]
{
new PopupDialogCancelButton
new PopupDialogOkButton
{
Text = @"Save my masterpiece!",
Action = saveAndExit
},
new PopupDialogOkButton
new PopupDialogDangerousButton
{
Text = @"Forget all changes",
Action = exit

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Edit.Setup
{
@ -27,7 +28,7 @@ namespace osu.Game.Screens.Edit.Setup
{
circleSizeSlider = new LabelledSliderBar<float>
{
Label = "Object Size",
Label = BeatmapsetsStrings.ShowStatsCs,
FixedLabelWidth = LABEL_WIDTH,
Description = "The size of all hit objects",
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
@ -40,7 +41,7 @@ namespace osu.Game.Screens.Edit.Setup
},
healthDrainSlider = new LabelledSliderBar<float>
{
Label = "Health Drain",
Label = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH,
Description = "The rate of passive health drain throughout playable time",
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
@ -53,7 +54,7 @@ namespace osu.Game.Screens.Edit.Setup
},
approachRateSlider = new LabelledSliderBar<float>
{
Label = "Approach Rate",
Label = BeatmapsetsStrings.ShowStatsAr,
FixedLabelWidth = LABEL_WIDTH,
Description = "The speed at which objects are presented to the player",
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
@ -66,7 +67,7 @@ namespace osu.Game.Screens.Edit.Setup
},
overallDifficultySlider = new LabelledSliderBar<float>
{
Label = "Overall Difficulty",
Label = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH,
Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)",
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Edit.Setup
{
@ -48,15 +49,15 @@ namespace osu.Game.Screens.Edit.Setup
creatorTextBox = createTextBox<LabelledTextBox>("Creator", metadata.Author.Username),
difficultyTextBox = createTextBox<LabelledTextBox>("Difficulty Name", Beatmap.BeatmapInfo.DifficultyName),
sourceTextBox = createTextBox<LabelledTextBox>("Source", metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>("Tags", metadata.Tags)
sourceTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
};
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
}
private TTextBox createTextBox<TTextBox>(string label, string initialValue)
private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue)
where TTextBox : LabelledTextBox, new()
=> new TTextBox
{
@ -71,7 +72,7 @@ namespace osu.Game.Screens.Edit.Setup
base.LoadComplete();
if (string.IsNullOrEmpty(ArtistTextBox.Current.Value))
GetContainingInputManager().ChangeFocus(ArtistTextBox);
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox));
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));

View File

@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Timing
set => slider.KeyboardStep = value;
}
public CompositeDrawable TabbableContentContainer
{
set => textBox.TabbableContentContainer = value;
}
private readonly BindableWithCurrent<T?> current = new BindableWithCurrent<T?>();
public Bindable<T?> Current

View File

@ -1,19 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class TimingSection : Section<TimingControlPoint>
{
private SettingsSlider<double> bpmSlider;
private LabelledTimeSignature timeSignature;
private BPMTextBox bpmTextEntry;
@ -23,7 +20,6 @@ namespace osu.Game.Screens.Edit.Timing
Flow.AddRange(new Drawable[]
{
bpmTextEntry = new BPMTextBox(),
bpmSlider = new BPMSlider(),
timeSignature = new LabelledTimeSignature
{
Label = "Time Signature"
@ -35,11 +31,8 @@ namespace osu.Game.Screens.Edit.Timing
{
if (point.NewValue != null)
{
bpmSlider.Current = point.NewValue.BeatLengthBindable;
bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
// no need to hook change handler here as it's the same bindable as above
bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
timeSignature.Current = point.NewValue.TimeSignatureBindable;
timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
@ -102,51 +95,6 @@ namespace osu.Game.Screens.Edit.Timing
}
}
private class BPMSlider : SettingsSlider<double>
{
private const double sane_minimum = 60;
private const double sane_maximum = 240;
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH)
{
MinValue = sane_minimum,
MaxValue = sane_maximum,
};
public BPMSlider()
{
beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true);
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
base.Current = bpmBindable;
TransferValueOnCommit = true;
}
public override Bindable<double> Current
{
get => base.Current;
set
{
// incoming will be beat length, not bpm
beatLengthBindable.UnbindBindings();
beatLengthBindable.BindTo(value);
}
}
private void updateCurrent(double newValue)
{
// we use a more sane range for the slider display unless overridden by the user.
// if a value comes in outside our range, we should expand temporarily.
bpmBindable.MinValue = Math.Min(newValue, sane_minimum);
bpmBindable.MaxValue = Math.Max(newValue, sane_maximum);
bpmBindable.Value = newValue;
}
}
private static double beatLengthToBpm(double beatLength) => 60000 / beatLength;
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
namespace osu.Game.Screens
{
/// <summary>
/// Manages a global screen stack to allow nested components a guarantee of where work is executed.
/// </summary>
[Cached]
public interface IPerformFromScreenRunner
{
/// <summary>
/// Perform an action only after returning to a specific screen as indicated by <paramref name="validScreens"/>.
/// Eagerly tries to exit the current screen until it succeeds.
/// </summary>
/// <param name="action">The action to perform once we are in the correct state.</param>
/// <param name="validScreens">An optional collection of valid screen types. If any of these screens are already current we can perform the action immediately, else the first valid parent will be made current before performing the action. <see cref="MainMenu"/> is used if not specified.</param>
void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null);
}
}

View File

@ -118,20 +118,20 @@ namespace osu.Game.Screens.Import
fileSelector.CurrentPath.BindValueChanged(directoryChanged);
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
contentContainer.ScaleTo(0.95f).ScaleTo(1, duration, Easing.OutQuint);
this.FadeInFromZero(duration);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
contentContainer.ScaleTo(0.95f, duration, Easing.OutQuint);
this.FadeOut(duration, Easing.OutQuint);
return base.OnExiting(next);
return base.OnExiting(e);
}
private void directoryChanged(ValueChangedEvent<DirectoryInfo> _)

View File

@ -69,9 +69,9 @@ namespace osu.Game.Screens
private EFToRealmMigrator realmMigrator;
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);

View File

@ -26,7 +26,6 @@ using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@ -79,15 +78,17 @@ namespace osu.Game.Screens.Menu
private readonly ButtonArea buttonArea;
private readonly Button backButton;
private readonly MainMenuButton backButton;
private readonly List<Button> buttonsTopLevel = new List<Button>();
private readonly List<Button> buttonsPlay = new List<Button>();
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private Sample sampleBack;
private readonly LogoTrackingContainer logoTrackingContainer;
public bool ReturnToTopOnIdle { get; set; } = true;
public ButtonSystem()
{
RelativeSizeAxes = Axes.Both;
@ -100,8 +101,9 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[]
{
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)
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
-WEDGE_WIDTH)
{
VisibleState = ButtonSystemState.Play,
},
@ -117,33 +119,32 @@ namespace osu.Game.Screens.Menu
[Resolved]
private IAPIProvider api { get; set; }
[Resolved(CanBeNull = true)]
private NotificationOverlay notifications { get; set; }
[Resolved(CanBeNull = true)]
private LoginOverlay loginOverlay { get; set; }
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
{
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.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new MainMenuButton(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(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));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH,
Key.P));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(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(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsTopLevel);
buttonArea.ForEach(b =>
{
if (b is Button)
if (b is MainMenuButton)
{
b.Origin = Anchor.CentreLeft;
b.Anchor = Anchor.CentreLeft;
@ -161,17 +162,7 @@ namespace osu.Game.Screens.Menu
{
if (api.State.Value != APIState.Online)
{
notifications?.Post(new SimpleNotification
{
Text = "You gotta be online to multi 'yo!",
Icon = FontAwesome.Solid.Globe,
Activated = () =>
{
loginOverlay?.Show();
return true;
}
});
loginOverlay?.Show();
return;
}
@ -182,17 +173,7 @@ namespace osu.Game.Screens.Menu
{
if (api.State.Value != APIState.Online)
{
notifications?.Post(new SimpleNotification
{
Text = "You gotta be online to view playlists 'yo!",
Icon = FontAwesome.Solid.Globe,
Activated = () =>
{
loginOverlay?.Show();
return true;
}
});
loginOverlay?.Show();
return;
}
@ -201,12 +182,18 @@ namespace osu.Game.Screens.Menu
private void updateIdleState(bool isIdle)
{
if (!ReturnToTopOnIdle)
return;
if (isIdle && State != ButtonSystemState.Exit && State != ButtonSystemState.EnteringMode)
State = ButtonSystemState.Initial;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false;
if (State == ButtonSystemState.Initial)
{
if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey))
@ -305,7 +292,7 @@ namespace osu.Game.Screens.Menu
{
buttonArea.ButtonSystemState = state;
foreach (var b in buttonArea.Children.OfType<Button>())
foreach (var b in buttonArea.Children.OfType<MainMenuButton>())
b.ButtonSystemState = state;
}

View File

@ -171,9 +171,9 @@ namespace osu.Game.Screens.Menu
((IBindable<APIUser>)currentUser).BindTo(api.LocalUser);
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
icon.RotateTo(10);
icon.FadeOut();

View File

@ -57,10 +57,10 @@ namespace osu.Game.Screens.Menu
}
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
this.FadeOut(300);
base.OnSuspending(next);
base.OnSuspending(e);
}
}
}

View File

@ -13,13 +13,17 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using Realms;
@ -54,7 +58,8 @@ namespace osu.Game.Screens.Menu
private const int exit_delay = 3000;
private Sample seeya;
private SkinnableSound skinnableSeeya;
private ISample seeya;
protected virtual string SeeyaSampleName => "Intro/seeya";
@ -71,6 +76,9 @@ namespace osu.Game.Screens.Menu
[CanBeNull]
private readonly Func<OsuScreen> createNextScreen;
[Resolved]
private RulesetStore rulesets { get; set; }
/// <summary>
/// Whether the <see cref="Track"/> is provided by osu! resources, rather than a user beatmap.
/// Only valid during or after <see cref="LogoArriving"/>.
@ -86,14 +94,18 @@ namespace osu.Game.Screens.Menu
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, Framework.Game game, RealmAccess realm)
private void load(OsuConfigManager config, Framework.Game game, RealmAccess realm, IAPIProvider api)
{
// prevent user from changing beatmap while the intro is still running.
beatmap = Beatmap.BeginLease(false);
MenuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice);
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
seeya = audio.Samples.Get(SeeyaSampleName);
if (api.LocalUser.Value.IsSupporter)
AddInternal(skinnableSeeya = new SkinnableSound(new SampleInfo(SeeyaSampleName)));
else
seeya = audio.Samples.Get(SeeyaSampleName);
// if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection.
if (!MenuMusic.Value)
@ -117,7 +129,11 @@ namespace osu.Game.Screens.Menu
// we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available.
if (initialBeatmap == null)
{
if (!loadThemedIntro())
// Intro beatmaps are generally made using the osu! ruleset.
// It might not be present in test projects for other rulesets.
bool osuRulesetPresent = rulesets.GetRuleset(0) != null;
if (!loadThemedIntro() && osuRulesetPresent)
{
// if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state.
// this could happen if a user has nuked their files store. for now, reimport to repair this.
@ -131,7 +147,7 @@ namespace osu.Game.Screens.Menu
bool loadThemedIntro()
{
var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == BeatmapHash);
if (setInfo == null)
return false;
@ -148,14 +164,14 @@ namespace osu.Game.Screens.Menu
}
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
ensureEventuallyArrivingAtMenu();
}
[Resolved]
private NotificationOverlay notifications { get; set; }
private INotificationOverlay notifications { get; set; }
private void ensureEventuallyArrivingAtMenu()
{
@ -178,7 +194,7 @@ namespace osu.Game.Screens.Menu
}, 5000);
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
this.FadeIn(300);
@ -193,7 +209,15 @@ namespace osu.Game.Screens.Menu
// we also handle the exit transition.
if (MenuVoice.Value)
{
seeya.Play();
if (skinnableSeeya != null)
{
// resuming a screen (i.e. calling OnResume) happens before the screen itself becomes alive,
// therefore skinnable samples may not be updated yet with the recently selected skin.
// schedule after children to ensure skinnable samples have processed skin changes before playing.
ScheduleAfterChildren(() => skinnableSeeya.Play());
}
else
seeya.Play();
// if playing the outro voice, we have more time to have fun with the background track.
// initially fade to almost silent then ramp out over the remaining time.
@ -213,12 +237,12 @@ namespace osu.Game.Screens.Menu
//don't want to fade out completely else we will stop running updates.
Game.FadeTo(0.01f, fadeOutTime).OnComplete(_ => this.Exit());
base.OnResuming(last);
base.OnResuming(e);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
initialBeatmap = null;
}

View File

@ -89,9 +89,9 @@ namespace osu.Game.Screens.Menu
}
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
// ensure the background is shown, even if the TriangleIntroSequence failed to do so.
background.ApplyToBackground(b => b.Show());
@ -100,9 +100,9 @@ namespace osu.Game.Screens.Menu
intro.Expire();
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
background.FadeOut(100);
}

View File

@ -13,7 +13,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Online.API;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
@ -23,8 +26,11 @@ namespace osu.Game.Screens.Menu
protected override string BeatmapHash => "64e00d7022195959bfa3109d09c2e2276c8f12f486b91fcf6175583e973b48f2";
protected override string BeatmapFile => "welcome.osz";
private const double delay_step_two = 2142;
private Sample welcome;
private Sample pianoReverb;
private SkinnableSound skinnableWelcome;
private ISample welcome;
private ISample pianoReverb;
protected override string SeeyaSampleName => "Intro/Welcome/seeya";
protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false)
@ -40,10 +46,15 @@ namespace osu.Game.Screens.Menu
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, IAPIProvider api)
{
if (MenuVoice.Value)
welcome = audio.Samples.Get(@"Intro/Welcome/welcome");
{
if (api.LocalUser.Value.IsSupporter)
AddInternal(skinnableWelcome = new SkinnableSound(new SampleInfo(@"Intro/Welcome/welcome")));
else
welcome = audio.Samples.Get(@"Intro/Welcome/welcome");
}
pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano");
}
@ -65,7 +76,10 @@ namespace osu.Game.Screens.Menu
AddInternal(intro);
welcome?.Play();
if (skinnableWelcome != null)
skinnableWelcome.Play();
else
welcome?.Play();
var reverbChannel = pianoReverb?.Play();
if (reverbChannel != null)
@ -92,21 +106,21 @@ namespace osu.Game.Screens.Menu
}
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
background.FadeOut(100);
}
private class WelcomeIntroSequence : Container
{
private Sprite welcomeText;
private Drawable welcomeText;
private Container scaleContainer;
public LogoVisualisation LogoVisualisation { get; private set; }
[BackgroundDependencyLoader]
private void load(TextureStore textures)
private void load(TextureStore textures, IAPIProvider api)
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
@ -135,15 +149,17 @@ namespace osu.Game.Screens.Menu
Size = new Vector2(480),
Colour = Color4.Black
},
welcomeText = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = textures.Get(@"Intro/Welcome/welcome_text")
},
}
},
};
if (api.LocalUser.Value.IsSupporter)
scaleContainer.Add(welcomeText = new SkinnableSprite(@"Intro/Welcome/welcome_text"));
else
scaleContainer.Add(welcomeText = new Sprite { Texture = textures.Get(@"Intro/Welcome/welcome_text") });
welcomeText.Anchor = Anchor.Centre;
welcomeText.Origin = Anchor.Centre;
}
protected override void LoadComplete()

View File

@ -176,7 +176,7 @@ namespace osu.Game.Screens.Menu
private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f);
private float[] audioData;
private readonly float[] audioData = new float[256];
private readonly QuadBatch<TexturedVertex2D> vertexBatch = new QuadBatch<TexturedVertex2D>(100, 10);
@ -192,7 +192,8 @@ namespace osu.Game.Screens.Menu
shader = Source.shader;
texture = Source.texture;
size = Source.DrawSize.X;
audioData = Source.frequencyAmplitudes;
Source.frequencyAmplitudes.AsSpan().CopyTo(audioData);
}
public override void Draw(Action<TexturedVertex2D> vertexAction)

View File

@ -2,15 +2,19 @@
// 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;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Online.API;
using osu.Game.Overlays;
@ -25,13 +29,13 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
public class MainMenu : OsuScreen, IHandlePresentBeatmap
public class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler<GlobalAction>
{
public const float FADE_IN_DURATION = 300;
public const float FADE_OUT_DURATION = 400;
public override bool HideOverlaysOnEnter => buttons == null || buttons.State == ButtonSystemState.Initial;
public override bool HideOverlaysOnEnter => Buttons == null || Buttons.State == ButtonSystemState.Initial;
public override bool AllowBackButton => false;
@ -41,7 +45,7 @@ namespace osu.Game.Screens.Menu
private MenuSideFlashes sideFlashes;
private ButtonSystem buttons;
protected ButtonSystem Buttons;
[Resolved]
private GameHost host { get; set; }
@ -56,13 +60,13 @@ namespace osu.Game.Screens.Menu
private IAPIProvider api { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
private IDialogOverlay dialogOverlay { get; set; }
private BackgroundScreenDefault background;
protected override BackgroundScreen CreateBackground() => background;
private Bindable<float> holdDelay;
private Bindable<double> holdDelay;
private Bindable<bool> loginDisplayed;
private ExitConfirmOverlay exitConfirmOverlay;
@ -73,7 +77,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)]
private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
{
holdDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay);
holdDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
if (host.CanExit)
@ -97,7 +101,7 @@ namespace osu.Game.Screens.Menu
ParallaxAmount = 0.01f,
Children = new Drawable[]
{
buttons = new ButtonSystem
Buttons = new ButtonSystem
{
OnEdit = delegate
{
@ -121,7 +125,7 @@ namespace osu.Game.Screens.Menu
exitConfirmOverlay?.CreateProxy() ?? Empty()
});
buttons.StateChanged += state =>
Buttons.StateChanged += state =>
{
switch (state)
{
@ -136,22 +140,22 @@ namespace osu.Game.Screens.Menu
}
};
buttons.OnSettings = () => settings?.ToggleVisibility();
buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
Buttons.OnSettings = () => settings?.ToggleVisibility();
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();
}
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
private IPerformFromScreenRunner performer { get; set; }
private void confirmAndExit()
{
if (exitConfirmed) return;
exitConfirmed = true;
game?.PerformFromScreen(menu => menu.Exit());
performer?.PerformFromScreen(menu => menu.Exit());
}
private void preloadSongSelect()
@ -172,12 +176,12 @@ namespace osu.Game.Screens.Menu
[Resolved]
private Storage storage { get; set; }
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
buttons.FadeInFromZero(500);
base.OnEntering(e);
Buttons.FadeInFromZero(500);
if (last is IntroScreen && musicController.TrackLoaded)
if (e.Last is IntroScreen && musicController.TrackLoaded)
{
var track = musicController.CurrentTrack;
@ -199,14 +203,14 @@ namespace osu.Game.Screens.Menu
{
base.LogoArriving(logo, resuming);
buttons.SetOsuLogo(logo);
Buttons.SetOsuLogo(logo);
logo.FadeColour(Color4.White, 100, Easing.OutQuint);
logo.FadeIn(100, Easing.OutQuint);
if (resuming)
{
buttons.State = ButtonSystemState.TopLevel;
Buttons.State = ButtonSystemState.TopLevel;
this.FadeIn(FADE_IN_DURATION, Easing.OutQuint);
buttonsContainer.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint);
@ -241,15 +245,15 @@ namespace osu.Game.Screens.Menu
var seq = logo.FadeOut(300, Easing.InSine)
.ScaleTo(0.2f, 300, Easing.InSine);
seq.OnComplete(_ => buttons.SetOsuLogo(null));
seq.OnAbort(_ => buttons.SetOsuLogo(null));
seq.OnComplete(_ => Buttons.SetOsuLogo(null));
seq.OnAbort(_ => Buttons.SetOsuLogo(null));
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
buttons.State = ButtonSystemState.EnteringMode;
Buttons.State = ButtonSystemState.EnteringMode;
this.FadeOut(FADE_OUT_DURATION, Easing.InSine);
buttonsContainer.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine);
@ -257,9 +261,9 @@ namespace osu.Game.Screens.Menu
sideFlashes.FadeOut(64, Easing.OutQuint);
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next());
@ -269,7 +273,7 @@ namespace osu.Game.Screens.Menu
musicController.EnsurePlayingSomething();
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
if (!exitConfirmed && dialogOverlay != null)
{
@ -281,13 +285,13 @@ namespace osu.Game.Screens.Menu
return true;
}
buttons.State = ButtonSystemState.Exit;
Buttons.State = ButtonSystemState.Exit;
OverlayActivationMode.Value = OverlayActivation.Disabled;
songTicker.Hide();
this.FadeOut(3000);
return base.OnExiting(next);
return base.OnExiting(e);
}
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
@ -297,5 +301,26 @@ namespace osu.Game.Screens.Menu
Schedule(loadSoloSongSelect);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.Back:
// In the case of a host being able to exit, the back action is handled by ExitConfirmOverlay.
Debug.Assert(!host.CanExit);
return host.SuspendToBackground();
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Menu
/// Button designed specifically for the osu!next main menu.
/// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
/// </summary>
public class Button : BeatSyncedContainer, IStateful<ButtonState>
public class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
{
public event Action<ButtonState> StateChanged;
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
{
this.sampleName = sampleName;
this.clickAction = clickAction;
@ -185,8 +185,7 @@ namespace osu.Game.Screens.Menu
private void load(AudioManager audio)
{
sampleHover = audio.Samples.Get(@"Menu/button-hover");
if (!string.IsNullOrEmpty(sampleName))
sampleClick = audio.Samples.Get($@"Menu/{sampleName}");
sampleClick = audio.Samples.Get(!string.IsNullOrEmpty(sampleName) ? $@"Menu/{sampleName}" : @"UI/button-select");
}
protected override bool OnMouseDown(MouseDownEvent e)
@ -209,7 +208,7 @@ namespace osu.Game.Screens.Menu
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed)
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false;
if (TriggerKey == e.Key && TriggerKey != Key.Unknown)

View File

@ -283,9 +283,15 @@ namespace osu.Game.Screens.Menu
this.Delay(early_activation).Schedule(() =>
{
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
sampleDownbeat.Play();
{
sampleDownbeat?.Play();
}
else
sampleBeat.Play();
{
var channel = sampleBeat.GetChannel();
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
channel.Play();
}
});
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Screens.Menu
public class StorageErrorDialog : PopupDialog
{
[Resolved]
private DialogOverlay dialogOverlay { get; set; }
private IDialogOverlay dialogOverlay { get; set; }
public StorageErrorDialog(OsuStorage storage, OsuStorageError error)
{

View File

@ -68,14 +68,14 @@ namespace osu.Game.Screens.OnlinePlay.Components
}
else
{
var metadataInfo = beatmap.Value.Metadata;
var metadataInfo = beatmap.Metadata;
string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode;
string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode;
var title = new RomanisableString($"{artistUnicode} - {titleUnicode}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title}".Trim());
textFlow.AddLink(title, LinkAction.OpenBeatmap, beatmap.Value.OnlineID.ToString(), "Open beatmap");
textFlow.AddLink(title, LinkAction.OpenBeatmap, beatmap.OnlineID.ToString(), "Open beatmap");
}
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Components
@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Components
private const float height = 28;
private const float transition_duration = 100;
[Resolved]
private RulesetStore rulesets { get; set; }
private Container drawableRuleset;
public ModeTypeInfo()
@ -56,11 +60,14 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void updateBeatmap()
{
var item = Playlist.FirstOrDefault();
var ruleset = item == null ? null : rulesets.GetRuleset(item.RulesetID)?.CreateInstance();
if (item?.Beatmap != null)
if (item?.Beatmap != null && ruleset != null)
{
var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray();
drawableRuleset.FadeIn(transition_duration);
drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) };
drawableRuleset.Child = new DifficultyIcon(item.Beatmap, ruleset.RulesetInfo, mods) { Size = new Vector2(height) };
}
else
drawableRuleset.FadeOut(transition_duration);

View File

@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
Schedule(() =>
{
var beatmap = playlistItem?.Beatmap.Value;
var beatmap = playlistItem?.Beatmap;
string? lastCover = (background?.Beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover;
string? newCover = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover;
@ -91,15 +91,15 @@ namespace osu.Game.Screens.OnlinePlay.Components
AddInternal(background = newBackground);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
this.MoveToX(0, TRANSITION_LENGTH);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
bool result = base.OnExiting(next);
bool result = base.OnExiting(e);
this.MoveToX(0);
return result;
}

View File

@ -23,6 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
InternalChild = sprite = CreateBackgroundSprite();
CurrentPlaylistItem.BindValueChanged(_ => updateBeatmap());
Playlist.CollectionChanged += (_, __) => updateBeatmap();
updateBeatmap();
@ -30,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void updateBeatmap()
{
sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap.Value;
sprite.Beatmap.Value = CurrentPlaylistItem.Value?.Beatmap ?? Playlist.GetCurrentItem()?.Beatmap;
}
protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@ -34,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private readonly Circle line;
private readonly OsuSpriteText details;
public OverlinedHeader(string title)
public OverlinedHeader(LocalisableString title)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;

View File

@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
public PlaylistItemBackground(PlaylistItem? playlistItem)
{
Beatmap = playlistItem?.Beatmap.Value;
Beatmap = playlistItem?.Beatmap;
}
[BackgroundDependencyLoader]

View File

@ -15,12 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
public new readonly BindableBool Enabled = new BindableBool();
private IBindable<BeatmapAvailability> availability;
private readonly IBindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>();
[BackgroundDependencyLoader]
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
{
availability = beatmapTracker.Availability.GetBoundCopy();
availability.BindTo(beatmapTracker.Availability);
availability.BindValueChanged(_ => updateState());
Enabled.BindValueChanged(_ => updateState(), true);

View File

@ -12,7 +12,6 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
namespace osu.Game.Screens.OnlinePlay.Components
{
@ -27,9 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
protected IBindable<Room> JoinedRoom => joinedRoom;
private readonly Bindable<Room> joinedRoom = new Bindable<Room>();
[Resolved]
private IRulesetStore rulesets { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
@ -116,9 +112,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
try
{
foreach (var pi in room.Playlist)
pi.MapObjects(rulesets);
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
if (existing == null)
rooms.Add(room);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -75,15 +74,29 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
base.LoadComplete();
Playlist.BindCollectionChanged(updateRange, true);
DifficultyRange.BindValueChanged(_ => updateRange());
Playlist.BindCollectionChanged((_, __) => updateRange(), true);
}
private void updateRange(object sender, NotifyCollectionChangedEventArgs e)
private void updateRange()
{
var orderedDifficulties = Playlist.Where(p => p.Beatmap.Value != null).Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray();
StarDifficulty minDifficulty;
StarDifficulty maxDifficulty;
StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0);
StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0);
if (DifficultyRange.Value != null)
{
minDifficulty = new StarDifficulty(DifficultyRange.Value.Min, 0);
maxDifficulty = new StarDifficulty(DifficultyRange.Value.Max, 0);
}
else
{
// In multiplayer rooms, the beatmaps of playlist items will not be populated to a point this can be correct.
// Either populating them via BeatmapLookupCache or polling the API for the room's DifficultyRange will be required.
var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray();
minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0);
maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0);
}
minDisplay.Current.Value = minDifficulty;
maxDisplay.Current.Value = maxDifficulty;

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -25,9 +24,9 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
@ -68,9 +67,10 @@ namespace osu.Game.Screens.OnlinePlay
private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
private readonly IBindable<bool> valid = new Bindable<bool>();
private readonly Bindable<IBeatmapInfo> beatmap = new Bindable<IBeatmapInfo>();
private readonly Bindable<IRulesetInfo> ruleset = new Bindable<IRulesetInfo>();
private readonly BindableList<Mod> requiredMods = new BindableList<Mod>();
private IBeatmapInfo beatmap;
private IRulesetInfo ruleset;
private Mod[] requiredMods;
private Container maskingContainer;
private Container difficultyIconContainer;
@ -86,16 +86,15 @@ namespace osu.Game.Screens.OnlinePlay
private PanelBackground panelBackground;
private FillFlowContainer mainFillFlow;
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private UserLookupCache userLookupCache { get; set; }
[CanBeNull]
[Resolved(CanBeNull = true)]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
@ -106,10 +105,7 @@ namespace osu.Game.Screens.OnlinePlay
{
Item = item;
beatmap.BindTo(item.Beatmap);
valid.BindTo(item.Valid);
ruleset.BindTo(item.Ruleset);
requiredMods.BindTo(item.RequiredMods);
if (item.Expired)
Colour = OsuColour.Gray(0.5f);
@ -119,6 +115,11 @@ namespace osu.Game.Screens.OnlinePlay
private void load()
{
maskingContainer.BorderColour = colours.Yellow;
ruleset = rulesets.GetRuleset(Item.RulesetID);
var rulesetInstance = ruleset?.CreateInstance();
requiredMods = Item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
protected override void LoadComplete()
@ -144,10 +145,7 @@ namespace osu.Game.Screens.OnlinePlay
maskingContainer.BorderThickness = isCurrent ? 5 : 0;
}, true);
beatmap.BindValueChanged(_ => Scheduler.AddOnce(refresh));
ruleset.BindValueChanged(_ => Scheduler.AddOnce(refresh));
valid.BindValueChanged(_ => Scheduler.AddOnce(refresh));
requiredMods.CollectionChanged += (_, __) => Scheduler.AddOnce(refresh);
onScreenLoader.DelayedLoadStarted += _ =>
{
@ -161,19 +159,9 @@ namespace osu.Game.Screens.OnlinePlay
Schedule(() => ownerAvatar.User = foundUser);
}
if (Item.Beatmap.Value == null)
{
IBeatmapInfo foundBeatmap;
beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.OnlineID).ConfigureAwait(false);
if (multiplayerClient != null)
// This call can eventually go away (and use the else case below).
// Currently required only due to the method being overridden to provide special behaviour in tests.
foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false);
else
foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false);
Schedule(() => Item.Beatmap.Value = foundBeatmap);
}
Scheduler.AddOnce(refresh);
}
catch (Exception e)
{
@ -275,32 +263,36 @@ namespace osu.Game.Screens.OnlinePlay
maskingContainer.BorderColour = colours.Red;
}
if (Item.Beatmap.Value != null)
difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) };
if (beatmap != null)
difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) };
else
difficultyIconContainer.Clear();
panelBackground.Beatmap.Value = Item.Beatmap.Value;
panelBackground.Beatmap.Value = beatmap;
beatmapText.Clear();
if (Item.Beatmap.Value != null)
if (beatmap != null)
{
beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text =>
{
text.Truncate = true;
});
beatmapText.AddLink(beatmap.GetDisplayTitleRomanisable(includeCreator: false),
LinkAction.OpenBeatmap,
beatmap.OnlineID.ToString(),
null,
text =>
{
text.Truncate = true;
});
}
authorText.Clear();
if (!string.IsNullOrEmpty(Item.Beatmap.Value?.Metadata.Author.Username))
if (!string.IsNullOrEmpty(beatmap?.Metadata.Author.Username))
{
authorText.AddText("mapped by ");
authorText.AddUserLink(Item.Beatmap.Value.Metadata.Author);
authorText.AddUserLink(beatmap.Metadata.Author);
}
bool hasExplicitContent = (Item.Beatmap.Value?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true;
bool hasExplicitContent = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true;
explicitContentPill.Alpha = hasExplicitContent ? 1 : 0;
modDisplay.Current.Value = requiredMods.ToArray();
@ -452,13 +444,13 @@ namespace osu.Game.Screens.OnlinePlay
Alpha = AllowShowingResults ? 1 : 0,
TooltipText = "View results"
},
Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item),
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
editButton = new PlaylistEditButton
{
Size = new Vector2(30, 30),
Alpha = AllowEditing ? 1 : 0,
Action = () => RequestEdit?.Invoke(Item),
TooltipText = "Edit"
TooltipText = CommonStrings.ButtonsEdit
},
removeButton = new PlaylistRemoveButton
{
@ -494,7 +486,7 @@ namespace osu.Game.Screens.OnlinePlay
private sealed class PlaylistDownloadButton : BeatmapDownloadButton
{
private readonly PlaylistItem playlistItem;
private readonly IBeatmapInfo beatmap;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@ -504,10 +496,10 @@ namespace osu.Game.Screens.OnlinePlay
private const float width = 50;
public PlaylistDownloadButton(PlaylistItem playlistItem)
: base(playlistItem.Beatmap.Value.BeatmapSet)
public PlaylistDownloadButton(IBeatmapInfo beatmap)
: base(beatmap.BeatmapSet)
{
this.playlistItem = playlistItem;
this.beatmap = beatmap;
Size = new Vector2(width, 30);
Alpha = 0;
@ -527,7 +519,7 @@ namespace osu.Game.Screens.OnlinePlay
{
case DownloadState.LocallyAvailable:
// Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching.
if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null)
if (beatmapManager.QueryBeatmap(b => b.MD5Hash == beatmap.MD5Hash) == null)
State.Value = DownloadState.NotDownloaded;
else
{

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay
{
public class FreeModSelectScreen : ModSelectScreen
{
protected override bool AllowCustomisation => false;
protected override bool ShowTotalMultiplier => false;
public new Func<Mod, bool> IsValidMod
{
get => base.IsValidMod;
set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value.Invoke(m);
}
public FreeModSelectScreen()
{
IsValidMod = _ => true;
}
protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new ModColumn(modType, true, toggleKeys);
}
}

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Threading;
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.Colour;
@ -12,6 +14,7 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -328,6 +331,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
private SpriteText statusText;
private LinkFlowContainer beatmapText;
@ -382,11 +388,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(onSelectedItemChanged, true);
CurrentPlaylistItem.BindValueChanged(onSelectedItemChanged, true);
}
private CancellationTokenSource beatmapLookupCancellation;
private void onSelectedItemChanged(ValueChangedEvent<PlaylistItem> item)
{
beatmapLookupCancellation?.Cancel();
beatmapText.Clear();
if (Type.Value == MatchType.Playlists)
@ -395,17 +404,31 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return;
}
if (item.NewValue?.Beatmap.Value != null)
{
statusText.Text = "Currently playing ";
beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(),
LinkAction.OpenBeatmap,
item.NewValue.Beatmap.Value.OnlineID.ToString(),
creationParameters: s =>
{
s.Truncate = true;
});
}
var beatmap = item.NewValue?.Beatmap;
if (beatmap == null)
return;
var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource();
beatmapLookupCache.GetBeatmapAsync(beatmap.OnlineID, cancellationSource.Token)
.ContinueWith(task => Schedule(() =>
{
if (cancellationSource.IsCancellationRequested)
return;
var retrievedBeatmap = task.GetResultSafely();
statusText.Text = "Currently playing ";
if (retrievedBeatmap != null)
{
beatmapText.AddLink(retrievedBeatmap.GetDisplayTitleRomanisable(),
LinkAction.OpenBeatmap,
retrievedBeatmap.OnlineID.ToString(),
creationParameters: s => s.Truncate = true);
}
else
beatmapText.AddText("unknown beatmap");
}), cancellationSource.Token);
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
@ -41,15 +41,22 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
base.LoadComplete();
Playlist.BindCollectionChanged(updateCount, true);
PlaylistItemStats.BindValueChanged(_ => updateCount());
Playlist.BindCollectionChanged((_, __) => updateCount(), true);
}
private void updateCount(object sender, NotifyCollectionChangedEventArgs e)
private void updateCount()
{
int activeItems = Playlist.Count > 0 || PlaylistItemStats.Value == null
// For now, use the playlist as the source of truth if it has any items.
// This allows the count to display correctly on the room screen (after joining a room).
? Playlist.Count(i => !i.Expired)
: PlaylistItemStats.Value.CountActive;
count.Clear();
count.AddText(Playlist.Count.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold));
count.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold));
count.AddText(" ");
count.AddText("Beatmap".ToQuantity(Playlist.Count, ShowQuantityAs.None));
count.AddText("Beatmap".ToQuantity(activeItems, ShowQuantityAs.None));
}
}
}

View File

@ -12,7 +12,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Extensions;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
@ -78,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
bool matchingFilter = true;
matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.MatchesOnlineID(criteria.Ruleset));
matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false;
if (!string.IsNullOrEmpty(criteria.SearchString))
matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
@ -125,7 +124,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void updateSorting()
{
foreach (var room in roomFlow)
roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0));
{
roomFlow.SetLayoutPosition(room, room.Room.Category.Value == RoomCategory.Spotlight
// Always show spotlight playlists at the top of the listing.
? float.MinValue
: -(room.Room.RoomID.Value ?? 0));
}
}
protected override bool OnClick(ClickEvent e)

View File

@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
lounge?.Open(Room.DeepClone());
lounge?.OpenCopy(Room);
})
};
@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.LoadComplete();
Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextBox));
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(passwordTextBox));
passwordTextBox.OnCommit += (_, __) => performJoin();
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
playlist.Clear();
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
// This screen never exits.
return true;

View File

@ -20,6 +20,7 @@ using osu.Framework.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@ -63,6 +64,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
@ -234,15 +238,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
#endregion
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
onReturning();
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
Debug.Assert(selectionLease != null);
@ -257,16 +261,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
onReturning();
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
onLeaving();
return base.OnExiting(next);
return base.OnExiting(e);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
onLeaving();
base.OnSuspending(next);
base.OnSuspending(e);
}
protected override void OnFocus(FocusEvent e)
@ -310,6 +314,46 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
});
});
/// <summary>
/// Copies a room and opens it as a fresh (not-yet-created) one.
/// </summary>
/// <param name="room">The room to copy.</param>
public void OpenCopy(Room room)
{
Debug.Assert(room.RoomID.Value != null);
if (joiningRoomOperation != null)
return;
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
var req = new GetRoomRequest(room.RoomID.Value.Value);
req.Success += r =>
{
// ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not.
r.RoomID.Value = null;
// Null out dates because end date is not supported client-side and the settings overlay will populate a duration.
r.EndDate.Value = null;
r.Duration.Value = null;
Open(r);
joiningRoomOperation?.Dispose();
joiningRoomOperation = null;
};
req.Failure += exception =>
{
Logger.Error(exception, "Couldn't create a copy of this room.");
joiningRoomOperation?.Dispose();
joiningRoomOperation = null;
};
api.Queue(req);
}
/// <summary>
/// Push a room as a new subscreen.
/// </summary>

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Match.Components
@ -30,8 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override IEnumerable<LeaderboardScoreStatistic> GetStatistics(ScoreInfo model) => new[]
{
new LeaderboardScoreStatistic(FontAwesome.Solid.Crosshairs, "Accuracy", model.DisplayAccuracy),
new LeaderboardScoreStatistic(FontAwesome.Solid.Sync, "Total Attempts", score.TotalAttempts.ToString()),
new LeaderboardScoreStatistic(FontAwesome.Solid.Crosshairs, RankingsStrings.StatAccuracy, model.DisplayAccuracy),
new LeaderboardScoreStatistic(FontAwesome.Solid.Sync, RankingsStrings.StatPlayCount, score.TotalAttempts.ToString()),
new LeaderboardScoreStatistic(FontAwesome.Solid.Check, "Completed Beatmaps", score.CompletedBeatmaps.ToString()),
};
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@ -93,7 +94,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{
}
protected class SectionContainer : FillFlowContainer<Section>
/// <remarks>
/// <see cref="ReverseChildIDFillFlowContainer{T}"/> is used to ensure that if the nested <see cref="Section"/>s
/// use expanded overhanging content (like an <see cref="OsuDropdown{T}"/>'s dropdown),
/// then the overhanging content will be correctly Z-ordered.
/// </remarks>
protected class SectionContainer : ReverseChildIDFillFlowContainer<Section>
{
public SectionContainer()
{

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
@ -49,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
RelativeSizeAxes = Axes.Y,
Size = new Vector2(100, 1),
Text = "Edit",
Text = CommonStrings.ButtonsEdit,
Action = () => OnEdit?.Invoke()
});
}
@ -62,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (editButton != null)
host.BindValueChanged(h => editButton.Alpha = h.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0, true);
SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap.Value, true);
SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap, true);
}
protected override Drawable CreateBackground() => background = new BackgroundSprite();

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -11,6 +12,7 @@ using osu.Framework.Bindables;
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.Screens;
using osu.Game.Audio;
@ -99,122 +101,126 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");
InternalChildren = new Drawable[]
InternalChild = new PopoverContainer
{
beatmapAvailabilityTracker,
new MultiplayerRoomSounds(),
new GridContainer
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
beatmapAvailabilityTracker,
new MultiplayerRoomSounds(),
new GridContainer
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 50)
},
Content = new[]
{
// Padded main content (drawable room + main content)
new Drawable[]
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Container
new Dimension(),
new Dimension(GridSizeMode.Absolute, 50)
},
Content = new[]
{
// Padded main content (drawable room + main content)
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
new Container
{
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
Bottom = 30
},
Children = new[]
{
mainContent = new GridContainer
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
Bottom = 30
},
Children = new[]
{
mainContent = new GridContainer
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10)
},
Content = new[]
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new DrawableMatchRoom(Room, allowEdit)
{
OnEdit = () => settingsOverlay.Show(),
SelectedItem = { BindTarget = SelectedItem }
}
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10)
},
null,
new Drawable[]
Content = new[]
{
new Container
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Children = new[]
new DrawableMatchRoom(Room, allowEdit)
{
new Container
OnEdit = () => settingsOverlay.Show(),
SelectedItem = { BindTarget = SelectedItem }
}
},
null,
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Child = new Box
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
Masking = true,
CornerRadius = 10,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = CreateMainContent(),
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = userModsSelectOverlay = new UserModSelectOverlay
new Container
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = CreateMainContent(),
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
}
}
}
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
// Resolves 1px masking errors between the settings overlay and the room panel.
Padding = new MarginPadding(-1),
Child = settingsOverlay = CreateRoomSettingsOverlay(Room)
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
// Resolves 1px masking errors between the settings overlay and the room panel.
Padding = new MarginPadding(-1),
Child = settingsOverlay = CreateRoomSettingsOverlay(Room)
}
},
},
},
// Footer
new Drawable[]
{
new Container
// Footer
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
new Container
{
new Box
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = CreateFooter()
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = CreateFooter()
},
}
}
}
}
@ -284,35 +290,35 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected void ShowUserModSelect() => userModsSelectOverlay.Show();
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
beginHandlingTrack();
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
endHandlingTrack();
base.OnSuspending(next);
base.OnSuspending(e);
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
updateWorkingBeatmap();
beginHandlingTrack();
Scheduler.AddOnce(UpdateMods);
Scheduler.AddOnce(updateRuleset);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
RoomManager?.PartRoom();
Mods.Value = Array.Empty<Mod>();
endHandlingTrack();
return base.OnExiting(next);
return base.OnExiting(e);
}
protected void StartPlay()
@ -350,10 +356,12 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (selected == null)
return;
var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
var allowedMods = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance));
// Remove any user mods that are no longer allowed.
UserMods.Value = UserMods.Value
.Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType()))
.ToList();
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
UpdateMods();
updateRuleset();
@ -367,13 +375,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
else
{
UserModsSection?.Show();
userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType());
userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
}
}
private void updateWorkingBeatmap()
{
var beatmap = SelectedItem.Value?.Beatmap.Value;
var beatmap = SelectedItem.Value?.Beatmap;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID);
@ -386,7 +394,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList();
var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList();
}
private void updateRuleset()

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -15,10 +16,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class GameplayChatDisplay : MatchChatDisplay, IKeyBindingHandler<GlobalAction>
{
[Resolved]
[Resolved(CanBeNull = true)]
[CanBeNull]
private ILocalUserPlayInfo localUserInfo { get; set; }
private IBindable<bool> localUserPlaying = new Bindable<bool>();
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
public override bool PropagatePositionalInputSubTree => !localUserPlaying.Value;
@ -46,7 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
if (localUserInfo != null)
localUserPlaying.BindTo(localUserInfo.IsPlaying);
localUserPlaying.BindValueChanged(playing =>
{
// for now let's never hold focus. this avoid misdirected gameplay keys entering chat.

View File

@ -0,0 +1,217 @@
// 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.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MatchStartControl : MultiplayerRoomComposite
{
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[CanBeNull]
private IDisposable clickOperation;
private Sample sampleReady;
private Sample sampleReadyAll;
private Sample sampleUnready;
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton;
private int countReady;
private ScheduledDelegate readySampleDelegate;
private IBindable<bool> operationInProgress;
public MatchStartControl()
{
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
readyButton = new MultiplayerReadyButton
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = onReadyClick,
},
countdownButton = new MultiplayerCountdownButton
{
RelativeSizeAxes = Axes.Y,
Size = new Vector2(40, 1),
Alpha = 0,
Action = startCountdown,
CancelAction = cancelCountdown
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentPlaylistItem.BindValueChanged(_ => updateState());
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
updateState();
}
protected override void OnRoomLoadRequested()
{
base.OnRoomLoadRequested();
endOperation();
}
private void onReadyClick()
{
if (Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
if (isReady() && Client.IsHost && Room.Countdown == null)
startMatch();
else
toggleReady();
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
void toggleReady() => Client.ToggleReady().FireAndForget(
onSuccess: endOperation,
onError: _ => endOperation());
void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () =>
{
// gameplay is starting, the button will be unblocked on load requested.
}, onError: _ =>
{
// gameplay was not started due to an exception; unblock button.
endOperation();
});
}
private void startCountdown(TimeSpan duration)
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
}
private void cancelCountdown()
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
}
private void endOperation()
{
clickOperation?.Dispose();
clickOperation = null;
}
private void updateState()
{
if (Room == null)
{
readyButton.Enabled.Value = false;
countdownButton.Enabled.Value = false;
return;
}
var localUser = Client.LocalUser;
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
if (!Client.IsHost || Room.Settings.AutoStartEnabled)
countdownButton.Hide();
else
{
switch (localUser?.State)
{
default:
countdownButton.Hide();
break;
case MultiplayerUserState.Idle:
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
countdownButton.Show();
break;
}
}
readyButton.Enabled.Value = countdownButton.Enabled.Value =
Room.State == MultiplayerRoomState.Open
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && Room.Countdown == null;
if (newCountReady == countReady)
return;
readySampleDelegate?.Cancel();
readySampleDelegate = Schedule(() =>
{
if (newCountReady > countReady)
{
if (newCountReady == newCountTotal)
sampleReadyAll?.Play();
else
sampleReady?.Play();
}
else if (newCountReady < countReady)
{
sampleUnready?.Play();
}
countReady = newCountReady;
});
}
}
}

View File

@ -0,0 +1,140 @@
// 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 Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerCountdownButton : IconButton, IHasPopover
{
private static readonly TimeSpan[] available_delays =
{
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(2)
};
public new Action<TimeSpan> Action;
public Action CancelAction;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private readonly Drawable background;
public MultiplayerCountdownButton()
{
Icon = FontAwesome.Regular.Clock;
Add(background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
});
base.Action = this.ShowPopover;
TooltipText = "Countdown settings";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Green;
}
protected override void LoadComplete()
{
base.LoadComplete();
multiplayerClient.RoomUpdated += onRoomUpdated;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
multiplayerClient.RoomUpdated -= onRoomUpdated;
}
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
bool countdownActive = multiplayerClient.Room?.Countdown != null;
if (countdownActive)
{
background
.FadeColour(colours.YellowLight, 100, Easing.In)
.Then()
.FadeColour(colours.YellowDark, 900, Easing.OutQuint)
.Loop();
}
else
{
background
.FadeColour(colours.Green, 200, Easing.OutQuint);
}
});
public Popover GetPopover()
{
var flow = new FillFlowContainer
{
Width = 200,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
};
foreach (var duration in available_delays)
{
flow.Add(new OsuButton
{
RelativeSizeAxes = Axes.X,
Text = $"Start match in {duration.Humanize()}",
BackgroundColour = colours.Green,
Action = () =>
{
Action(duration);
this.HidePopover();
}
});
}
if (multiplayerClient.Room?.Countdown != null && multiplayerClient.IsHost)
{
flow.Add(new OsuButton
{
RelativeSizeAxes = Axes.X,
Text = "Stop countdown",
BackgroundColour = colours.Red,
Action = () =>
{
CancelAction();
this.HidePopover();
}
});
}
return new OsuPopover { Child = flow };
}
}
}

View File

@ -1,7 +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;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -12,19 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private const float ready_button_width = 600;
private const float spectate_button_width = 200;
public Action OnReadyClick
{
set => readyButton.OnReadyClick = value;
}
public Action OnSpectateClick
{
set => spectateButton.OnSpectateClick = value;
}
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerSpectateButton spectateButton;
public MultiplayerMatchFooter()
{
RelativeSizeAxes = Axes.Both;
@ -37,12 +23,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
new Drawable[]
{
null,
spectateButton = new MultiplayerSpectateButton
new MultiplayerSpectateButton
{
RelativeSizeAxes = Axes.Both,
},
null,
readyButton = new MultiplayerReadyButton
new MatchStartControl
{
RelativeSizeAxes = Axes.Both,
},

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.ComponentModel;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -21,6 +22,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
@ -56,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public Action SettingsApplied;
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public MatchTypePicker TypePicker;
public OsuEnumDropdown<QueueMode> QueueModeDropdown;
public OsuTextBox PasswordTextBox;
@ -64,6 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuSpriteText ErrorText;
private OsuEnumDropdown<StartMode> startModeDropdown;
private OsuSpriteText typeLabel;
private LoadingLayer loadingLayer;
@ -163,14 +165,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
LengthLimit = 100,
},
},
new Section("Room visibility")
{
Alpha = disabled_alpha,
Child = AvailabilityPicker = new RoomAvailabilityPicker
{
Enabled = { Value = false }
},
},
// new Section("Room visibility")
// {
// Alpha = disabled_alpha,
// Child = AvailabilityPicker = new RoomAvailabilityPicker
// {
// Enabled = { Value = false }
// },
// },
new Section("Game type")
{
Child = new FillFlowContainer
@ -204,6 +206,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X
}
}
},
new Section("Auto start")
{
Child = new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Child = startModeDropdown = new OsuEnumDropdown<StartMode>
{
RelativeSizeAxes = Axes.X
}
}
}
},
},
@ -321,12 +335,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue.GetLocalisableDescription(), true);
RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => playlistContainer.Alpha = roomId.NewValue == null ? 1 : 0, true);
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true);
QueueMode.BindValueChanged(mode => QueueModeDropdown.Current.Value = mode.NewValue, true);
AutoStartDuration.BindValueChanged(duration => startModeDropdown.Current.Value = (StartMode)(int)duration.NewValue.TotalSeconds, true);
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(v =>
@ -343,7 +357,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
base.LoadComplete();
drawablePlaylist.Items.BindTo(Playlist);
drawablePlaylist.SelectedItem.BindTo(SelectedItem);
drawablePlaylist.SelectedItem.BindTo(CurrentPlaylistItem);
}
protected override void Update()
@ -363,6 +377,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(applyingSettingsOperation == null);
applyingSettingsOperation = ongoingOperationTracker.BeginOperation();
TimeSpan autoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value);
// If the client is already in a room, update via the client.
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
@ -371,7 +387,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
name: NameField.Text,
password: PasswordTextBox.Text,
matchType: TypePicker.Current.Value,
queueMode: QueueModeDropdown.Current.Value)
queueMode: QueueModeDropdown.Current.Value,
autoStartDuration: autoStartDuration)
.ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
@ -383,10 +400,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
else
{
room.Name.Value = NameField.Text;
room.Availability.Value = AvailabilityPicker.Current.Value;
room.Type.Value = TypePicker.Current.Value;
room.Password.Value = PasswordTextBox.Current.Value;
room.QueueMode.Value = QueueModeDropdown.Current.Value;
room.AutoStartDuration.Value = autoStartDuration;
if (int.TryParse(MaxParticipantsField.Text, out int max))
room.MaxParticipants.Value = max;
@ -419,7 +436,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (text.StartsWith(not_found_prefix, StringComparison.Ordinal))
{
ErrorText.Text = "The selected beatmap is not available online.";
SelectedItem.Value.MarkInvalid();
CurrentPlaylistItem.Value.MarkInvalid();
}
else
{
@ -452,5 +469,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Triangles.ColourDark = colours.YellowDark;
}
}
private enum StartMode
{
[Description("Off")]
Off = 0,
[Description("30 seconds")]
Seconds_30 = 30,
[Description("1 minute")]
Seconds_60 = 60,
[Description("3 minutes")]
Seconds_180 = 180,
[Description("5 minutes")]
Seconds_300 = 300
}
}
}

View File

@ -3,165 +3,240 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerReadyButton : MultiplayerRoomComposite
public class MultiplayerReadyButton : ReadyButton
{
public Action OnReadyClick
{
set => button.Action = value;
}
public new Triangles Triangles => base.Triangles;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[CanBeNull]
private MultiplayerRoom room => multiplayerClient.Room;
private IBindable<bool> operationInProgress;
private Sample sampleReady;
private Sample sampleReadyAll;
private Sample sampleUnready;
private readonly ButtonWithTrianglesExposed button;
private int countReady;
private ScheduledDelegate readySampleDelegate;
public MultiplayerReadyButton()
{
InternalChild = button = new ButtonWithTrianglesExposed
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Enabled = { Value = true },
};
}
private Sample countdownTickSample;
private Sample countdownWarnSample;
private Sample countdownWarnFinalSample;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick");
countdownWarnSample = audio.Samples.Get(@"Multiplayer/countdown-warn");
countdownWarnFinalSample = audio.Samples.Get(@"Multiplayer/countdown-warn-final");
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => updateState());
multiplayerClient.RoomUpdated += onRoomUpdated;
onRoomUpdated();
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
private MultiplayerCountdown countdown;
private double countdownChangeTime;
private ScheduledDelegate countdownUpdateDelegate;
updateState();
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
if (countdown != room?.Countdown)
{
countdown = room?.Countdown;
countdownChangeTime = Time.Current;
}
scheduleNextCountdownUpdate();
updateButtonText();
updateButtonColour();
});
private void scheduleNextCountdownUpdate()
{
countdownUpdateDelegate?.Cancel();
if (countdown != null)
{
// The remaining time on a countdown may be at a fractional portion between two seconds.
// We want to align certain audio/visual cues to the point at which integer seconds change.
// To do so, we schedule to the next whole second. Note that scheduler invocation isn't
// guaranteed to be accurate, so this may still occur slightly late, but even in such a case
// the next invocation will be roughly correct.
double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000;
countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond);
}
else
{
countdownUpdateDelegate?.Cancel();
countdownUpdateDelegate = null;
}
void onCountdownTick()
{
updateButtonText();
int secondsRemaining = countdownTimeRemaining.Seconds;
playTickSound(secondsRemaining);
if (secondsRemaining > 0)
scheduleNextCountdownUpdate();
}
}
private void updateState()
private void playTickSound(int secondsRemaining)
{
var localUser = Client.LocalUser;
if (secondsRemaining < 10) countdownTickSample?.Play();
int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0;
int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0;
if (secondsRemaining <= 3)
{
if (secondsRemaining > 0)
countdownWarnSample?.Play();
else
countdownWarnFinalSample?.Play();
}
}
private void updateButtonText()
{
if (room == null)
{
Text = "Ready";
return;
}
var localUser = multiplayerClient.LocalUser;
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countText = $"({countReady} / {countTotal} ready)";
if (countdown != null)
{
string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}";
switch (localUser?.State)
{
default:
Text = $"Ready ({countdownText.ToLowerInvariant()})";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = $"{countdownText} {countText}";
break;
}
}
else
{
switch (localUser?.State)
{
default:
Text = "Ready";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = room.Host?.Equals(localUser) == true
? $"Start match {countText}"
: $"Waiting for host... {countText}";
break;
}
}
}
private TimeSpan countdownTimeRemaining
{
get
{
double timeElapsed = Time.Current - countdownChangeTime;
TimeSpan remaining;
if (timeElapsed > countdown.TimeRemaining.TotalMilliseconds)
remaining = TimeSpan.Zero;
else
remaining = countdown.TimeRemaining - TimeSpan.FromMilliseconds(timeElapsed);
return remaining;
}
}
private void updateButtonColour()
{
if (room == null)
{
setGreen();
return;
}
var localUser = multiplayerClient.LocalUser;
switch (localUser?.State)
{
default:
button.Text = "Ready";
updateButtonColour(true);
setGreen();
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
string countText = $"({newCountReady} / {newCountTotal} ready)";
if (Room?.Host?.Equals(localUser) == true)
{
button.Text = $"Start match {countText}";
updateButtonColour(true);
}
if (room?.Host?.Equals(localUser) == true && room.Countdown == null)
setGreen();
else
{
button.Text = $"Waiting for host... {countText}";
updateButtonColour(false);
}
setYellow();
break;
}
bool enableButton =
Room?.State == MultiplayerRoomState.Open
&& SelectedItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !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)
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
button.Enabled.Value = enableButton;
if (newCountReady == countReady)
return;
readySampleDelegate?.Cancel();
readySampleDelegate = Schedule(() =>
void setYellow()
{
if (newCountReady > countReady)
{
if (newCountReady == newCountTotal)
sampleReadyAll?.Play();
else
sampleReady?.Play();
}
else if (newCountReady < countReady)
{
sampleUnready?.Play();
}
countReady = newCountReady;
});
}
private void updateButtonColour(bool green)
{
if (green)
{
button.BackgroundColour = colours.Green;
button.Triangles.ColourDark = colours.Green;
button.Triangles.ColourLight = colours.GreenLight;
BackgroundColour = colours.YellowDark;
Triangles.ColourDark = colours.YellowDark;
Triangles.ColourLight = colours.Yellow;
}
else
void setGreen()
{
button.BackgroundColour = colours.YellowDark;
button.Triangles.ColourDark = colours.YellowDark;
button.Triangles.ColourLight = colours.Yellow;
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
}
private class ButtonWithTrianglesExposed : ReadyButton
protected override void Dispose(bool isDisposing)
{
public new Triangles Triangles => base.Triangles;
base.Dispose(isDisposing);
if (multiplayerClient != null)
multiplayerClient.RoomUpdated -= onRoomUpdated;
}
public override LocalisableString TooltipText
{
get
{
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && !room.Settings.AutoStartEnabled)
return "Cancel countdown";
return base.TooltipText;
}
}
}
}

View File

@ -1,7 +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;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -15,11 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerSpectateButton : MultiplayerRoomComposite
{
public Action OnSpectateClick
{
set => button.Action = value;
}
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
@ -37,9 +31,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Enabled = { Value = true },
Action = onClick
};
}
private void onClick()
{
var clickOperation = ongoingOperationTracker.BeginOperation();
Client.ToggleSpectate().ContinueWith(t => endOperation());
void endOperation() => clickOperation?.Dispose();
}
[BackgroundDependencyLoader]
private void load()
{

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -25,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
/// </summary>
public Action<PlaylistItem> RequestEdit;
private MultiplayerPlaylistTabControl playlistTabControl;
private MultiplayerQueueList queueList;
private MultiplayerHistoryList historyList;
private bool firstPopulation = true;
@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
InternalChildren = new Drawable[]
{
new OsuTabControl<MultiplayerPlaylistDisplayMode>
playlistTabControl = new MultiplayerPlaylistTabControl
{
RelativeSizeAxes = Axes.X,
Height = tab_control_height,
@ -52,18 +52,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList = new MultiplayerQueueList
{
RelativeSizeAxes = Axes.Both,
SelectedItem = { BindTarget = SelectedItem },
SelectedItem = { BindTarget = CurrentPlaylistItem },
RequestEdit = item => RequestEdit?.Invoke(item)
},
historyList = new MultiplayerHistoryList
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
SelectedItem = { BindTarget = SelectedItem }
SelectedItem = { BindTarget = CurrentPlaylistItem }
}
}
}
};
playlistTabControl.QueueItems.BindTarget = queueList.Items;
}
protected override void LoadComplete()
@ -115,8 +117,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
base.PlaylistItemChanged(item);
removeItemFromLists(item.ID);
addItemToLists(item);
var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
// Test if the only change between the two playlist items is the order.
if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
{
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
// The following isn't really required, but is here for safety and explicitness.
// MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation.
queueList.Invalidate();
}
else
{
removeItemFromLists(item.ID);
addItemToLists(item);
}
}
private void addItemToLists(MultiplayerPlaylistItem item)

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
public class MultiplayerPlaylistTabControl : OsuTabControl<MultiplayerPlaylistDisplayMode>
{
public readonly IBindableList<PlaylistItem> QueueItems = new BindableList<PlaylistItem>();
protected override TabItem<MultiplayerPlaylistDisplayMode> CreateTabItem(MultiplayerPlaylistDisplayMode value)
{
if (value == MultiplayerPlaylistDisplayMode.Queue)
return new QueueTabItem { QueueItems = { BindTarget = QueueItems } };
return base.CreateTabItem(value);
}
private class QueueTabItem : OsuTabItem
{
public readonly IBindableList<PlaylistItem> QueueItems = new BindableList<PlaylistItem>();
public QueueTabItem()
: base(MultiplayerPlaylistDisplayMode.Queue)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
QueueItems.BindCollectionChanged((_, __) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true);
}
}
}
}

View File

@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
base.LoadComplete();
RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID);
RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID).FireAndForget();
multiplayerClient.RoomUpdated += onRoomUpdated;
onRoomUpdated();

View File

@ -35,20 +35,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
transitionFromResults();
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
if (client.Room == null)
return;
if (!(last is MultiplayerPlayerLoader playerLoader))
if (!(e.Last is MultiplayerPlayerLoader playerLoader))
return;
// If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay.
if (!playerLoader.GameplayPassed)
{
client.AbortGameplay();
client.AbortGameplay().FireAndForget();
return;
}

View File

@ -25,13 +25,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; }
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
// Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it.
// To work around this, temporarily remove the room and trigger an immediate listing poll.
if (last is MultiplayerMatchSubScreen match)
if (e.Last is MultiplayerMatchSubScreen match)
{
RoomManager.RemoveRoom(match.Room);
ListingPollingComponent.PollImmediately();

View File

@ -1,17 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -68,45 +63,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
var multiplayerItem = new MultiplayerPlaylistItem
{
ID = itemToEdit ?? 0,
BeatmapID = item.BeatmapID,
BeatmapChecksum = item.Beatmap.Value.MD5Hash,
BeatmapID = item.Beatmap.OnlineID,
BeatmapChecksum = item.Beatmap.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(),
AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray()
RequiredMods = item.RequiredMods.ToArray(),
AllowedMods = item.AllowedMods.ToArray()
};
Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem);
task.ContinueWith(t =>
task.FireAndForget(onSuccess: () => Schedule(() =>
{
Schedule(() =>
{
loadingLayer.Hide();
if (t.IsFaulted)
{
Exception exception = t.Exception;
if (exception is AggregateException ae)
exception = ae.InnerException;
Debug.Assert(exception != null);
string message = exception is HubException
// HubExceptions arrive with additional message context added, but we want to display the human readable message:
// "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once."
// We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now.
? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim()
: exception.Message;
Logger.Log(message, level: LogLevel.Important);
Carousel.AllowSelection = true;
return;
}
loadingLayer.Hide();
// If an error or server side trigger occurred this screen may have already exited by external means.
if (this.IsCurrentScreen())
this.Exit();
});
});
}), onError: _ => Schedule(() =>
{
loadingLayer.Hide();
Carousel.AllowSelection = true;
}));
}
else
{

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -47,14 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private readonly IBindable<bool> isConnected = new Bindable<bool>();
[CanBeNull]
private IDisposable readyClickOperation;
private AddItemButton addItemButton;
public MultiplayerMatchSubScreen(Room room)
@ -231,11 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
}
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
{
OnReadyClick = onReadyClick,
OnSpectateClick = onSpectateClick
};
protected override Drawable CreateFooter() => new MultiplayerMatchFooter();
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room);
@ -247,22 +234,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// update local mods based on room's reported status for the local user (omitting the base call implementation).
// this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed).
var ruleset = Ruleset.Value.CreateInstance();
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList();
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset))).ToList();
}
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
private IDialogOverlay dialogOverlay { get; set; }
private bool exitConfirmed;
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
// 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);
return base.OnExiting(e);
}
if (!exitConfirmed && dialogOverlay != null)
@ -281,7 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
return base.OnExiting(next);
return base.OnExiting(e);
}
private ModSettingChangeTracker modSettingChangeTracker;
@ -294,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
client.ChangeUserMods(mods.NewValue);
client.ChangeUserMods(mods.NewValue).FireAndForget();
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
@ -309,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
client.ChangeUserMods(UserMods.Value);
client.ChangeUserMods(UserMods.Value).FireAndForget();
}, 500);
}
@ -318,7 +305,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
client.ChangeBeatmapAvailability(availability.NewValue);
client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget();
if (availability.NewValue.State != DownloadState.LocallyAvailable)
{
@ -333,52 +320,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
private void onReadyClick()
{
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating))
{
client.StartMatch()
.ContinueWith(t =>
{
// accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null)
{
// gameplay was not started due to an exception; unblock button.
endOperation();
}
// gameplay is starting, the button will be unblocked on load requested.
});
return;
}
client.ToggleReady()
.ContinueWith(t => endOperation());
void endOperation()
{
readyClickOperation?.Dispose();
readyClickOperation = null;
}
}
private void onSpectateClick()
{
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
client.ToggleSpectate().ContinueWith(t => endOperation());
void endOperation()
{
readyClickOperation?.Dispose();
readyClickOperation = null;
}
}
private void onRoomUpdated()
{
// may happen if the client is kicked or otherwise removed from the room.
@ -398,38 +339,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void updateCurrentItem()
{
Debug.Assert(client.Room != null);
var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
if (expectedSelectedItem == null)
return;
// There's no reason to renew the selected item if its content hasn't changed.
if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null)
return;
// Clear the selected item while the lookup is performed, so components like the ready button can enter their disabled states.
SelectedItem.Value = null;
if (expectedSelectedItem.Beatmap.Value == null)
{
Task.Run(async () =>
{
var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false);
Schedule(() =>
{
expectedSelectedItem.Beatmap.Value = beatmap;
if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true)
applyCurrentItem();
});
});
}
else
applyCurrentItem();
void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem;
SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
}
private void handleRoomLost() => Schedule(() =>
@ -465,9 +375,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
StartPlay();
readyClickOperation?.Dispose();
readyClickOperation = null;
}
protected override Screen CreateGameplayScreen()
@ -481,7 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
return new MultiSpectatorScreen(Room, users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
default:
return new MultiplayerPlayerLoader(() => new MultiplayerPlayer(Room, SelectedItem.Value, users));

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -76,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
});
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, users), l =>
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(GameplayState.Ruleset.RulesetInfo, ScoreProcessor, users), l =>
{
if (!LoadedBeatmapSuccessfully)
return;
@ -132,6 +133,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
failAndBail();
}
}), true);
}
protected override void LoadComplete()
{
base.LoadComplete();
Debug.Assert(client.Room != null);
}
@ -171,11 +177,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onMatchStarted() => Scheduler.Add(() =>
{
if (!this.IsCurrentScreen())
return;
loadingDisplay.Hide();
base.StartGameplay();
});
private void onResultsReady() => resultsReady.SetResult(true);
private void onResultsReady()
{
// Schedule is required to ensure that `TaskCompletionSource.SetResult` is not called more than once.
// A scenario where this can occur is if this instance is not immediately disposed (ie. async disposal queue).
Schedule(() =>
{
if (!this.IsCurrentScreen())
return;
resultsReady.SetResult(true);
});
}
protected override async Task PrepareScoreForResultsAsync(Score score)
{

View File

@ -18,10 +18,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
player = (Player)next;
base.OnSuspending(e);
player = (Player)e.Next;
}
}
}

View File

@ -21,6 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
Client.RoomUpdated += invokeOnRoomUpdated;
Client.LoadRequested += invokeOnRoomLoadRequested;
Client.UserLeft += invokeUserLeft;
Client.UserKicked += invokeUserKicked;
Client.UserJoined += invokeUserJoined;
@ -38,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
private void invokeOnRoomLoadRequested() => Scheduler.AddOnce(OnRoomLoadRequested);
/// <summary>
/// Invoked when a user has joined the room.
@ -94,6 +96,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
}
/// <summary>
/// Invoked when the room requests the local user to load into gameplay.
/// </summary>
protected virtual void OnRoomLoadRequested()
{
}
protected override void Dispose(bool isDisposing)
{
if (Client != null)

View File

@ -37,21 +37,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.UserJoined(user);
userJoinedSample?.Play();
Scheduler.AddOnce(() => userJoinedSample?.Play());
}
protected override void UserLeft(MultiplayerRoomUser user)
{
base.UserLeft(user);
userLeftSample?.Play();
Scheduler.AddOnce(() => userLeftSample?.Play());
}
protected override void UserKicked(MultiplayerRoomUser user)
{
base.UserKicked(user);
userKickedSample?.Play();
Scheduler.AddOnce(() => userKickedSample?.Play());
}
private void hostChanged(ValueChangedEvent<APIUser> value)
@ -59,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// only play sound when the host changes from an already-existing host.
if (value.OldValue == null) return;
hostChangedSample?.Play();
Scheduler.AddOnce(() => hostChangedSample?.Play());
}
}
}

View File

@ -24,12 +24,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class MultiplayerTeamResultsScreen : MultiplayerResultsScreen
{
private readonly SortedDictionary<int, BindableInt> teamScores;
private readonly SortedDictionary<int, BindableLong> teamScores;
private Container winnerBackground;
private Drawable winnerText;
public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary<int, BindableInt> teamScores)
public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary<int, BindableLong> teamScores)
: base(score, roomId, playlistItem)
{
if (teamScores.Count != 2)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@ -170,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.Centre,
Alpha = 0,
Margin = new MarginPadding(4),
Action = () => Client.KickUser(User.UserID),
Action = () => Client.KickUser(User.UserID).FireAndForget(),
},
},
}
@ -187,9 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
var currentItem = Playlist.GetCurrentItem();
Debug.Assert(currentItem != null);
var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance();
var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
@ -201,15 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else
userModsDisplay.FadeOut(fade_time);
if (Client.IsHost && !User.Equals(Client.LocalUser))
kickButton.FadeIn(fade_time);
else
kickButton.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
else
crown.FadeOut(fade_time);
kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
@ -241,7 +231,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (!Client.IsHost)
return;
Client.TransferHost(targetUser);
Client.TransferHost(targetUser).FireAndForget();
}),
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
@ -249,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (!Client.IsHost)
return;
Client.KickUser(targetUser);
Client.KickUser(targetUser).FireAndForget();
})
};
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
private FillFlowContainer<ParticipantPanel> panels;
[CanBeNull]
private ParticipantPanel currentHostPanel;
[BackgroundDependencyLoader]
private void load()
{
@ -55,6 +59,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user));
if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
{
// Reset position of previous host back to normal, if one existing.
if (currentHostPanel != null && panels.Contains(currentHostPanel))
panels.SetLayoutPosition(currentHostPanel, 0);
currentHostPanel = null;
// Change position of new host to display above all participants.
if (Room.Host != null)
{
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
if (currentHostPanel != null)
panels.SetLayoutPosition(currentHostPanel, -1);
}
}
}
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Game.Online.Multiplayer;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.OnlinePlay.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
@ -13,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private MultiplayerClient client { get; set; }
public ParticipantsListHeader()
: base("Participants")
: base(RankingsStrings.SpotlightParticipants)
{
}

View File

@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Client.SendMatchRequest(new ChangeTeamRequest
{
TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
});
}).FireAndForget();
}
public int? DisplayedTeam { get; private set; }

View File

@ -17,8 +17,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
Bindable<bool> WaitingOnFrames { get; }
/// <summary>
/// Whether this clock is resynchronising to the master clock.
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
/// </summary>
/// <remarks>
/// Of note, this will be false if this clock is *ahead* of the master clock.
/// </remarks>
bool IsCatchingUp { get; set; }
/// <summary>

View File

@ -5,6 +5,7 @@ using System;
using JetBrains.Annotations;
using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
@ -12,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
{
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
: base(scoreProcessor, users)
public MultiSpectatorLeaderboard(RulesetInfo ruleset, [NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
: base(ruleset, scoreProcessor, users)
{
}
@ -33,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
((SpectatingTrackedUserData)data).Clock = null;
}
protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, scoreProcessor);
protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, ruleset, scoreProcessor);
protected override void Update()
{
@ -48,8 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[CanBeNull]
public IClock Clock;
public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor)
: base(user, scoreProcessor)
public SpectatingTrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor)
: base(user, ruleset, scoreProcessor)
{
}

View File

@ -55,12 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public SpectatorGameplayClockContainer([NotNull] IClock sourceClock)
: base(sourceClock)
{
// the container should initially be in a stopped state until the catch-up clock is started by the sync manager.
Stop();
}
protected override void Update()
{
// The SourceClock here is always a CatchUpSpectatorPlayerClock.
// 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();

View File

@ -11,10 +11,13 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Spectate;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
@ -34,6 +37,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved]
private OsuColour colours { get; set; }
@ -48,15 +53,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private PlayerArea currentAudioSource;
private bool canStartMasterClock;
private readonly Room room;
private readonly MultiplayerRoomUser[] users;
/// <summary>
/// Creates a new <see cref="MultiSpectatorScreen"/>.
/// </summary>
/// <param name="room">The room.</param>
/// <param name="users">The players to spectate.</param>
public MultiSpectatorScreen(MultiplayerRoomUser[] users)
public MultiSpectatorScreen(Room room, MultiplayerRoomUser[] users)
: base(users.Select(u => u.UserID).ToArray())
{
this.room = room;
this.users = users;
instances = new PlayerArea[Users.Count];
@ -65,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[BackgroundDependencyLoader]
private void load()
{
Container leaderboardContainer;
FillFlowContainer leaderboardFlow;
Container scoreDisplayContainer;
masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value);
@ -97,10 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
new Drawable[]
{
leaderboardContainer = new Container
leaderboardFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5)
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
}
@ -122,17 +133,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users)
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, users)
{
Expanded = { Value = true },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}, l =>
{
foreach (var instance in instances)
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
leaderboardContainer.Add(leaderboard);
leaderboardFlow.Insert(0, leaderboard);
if (leaderboard.TeamScores.Count == 2)
{
@ -143,6 +152,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}, scoreDisplayContainer.Add);
}
});
LoadComponentAsync(new GameplayChatDisplay(room)
{
Expanded = { Value = true },
}, chat => leaderboardFlow.Insert(1, chat));
}
protected override void LoadComplete()
@ -150,7 +164,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete();
masterClockContainer.Reset();
masterClockContainer.Stop();
syncManager.ReadyToStart += onReadyToStart;
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
@ -184,8 +197,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
.DefaultIfEmpty(0)
.Min();
masterClockContainer.Seek(startTime);
masterClockContainer.Start();
masterClockContainer.StartTime = startTime;
masterClockContainer.Reset(true);
// 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;

View File

@ -32,9 +32,22 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<MatchType> Type { get; private set; }
/// <summary>
/// The currently selected item in the <see cref="RoomSubScreen"/>, or the current item from <see cref="Playlist"/>
/// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>.
/// </summary>
[Resolved(typeof(Room))]
protected Bindable<PlaylistItem> CurrentPlaylistItem { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<Room.RoomPlaylistItemStats> PlaylistItemStats { get; private set; }
[Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<Room.RoomDifficultyRange> DifficultyRange { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<RoomCategory> Category { get; private set; }
@ -68,15 +81,12 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<QueueMode> QueueMode { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<TimeSpan> AutoStartDuration { get; private set; }
[Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }
/// <summary>
/// The currently selected item in the <see cref="RoomSubScreen"/>, or the current item from <see cref="Playlist"/>
/// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>.
/// </summary>
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
protected override void LoadComplete()
{
base.LoadComplete();
@ -85,9 +95,13 @@ namespace osu.Game.Screens.OnlinePlay
Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true);
}
protected virtual void UpdateSelectedItem()
=> SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null
? Playlist.GetCurrentItem()
: subScreenSelectedItem.Value;
protected void UpdateSelectedItem()
{
// null room ID means this is a room in the process of being created.
if (RoomID.Value == null)
CurrentPlaylistItem.Value = Playlist.GetCurrentItem();
else if (subScreenSelectedItem != null)
CurrentPlaylistItem.Value = subScreenSelectedItem.Value;
}
}
}

View File

@ -110,41 +110,43 @@ namespace osu.Game.Screens.OnlinePlay
}
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
this.FadeIn();
waves.Show();
Mods.SetDefault();
if (loungeSubScreen.IsCurrentScreen())
loungeSubScreen.OnEntering(last);
loungeSubScreen.OnEntering(e);
else
loungeSubScreen.MakeCurrent();
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
this.FadeIn(250);
this.ScaleTo(1, 250, Easing.OutSine);
Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnResuming(last);
screenStack.CurrentScreen.OnResuming(e);
base.OnResuming(last);
base.OnResuming(e);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
this.ScaleTo(1.1f, 250, Easing.InSine);
this.FadeOut(250);
Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnSuspending(next);
screenStack.CurrentScreen.OnSuspending(e);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
var subScreen = screenStack.CurrentScreen as Drawable;
if (subScreen?.IsLoaded == true && screenStack.CurrentScreen.OnExiting(next))
if (subScreen?.IsLoaded == true && screenStack.CurrentScreen.OnExiting(e))
return true;
RoomManager.PartRoom();
@ -153,7 +155,7 @@ namespace osu.Game.Screens.OnlinePlay
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
base.OnExiting(next);
base.OnExiting(e);
return false;
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@ -37,6 +38,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(CanBeNull = true)]
protected IBindable<PlaylistItem> SelectedItem { get; private set; }
[Resolved]
private RulesetStore rulesets { get; set; }
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
@ -78,10 +82,15 @@ namespace osu.Game.Screens.OnlinePlay
{
base.LoadComplete();
// 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.DeepClone()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
if (rulesetInstance != null)
{
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
Mods.BindValueChanged(onModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);
@ -104,21 +113,13 @@ namespace osu.Game.Screens.OnlinePlay
{
itemSelected = true;
var item = new PlaylistItem
var item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
Beatmap =
{
Value = Beatmap.Value.BeatmapInfo
},
Ruleset =
{
Value = Ruleset.Value
}
RulesetID = Ruleset.Value.OnlineID,
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
};
item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone()));
SelectItem(item);
return true;
}
@ -140,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay
return base.OnBackButton();
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
if (!itemSelected)
{
@ -149,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay
Mods.Value = initialMods;
}
return base.OnExiting(next);
return base.OnExiting(e);
}
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay

View File

@ -27,28 +27,28 @@ namespace osu.Game.Screens.OnlinePlay
public const double DISAPPEAR_DURATION = 500;
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(last);
base.OnEntering(e);
this.FadeInFromZero(APPEAR_DURATION, Easing.OutQuint);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
base.OnExiting(next);
base.OnExiting(e);
this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint);
return false;
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(last);
base.OnResuming(e);
this.FadeIn(APPEAR_DURATION, Easing.OutQuint);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint);
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -33,19 +34,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void load(IBindable<RulesetInfo> ruleset)
{
// Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem
if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap.Value))
if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap))
throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
if (!ruleset.Value.MatchesOnlineID(PlaylistItem.Ruleset.Value))
if (ruleset.Value.OnlineID != PlaylistItem.RulesetID)
throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset");
if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
var requiredLocalMods = PlaylistItem.RequiredMods.Select(m => m.ToMod(GameplayState.Ruleset));
if (!requiredLocalMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
if (base.OnExiting(next))
if (base.OnExiting(e))
return true;
Exited?.Invoke();
@ -63,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo));
}
protected override void Dispose(bool isDisposing)

View File

@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
foreach (var item in Playlist)
{
if (invalidBeatmapIDs.Contains(item.BeatmapID))
if (invalidBeatmapIDs.Contains(item.Beatmap.OnlineID))
item.MarkInvalid();
}
}

View File

@ -69,132 +69,155 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Room.MaxAttempts.BindValueChanged(attempts => progressSection.Alpha = Room.MaxAttempts.Value != null ? 1 : 0, true);
}
protected override Drawable CreateMainContent() => new GridContainer
protected override Drawable CreateMainContent() => new Container
{
RelativeSizeAxes = Axes.Both,
Content = new[]
Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
Child = new GridContainer
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Container
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
Child = new GridContainer
// Playlist items column
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { new OverlinedPlaylistHeader(), },
new Drawable[]
{
new DrawableRoomPlaylist
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Room.Playlist },
SelectedItem = { BindTarget = SelectedItem },
AllowSelection = true,
AllowShowingResults = true,
RequestResults = item =>
{
Debug.Assert(RoomId.Value != null);
ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false));
}
}
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
}
},
// Spacer
null,
// Middle column (mods and leaderboard)
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { new OverlinedPlaylistHeader(), },
new[]
{
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
}
},
},
new Drawable[]
{
new DrawableRoomPlaylist
progressSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Room.Playlist },
SelectedItem = { BindTarget = SelectedItem },
AllowSelection = true,
AllowShowingResults = true,
RequestResults = item =>
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Debug.Assert(RoomId.Value != null);
ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false));
new OverlinedHeader("Progress"),
new RoomLocalUserInfo(),
}
}
},
},
new Drawable[]
{
new OverlinedHeader("Leaderboard")
},
new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, },
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
},
// Spacer
null,
// Main right column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { new OverlinedHeader("Chat") },
new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } }
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
}
},
null,
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new[]
{
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
}
},
},
new Drawable[]
{
progressSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OverlinedHeader("Progress"),
new RoomLocalUserInfo(),
}
},
},
new Drawable[]
{
new OverlinedHeader("Leaderboard")
},
new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, },
new Drawable[] { new OverlinedHeader("Chat"), },
new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } }
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120),
}
},
},
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400),
new Dimension(),
new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600),
}
};

View File

@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.Select;
@ -30,7 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
break;
case 1:
populateItemFromCurrent(Playlist.Single());
Playlist.Clear();
createNewItem();
break;
}
@ -39,26 +41,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void createNewItem()
{
PlaylistItem item = new PlaylistItem
PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1
ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1,
RulesetID = Ruleset.Value.OnlineID,
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
};
populateItemFromCurrent(item);
Playlist.Add(item);
}
private void populateItemFromCurrent(PlaylistItem item)
{
item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone()));
}
}
}

View File

@ -48,7 +48,7 @@ namespace osu.Game.Screens
/// </summary>
protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All;
protected readonly Bindable<OverlayActivation> OverlayActivationMode;
public readonly Bindable<OverlayActivation> OverlayActivationMode;
IBindable<OverlayActivation> IOsuScreen.OverlayActivationMode => OverlayActivationMode;
@ -171,7 +171,7 @@ namespace osu.Game.Screens
background.ApplyToBackground(action);
}
public override void OnResuming(IScreen last)
public override void OnResuming(ScreenTransitionEvent e)
{
if (PlayResumeSound)
sampleExit?.Play();
@ -183,19 +183,19 @@ namespace osu.Game.Screens
if (trackAdjustmentStateAtSuspend != null)
musicController.AllowTrackAdjustments = trackAdjustmentStateAtSuspend.Value;
base.OnResuming(last);
base.OnResuming(e);
}
public override void OnSuspending(IScreen next)
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(next);
base.OnSuspending(e);
trackAdjustmentStateAtSuspend = musicController.AllowTrackAdjustments;
onSuspendingLogo();
}
public override void OnEntering(IScreen last)
public override void OnEntering(ScreenTransitionEvent e)
{
applyArrivingDefaults(false);
@ -210,15 +210,15 @@ namespace osu.Game.Screens
}
background = backgroundStack?.CurrentScreen as BackgroundScreen;
base.OnEntering(last);
base.OnEntering(e);
}
public override bool OnExiting(IScreen next)
public override bool OnExiting(ScreenExitEvent e)
{
if (ValidForResume && logo != null)
onExitingLogo();
if (base.OnExiting(next))
if (base.OnExiting(e))
return true;
if (ownedBackground != null && backgroundStack?.CurrentScreen == ownedBackground)

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
@ -106,7 +107,7 @@ namespace osu.Game.Screens.Play
new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = beatmap?.Background,
Texture = beatmap.Background,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
FillMode = FillMode.Fill,
@ -126,7 +127,7 @@ namespace osu.Game.Screens.Play
{
new OsuSpriteText
{
Text = beatmap?.BeatmapInfo?.DifficultyName,
Text = beatmap.BeatmapInfo.DifficultyName,
Font = OsuFont.GetFont(size: 26, italics: true),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@ -158,7 +159,7 @@ namespace osu.Game.Screens.Play
{
new Drawable[]
{
new MetadataLineLabel("Source"),
new MetadataLineLabel(BeatmapsetsStrings.ShowInfoSource),
new MetadataLineInfo(metadata.Source)
},
new Drawable[]
@ -213,7 +214,7 @@ namespace osu.Game.Screens.Play
private class MetadataLineLabel : OsuSpriteText
{
public MetadataLineLabel(string text)
public MetadataLineLabel(LocalisableString text)
{
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osuTK;
@ -42,7 +43,7 @@ namespace osu.Game.Screens.Play.Break
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"),
AccuracyDisplay = new PercentageBreakInfoLine(BeatmapsetsStrings.ShowStatsAccuracy),
// See https://github.com/ppy/osu/discussions/15185
// RankDisplay = new BreakInfoLine<int>("Rank"),

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.Play.Break
private readonly string prefix;
public BreakInfoLine(string name, string prefix = @"")
public BreakInfoLine(LocalisableString name, string prefix = @"")
{
this.prefix = prefix;
@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play.Break
public class PercentageBreakInfoLine : BreakInfoLine<double>
{
public PercentageBreakInfoLine(string name, string prefix = "")
public PercentageBreakInfoLine(LocalisableString name, string prefix = "")
: base(name, prefix)
{
}

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