Fix merge conflicts.

This commit is contained in:
Lucas A
2021-08-16 14:50:00 +02:00
180 changed files with 4832 additions and 1813 deletions

View File

@ -15,7 +15,6 @@ using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning;
namespace osu.Game.Screens.Edit.Compose
{
@ -73,7 +72,7 @@ namespace osu.Game.Screens.Edit.Compose
{
Debug.Assert(ruleset != null);
return new RulesetSkinProvidingContainer(ruleset, EditorBeatmap.PlayableBeatmap, beatmap.Value.Skin).WithChild(content);
return new EditorSkinProvidingContainer(EditorBeatmap).WithChild(content);
}
#region Input Handling

View File

@ -153,7 +153,7 @@ namespace osu.Game.Screens.Edit
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.Skin));
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin()));
dependencies.CacheAs(editorBeatmap);
changeHandler = new EditorChangeHandler(editorBeatmap);
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);

View File

@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit
private readonly Bindable<bool> hasTiming = new Bindable<bool>();
[CanBeNull]
public readonly ISkin BeatmapSkin;
public readonly EditorBeatmapSkin BeatmapSkin;
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
@ -69,7 +69,8 @@ namespace osu.Game.Screens.Edit
public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null)
{
PlayableBeatmap = playableBeatmap;
BeatmapSkin = beatmapSkin;
if (beatmapSkin is Skin skin)
BeatmapSkin = new EditorBeatmapSkin(skin);
beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap);

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A beatmap skin which is being edited.
/// </summary>
public class EditorBeatmapSkin : ISkin
{
public event Action BeatmapSkinChanged;
/// <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 EditorBeatmapSkin(Skin skin)
{
this.skin = skin;
ComboColours = new BindableList<Colour4>();
if (skin.Configuration.ComboColours != null)
ComboColours.AddRange(skin.Configuration.ComboColours.Select(c => (Colour4)c));
ComboColours.BindCollectionChanged((_, __) => updateColours());
}
private void invokeSkinChanged() => BeatmapSkinChanged?.Invoke();
private void updateColours()
{
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);
#endregion
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Skinning;
#nullable enable
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A <see cref="SkinProvidingContainer"/> that fires <see cref="ISkinSource.SourceChanged"/> when users have made a change to the beatmap skin
/// of the map being edited.
/// </summary>
public class EditorSkinProvidingContainer : RulesetSkinProvidingContainer
{
private readonly EditorBeatmapSkin? beatmapSkin;
public EditorSkinProvidingContainer(EditorBeatmap editorBeatmap)
: base(editorBeatmap.PlayableBeatmap.BeatmapInfo.Ruleset.CreateInstance(), editorBeatmap, editorBeatmap.BeatmapSkin)
{
beatmapSkin = editorBeatmap.BeatmapSkin;
}
protected override void LoadComplete()
{
base.LoadComplete();
if (beatmapSkin != null)
beatmapSkin.BeatmapSkinChanged += TriggerSourceChanged;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmapSkin != null)
beatmapSkin.BeatmapSkinChanged -= TriggerSourceChanged;
}
}
}

View File

@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
}

View File

@ -1,14 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Setup
{
@ -31,9 +27,8 @@ namespace osu.Game.Screens.Edit.Setup
}
};
var colours = Beatmap.BeatmapSkin?.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value;
if (colours != null)
comboColours.Colours.AddRange(colours.Select(c => (Colour4)c));
if (Beatmap.BeatmapSkin != null)
comboColours.Colours.BindTo(Beatmap.BeatmapSkin.ComboColours);
}
}
}

View File

@ -10,12 +10,12 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
public class OnlinePlayBackgroundSprite : OnlinePlayComposite
{
private readonly BeatmapSetCoverType beatmapSetCoverType;
protected readonly BeatmapSetCoverType BeatmapSetCoverType;
private UpdateableBeatmapBackgroundSprite sprite;
public OnlinePlayBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
{
this.beatmapSetCoverType = beatmapSetCoverType;
BeatmapSetCoverType = beatmapSetCoverType;
}
[BackgroundDependencyLoader]
@ -33,6 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value;
}
protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(beatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
}
}

View File

@ -1,117 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
namespace osu.Game.Screens.OnlinePlay.Components
{
public class RoomStatusInfo : OnlinePlayComposite
{
public RoomStatusInfo()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
StatusPart statusPart;
EndDatePart endDatePart;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
statusPart = new StatusPart
{
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14)
},
endDatePart = new EndDatePart { Font = OsuFont.GetFont(size: 14) }
}
};
statusPart.EndDate.BindTo(EndDate);
statusPart.Status.BindTo(Status);
statusPart.Availability.BindTo(Availability);
endDatePart.EndDate.BindTo(EndDate);
}
private class EndDatePart : DrawableDate
{
public readonly IBindable<DateTimeOffset?> EndDate = new Bindable<DateTimeOffset?>();
public EndDatePart()
: base(DateTimeOffset.UtcNow)
{
EndDate.BindValueChanged(date =>
{
// If null, set a very large future date to prevent unnecessary schedules.
Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1);
}, true);
}
protected override string Format()
{
if (EndDate.Value == null)
return string.Empty;
var diffToNow = Date.Subtract(DateTimeOffset.Now);
if (diffToNow.TotalSeconds < -5)
return $"Closed {base.Format()}";
if (diffToNow.TotalSeconds < 0)
return "Closed";
if (diffToNow.TotalSeconds < 5)
return "Closing soon";
return $"Closing {base.Format()}";
}
}
private class StatusPart : EndDatePart
{
public readonly IBindable<RoomStatus> Status = new Bindable<RoomStatus>();
public readonly IBindable<RoomAvailability> Availability = new Bindable<RoomAvailability>();
[Resolved]
private OsuColour colours { get; set; }
public StatusPart()
{
EndDate.BindValueChanged(_ => Format());
Status.BindValueChanged(_ => Format());
Availability.BindValueChanged(_ => Format());
}
protected override void LoadComplete()
{
base.LoadComplete();
Text = Format();
}
protected override string Format()
{
if (!IsLoaded)
return string.Empty;
RoomStatus status = Date < DateTimeOffset.Now ? new RoomStatusEnded() : Status.Value ?? new RoomStatusOpen();
this.FadeColour(status.GetAppropriateColour(colours), 100);
return $"{Availability.Value.GetDescription()}, {status.Message}";
}
}
}
}

View File

@ -1,12 +1,14 @@
// 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.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Ranking.Expanded;
@ -85,6 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
minDisplay.Current.Value = minDifficulty;
maxDisplay.Current.Value = maxDifficulty;
maxDisplay.Alpha = Precision.AlmostEquals(Math.Round(minDifficulty.Stars, 2), Math.Round(maxDifficulty.Stars, 2)) ? 0 : 1;
minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars);
maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars);

View File

@ -2,19 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
@ -22,52 +18,30 @@ namespace osu.Game.Screens.OnlinePlay
{
public const float HEIGHT = 80;
private readonly ScreenStack stack;
private readonly MultiHeaderTitle title;
public Header(string mainTitle, ScreenStack stack)
{
this.stack = stack;
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING };
HeaderBreadcrumbControl breadcrumbs;
MultiHeaderTitle title;
Children = new Drawable[]
Child = title = new MultiHeaderTitle(mainTitle)
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"#1f1921"),
},
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Children = new Drawable[]
{
title = new MultiHeaderTitle(mainTitle)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomLeft,
},
breadcrumbs = new HeaderBreadcrumbControl(stack)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft
}
},
},
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
};
breadcrumbs.Current.ValueChanged += screen =>
{
if (screen.NewValue is IOnlinePlaySubScreen onlineSubScreen)
title.Screen = onlineSubScreen;
};
breadcrumbs.Current.TriggerChange();
// unnecessary to unbind these as this header has the same lifetime as the screen stack we are attaching to.
stack.ScreenPushed += (_, __) => updateSubScreenTitle();
stack.ScreenExited += (_, __) => updateSubScreenTitle();
}
private void updateSubScreenTitle() => title.Screen = stack.CurrentScreen as IOnlinePlaySubScreen;
private class MultiHeaderTitle : CompositeDrawable
{
private const float spacing = 6;
@ -75,9 +49,10 @@ namespace osu.Game.Screens.OnlinePlay
private readonly OsuSpriteText dot;
private readonly OsuSpriteText pageTitle;
[CanBeNull]
public IOnlinePlaySubScreen Screen
{
set => pageTitle.Text = value.ShortTitle.Titleize();
set => pageTitle.Text = value?.ShortTitle.Titleize() ?? string.Empty;
}
public MultiHeaderTitle(string mainTitle)
@ -125,35 +100,5 @@ namespace osu.Game.Screens.OnlinePlay
pageTitle.Colour = dot.Colour = colours.Yellow;
}
}
private class HeaderBreadcrumbControl : ScreenBreadcrumbControl
{
public HeaderBreadcrumbControl(ScreenStack stack)
: base(stack)
{
RelativeSizeAxes = Axes.X;
StripColour = Color4.Transparent;
}
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour = Color4Extensions.FromHex("#e35c99");
}
protected override TabItem<IScreen> CreateTabItem(IScreen value) => new HeaderBreadcrumbTabItem(value)
{
AccentColour = AccentColour
};
private class HeaderBreadcrumbTabItem : BreadcrumbTabItem
{
public HeaderBreadcrumbTabItem(IScreen value)
: base(value)
{
Bar.Colour = Color4.Transparent;
}
}
}
}
}

