mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into osu-distance-spacing
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Screens.Backgrounds
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
@ -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;
|
||||
|
55
osu.Game/Screens/Edit/Components/EditorSidebar.cs
Normal file
55
osu.Game/Screens/Edit/Components/EditorSidebar.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
83
osu.Game/Screens/Edit/Components/EditorSidebarSection.cs
Normal file
83
osu.Game/Screens/Edit/Components/EditorSidebarSection.cs
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.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),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
23
osu.Game/Screens/Edit/Compose/Components/BeatDivisorType.cs
Normal file
23
osu.Game/Screens/Edit/Compose/Components/BeatDivisorType.cs
Normal 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,
|
||||
}
|
||||
}
|
@ -73,6 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
SelectionHandler = CreateSelectionHandler();
|
||||
SelectionHandler.DeselectAll = deselectAll;
|
||||
SelectionHandler.SelectedItems.BindTo(SelectedItems);
|
||||
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
{
|
||||
|
45
osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs
Normal file
45
osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs
Normal 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 = () => { }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
26
osu.Game/Screens/IPerformFromScreenRunner.cs
Normal file
26
osu.Game/Screens/IPerformFromScreenRunner.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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> _)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
|
||||
public PlaylistItemBackground(PlaylistItem? playlistItem)
|
||||
{
|
||||
Beatmap = playlistItem?.Beatmap.Value;
|
||||
Beatmap = playlistItem?.Beatmap;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
29
osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs
Normal file
29
osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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()),
|
||||
};
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
})
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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"),
|
||||
|
@ -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
Reference in New Issue
Block a user