View File

@ -5,10 +5,13 @@ using System;
using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
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.Effects;
@ -18,7 +21,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -26,6 +28,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
using osuTK.Graphics;
@ -35,19 +38,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler<GlobalAction>
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
private const float corner_radius = 10;
private const float transition_duration = 60;
private const float content_padding = 10;
private const float height = 110;
private const float side_strip_width = 5;
private const float cover_width = 145;
private const float height = 100;
public event Action<SelectionState> StateChanged;
private readonly Box selectionBox;
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
private Drawable selectionBox;
[Resolved(canBeNull: true)]
private OnlinePlayScreen parentScreen { get; set; }
private LoungeSubScreen loungeScreen { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -62,19 +64,26 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private SelectionState state;
private Sample sampleSelect;
private Sample sampleJoin;
public SelectionState State
{
get => state;
set
{
if (value == state) return;
if (value == state)
return;
state = value;
if (state == SelectionState.Selected)
selectionBox.FadeIn(transition_duration);
else
selectionBox.FadeOut(transition_duration);
if (selectionBox != null)
{
if (state == SelectionState.Selected)
selectionBox.FadeIn(transition_duration);
else
selectionBox.FadeOut(transition_duration);
}
StateChanged?.Invoke(State);
}
@ -101,6 +110,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
private int numberOfAvatars = 7;
public int NumberOfAvatars
{
get => numberOfAvatars;
set
{
numberOfAvatars = value;
if (recentParticipantsList != null)
recentParticipantsList.NumberOfCircles = value;
}
}
private readonly Bindable<RoomCategory> roomCategory = new Bindable<RoomCategory>();
private RecentParticipantsList recentParticipantsList;
private RoomSpecialCategoryPill specialCategoryPill;
public bool FilteringActive { get; set; }
private PasswordProtectedIcon passwordIcon;
@ -112,115 +140,197 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Room = room;
RelativeSizeAxes = Axes.X;
Height = height + SELECTION_BORDER_WIDTH * 2;
CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2;
Masking = true;
Height = height;
// create selectionBox here so State can be set before being loaded
selectionBox = new Box
Masking = true;
CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2;
EdgeEffect = new EdgeEffectParameters
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load(OverlayColourProvider colours, AudioManager audio)
{
float stripWidth = side_strip_width * (Room.Category.Value == RoomCategory.Spotlight ? 2 : 1);
Children = new Drawable[]
{
new StatusColouredContainer(transition_duration)
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new Box
{
RelativeSizeAxes = Axes.Both,
Child = selectionBox
Colour = colours.Background5,
},
new OnlinePlayBackgroundSprite
{
RelativeSizeAxes = Axes.Both
},
new Container
{
Name = @"Room content",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(SELECTION_BORDER_WIDTH),
// This negative padding resolves 1px gaps between this background and the background above.
Padding = new MarginPadding { Left = 20, Vertical = -0.5f },
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = corner_radius,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
},
Children = new Drawable[]
{
new Box
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"212121"),
},
new StatusColouredContainer(transition_duration)
{
RelativeSizeAxes = Axes.Y,
Width = stripWidth,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
new Container
{
RelativeSizeAxes = Axes.Y,
Width = cover_width,
Masking = true,
Margin = new MarginPadding { Left = stripWidth },
Child = new OnlinePlayBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both }
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 0.2f)
},
Content = new[]
{
new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
new Container
{
Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Vertical = content_padding,
Left = stripWidth + cover_width + content_padding,
Right = content_padding,
Left = 20,
Vertical = 5
},
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5f),
Children = new Drawable[]
{
new RoomName { Font = OsuFont.GetFont(size: 18) },
new ParticipantInfo(),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
specialCategoryPill = new RoomSpecialCategoryPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new EndDateInfo
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new RoomNameText(),
new RoomHostText(),
}
}
},
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusInfo(),
new BeatmapTitle { TextSize = 14 },
},
},
new ModeTypeInfo
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
new PlaylistCountPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new StarRatingRangeDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f)
}
}
}
}
},
new FillFlowContainer
{
Name = "Right content",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Right = 10,
Vertical = 5
},
Children = new Drawable[]
{
recentParticipantsList = new RecentParticipantsList
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
NumberOfCircles = NumberOfAvatars
}
}
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
},
},
new StatusColouredContainer(transition_duration)
{
RelativeSizeAxes = Axes.Both,
Child = selectionBox = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = state == SelectionState.Selected ? 1 : 0,
Masking = true,
CornerRadius = corner_radius,
BorderThickness = SELECTION_BORDER_WIDTH,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
},
};
sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Submit.GetDescription()}-select");
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -240,6 +350,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
else
Alpha = 0;
roomCategory.BindTo(Room.Category);
roomCategory.BindValueChanged(c =>
{
if (c.NewValue == RoomCategory.Spotlight)
specialCategoryPill.Show();
else
specialCategoryPill.Hide();
}, true);
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
}
@ -250,7 +369,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
parentScreen?.OpenNewRoom(Room.DeepClone());
lounge?.Open(Room.DeepClone());
})
};
@ -273,32 +392,40 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
}
protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected;
protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected || child is HoverSounds;
protected override bool OnClick(ClickEvent e)
{
if (Room != selectedRoom.Value)
{
sampleSelect?.Play();
selectedRoom.Value = Room;
return true;
}
if (Room.HasPassword.Value)
{
sampleJoin?.Play();
this.ShowPopover();
return true;
}
sampleJoin?.Play();
lounge?.Join(Room, null);
return base.OnClick(e);
}
private class RoomName : OsuSpriteText
private class RoomNameText : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
private Bindable<string> name { get; set; }
public RoomNameText()
{
Font = OsuFont.GetFont(size: 28);
}
[BackgroundDependencyLoader]
private void load()
{
@ -306,6 +433,41 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
private class RoomHostText : OnlinePlayComposite
{
private LinkFlowContainer hostText;
public RoomHostText()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 16))
{
AutoSizeAxes = Axes.Both
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Host.BindValueChanged(host =>
{
hostText.Clear();
if (host.NewValue != null)
{
hostText.AddText("hosted by ");
hostText.AddUserLink(host.NewValue);
}
}, true);
}
}
public class PasswordProtectedIcon : CompositeDrawable
{
[BackgroundDependencyLoader]
@ -353,7 +515,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private OsuPasswordTextBox passwordTextbox;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
Child = new FillFlowContainer
{

View File

@ -0,0 +1,65 @@
// 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.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class EndDateInfo : OnlinePlayComposite
{
public EndDateInfo()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new EndDatePart
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
EndDate = { BindTarget = EndDate }
};
}
private class EndDatePart : DrawableDate
{
public readonly IBindable<DateTimeOffset?> EndDate = new Bindable<DateTimeOffset?>();
public EndDatePart()
: base(DateTimeOffset.UtcNow)
{
EndDate.BindValueChanged(date =>
{
// If null, set a very large future date to prevent unnecessary schedules.
Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1);
}, true);
}
protected override string Format()
{
if (EndDate.Value == null)
return string.Empty;
var diffToNow = Date.Subtract(DateTimeOffset.Now);
if (diffToNow.TotalSeconds < -5)
return $"Closed {base.Format()}";
if (diffToNow.TotalSeconds < 0)
return "Closed";
if (diffToNow.TotalSeconds < 5)
return "Closing soon";
return $"Closing {base.Format()}";
}
}
}
}

View File

@ -1,135 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public abstract class FilterControl : CompositeDrawable
{
protected const float VERTICAL_PADDING = 10;
protected const float HORIZONTAL_PADDING = 80;
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
private readonly Box tabStrip;
private readonly SearchTextBox search;
private readonly PageTabControl<RoomStatusFilter> tabs;
protected FilterControl()
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.25f,
},
tabStrip = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Top = VERTICAL_PADDING,
Horizontal = HORIZONTAL_PADDING
},
Children = new Drawable[]
{
search = new FilterSearchTextBox
{
RelativeSizeAxes = Axes.X,
},
tabs = new PageTabControl<RoomStatusFilter>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
},
}
}
};
tabs.Current.Value = RoomStatusFilter.Open;
tabs.Current.TriggerChange();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
filter ??= new Bindable<FilterCriteria>();
tabStrip.Colour = colours.Yellow;
}
protected override void LoadComplete()
{
base.LoadComplete();
search.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
tabs.Current.BindValueChanged(_ => UpdateFilter(), true);
}
private ScheduledDelegate scheduledFilterUpdate;
private void updateFilterDebounced()
{
scheduledFilterUpdate?.Cancel();
scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200);
}
protected void UpdateFilter() => Scheduler.AddOnce(updateFilter);
private void updateFilter()
{
scheduledFilterUpdate?.Cancel();
var criteria = CreateCriteria();
criteria.SearchString = search.Current.Value;
criteria.Status = tabs.Current.Value;
criteria.Ruleset = ruleset.Value;
filter.Value = criteria;
}
protected virtual FilterCriteria CreateCriteria() => new FilterCriteria();
public bool HoldFocus
{
get => search.HoldFocus;
set => search.HoldFocus = value;
}
public void TakeFocus() => search.TakeFocus();
private class FilterSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
}
}

View File

@ -1,88 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class ParticipantInfo : OnlinePlayComposite
{
public ParticipantInfo()
{
RelativeSizeAxes = Axes.X;
Height = 15f;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
OsuSpriteText summary;
Container flagContainer;
LinkFlowContainer hostText;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
Children = new Drawable[]
{
flagContainer = new Container
{
Width = 22f,
RelativeSizeAxes = Axes.Y,
},
hostText = new LinkFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both
}
},
},
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Colour = colours.Gray9,
Children = new[]
{
summary = new OsuSpriteText
{
Text = "0 participants",
}
},
},
};
Host.BindValueChanged(host =>
{
hostText.Clear();
flagContainer.Clear();
if (host.NewValue != null)
{
hostText.AddText("hosted by ");
hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true));
flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both };
}
}, true);
ParticipantCount.BindValueChanged(count => summary.Text = "participant".ToQuantity(count.NewValue), true);
}
}
}

View File

@ -0,0 +1,81 @@
// 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;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
/// <summary>
/// Displays contents in a "pill".
/// </summary>
public class PillContainer : Container
{
private const float padding = 8;
public readonly Drawable Background;
protected override Container<Drawable> Content => content;
private readonly Container content;
public PillContainer()
{
AutoSizeAxes = Axes.X;
Height = 16;
InternalChild = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Masking = true,
Children = new[]
{
Background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = padding },
Child = new GridContainer
{
AutoSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding)
},
Content = new[]
{
new[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 2 },
Child = content = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
}
}
}
}
}
};
}
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
/// <summary>
/// A pill that displays the playlist item count.
/// </summary>
public class PlaylistCountPill : OnlinePlayComposite
{
private OsuTextFlowContainer count;
public PlaylistCountPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = count = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged(updateCount, true);
}
private void updateCount(object sender, NotifyCollectionChangedEventArgs e)
{
count.Clear();
count.AddText(Playlist.Count.ToString(), s => s.Font = s.Font.With(weight: FontWeight.Bold));
count.AddText(" ");
count.AddText("Beatmap".ToQuantity(Playlist.Count, ShowQuantityAs.None));
}
}
}

View File

@ -1,59 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class PlaylistsFilterControl : FilterControl
{
private readonly Dropdown<PlaylistsCategory> dropdown;
public PlaylistsFilterControl()
{
AddInternal(dropdown = new SlimEnumDropdown<PlaylistsCategory>
{
Anchor = Anchor.BottomRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160,
X = -HORIZONTAL_PADDING,
Y = -30
});
}
protected override void LoadComplete()
{
base.LoadComplete();
dropdown.Current.BindValueChanged(_ => UpdateFilter());
}
protected override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
switch (dropdown.Current.Value)
{
case PlaylistsCategory.Normal:
criteria.Category = "normal";
break;
case PlaylistsCategory.Spotlight:
criteria.Category = "spotlight";
break;
}
return criteria;
}
private enum PlaylistsCategory
{
Any,
Normal,
Spotlight
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RankRangePill : MultiplayerRoomComposite
{
private OsuTextFlowContainer rankFlow;
public RankRangePill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(4),
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(8),
Icon = FontAwesome.Solid.User
},
rankFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
}
}
}
};
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
rankFlow.Clear();
if (Room == null || Room.Users.All(u => u.User == null))
{
rankFlow.AddText("-");
return;
}
int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
rankFlow.AddText("#");
rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
rankFlow.AddText(" - ");
rankFlow.AddText("#");
rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
}
}
}

View File

@ -0,0 +1,278 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RecentParticipantsList : OnlinePlayComposite
{
private const float avatar_size = 36;
private FillFlowContainer<CircularAvatar> avatarFlow;
private HiddenUserCount hiddenUsers;
private OsuSpriteText totalCount;
public RecentParticipantsList()
{
AutoSizeAxes = Axes.X;
Height = 60;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Shear = new Vector2(0.2f, 0),
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background4,
}
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Padding = new MarginPadding { Right = 16 },
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(16),
Margin = new MarginPadding { Left = 8 },
Icon = FontAwesome.Solid.User,
},
totalCount = new OsuSpriteText
{
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
avatarFlow = new FillFlowContainer<CircularAvatar>
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Margin = new MarginPadding { Left = 4 },
},
hiddenUsers = new HiddenUserCount
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
RecentParticipants.BindCollectionChanged(onParticipantsChanged, true);
ParticipantCount.BindValueChanged(_ =>
{
updateHiddenUsers();
totalCount.Text = ParticipantCount.Value.ToString();
}, true);
}
private int numberOfCircles = 4;
/// <summary>
/// The maximum number of circles visible (including the "hidden count" circle in the overflow case).
/// </summary>
public int NumberOfCircles
{
get => numberOfCircles;
set
{
numberOfCircles = value;
if (LoadState < LoadState.Loaded)
return;
// Reinitialising the list looks janky, but this is unlikely to be used in a setting where it's visible.
clearUsers();
foreach (var u in RecentParticipants)
addUser(u);
updateHiddenUsers();
}
}
private void onParticipantsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var added in e.NewItems.OfType<User>())
addUser(added);
break;
case NotifyCollectionChangedAction.Remove:
foreach (var removed in e.OldItems.OfType<User>())
removeUser(removed);
break;
case NotifyCollectionChangedAction.Reset:
clearUsers();
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
// Easiest is to just reinitialise the whole list. These are unlikely to ever be use cases.
clearUsers();
foreach (var u in RecentParticipants)
addUser(u);
break;
}
updateHiddenUsers();
}
private int displayedCircles => avatarFlow.Count + (hiddenUsers.Count > 0 ? 1 : 0);
private void addUser(User user)
{
if (displayedCircles < NumberOfCircles)
avatarFlow.Add(new CircularAvatar { User = user });
}
private void removeUser(User user)
{
avatarFlow.RemoveAll(a => a.User == user);
}
private void clearUsers()
{
avatarFlow.Clear();
updateHiddenUsers();
}
private void updateHiddenUsers()
{
int hiddenCount = 0;
if (RecentParticipants.Count > NumberOfCircles)
hiddenCount = ParticipantCount.Value - NumberOfCircles + 1;
hiddenUsers.Count = hiddenCount;
if (displayedCircles > NumberOfCircles)
avatarFlow.Remove(avatarFlow.Last());
else if (displayedCircles < NumberOfCircles)
{
var nextUser = RecentParticipants.FirstOrDefault(u => avatarFlow.All(a => a.User != u));
if (nextUser != null) addUser(nextUser);
}
}
private class CircularAvatar : CompositeDrawable
{
public User User
{
get => avatar.User;
set => avatar.User = value;
}
private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both };
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
Size = new Vector2(avatar_size);
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colours.Background5,
RelativeSizeAxes = Axes.Both,
},
avatar
}
};
}
}
public class HiddenUserCount : CompositeDrawable
{
public int Count
{
get => count;
set
{
count = value;
countText.Text = $"+{count}";
if (count > 0)
Show();
else
Hide();
}
}
private int count;
private readonly SpriteText countText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.Bold),
};
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
Size = new Vector2(avatar_size);
Alpha = 0;
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
countText
}
};
}
}
}
}

View File

@ -1,86 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomInfo : OnlinePlayComposite
{
private readonly List<Drawable> statusElements = new List<Drawable>();
private readonly OsuTextFlowContainer roomName;
public RoomInfo()
{
AutoSizeAxes = Axes.Y;
RoomLocalUserInfo localUserInfo;
RoomStatusInfo statusInfo;
ModeTypeInfo typeInfo;
ParticipantInfo participantInfo;
InternalChild = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 10),
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
roomName = new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 30))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
participantInfo = new ParticipantInfo(),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
statusInfo = new RoomStatusInfo(),
typeInfo = new ModeTypeInfo
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
}
}
},
localUserInfo = new RoomLocalUserInfo(),
}
};
statusElements.AddRange(new Drawable[]
{
statusInfo, typeInfo, participantInfo, localUserInfo
});
}
protected override void LoadComplete()
{
base.LoadComplete();
if (RoomID.Value == null)
statusElements.ForEach(e => e.FadeOut());
RoomID.BindValueChanged(id =>
{
if (id.NewValue == null)
statusElements.ForEach(e => e.FadeOut(100));
else
statusElements.ForEach(e => e.FadeIn(100));
}, true);
RoomName.BindValueChanged(name =>
{
roomName.Text = name.NewValue ?? "No room selected";
}, true);
}
}
}

View File

@ -1,91 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomInspector : OnlinePlayComposite
{
private const float transition_duration = 100;
private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 };
[Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
OverlinedHeader participantsHeader;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.25f
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 30 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new RoomInfo
{
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Vertical = 60 },
},
participantsHeader = new OverlinedHeader("Recent Participants"),
new ParticipantsDisplay(Direction.Vertical)
{
RelativeSizeAxes = Axes.X,
Height = ParticipantsList.TILE_SIZE * 3,
Details = { BindTarget = participantsHeader.Details }
}
}
}
},
new Drawable[] { new OverlinedPlaylistHeader(), },
new Drawable[]
{
new DrawableRoomPlaylist(false, false)
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Playlist }
},
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
}
}
};
}
}
}

View File

@ -0,0 +1,49 @@
// 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.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomSpecialCategoryPill : OnlinePlayComposite
{
private SpriteText text;
public RoomSpecialCategoryPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = new PillContainer
{
Background =
{
Colour = colours.Pink,
Alpha = 1
},
Child = text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
Colour = Color4.Black
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Category.BindValueChanged(c => text.Text = c.NewValue.ToString(), true);
}
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
/// <summary>
/// A pill that displays the room's current status.
/// </summary>
public class RoomStatusPill : OnlinePlayComposite
{
[Resolved]
private OsuColour colours { get; set; }
private PillContainer pill;
private SpriteText statusText;
public RoomStatusPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = pill = new PillContainer
{
Child = statusText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
Colour = Color4.Black
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
EndDate.BindValueChanged(_ => updateDisplay());
Status.BindValueChanged(_ => updateDisplay(), true);
FinishTransforms(true);
}
private void updateDisplay()
{
RoomStatus status = getDisplayStatus();
pill.Background.Alpha = 1;
pill.Background.FadeColour(status.GetAppropriateColour(colours), 100);
statusText.Text = status.Message;
}
private RoomStatus getDisplayStatus()
{
if (EndDate.Value < DateTimeOffset.Now)
return new RoomStatusEnded();
return Status.Value;
}
}
}

View File

@ -50,6 +50,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
// account for the fact we are in a scroll container and want a bit of spacing from the scroll bar.
Padding = new MarginPadding { Right = 5 };
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.X,
@ -59,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Spacing = new Vector2(10),
}
};
}
@ -137,7 +140,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.Remove(toRemove);
selectedRoom.Value = null;
// selection may have a lease due to being in a sub screen.
if (!selectedRoom.Disabled)
selectedRoom.Value = null;
}
}
@ -149,7 +154,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
protected override bool OnClick(ClickEvent e)
{
selectedRoom.Value = null;
if (!selectedRoom.Disabled)
selectedRoom.Value = null;
return base.OnClick(e);
}
@ -211,6 +217,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void selectNext(int direction)
{
if (selectedRoom.Disabled)
return;
var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent);
Room room;

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -9,15 +11,20 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge
{
@ -28,11 +35,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
protected Container<OsuButton> Buttons { get; } = new Container<OsuButton>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both
};
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private FilterControl filter;
private Container content;
private LoadingLayer loadingLayer;
[Resolved]
@ -44,53 +56,112 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved(CanBeNull = true)]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
private RoomsContainer roomsContainer;
private SearchTextBox searchTextBox;
private Dropdown<RoomStatusFilter> statusDropdown;
[CanBeNull]
private LeasedBindable<Room> selectionLease;
[BackgroundDependencyLoader]
private void load()
{
filter ??= new Bindable<FilterCriteria>(new FilterCriteria());
OsuScrollContainer scrollContainer;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
content = new Container
loadingLayer = new LoadingLayer(true),
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Padding = new MarginPadding
{
new Container
Left = WaveOverlayContainer.WIDTH_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING,
},
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
RelativeSizeAxes = Axes.Both,
Width = 0.55f,
Children = new Drawable[]
new Dimension(GridSizeMode.Absolute, Header.HEIGHT),
new Dimension(GridSizeMode.Absolute, 25),
new Dimension(GridSizeMode.Absolute, 20)
},
Content = new[]
{
new Drawable[]
{
scrollContainer = new OsuScrollContainer
searchTextBox = new LoungeSearchTextBox
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Width = 0.6f,
},
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Padding = new MarginPadding(10),
Child = roomsContainer = new RoomsContainer()
Depth = float.MinValue, // Contained filters should appear over the top of rooms.
Children = new Drawable[]
{
Buttons.WithChild(CreateNewRoomButton().With(d =>
{
d.Anchor = Anchor.BottomLeft;
d.Origin = Anchor.BottomLeft;
d.Size = new Vector2(150, 37.5f);
d.Action = () => Open();
})),
new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d =>
{
d.Anchor = Anchor.TopRight;
d.Origin = Anchor.TopRight;
}))
}
}
}
},
null,
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer()
},
}
},
loadingLayer = new LoadingLayer(true),
}
},
new RoomInspector
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Width = 0.45f,
},
}
},
},
filter = CreateFilterControl().With(d =>
{
d.RelativeSizeAxes = Axes.X;
d.Height = 80;
})
};
// scroll selected room into view on selection.
@ -106,6 +177,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.LoadComplete();
searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived);
initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer());
@ -114,24 +188,49 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
}
updateFilter();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
#region Filtering
content.Padding = new MarginPadding
protected void UpdateFilter() => Scheduler.AddOnce(updateFilter);
private ScheduledDelegate scheduledFilterUpdate;
private void updateFilterDebounced()
{
scheduledFilterUpdate?.Cancel();
scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200);
}
private void updateFilter()
{
scheduledFilterUpdate?.Cancel();
filter.Value = CreateFilterCriteria();
}
protected virtual FilterCriteria CreateFilterCriteria() => new FilterCriteria
{
SearchString = searchTextBox.Current.Value,
Ruleset = ruleset.Value,
Status = statusDropdown.Current.Value
};
protected virtual IEnumerable<Drawable> CreateFilterControls()
{
statusDropdown = new SlimEnumDropdown<RoomStatusFilter>
{
Top = filter.DrawHeight,
Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING,
RelativeSizeAxes = Axes.None,
Width = 160,
};
statusDropdown.Current.BindValueChanged(_ => UpdateFilter());
yield return statusDropdown;
}
protected override void OnFocus(FocusEvent e)
{
filter.TakeFocus();
}
#endregion
public override void OnEntering(IScreen last)
{
@ -144,6 +243,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.OnResuming(last);
Debug.Assert(selectionLease != null);
selectionLease.Return();
selectionLease = null;
if (selectedRoom.Value?.RoomID.Value == null)
selectedRoom.Value = new Room();
@ -164,14 +268,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
base.OnSuspending(next);
}
protected override void OnFocus(FocusEvent e)
{
searchTextBox.TakeFocus();
}
private void onReturning()
{
filter.HoldFocus = true;
searchTextBox.HoldFocus = true;
}
private void onLeaving()
{
filter.HoldFocus = false;
searchTextBox.HoldFocus = false;
// ensure any password prompt is dismissed.
this.HidePopover();
@ -199,23 +308,32 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
/// <summary>
/// Push a room as a new subscreen.
/// </summary>
public void Open(Room room) => Schedule(() =>
/// <param name="room">An optional template to use when creating the room.</param>
public void Open(Room room = null) => Schedule(() =>
{
// Handles the case where a room is clicked 3 times in quick succession
if (!this.IsCurrentScreen())
return;
OpenNewRoom(room);
OpenNewRoom(room ?? CreateNewRoom());
});
protected virtual void OpenNewRoom(Room room)
{
selectedRoom.Value = room;
selectionLease = selectedRoom.BeginLease(false);
Debug.Assert(selectionLease != null);
selectionLease.Value = room;
this.Push(CreateRoomSubScreen(room));
}
protected abstract FilterControl CreateFilterControl();
protected abstract OsuButton CreateNewRoomButton();
/// <summary>
/// Creates a new room.
/// </summary>
/// <returns>The created <see cref="Room"/>.</returns>
protected abstract Room CreateNewRoom();
protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
@ -226,5 +344,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
else
loadingLayer.Hide();
}
private class LoungeSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
}
}

View File

@ -12,6 +12,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
[BackgroundDependencyLoader]
private void load()
{
SpriteText.Font = SpriteText.Font.With(size: 14);
Triangles.TriangleScale = 1.5f;
}

View File

@ -8,8 +8,10 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -17,6 +19,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Match
{
@ -61,8 +64,15 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected RoomSubScreen()
{
Padding = new MarginPadding { Top = Header.HEIGHT };
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"3e3a44") // This is super temporary.
},
BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = SelectedItem }
@ -250,5 +260,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
}
public class UserModSelectButton : PurpleTriangleButton
{
}
}
}

View File

@ -0,0 +1,40 @@
// 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;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class GameplayMatchScoreDisplay : MatchScoreDisplay
{
public Bindable<bool> Expanded = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
Scale = new Vector2(0.5f);
Expanded.BindValueChanged(expandedChanged, true);
}
private void expandedChanged(ValueChangedEvent<bool> expanded)
{
if (expanded.NewValue)
{
Score1Text.FadeIn(500, Easing.OutQuint);
Score2Text.FadeIn(500, Easing.OutQuint);
this.ResizeWidthTo(2, 500, Easing.OutQuint);
}
else
{
Score1Text.FadeOut(500, Easing.OutQuint);
Score2Text.FadeOut(500, Easing.OutQuint);
this.ResizeWidthTo(1, 500, Easing.OutQuint);
}
}
}
}

View File

@ -4,9 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
@ -54,20 +52,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})");
}
protected override Room CreateNewRoom() =>
new Room
{
Name = { Value = $"{API.LocalUser}'s awesome room" },
Category = { Value = RoomCategory.Realtime },
Type = { Value = MatchType.HeadToHead },
};
protected override string ScreenTitle => "Multiplayer";
protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager();
protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen();
protected override OsuButton CreateNewMultiplayerGameButton() => new CreateMultiplayerMatchButton();
}
}

View File

@ -1,17 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class MultiplayerFilterControl : FilterControl
{
protected override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
criteria.Category = "realtime";
return criteria;
}
}
}

View File

@ -3,6 +3,8 @@
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
@ -13,13 +15,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class MultiplayerLoungeSubScreen : LoungeSubScreen
{
protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl();
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private MultiplayerClient client { get; set; }
protected override FilterCriteria CreateFilterCriteria()
{
var criteria = base.CreateFilterCriteria();
criteria.Category = @"realtime";
return criteria;
}
protected override OsuButton CreateNewRoomButton() => new CreateMultiplayerMatchButton();
protected override Room CreateNewRoom() => new Room
{
Name = { Value = $"{api.LocalUser}'s awesome room" },
Category = { Value = RoomCategory.Realtime },
Type = { Value = MatchType.HeadToHead },
};
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
protected override void OpenNewRoom(Room room)
{
if (client?.IsConnected.Value != true)

View File

@ -176,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new PurpleTriangleButton
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
Schedule(this.Exit);
handleRoomLost();
}, true);
currentRoom.BindValueChanged(room =>
@ -284,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// the room has gone away.
// this could mean something happened during the join process, or an external connection issue occurred.
// one specific scenario is where the underlying room is created, but the signalr server returns an error during the join process. this triggers a PartRoom operation (see https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs#L97)
Schedule(this.Exit);
handleRoomLost();
}
}, true);
}
@ -448,9 +448,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onRoomUpdated()
{
// may happen if the client is kicked or otherwise removed from the room.
if (client.Room == null)
{
handleRoomLost();
return;
}
Scheduler.AddOnce(UpdateMods);
}
private void handleRoomLost() => Schedule(() =>
{
if (this.IsCurrentScreen())
this.Exit();
else
ValidForResume = false;
});
private void onLoadRequested()
{
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
@ -475,16 +490,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override Screen CreateGameplayScreen()
{
Debug.Assert(client.LocalUser != null);
Debug.Assert(client.Room != null);
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray();
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
return new MultiSpectatorScreen(userIds);
return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
default:
return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, users));
}
}

View File

@ -3,9 +3,12 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
@ -34,16 +37,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private MultiplayerGameplayLeaderboard leaderboard;
private readonly int[] userIds;
private readonly MultiplayerRoomUser[] users;
private LoadingLayer loadingDisplay;
private FillFlowContainer leaderboardFlow;
/// <summary>
/// Construct a multiplayer player.
/// </summary>
/// <param name="playlistItem">The playlist item to be played.</param>
/// <param name="userIds">The users which are participating in this game.</param>
public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds)
/// <param name="users">The users which are participating in this game.</param>
public MultiplayerPlayer(PlaylistItem playlistItem, MultiplayerRoomUser[] users)
: base(playlistItem, new PlayerConfiguration
{
AllowPause = false,
@ -51,14 +55,41 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
AllowSkipping = false,
})
{
this.userIds = userIds;
this.users = users;
}
[BackgroundDependencyLoader]
private void load()
{
if (!LoadedBeatmapSuccessfully)
return;
HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
});
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, users), l =>
{
if (!LoadedBeatmapSuccessfully)
return;
((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud);
leaderboardFlow.Add(l);
if (leaderboard.TeamScores.Count >= 2)
{
LoadComponentAsync(new GameplayMatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
Expanded = { BindTarget = HUDOverlay.ShowHud },
}, leaderboardFlow.Add);
}
});
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
}
@ -67,6 +98,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadAsyncComplete();
if (!LoadedBeatmapSuccessfully)
return;
if (!ValidForResume)
return; // token retrieval may have failed.
@ -92,13 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.Room != null);
}
protected override void LoadComplete()
{
base.LoadComplete();
((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud);
}
protected override void StartGameplay()
{
// block base call, but let the server know we are ready to start.
@ -118,6 +145,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override void Update()
{
base.Update();
if (!LoadedBeatmapSuccessfully)
return;
adjustLeaderboardPosition();
}
@ -125,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
const float padding = 44; // enough margin to avoid the hit error display.
leaderboard.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
}
private void onMatchStarted() => Scheduler.Add(() =>
@ -150,7 +181,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(RoomId.Value != null);
return new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem);
return leaderboard.TeamScores.Count == 2
? new MultiplayerTeamResultsScreen(score, RoomId.Value.Value, PlaylistItem, leaderboard.TeamScores)
: new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem);
}
protected override void Dispose(bool isDisposing)

View File

@ -0,0 +1,152 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class MultiplayerTeamResultsScreen : MultiplayerResultsScreen
{
private readonly SortedDictionary<int, BindableInt> teamScores;
private Container winnerBackground;
private Drawable winnerText;
public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary<int, BindableInt> teamScores)
: base(score, roomId, playlistItem)
{
if (teamScores.Count != 2)
throw new NotSupportedException(@"This screen currently only supports 2 teams");
this.teamScores = teamScores;
}
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
const float winner_background_half_height = 250;
VerticalScrollContent.Anchor = VerticalScrollContent.Origin = Anchor.TopCentre;
VerticalScrollContent.Scale = new Vector2(0.9f);
VerticalScrollContent.Y = 75;
var redScore = teamScores.First().Value;
var blueScore = teamScores.Last().Value;
LocalisableString winner;
Colour4 winnerColour;
int comparison = redScore.Value.CompareTo(blueScore.Value);
if (comparison < 0)
{
// team name should eventually be coming from the multiplayer match state.
winner = MultiplayerTeamResultsScreenStrings.TeamWins(@"Blue");
winnerColour = colours.TeamColourBlue;
}
else if (comparison > 0)
{
// team name should eventually be coming from the multiplayer match state.
winner = MultiplayerTeamResultsScreenStrings.TeamWins(@"Red");
winnerColour = colours.TeamColourRed;
}
else
{
winner = MultiplayerTeamResultsScreenStrings.TheTeamsAreTied;
winnerColour = Colour4.White.Opacity(0.5f);
}
AddRangeInternal(new Drawable[]
{
new MatchScoreDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Team1Score = { BindTarget = redScore },
Team2Score = { BindTarget = blueScore },
},
winnerBackground = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.X,
Height = winner_background_half_height,
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0), Colour4.Black.Opacity(0.4f))
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = winner_background_half_height,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0.4f), Colour4.Black.Opacity(0))
}
}
},
(winnerText = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.Torus.With(size: 80, weight: FontWeight.Bold),
Text = winner,
Blending = BlendingParameters.Additive
}).WithEffect(new GlowEffect
{
Colour = winnerColour,
}).With(e =>
{
e.Anchor = Anchor.Centre;
e.Origin = Anchor.Centre;
})
});
}
protected override void LoadComplete()
{
base.LoadComplete();
using (BeginDelayedSequence(300))
{
const double fade_in_duration = 600;
winnerText.FadeInFromZero(fade_in_duration, Easing.InQuint);
winnerBackground.FadeInFromZero(fade_in_duration, Easing.InQuint);
winnerText
.ScaleTo(10)
.ScaleTo(1, 600, Easing.InQuad)
.Then()
.ScaleTo(1.02f, 1600, Easing.OutQuint)
.FadeOut(5000, Easing.InQuad);
winnerBackground.Delay(2200).FadeOut(2000);
}
}
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay;
private IconButton kickButton;
public ParticipantPanel(MultiplayerRoomUser user)
{
User = user;
@ -64,7 +66,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
new Dimension(GridSizeMode.Absolute, 18),
new Dimension(GridSizeMode.AutoSize),
new Dimension()
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@ -79,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Colour = Color4Extensions.FromHex("#F7E65D"),
Alpha = 0
},
new TeamDisplay(user),
new TeamDisplay(User),
new Container
{
RelativeSizeAxes = Axes.Both,
@ -157,7 +160,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Margin = new MarginPadding { Right = 10 },
}
}
}
},
kickButton = new KickButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Margin = new MarginPadding(4),
Action = () => Client.KickUser(User.UserID),
},
},
}
};
@ -167,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
base.OnRoomUpdated();
if (Room == null)
if (Room == null || Client.LocalUser == null)
return;
const double fade_time = 50;
@ -179,6 +190,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
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
@ -211,13 +227,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
if (!Client.IsHost)
return;
Client.TransferHost(targetUser);
}),
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
// Ensure the local user is still host.
if (!Client.IsHost)
return;
Client.KickUser(targetUser);
})
};
}
}
public class KickButton : IconButton
{
public KickButton()
{
Icon = FontAwesome.Solid.UserTimes;
TooltipText = "Kick";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IconHoverColour = colours.Red;
}
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
@ -19,16 +18,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
internal class TeamDisplay : MultiplayerRoomComposite
{
private readonly User user;
private readonly MultiplayerRoomUser user;
private Drawable box;
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private MultiplayerClient client { get; set; }
public TeamDisplay(User user)
public TeamDisplay(MultiplayerRoomUser user)
{
this.user = user;
@ -61,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
};
if (user.Id == client.LocalUser?.UserID)
if (Client.LocalUser?.Equals(user) == true)
{
InternalChild = new OsuClickableContainer
{
@ -79,9 +76,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private void changeTeam()
{
client.SendMatchRequest(new ChangeTeamRequest
Client.SendMatchRequest(new ChangeTeamRequest
{
TeamID = ((client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
});
}
@ -93,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// we don't have a way of knowing when an individual user's state has updated, so just handle on RoomUpdated for now.
var userRoomState = Room?.Users.FirstOrDefault(u => u.UserID == user.Id)?.MatchState;
var userRoomState = Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
const double duration = 400;

View File

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

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Spectate;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
@ -45,20 +46,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private PlayerArea currentAudioSource;
private bool canStartMasterClock;
private readonly MultiplayerRoomUser[] users;
/// <summary>
/// Creates a new <see cref="MultiSpectatorScreen"/>.
/// </summary>
/// <param name="userIds">The players to spectate.</param>
public MultiSpectatorScreen(int[] userIds)
: base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray())
/// <param name="users">The players to spectate.</param>
public MultiSpectatorScreen(MultiplayerRoomUser[] users)
: base(users.Select(u => u.UserID).ToArray())
{
instances = new PlayerArea[UserIds.Count];
this.users = users;
instances = new PlayerArea[Users.Count];
}
[BackgroundDependencyLoader]
private void load()
{
Container leaderboardContainer;
Container scoreDisplayContainer;
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0);
InternalChildren = new[]
@ -67,28 +74,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
masterClockContainer.WithChild(new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
new Drawable[]
{
leaderboardContainer = new Container
scoreDisplayContainer = new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
new Drawable[]
{
leaderboardContainer = new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
}
}
}
}
}
})
};
for (int i = 0; i < UserIds.Count; i++)
for (int i = 0; i < Users.Count; i++)
{
grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock));
grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer.GameplayClock));
syncManager.AddPlayerClock(instances[i].GameplayClock);
}
@ -97,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray())
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users)
{
Expanded = { Value = true },
Anchor = Anchor.CentreLeft,
@ -108,6 +131,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
leaderboardContainer.Add(leaderboard);
if (leaderboard.TeamScores.Count == 2)
{
LoadComponentAsync(new MatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
}, scoreDisplayContainer.Add);
}
});
}

View File

@ -35,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<RoomCategory> Category { get; private set; }
[Resolved(typeof(Room))]
protected BindableList<User> RecentParticipants { get; private set; }

View File

@ -11,9 +11,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@ -22,15 +22,18 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
[Cached]
public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack
{
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
// this is required due to PlayerLoader eventually being pushed to the main stack
@ -38,12 +41,8 @@ namespace osu.Game.Screens.OnlinePlay
public override bool DisallowExternalBeatmapRulesetChanges => true;
private MultiplayerWaveContainer waves;
private OsuButton createButton;
private ScreenStack screenStack;
private LoungeSubScreen loungeSubScreen;
private ScreenStack screenStack;
private readonly IBindable<bool> isIdle = new BindableBool();
@ -74,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; }
private Drawable header;
private Drawable headerBackground;
protected OnlinePlayScreen()
{
Anchor = Anchor.Centre;
@ -107,59 +103,26 @@ namespace osu.Game.Screens.OnlinePlay
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = Header.HEIGHT },
Children = new[]
Children = new Drawable[]
{
header = new Container
new BeatmapBackgroundSprite
{
RelativeSizeAxes = Axes.X,
Height = 400,
Children = new[]
{
headerBackground = new Container
{
RelativeSizeAxes = Axes.Both,
Width = 1.25f,
Masking = true,
Children = new Drawable[]
{
new HeaderBackgroundSprite
{
RelativeSizeAxes = Axes.X,
Height = 400 // Keep a static height so the header doesn't change as it's resized between subscreens
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = -1 }, // 1px padding to avoid a 1px gap due to masking
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.5f), backgroundColour)
},
}
}
RelativeSizeAxes = Axes.Both
},
screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.9f), Color4.Black.Opacity(0.6f))
},
screenStack = new OnlinePlaySubScreenStack
{
RelativeSizeAxes = Axes.Both
}
}
},
new Header(ScreenTitle, screenStack),
createButton = CreateNewMultiplayerGameButton().With(button =>
{
button.Anchor = Anchor.TopRight;
button.Origin = Anchor.TopRight;
button.Size = new Vector2(150, Header.HEIGHT - 20);
button.Margin = new MarginPadding
{
Top = 10,
Right = 10 + HORIZONTAL_OVERFLOW_PADDING,
};
button.Action = () => OpenNewRoom();
}),
RoomManager,
ongoingOperationTracker,
ongoingOperationTracker
}
};
}
@ -292,18 +255,6 @@ namespace osu.Game.Screens.OnlinePlay
logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut();
}
/// <summary>
/// Creates and opens the newly-created room.
/// </summary>
/// <param name="room">An optional template to use when creating the room.</param>
public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom());
/// <summary>
/// Creates a new room.
/// </summary>
/// <returns>The created <see cref="Room"/>.</returns>
protected abstract Room CreateNewRoom();
private void screenPushed(IScreen lastScreen, IScreen newScreen)
{
subScreenChanged(lastScreen, newScreen);
@ -319,19 +270,6 @@ namespace osu.Game.Screens.OnlinePlay
private void subScreenChanged(IScreen lastScreen, IScreen newScreen)
{
switch (newScreen)
{
case LoungeSubScreen _:
header.Delay(OnlinePlaySubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(0, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
case RoomSubScreen _:
header.ResizeHeightTo(135, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(-OnlinePlaySubScreen.X_SHIFT, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
}
if (lastScreen is IOsuScreen lastOsuScreen)
Activity.UnbindFrom(lastOsuScreen.Activity);
@ -339,7 +277,6 @@ namespace osu.Game.Screens.OnlinePlay
((IBindable<UserActivity>)Activity).BindTo(newOsuScreen.Activity);
UpdatePollingRate(isIdle.Value);
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
}
protected IScreen CurrentSubScreen => screenStack.CurrentScreen;
@ -350,8 +287,6 @@ namespace osu.Game.Screens.OnlinePlay
protected abstract LoungeSubScreen CreateLounge();
protected abstract OsuButton CreateNewMultiplayerGameButton();
private class MultiplayerWaveContainer : WaveContainer
{
protected override bool StartHidden => true;
@ -365,13 +300,48 @@ namespace osu.Game.Screens.OnlinePlay
}
}
private class HeaderBackgroundSprite : OnlinePlayBackgroundSprite
private class BeatmapBackgroundSprite : OnlinePlayBackgroundSprite
{
protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BackgroundSprite { RelativeSizeAxes = Axes.Both };
protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BlurredBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
private class BackgroundSprite : UpdateableBeatmapBackgroundSprite
public class BlurredBackgroundSprite : UpdateableBeatmapBackgroundSprite
{
protected override double TransformDuration => 200;
public BlurredBackgroundSprite(BeatmapSetCoverType type)
: base(type)
{
}
protected override double LoadDelay => 200;
protected override Drawable CreateDrawable(BeatmapInfo model) =>
new BufferedLoader(base.CreateDrawable(model));
}
// This class is an unfortunate requirement due to `LongRunningLoad` requiring direct async loading.
// It means that if the web request fetching the beatmap background takes too long, it will suddenly appear.
internal class BufferedLoader : BufferedContainer
{
private readonly Drawable drawable;
public BufferedLoader(Drawable drawable)
{
this.drawable = drawable;
RelativeSizeAxes = Axes.Both;
BlurSigma = new Vector2(10);
FrameBufferScale = new Vector2(0.5f);
CacheDrawnFrameBuffer = true;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponentAsync(drawable, d =>
{
Add(d);
ForceRedraw();
});
}
}
}

View File

@ -59,6 +59,8 @@ namespace osu.Game.Screens.OnlinePlay
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
initialBeatmap = Beatmap.Value;
initialRuleset = Ruleset.Value;
initialMods = Mods.Value.ToList();

View File

@ -3,8 +3,6 @@
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Match;
@ -46,21 +44,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})");
}
protected override Room CreateNewRoom()
{
return new Room
{
Name = { Value = $"{API.LocalUser}'s awesome playlist" },
Type = { Value = MatchType.Playlists }
};
}
protected override string ScreenTitle => "Playlists";
protected override RoomManager CreateRoomManager() => new PlaylistsRoomManager();
protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen();
protected override OsuButton CreateNewMultiplayerGameButton() => new CreatePlaylistsRoomButton();
}
}

View File

@ -1,6 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@ -10,8 +17,60 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
public class PlaylistsLoungeSubScreen : LoungeSubScreen
{
protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl();
[Resolved]
private IAPIProvider api { get; set; }
private Dropdown<PlaylistsCategory> categoryDropdown;
protected override IEnumerable<Drawable> CreateFilterControls()
{
categoryDropdown = new SlimEnumDropdown<PlaylistsCategory>
{
RelativeSizeAxes = Axes.None,
Width = 160,
};
categoryDropdown.Current.BindValueChanged(_ => UpdateFilter());
return base.CreateFilterControls().Append(categoryDropdown);
}
protected override FilterCriteria CreateFilterCriteria()
{
var criteria = base.CreateFilterCriteria();
switch (categoryDropdown.Current.Value)
{
case PlaylistsCategory.Normal:
criteria.Category = @"normal";
break;
case PlaylistsCategory.Spotlight:
criteria.Category = @"spotlight";
break;
}
return criteria;
}
protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton();
protected override Room CreateNewRoom()
{
return new Room
{
Name = { Value = $"{api.LocalUser}'s awesome playlist" },
Type = { Value = MatchType.Playlists }
};
}
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room);
private enum PlaylistsCategory
{
Any,
Normal,
Spotlight
}
}
}

View File

@ -163,7 +163,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new PurpleTriangleButton
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,

View File

@ -6,29 +6,58 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboard : FillFlowContainer<GameplayLeaderboardScore>
public class GameplayLeaderboard : CompositeDrawable
{
private readonly int maxPanels;
private readonly Cached sorting = new Cached();
public Bindable<bool> Expanded = new Bindable<bool>();
public GameplayLeaderboard()
protected readonly FillFlowContainer<GameplayLeaderboardScore> Flow;
private bool requiresScroll;
private readonly OsuScrollContainer scroll;
private GameplayLeaderboardScore trackedScore;
/// <summary>
/// Create a new leaderboard.
/// </summary>
/// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
public GameplayLeaderboard(int maxPanels = 8)
{
this.maxPanels = maxPanels;
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
Direction = FillDirection.Vertical;
Spacing = new Vector2(2.5f);
LayoutDuration = 250;
LayoutEasing = Easing.OutQuint;
InternalChildren = new Drawable[]
{
scroll = new InputDisabledScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
{
RelativeSizeAxes = Axes.X,
X = GameplayLeaderboardScore.SHEAR_WIDTH,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2.5f),
LayoutDuration = 450,
LayoutEasing = Easing.OutQuint,
}
}
};
}
protected override void LoadComplete()
@ -46,24 +75,87 @@ namespace osu.Game.Screens.Play.HUD
/// Whether the player should be tracked on the leaderboard.
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
/// </param>
public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked)
public ILeaderboardScore Add([CanBeNull] User user, bool isTracked)
{
var drawable = new GameplayLeaderboardScore(user, isTracked)
{
Expanded = { BindTarget = Expanded },
};
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
base.Add(drawable);
if (isTracked)
{
if (trackedScore != null)
throw new InvalidOperationException("Cannot track more than one score.");
trackedScore = drawable;
}
drawable.Expanded.BindTo(Expanded);
Flow.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y);
int displayCount = Math.Min(Flow.Count, maxPanels);
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
requiresScroll = displayCount != Flow.Count;
return drawable;
}
public sealed override void Add(GameplayLeaderboardScore drawable)
public void Clear()
{
throw new NotSupportedException($"Use {nameof(AddPlayer)} instead.");
Flow.Clear();
trackedScore = null;
scroll.ScrollToStart(false);
}
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) =>
new GameplayLeaderboardScore(user, isTracked);
protected override void Update()
{
base.Update();
if (requiresScroll && trackedScore != null)
{
float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
scroll.ScrollTo(scrollTarget, false);
}
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
float fadeBottom = scroll.Current + scroll.DrawHeight;
float fadeTop = scroll.Current + panel_height;
if (scroll.Current <= 0) fadeTop -= panel_height;
if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;
// logic is mostly shared with Leaderboard, copied here for simplicity.
foreach (var c in Flow.Children)
{
float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y;
float bottomY = topY + panel_height;
bool requireTopFade = requiresScroll && topY <= fadeTop;
bool requireBottomFade = requiresScroll && bottomY >= fadeBottom;
if (!requireTopFade && !requireBottomFade)
c.Colour = Color4.White;
else if (topY > fadeBottom + panel_height || bottomY < fadeTop - panel_height)
c.Colour = Color4.Transparent;
else
{
if (requireBottomFade)
{
c.Colour = ColourInfo.GradientVertical(
Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / panel_height, 1)),
Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / panel_height, 1)));
}
else if (requiresScroll)
{
c.Colour = ColourInfo.GradientVertical(
Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / panel_height, 1)),
Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / panel_height, 1)));
}
}
}
}
private void sort()
@ -71,15 +163,26 @@ namespace osu.Game.Screens.Play.HUD
if (sorting.IsValid)
return;
var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList();
var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
for (int i = 0; i < Count; i++)
for (int i = 0; i < Flow.Count; i++)
{
SetLayoutPosition(orderedByScore[i], i);
Flow.SetLayoutPosition(orderedByScore[i], i);
orderedByScore[i].ScorePosition = i + 1;
}
sorting.Validate();
}
private class InputDisabledScrollContainer : OsuScrollContainer
{
public InputDisabledScrollContainer()
{
ScrollbarVisible = false;
}
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => false;
}
}
}

View File

@ -54,6 +54,10 @@ namespace osu.Game.Screens.Play.HUD
public BindableInt Combo { get; } = new BindableInt();
public BindableBool HasQuit { get; } = new BindableBool();
public Color4? BackgroundColour { get; set; }
public Color4? TextColour { get; set; }
private int? scorePosition;
public int? ScorePosition
@ -77,7 +81,10 @@ namespace osu.Game.Screens.Play.HUD
[CanBeNull]
public User User { get; }
private readonly bool trackedPlayer;
/// <summary>
/// Whether this score is the local user or a replay player (and should be focused / always visible).
/// </summary>
public readonly bool Tracked;
private Container mainFillContainer;
@ -93,11 +100,11 @@ namespace osu.Game.Screens.Play.HUD
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary>
/// <param name="user">The score's player.</param>
/// <param name="trackedPlayer">Whether the player is the local user or a replay player.</param>
public GameplayLeaderboardScore([CanBeNull] User user, bool trackedPlayer)
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
public GameplayLeaderboardScore([CanBeNull] User user, bool tracked)
{
User = user;
this.trackedPlayer = trackedPlayer;
Tracked = tracked;
AutoSizeAxes = Axes.X;
Height = PANEL_HEIGHT;
@ -331,19 +338,19 @@ namespace osu.Game.Screens.Play.HUD
if (scorePosition == 1)
{
widthExtension = true;
panelColour = Color4Extensions.FromHex("7fcc33");
textColour = Color4.White;
panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
textColour = TextColour ?? Color4.White;
}
else if (trackedPlayer)
else if (Tracked)
{
widthExtension = true;
panelColour = Color4Extensions.FromHex("ffd966");
textColour = Color4Extensions.FromHex("2e576b");
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
textColour = TextColour ?? Color4Extensions.FromHex("2e576b");
}
else
{
panelColour = Color4Extensions.FromHex("3399cc");
textColour = Color4.White;
panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc");
textColour = TextColour ?? Color4.White;
}
this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic);

View File

@ -0,0 +1,179 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class MatchScoreDisplay : CompositeDrawable
{
private const float bar_height = 18;
private const float font_size = 50;
public BindableInt Team1Score = new BindableInt();
public BindableInt Team2Score = new BindableInt();
protected MatchScoreCounter Score1Text;
protected MatchScoreCounter Score2Text;
private Drawable score1Bar;
private Drawable score2Bar;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new Box
{
Name = "top bar red (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = colours.TeamColourRed,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
new Box
{
Name = "top bar blue (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = colours.TeamColourBlue,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
score1Bar = new Box
{
Name = "top bar red",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = colours.TeamColourRed,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
score2Bar = new Box
{
Name = "top bar blue",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = colours.TeamColourBlue,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = font_size + bar_height,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Children = new Drawable[]
{
Score1Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
Score2Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Team1Score.BindValueChanged(_ => updateScores());
Team2Score.BindValueChanged(_ => updateScores(), true);
}
private void updateScores()
{
Score1Text.Current.Value = Team1Score.Value;
Score2Text.Current.Value = Team2Score.Value;
int comparison = Team1Score.Value.CompareTo(Team2Score.Value);
if (comparison > 0)
{
Score1Text.Winning = true;
Score2Text.Winning = false;
}
else if (comparison < 0)
{
Score1Text.Winning = false;
Score2Text.Winning = true;
}
else
{
Score1Text.Winning = false;
Score2Text.Winning = false;
}
var winningBar = Team1Score.Value > Team2Score.Value ? score1Bar : score2Bar;
var losingBar = Team1Score.Value <= Team2Score.Value ? score1Bar : score2Bar;
var diff = Math.Max(Team1Score.Value, Team2Score.Value) - Math.Min(Team1Score.Value, Team2Score.Value);
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Score1Text.X = -Math.Max(5 + Score1Text.DrawWidth / 2, score1Bar.DrawWidth);
Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
protected class MatchScoreCounter : ScoreCounter
{
private OsuSpriteText displayedSpriteText;
public MatchScoreCounter()
{
Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
}
public bool Winning
{
set => updateFont(value);
}
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
displayedSpriteText = s;
displayedSpriteText.Spacing = new Vector2(-6);
updateFont(false);
});
private void updateFont(bool winning)
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0");
}
}
}

View File

@ -7,12 +7,17 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
@ -21,6 +26,11 @@ namespace osu.Game.Screens.Play.HUD
{
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
public readonly SortedDictionary<int, BindableInt> TeamScores = new SortedDictionary<int, BindableInt>();
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private SpectatorClient spectatorClient { get; set; }
@ -31,21 +41,24 @@ namespace osu.Game.Screens.Play.HUD
private UserLookupCache userLookupCache { get; set; }
private readonly ScoreProcessor scoreProcessor;
private readonly BindableList<int> playingUsers;
private readonly MultiplayerRoomUser[] playingUsers;
private Bindable<ScoringMode> scoringMode;
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
private bool hasTeams => TeamScores.Count > 0;
/// <summary>
/// Construct a new leaderboard.
/// </summary>
/// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param>
/// <param name="userIds">IDs of all users in this match.</param>
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
/// <param name="users">IDs of all users in this match.</param>
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
{
// todo: this will eventually need to be created per user to support different mod combinations.
this.scoreProcessor = scoreProcessor;
// todo: this will likely be passed in as User instances.
playingUsers = new BindableList<int>(userIds);
playingUsers = users;
}
[BackgroundDependencyLoader]
@ -53,14 +66,17 @@ namespace osu.Game.Screens.Play.HUD
{
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
foreach (var userId in playingUsers)
foreach (var user in playingUsers)
{
var trackedUser = CreateUserData(userId, scoreProcessor);
var trackedUser = CreateUserData(user, scoreProcessor);
trackedUser.ScoringMode.BindTo(scoringMode);
UserScores[userId] = trackedUser;
UserScores[user.UserID] = trackedUser;
if (trackedUser.Team is int team && !TeamScores.ContainsKey(team))
TeamScores.Add(team, new BindableInt());
}
userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() =>
userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(users => Schedule(() =>
{
foreach (var user in users.Result)
{
@ -69,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD
var trackedUser = UserScores[user.Id];
var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id);
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
leaderboardScore.TotalScore.BindTo(trackedUser.Score);
leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
@ -83,23 +99,50 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete();
// BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
foreach (int userId in playingUsers)
foreach (var user in playingUsers)
{
spectatorClient.WatchUser(userId);
spectatorClient.WatchUser(user.UserID);
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId))
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId }));
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID))
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
}
// bind here is to support players leaving the match.
// new players are not supported.
playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUsers.BindCollectionChanged(usersChanged);
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUserIds.BindCollectionChanged(usersChanged);
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
spectatorClient.OnNewFrames += handleIncomingFrames;
}
protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new TrackedUserData(user, scoreProcessor);
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked)
{
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
if (UserScores[user.Id].Team is int team)
{
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
leaderboardScore.TextColour = Color4.White;
}
return leaderboardScore;
}
private Color4 getTeamColour(int team)
{
switch (team)
{
case 0:
return colours.TeamColourRed;
default:
return colours.TeamColourBlue;
}
}
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@ -124,9 +167,26 @@ namespace osu.Game.Screens.Play.HUD
trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
trackedData.UpdateScore();
updateTotals();
});
protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor);
private void updateTotals()
{
if (!hasTeams)
return;
foreach (var scores in TeamScores.Values) scores.Value = 0;
foreach (var u in UserScores.Values)
{
if (u.Team == null)
continue;
if (TeamScores.TryGetValue(u.Team.Value, out var team))
team.Value += (int)Math.Round(u.Score.Value);
}
}
protected override void Dispose(bool isDisposing)
{
@ -136,7 +196,7 @@ namespace osu.Game.Screens.Play.HUD
{
foreach (var user in playingUsers)
{
spectatorClient.StopWatchingUser(user);
spectatorClient.StopWatchingUser(user.UserID);
}
spectatorClient.OnNewFrames -= handleIncomingFrames;
@ -145,7 +205,7 @@ namespace osu.Game.Screens.Play.HUD
protected class TrackedUserData
{
public readonly int UserId;
public readonly MultiplayerRoomUser User;
public readonly ScoreProcessor ScoreProcessor;
public readonly BindableDouble Score = new BindableDouble();
@ -157,9 +217,11 @@ namespace osu.Game.Screens.Play.HUD
public readonly List<TimedFrame> Frames = new List<TimedFrame>();
public TrackedUserData(int userId, ScoreProcessor scoreProcessor)
public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID;
public TrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor)
{
UserId = userId;
User = user;
ScoreProcessor = scoreProcessor;
ScoringMode.BindValueChanged(_ => UpdateScore());

View File

@ -57,8 +57,6 @@ namespace osu.Game.Screens.Play
private Bindable<HUDVisibilityMode> configVisibilityMode;
private readonly Container visibilityContainer;
private readonly BindableBool replayLoaded = new BindableBool();
private static bool hasShownNotificationOnce;
@ -72,7 +70,7 @@ namespace osu.Game.Screens.Play
private readonly SkinnableTargetContainer mainComponents;
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
{
@ -84,13 +82,9 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
CreateFailingLayer(),
visibilityContainer = new Container
mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
{
RelativeSizeAxes = Axes.Both,
Child = mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
{
RelativeSizeAxes = Axes.Both,
},
},
topRightElements = new FillFlowContainer
{

View File

@ -125,7 +125,7 @@ namespace osu.Game.Screens.Play
Objects = drawableRuleset.Objects;
}
config.BindWith(OsuSetting.ShowProgressGraph, ShowGraph);
config.BindWith(OsuSetting.ShowDifficultyGraph, ShowGraph);
graph.FillColour = bar.FillColour = colours.BlueLighter;
}

View File

@ -40,6 +40,8 @@ namespace osu.Game.Screens.Ranking
protected ScorePanelList ScorePanelList { get; private set; }
protected VerticalScrollContainer VerticalScrollContent { get; private set; }
[Resolved(CanBeNull = true)]
private Player player { get; set; }
@ -77,7 +79,7 @@ namespace osu.Game.Screens.Ranking
{
new Drawable[]
{
new VerticalScrollContainer
VerticalScrollContent = new VerticalScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
@ -343,7 +345,7 @@ namespace osu.Game.Screens.Ranking
{
}
private class VerticalScrollContainer : OsuScrollContainer
protected class VerticalScrollContainer : OsuScrollContainer
{
protected override Container<Drawable> Content => content;
@ -351,6 +353,8 @@ namespace osu.Game.Screens.Ranking
public VerticalScrollContainer()
{
Masking = false;
base.Content.Add(content = new Container { RelativeSizeAxes = Axes.X });
}

View File

@ -35,6 +35,7 @@ namespace osu.Game.Screens.Select
{
public class BeatmapInfoWedge : VisibilityContainer
{
public const float BORDER_THICKNESS = 2.5f;
private const float shear_width = 36.75f;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
@ -59,7 +60,7 @@ namespace osu.Game.Screens.Select
Shear = wedged_container_shear;
Masking = true;
BorderColour = new Color4(221, 255, 255, 255);
BorderThickness = 2.5f;
BorderThickness = BORDER_THICKNESS;
Alpha = 0;
EdgeEffect = new EdgeEffectParameters
{

View File

@ -56,7 +56,7 @@ namespace osu.Game.Screens.Select
set
{
searchText = value;
SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
SearchTerms = searchText.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
SearchNumber = null;

View File

@ -79,6 +79,8 @@ namespace osu.Game.Screens.Select
protected BeatmapCarousel Carousel { get; private set; }
protected Container LeftArea { get; private set; }
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@ -186,12 +188,12 @@ namespace osu.Game.Screens.Select
{
new Drawable[]
{
new Container
LeftArea = new Container
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = left_area_padding },
Children = new Drawable[]
{
beatmapInfoWedge = new BeatmapInfoWedge
@ -200,8 +202,8 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding
{
Top = left_area_padding,
Right = left_area_padding,
Left = -BeatmapInfoWedge.BORDER_THICKNESS, // Hide the left border
},
},
new Container
@ -210,7 +212,7 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding
{
Bottom = Footer.HEIGHT,
Top = WEDGE_HEIGHT + left_area_padding,
Top = WEDGE_HEIGHT,
Left = left_area_padding,
Right = left_area_padding * 2,
},

View File

@ -24,9 +24,9 @@ namespace osu.Game.Screens.Spectate
/// </summary>
public abstract class SpectatorScreen : OsuScreen
{
protected IReadOnlyList<int> UserIds => userIds;
protected IReadOnlyList<int> Users => users;
private readonly List<int> userIds = new List<int>();
private readonly List<int> users = new List<int>();
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -50,17 +50,17 @@ namespace osu.Game.Screens.Spectate
/// <summary>
/// Creates a new <see cref="SpectatorScreen"/>.
/// </summary>
/// <param name="userIds">The users to spectate.</param>
protected SpectatorScreen(params int[] userIds)
/// <param name="users">The users to spectate.</param>
protected SpectatorScreen(params int[] users)
{
this.userIds.AddRange(userIds);
this.users.AddRange(users);
}
protected override void LoadComplete()
{
base.LoadComplete();
userLookupCache.GetUsersAsync(userIds.ToArray()).ContinueWith(users => Schedule(() =>
userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(users => Schedule(() =>
{
foreach (var u in users.Result)
{
@ -207,7 +207,7 @@ namespace osu.Game.Screens.Spectate
{
onUserStateRemoved(userId);
userIds.Remove(userId);
users.Remove(userId);
userMap.Remove(userId);
spectatorClient.StopWatchingUser(userId);